eax ассемблер что это

Регистры процессора

Начиная с модели 80386 процессоры Intel предоставляют 16 основных регистров для пользовательских программ и ещё 11 регистров для работы с мультимедийными приложениями (MMX) и числами с плавающей точкой (FPU/NPX). Все команды так или иначе изменяют содержимое регистров. Как уже говорилось, обращаться к регистрам быстрее и удобнее, чем к памяти. Поэтому при программировании на языке Ассемблера регистры используются очень широко.

В этом разделе мы рассмотрим основные регистры процессоров Intel. Названия и состав/количество регистров для других процессоров могут отличаться. Итак, основные регистры процессоров Intel.

Таблица 2.1. Основные регистры процессора.

Название Разрядность Основное назначение
EAX 32 Аккумулятор
EBX 32 База
ECX 32 Счётчик
EDX 32 Регистр данных
EBP 32 Указатель базы
ESP 32 Указатель стека
ESI 32 Индекс источника
EDI 32 Индекс приёмника
EFLAGS 32 Регистр флагов
EIP 32 Указатель инструкции (команды)
CS 16 Сегментный регистр
DS 16 Сегментный регистр
ES 16 Сегментный регистр
FS 16 Сегментный регистр
GS 16 Сегментный регистр
SS 16 Сегментный регистр

Регистры EAX, EBX, ECX, EDX – это регистры общего назначения. Они имеют определённое назначение (так уж сложилось исторически), однако в них можно хранить любую информацию.

Регистры EBP, ESP, ESI, EDI – это также регистры общего назначения. Они имеют уже более конкретное назначение. В них также можно хранить пользовательские данные, но делать это нужно уже более осторожно, чтобы не получить «неожиданный» результат.

Регистр флагов и сегментные регистры требуют отдельного описания и будут более подробно рассмотрены далее.

Пока для вас здесь слишком много непонятных слов, но со временем всё прояснится)))

Когда-то процессоры были 16-разрядными, и, соответственно, все их регистры были также 16-разрядными. Для совместимости со старыми программами, а также для удобства программирования некоторые регистры разделены на 2 или 4 «маленьких» регистра, у каждого из которых есть свои имена. В таблице 2.2 перечислены такие регистры.

Вот пример такого регистра.

Из этого следует, что вы можете написать в своей программе, например, такие команды: Обе команды поместят в регистр AX число 1. Разница будет заключаться только в том, что вторая команда обнулит старшие разряды регистра EAX, то есть после выполнения второй команды в регистре EAX будет число 1. А первая команда оставит в старших разрядах регистра EAX старые данные. И если там были данные, отличные от нуля, то после выполнения первой команды в регистре EAX будет какое-то число, но не 1. А вот в регистре AX будет число 1. Сложно? Ну это пока… Со временем вы к таким вещам привыкните.

Мы пока не говорили о разрядах (битах). Эту тему мы обсудим в разделах, посвящённых системам счисления. А сейчас пока вам достаточно знать, что нулевой разряд (бит) – это младший бит. Он крайний справа. Старший бит – крайний слева. Номер старшего бита зависит от разрядности числа/регистра. Например, в 32-разрядном регистре старшим битом является 31-й бит (потому что отсчёт начинается с 0, а не с 1).

Ниже приведён список регистров общего назначения, которые можно поделить описанным выше способом и при этом к «половинкам» и «четвертинкам» этих регистров можно обращаться в программе как к отдельному регистру.

Таблица 2.2. «Делимые» регистры..

Регистр Старшие разряды Имена 16-ти и 8-ми битных регистров
31…16 15…8 7…0
EAX . AX
AH AL
EBX . BX
BH BL
ECX . CX
CH CL
EDX . DX
DH DL
ESI . SI
EDI . DI
EBP . BP
ESP . SP
EIP . IP

На этом мы закончим наше краткое знакомство с регистрами. Если вам пока не всё понятно – просто прочитайте этот раздел, чтобы более-менее представлять себе, что такое регистры. По мере приобретения новых знаний вы можете вернуться к этому разделу и уже на новом уровне воспринять эту информацию. А в следующем разделе мы коротко опишем процесс выполнения команды.

Источник

Уроки FASM. Регистры. Window x32

Что это такое и зачем нужны?

Это самая быстрая память в ПК по сравнению с ОЗУ. Но при этом размер регистров намного меньше чем ОЗУ, по этой причине используют ОЗУ.

Регистр RAX это дополнение EAX, EAX это дополнение AX, AX это объединение 2 регистров AH и AL.

wikipedia

OllyDbg

Ollydbg

Зачем так много разновидностей 1 регистра?

Для поддержке более старых версий процессоров x86 (обратная совместимость, например: на 64 битном ЦП запустить 32 битную программу)

Какие бывают регистры?

Их очень много и каждый под свои задачи, но есть регистры общего доступа, с которыми мы будем работать.

Это 32 битные регистры (16, 8):

ESI (SI, это 16 битный регистр, меньше нет)

EDI (DI, это 16 битный регистр, меньше нет)

Как работать с регистрами?

В них можно хранить любую информацию: числа со знаком (int), числа без знака (unsigned int), числа с плавающей запятой (float, в x64 можно хранить double в регистре), адрес, символы (не превышая размер регистра), и другую информацию.

Попробуем записать значение в регистры:

Что за регистр FLAGS?

Это регистр состояния ЦП, он нужен для проверки и сравнению чисел, проверки регистров.

Источник

Ассемблер. Сегменты памяти и регистры

Обновл. 27 Сен 2021 |

Результат выполнения программы:

Сегменты памяти

Модель сегментированной памяти разбивает системную память на группы независимых сегментов, на которые указывают указатели, расположенные в регистрах сегментов. Каждый сегмент используется для хранения данных определенного типа. Первый сегмент используется для хранения кода инструкций, второй — для хранения элементов данных, а третий — для программного стека.

Сегменты памяти:

Сегмент данных (data segment) — представлен секциями .data и .bss. Секция .data используется для объявления области памяти, где хранятся элементы данных для программы. Эта секция не может быть расширена после объявления элементов данных, и она остается статической во всей программе. Секция .bss также является секцией статической памяти, содержащей буферы для данных, которые будут объявлены в программе позже. Эта буферная память заполнена нулями.

Сегмент кода (code segment) — представлен секцией .text. Он определяет область в памяти, в которой хранятся коды инструкций. Это также фиксированная область.

Стек (stack) — это сегмент, который содержит значения данных, передаваемые в функции и процедуры в программе.

Регистры

Обычно операции с процессором включают в себя обработку данных. Эти данные могут быть как сохранены в памяти, так и извлечены оттуда. Однако процесс чтения данных из памяти и хранения данных в памяти замедляет работу процессора, так как это предполагает сложный процесс отправки запроса данных в блок памяти и получение данных обратно из блока по одному и тому же каналу — через шину управления.

Чтобы ускорить свою работу, процессор подключает определенные внутренние места хранения памяти, которые называются регистрами. Регистры хранят элементы данных для обработки без необходимости получать доступ к памяти. Ограниченное количество регистров встроено в чип процессора.

Регистры процессора

В архитектуре IA-32 есть десять 32-битных и шесть 16-битных процессорных регистров. Регистры делятся на три категории:

Общие регистры (General Registers);

Регистры управления (Control Registers);

Сегментные регистры (Segment Registers).

В свою очередь, общие регистры делятся на следующие:

Читайте также:  какой объем груди 2 размера

Регистры данных (Data Registers);

Регистры-указатели (Pointer Registers);

Индексные регистры (Index Registers).

Регистры данных

Регистры данных — это четыре 32-битных регистра, которые используются для арифметических, логических и других операций. Эти 32-битные регистры могут быть использованы следующими тремя способами:

как полные 32-битные регистры данных: EAX, EBX, ECX, EDX;

нижние половины 32-битных регистров могут использоваться как четыре 16-битных регистра данных: AX, BX, CX и DX;

нижняя и верхняя половины вышеупомянутых четырех 16-битных регистров могут использоваться как восемь 8-битных регистров данных: AH, AL, BH, BL, CH, CL, DH и DL.

Некоторые из этих регистров данных имеют специфическое применение в арифметических операциях:

AX (primary accumulator) — используется для ввода/вывода и в большинстве арифметических операций. Например, в операции умножения один операнд сохраняется в регистре EAX/AX/AL в соответствии с размером операнда.

BX (base register) — используется при индексированной адресации.

CX (count register) — хранит количество циклов в повторяющихся операциях (также, как и регистры ECX и CX).

DX (data register) — используется в операциях ввода/вывода, а также с регистрами AX и DX для выполнения операций умножения и деления, связанных с большими значениями.

Регистры-указатели

Регистрами-указателями являются 32-битные регистры EIP, ESP и EBP и соответствующие им 16-битные регистры IP, SP и BP. Есть три категории регистров-указателей:

Указатель на инструкцию или команду (Instruction Pointer или IP) — 16-битный регистр IP хранит смещение адреса следующей команды, которая должна быть выполнена. IP в сочетании с регистром CS (как CS:IP) предоставляет полный адрес текущей инструкции в сегменте кода.

Указатель на стек (Stack Pointer или SP) — 16-битный регистр SP обеспечивает значение смещения в программном стеке. SP в сочетании с регистром SS (SS:SP) означает текущее положение данных или адреса в программном стеке.

Базовый указатель (Base Pointer или BP) — 16-битный регистр BP используется в основном при передаче параметров в подпрограммы. Адрес в регистре SS объединяется со смещением в BP, чтобы получить местоположение параметра. BP также можно комбинировать с DI и SI в качестве базового регистра для специальной адресации.

Индексные регистры

В процессоре существуют 32-битные индексные регистры ESI и EDI и их 16-битные версии: SI и DI. Все они используются в индексированной адресации, и, иногда, в операциях сложения/вычитания. Есть два типа индексных указателей:

Исходный индекс (Source Index или SI) — используется в качестве исходного индекса в строковых операциях.

Индекс назначения (Destination Index или DI) — используется в качестве индекса назначения в строковых операциях.

Регистры управления

Регистром управления является объединенный 32-битный регистр инструкций и 32-битный регистр флагов (регистр процессора, отражающий его текущее состояние). Многие инструкции включают в себя операции сравнения и математические вычисления, которые способны изменить состояния флагов, а некоторые другие условные инструкции проверяют значения флагов состояния, чтобы перенести поток управления в другое место.

Распространенные битовые флаги:

Флаг переполнения (Overflow Flag или OF) — указывает на переполнение старшего бита данных (крайнего левого бита) после signed арифметической операции.

Флаг ловушка (Trap Flag или TF) — позволяет настроить работу процессора в одношаговом режиме.

Вспомогательный флаг переноса (Auxiliary Carry Flag или AF) — после выполнения арифметической операции содержит перенос от бита 3 до бита 4. Используется для специализированной арифметики. AF устанавливается, когда 1-байтовая арифметическая операция вызывает перенос из бита 3 в бит 4.

Флаг переноса (Carry Flag или CF) — после выполнения арифметической операции содержит перенос 0 или 1 из старшего бита (крайнего слева). Кроме того, хранит содержимое последнего бита операции сдвига или поворота.

В следующей таблице указано положение битовых флагов в 16-битном регистре флагов:

Флаг: O D I T S Z A P C
Бит №: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

Сегментные регистры

Сегменты — это специфические части программы, которые содержат данные, код и стек. Есть три основных сегмента:

Сегмент кода (Code Segment или CS) — содержит все команды и инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.

Сегмент данных (Data Segment или DS) — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.

Сегмент стека (Stack Segment или SS) — содержит данные и возвращаемые адреса процедур или подпрограмм. Он представлен в виде структуры данных «Стек». Регистр сегмента стека или регистр SS хранит начальный адрес стека.

Кроме регистров CS, DS и SS существуют и другие регистры дополнительных сегментов — ES (Extra Segment), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.

Сегментные регистры хранят начальные адреса сегмента. Чтобы получить точное местоположение данных или команды в сегменте, требуется значение смещения. Чтобы сослаться на любую ячейку памяти в сегменте, процессор объединяет адрес сегмента в сегментном регистре со значением смещения местоположения.

Пример на практике

Посмотрите на следующую простую программу, чтобы понять, как используются регистры в программировании на ассемблере. Эта программа выводит 9 звёздочек с простым сообщением:

Источник

Основы Ассемблера

В этом уроке мы обсудим несколько опкодов, которые имеют отношение к вычислению, поразрядным операциям, и т.д. Другие опкоды: команды перехода, сравнения и т.д, будут обсуждены позже.

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.

Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

Вы можете копировать значение из одного регистра в другой.

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми,

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:

Вы также можете получить значение из памяти и поместить эго в регистр. Для примера возьмем следующую схему памяти:

смещение 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42
данные 0D 0A 50 32 44 57 25 7A 5E 72 EF 7D FF AD C7

(Каждый блок представляет байт)

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

Вы, наверное, уже поняли, что префикс ptr обозначает, что надо брать из памяти некоторый размер. А префикс перед ptr обозначает размер данных:

Иногда размер можно не указывать:

Можно также непосредственные значения:

Читайте также:  igs формат чем открыть

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

Когда выполняется команда call, процессор передает управление на код с адреса 455659, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой. Вы можете поместить код, который вы часто используете в процедуру и каждый раз когда он вам нужен вызывать его командой call.

Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

Одно важное замечание: регистр eax почти всегда используется для хранения результата процедуры.

Это также применимо к функциям windows. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.

Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.

Источник

Постигаем Си глубже, используя ассемблер

Вдохновением послужила эта статья: Разбираемся в С, изучая ассемблер. Продолжение так и не вышло, хотя тема интересная. Многие бы хотели писать код и понимать, как он работает. Поэтому я запущу цикл статей о том, как выглядит Си-код после декомпиляции, попутно разбирая основные структуры кода.

От читающих потребуются хотя бы базовые знания в следующих вещах:

Что будем использовать?

При более основательном подходе к изучению, лучше пользоваться оффлайн версиями компиляторов, можете взять связку из актуального gcc, OllyDbg и NASM. Отличия должны быть минимальны.

Простейшая программа

Эта статья не стремится повторить ту, которую я приводил в самом начале. Но начинать нужно с азов, поэтому часть материала будет вынуждено пересекаться. Надеюсь на понимание.

Первое, что нужно усвоить, компилятор даже при оптимизации нулевого уровня (-O0), может вырезать код, написанный программистом. Поэтому код следующего вида:

Ничем не будет отличаться от:

Поэтому придется писать таким образом, чтобы при декомпиляции мы, все же, увидели превращение нашего кода во что-то осмысленное, поэтому примеры могут выглядеть, как минимум странно.

Второе, нам нужны флаги компиляции. Достаточно двух: -O0 и -m32. Этим мы задаем нулевой уровень оптимизации и 32-битный режим. С оптимизаций должно быть очевидно: нам не хочется видеть интерпретацию нашего кода в asm, а не оптимизированного. С режимом тоже должно быть очевидно: меньше регистров — больше внимания к сути. Хотя эти флаги я буду периодически менять, чтобы углубляться в материал.

Таким образом, если вы пользуетесь gcc, то компиляция может выглядеть так:

Соответственно, если вы пользуетесь godbolt, то вам нужно указать эти флаги в строку ввода рядом с выбором компилятора. (Первые примеры я демонстрирую на gcc 4.4.7, потом поменяю на более поздний)

Теперь, можно посмотреть первый пример:

Итак, следующий код соответствует этому:

Первые две строчки соответствую прологу функции (точнее три, но третью хочу пояснить сейчас), и мы их разберем в статье о функциях. Сейчас просто не обращайте на них внимание, тоже самое касается последних 3х строчек. Если вы не знаете asm, давайте смотреть, что означают эти команды.

Инструкции ассемблера имеют вид:

mnemonic dst, src
т. е.

инструкция получатель, источник

Тут нужно оговориться, что AT&T-синтаксис имеет другой порядок, и потом мы к нему еще вернемся, но сейчас нас интересует синтаксис схожий с NASM.

Начнем с инструкции mov. Эта инструкция перемещает из памяти в регистры или из регистров в память. В нашем случае она перемещает число 1 в регистр ebx.

Давайте кратко о регистрах: в архитектуре x86 восемь 32х битных регистров общего назначения, это значит, что эти регистры могут быть использованы программистом (в нашем случае компилятором) при написании программ. Регистры ebp, esp, esi и edi компилятор будет использовать в особых случаях, которые мы рассмотрим позже, а регистры eax, ebx, ecx и edx компилятор будет использовать для всех остальных нужд.

Таким образом mov ebx, 1, прямо соответствует строке register int a = 1;

И означает, что в регистр ebx было перемещено значение 1.

А строчка mov eax, ebx, будет означать, что в регистр eax будет перемещено значение из регистра ebx.

Есть еще две строчки push ebx и pop ebx. Если вы знакомы с понятием «стек», то догадываетесь, что сначала компилятор поместил ebx в стек, тем самым запомнил старое значение регистра, а после окончания работы программы, вернул из стека это значение обратно в регистр ebx.

Почему компилятор помещает значение 1 из регистра ebx в eax? Это связано с соглашением о вызовах функций языка Си. Там несколько пунктов, все они нас сейчас не интересуют. Важно то, что результат возвращается в eax, если это возможно. Таким образом понятно, почему единица в итоге оказывается в eax.

Но теперь логичный вопрос, а зачем понадобился ebx? Почему нельзя было написать сразу mov eax, 1? Все дело в уровне оптимизации. Я же говорил: компилятор не должен вырезать наш код, а мы написали не return 1, мы использовали регистровую переменную. Т. е. компилятор сначала поместил значение в регистр, а затем, следуя соглашению, вернул результат. Поменяйте уровень оптимизации на любой другой, и вы увидите, что регистр ebx, действительно, не нужен.

Кстати, если вы пользуетесь godbolt, то вы можете наводить мышкой на строку в Си, и вам подсветится соответствующий этой строке код в asm, при условии, что эта строка выделена цветом.

Усложним пример и перестанем пользоваться регистровыми переменными (Вы же их нечасто используете?). Посмотрим во что превратится такой код:

Опять же, пропустим верхние 3 строчки и нижние 2. Теперь у нас переменная а локальная, следовательно память ей выделяется на стеке. Поэтому мы видим следующую магию: DWORD PTR [ebp-8], что же она означает? DWORD PTR — это переменная типа двойного слова. Слово — это 16 бит. Термин получил распространение в эпоху 16-ти битных процессоров, тогда в регистр помещалось ровно 16 бит. Такой объем информации стали называть словом (word). Т. е. в нашем случае dword (double word) 2*16 = 32 бита = 4 байта (обычный int).

Читайте также:  какой код оквэд выбрать для интернет магазина

В регистре ebp содержится адрес на вершину стека для текущей функции (мы к этому еще вернемся, потом), поэтому он смещается на 4 байта, чтобы не затереть сам адрес и дописывает значение нашей переменной. Только, в нашем случае он смещается на 8 байт для переменной a. Но если вы посмотрите на код ниже, то увидите, что переменная b лежит со смещением в 4 байта. Квадратные скобки означают адрес. Т. е. это строка работает следующим образом: на основе адреса, хранящегося в ebp, компилятор помещает значение 1 по адресу ebp-8 размера 4 байта. Почему минус восемь, а не плюс. Потому что плюсу бы соответствовали параметры, переданные в эту функцию, но опять же, обсудим это позже.

Следующая строка перемещает значение 1 в регистр eax. Думаю, это не нуждается в подробных объяснениях.

Далее у нас новая инструкция add, которая осуществляет добавление (сложение). Т. е. к значению в eax (1) добавляется 5, теперь в eax находится значение 6.

После этого нужно переместить значение 6 в переменную b, что и делается следующей строкой (переменная b находится в стеке по смещению 4).

Наконец, нам нужно вернуть значение переменной b, следовательно нужно переместить
значение в регистр eax (mov eax, DWORD PTR [ebp-4]).

Если с предыдущим все понятно, то можно переходить, к более сложному.

Интересные и не очень очевидные вещи.

Что произойдет, если мы напишем следующее: int var = 2.5;

Каждый из вас, я думаю, ответит верно, что в var будет значение 2. Но что произойдет с дробной частью? Она отбросится, проигнорируется, будет ли преобразование типа? Давайте посмотрим:

Компилятор сам отбросил дробную часть за ненадобностью.

Что произойдет, если написать так: int var = 2 + 3;

И мы узнаем, что компилятор сам способен вычислять константы. А в данном случае: так как 2 и 3 являются константами, то их сумму можно вычислить на этапе компиляции. Поэтому можно не забивать себе голову вычислением таких констант, компилятор может сделать работу за вас. Например, перевод в секунды из часов можно записать, как hours * 60 * 60. Но скорее, в пример тут стоит поставить операции над константами, которые объявлены в коде.

Что произойдет, если напишем такой код:

Интересно, не правда ли? Компилятор решил не пользоваться операцией умножения, а просто сложил два числа, что и есть — умножить на 2. (Я уже не буду подробно описывать эти строки, вы должны понять их, исходя из предыдущего материала)

Вы могли слышать, что операция «умножение» выполняется дольше, чем операция «сложение». Именно по этим соображениям компилятор оптимизирует такие простые вещи.

Но усложним ему задачу и напишем так:

Пусть вас не вводит в заблуждение использование нового регистра edx, он ничем не хуже eax или ebx. Может понадобиться время, но вы должны увидеть, что единица попадает в регистр edx, затем в регистр eax, после чего значение eax складывается само с собой и после уже добавляется еще одна единица из edx. Таким образом, мы получили 1+1+1.

Знаете, бесконечно он так делать не будет, уже на *4, компилятор выдаст следующее:

Итак, у нас новая инструкция sal, что же она делает? Это двоичный сдвиг влево. Эквивалентно следующему коду в Си:

Для тех, кто не очень понимает, как работает этот оператор:

0001 сдвигаем влево (или добавляем справа) на два нуля: 0100 (т. е. 4 в 10ой системе счисления). По своей сути сдвиг влево на 2 разряда — это умножение на 4.

Забавно, что если вы умножите на 5, то компилятор сделает один sal и один add, можете сами потестировать разные числа.

На 22, компилятор на godbolt.org сдается и использует умножение, но до этого числа он пытается выкрутиться самыми разными способами. Даже вычитание использует и еще некоторые инструкции, которые мы еще не обсуждали.

Ладно, это были цветочки, а что вы думаете по поводу следующего кода:

Если вы ожидаете вычитания, то увы — нет. Компилятор будет выдавать более изощренные методы. Операция «деление» еще медленнее умножения, поэтому компилятор будет также выкручиваться:

Следует сказать, что для этого кода я выбрал компилятор существенно более поздней версии (gcc 7.2), до этого я приводил в пример gcc 4.4.7. Для ранних примеров существенных отличий не было, для этого примера они используют разные инструкции в 5ой строчке кода. И пример, сгенерированный 7.2, мне сейчас легче вам объяснить.

Стоит обратить внимание, что теперь переменная a находится в стеке по смещению 4, а не 8 и сразу же забыть об этом незначительном отличии. Ключевые моменты начинаются с mov edx, eax. Но пока пропустим значение этой строки. Инструкция shr осуществляет двоичный сдвиг вправо (т. е. деление на 2, если бы было shr edx, 1). И тут некоторые смогут подумать, а почему, действительно, не написать shr edx, 1, это же то, что делает код в Си? Но не все так просто.

Давайте проведем небольшую оптимизацию и посмотрим на что это повлияет. В действительности, мы нашим кодом выполняем целочисленное деление. Так как переменная «a» является целочисленным типом и 2 константа типа int, то результат никак не может получиться дробным по логике Си. И это хорошо, так как делить целочисленные числа быстрее и проще, но у нас знаковые числа, а это значит, что отрицательное число при делении инструкцией shr может отличаться на единицу от правильного ответа. (Это все из-за того, что 0 влезает по середине диапазона для знаковых типов). Если мы заменим знаковое деление на unsigned:

То получим ожидаемое. Стоит учесть, что godbolt опустит единицу в инструкции shr, и это не скомпилируется в NASM, но она там подразумевается. Измените 2 на 4, и вы увидите второй операнд в виде 2.

Теперь посмотрим на предыдущий код. В нем мы видим sar eax, это то же самое, что и shr, только для знаковых чисел. Остальной же код просто учитывает эту единицу, когда мы делим отрицательное число (или на отрицательное число, хотя код немного изменится). Если вы знаете, как представляются отрицательные числа в компьютере, вам будет не трудно догадаться, почему мы делаем сдвиг вправо на 31 разряд и добавляем это значение к исходному числу.

С делением на большие числа, все еще проще. Там деление заменяется на умножение, в качестве второго операнда вычисляется константа. Если вам будет интересно как, можете поломать над этим голову самостоятельно, там нет ничего сложного. Нужно просто понимать, как представляются вещественные числа в памяти.

Заключение

Для первой статьи материала уже больше, чем достаточно. Пора закруглятся и подводить итоги. Мы ознакомились с базовым синтаксисом ассемблера, выяснили, что компилятор может брать на себя простейшие оптимизации при вычислениях. Увидели разницу между регистровыми и стековыми переменными. И некоторые другие вещи. Это была вводная статья, пришлось много времени уделять очевидным вещам, но они очевидны не для всех, в будущем мы постигнем больше тонкостей языка Си.

Источник

Сказочный портал