Деление целых чисел. Умножение целых чисел

12.04.2019 Сотовые операторы

Операнд – объект, над которым выполняется машинная команда.

Операнды ассембле­ра описываются выражениями с числовыми и текстовыми константами, мет­ками и идентификаторами переменных с использованием знаков операций и некоторых зарезервированных слов.

Операнды могут комбинироваться с арифметическими, логическими, побитовы­ми и атрибутивными операторами для расчета некоторого значения или опреде­ления ячейки памяти, на которую будет воздействовать данная команда или ди­ректива.

Способы адресации операндов

Под способами адресации понимаются существующие способы задания адреса хранения операндов:


Операнд задается на микропрограммном уровне (операнд по умолчанию) : в этом случае команда явно не содержит операнда, алгоритм выполнения команды использует некоторые объекты по умолчанию (регистры, признаки и т.д.).

mul ebx ; eax = eax*ebx, неявно использует регистр eax


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

mov eax , 5 ; eax = 5;
add ebx , 2 ; ebx = ebx + 2;


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

  • 32-разрядные регистры ЕАХ, ЕВХ, ЕСХ, EDX, ESI, EDI, ESP, EBP;
  • 16-разрядные регистры АХ, ВХ, СХ, DX, SI, DI, SP, ВР;
  • 8-разрядные регистры АН, AL, BH, BL, CH, CL, DH, DL;
  • сегментные регистры CS, DS,SS, ES, FS, GS.

add eax, ebx ; eах = eax + ebх
dec esi ; esi = esi — 1

Операнд располагается в памяти . Данный способ позволяет реализовать два основных вида адресации:

  • прямую адресацию;
  • косвенную адресацию.

Прямая адресация : эффективный адрес определяется непосредственно полем смещения машинной команды, которое может иметь размер 8, 16 или 32 бита.

mov eax, sum ; eax = sum

Ассемблер заменяет sum на соответствующий адрес, хранящийся в сегменте данных (по умолчанию адресуется регистром ds ) и значение, хранящееся по адресу sum , помещает в регистр eax .

Косвенная адресация в свою очередь имеет следующие виды:

  • косвенная базовая (регистровая) адресация;
  • косвенная базовая (регистровая) адресация со смещением;
  • косвенная индексная адресация;
  • косвенная базовая индексная адресация.

Косвенная базовая (регистровая) адресация. При такой адресации эффективный адрес операнда может находиться в любом из регистров общего назначения, кроме sp/esp и bp/ebp (это специфические регистры для работы с сегментом стека). Синтаксически в команде этот режим адресации выражается заключением имени регистра в квадратные скобки .

mov eax , ; eax = *esi; *esi значение по адресу esi

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

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

mov eax , ; eax = *(esi+4)

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

mov eax , mas

Значение эффективного адреса второго операнда вычисляется выражением mas+(esi *4) и представляет собой смещение относительно начала сегмента данных.

Наличие возможности масштабирования существенно помогает в решении проблемы индексации при условии, что размер элементов массива постоянен и составляет 1, 2, 4 или 8 байт.

Данный вид адресации также может использоваться со смещением.

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

mov eax ,

Эффективный адрес второго операнда формируется как esi+edx . Значение по этому адресу помещается в регистр eax.

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


Операндом является порт ввода-вывода .
Помимо адресного пространства оперативной памяти микропроцессор поддерживает адресное пространство ввода-вывода, которое используется для доступа к устройствам ввода-вывода. Объем адресного пространства ввода-вывода составляет 64 Кбайт. Для любого устройства компьютера в этом пространстве выделяются адреса. Конкретное значение адреса в пределах этого пространства называется портом ввода-вывода. Физически порту ввода-вывода соответствует аппаратный регистр (не путать с регистром микропроцессора), доступ к которому осуществляется с помощью специальных команд ассемблера in и out .

in al ,60h ; ввести байт из порта 60h

Регистры, адресуемые с помощью порта ввода-вывода, могут иметь разрядность 8, 16 или 32 бит, но для конкретного порта разрядность регистра фиксирована. В качестве источника информации или получателя применяются регистры-аккумуляторы eax , ax , al . Выбор регистра определяется разрядностью порта. Номер порта может задаваться непосредственным операндом в командах in и out или значением в регистре dx . Последний способ позволяет динамически определить номер порта в программе.

mov dx ,20h ; записать номер порта 20h в регистр dx
mov al ,21h ; записать значение 21h в регистр al
out dx ,al ; вывести значение 21h в порт 20h


Счетчик адреса – специфический вид операнда. Он обозначается знаком $. Специфика этого операнда в том, что когда транслятор ассемблера встречает в исходной программе этот символ, он подставляет вместо него текущее значение счетчика адреса (регистр EIP ). Значение счетчика адреса представляет собой смещение текущей машин­ной команды относительно начала сегмента кода, адресуемого сегментным регистром CS . При обработке транслятором очередной команды ассемблера счетчик адреса увеличивается на длину сформированной машинной команды. Обработка директив ассемблера не вле­чет за собой изменения счетчика. В качестве примера использования в команде значения счетчика адреса можно привести следующий фрагмент:

jmp $+3 ;безусловный переход на команду mov
nop ; длина команды nop составляет 1 байт
mov al ,1

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


Структурные операнды используются для доступа к конкретному элементу сложного типа данных, называемого структурой.

Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи. Для доступа к битовому полю записи используется директива RECORD .

Операторы в языке ассемблера

Операнды являются элементарными компонентами, из которых формируется часть машинной команды, обозначающая объекты, над которыми выполняется операция. В более общем случае операнды могут входить как составные части в более сложные образования, называемые выражениями . Выражения представляют собой комбинации операндов и операторов , рассматриваемые как единое целое. Результатом вычисления выражения может быть адрес некоторой ячейки памяти или некоторое константное (абсолютное) значение.
Выполнение операторов ассемблера при вычислении выражений осуществляется в соответствии с их приоритетами. Операции с одинаковыми приоритетами выполняются последовательно слева направо. Изменение порядка выполнения возможно путем расстановки круглых скобок, которые имеют наивысший приоритет.

Приоритет Оператор
1 length, size, width, mask, (), , < >
2 .
3 :
4 ptr, offset, seg, this
5 high, low
6 +, — (унарные)
7 *, /, mod, shl, shr
8 +, -, (бинарные)
9 eq, ne, lt, le, gt, ge
10 not
11 and
12 or, xor
13 short, type

Характеристика основных операторов.

Арифметические операторы . К ним относятся унарные операторы + и , бинарные + и , операторы умножения * , целочисленного деления / , получения остатка от деления mod . Например,

size equ 48 ;размер массива в байтах
el equ 4 ;размер элемента
;вычисляется количество элементов
mov ecx , size / el ;оператор /

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

msk equ 10111011 ; константа
mov al , msk shr 3 ; al=00010111 /

Операторы сравнения (возвращают значение истина или ложь ) предназначены для формирования логических выражений. Логическое значение истина соответствует логической единице, а ложь – логическому нулю. Логическая единица – значение бита равное 1, логический ноль – значение бита, равное 0.

size equ 30 ;размер таблицы

mov al , tab_size ge 50 ;al = 0
cmp al , 0 ;если size < 50, то
je m1 ;переход на m1

m1: …

Если значение size больше или равно 50, то результат в аl равен 1, в противном случае — 0. Команда cmp сравнивает значение аl с нулем и устанавливает соответствующие флаги в EFLAGS . Команда je на основе анализа этих флагов передает или не передает управление на метку m1 .

Назначение операторов сравнения приведено в таблице

Оператор Условие
eq ==
ne !=
lt <
le <=
gt >
ge >=

Логические операторы выполняют над выражениями побитовые операции. Выражения должны быть константными. Например,

L1 equ 10010011

mov al , L1
xor al , 01h ;al=10010010

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

mov eax , mas ;eax=*(mas+(esi))

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

Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением. Тип может принимать одно из следующих значений.

Тип Пояснение Назначение
byte 1 байт переменная
word 2 байта переменная
dword 4 байта переменная
qword 8 байт переменная
tword 10 байт переменная
near ближний указатель функция
far дальний указатель функция

Например,

str1 db «Привет» , 0

lea esi , str1

cmp byte ptr , 0 ; ==0?

В примере для сравнения значения по адресу esi с константой необходимо явно указать, данные какого типа будут сравниваться.

Оператор переопределения сегмента : (двоеточие) вычисляет физический адрес относительно конкретно задаваемой сегментной составляющей, в качестве которой могут выступать:

  • имя сегментного регистра,
  • имя сегмента из соответствующей директивы SEGMENT
  • имя группы.

Для выборки на выполнение очередной команды микропроцессор анализирует содержимое сегментного регистра CS , в котором содержится физический адрес начала сегмента кода. Для получения адреса конкретной команды микропроцессор складывает промасштабированное (умноженное на 16) значение сегментного регистра CS с содержимым регистра EIP . Запись CS:EIP содержит адрес текущей выполняемой команды. Аналогично обрабатываются операнды в машинных командах.

Оператор именования типа структуры . (точка) также заставляет транслятор производить определенные вычисления, если встречается в выражении.

Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения, в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.

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

Data
str1 db «Привет» ,0
.code
mov esi, offset str1
mov al , ; al = ‘П’

Оператор определения длины массива length возвращает число элементов, определенных операндом dup . Если операнд dup отсутствует, то оператор length возвращает значение 1.Например,

tabl dw 10 dup (?)

mov edx , length tabl ; edx=10

Оператор type возвращает число байтов, соответствующее определению указанной переменной:

fldb db ?
tabl dw 10 dup (?)

mov eax , type fldb ;eax = 1
mov eax , type tabl ;eax = 2

Оператор size возвращает произведение длины length и типа type и используется при ссылках на переменную с операндом dup .
Для предыдущего примера

mov edx , size tabl ;edx = 20 байт

Оператор short –модификация атрибута near в команде jmp, если переход не превышает границы +127 и -128 байт. Например,

jmp short метка

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

Оператор width возвращает размер в битах объекта типа RECORD или его поля.

  • NASM/YASM требует word , когда размер операнда не подразумевается другим операндом. (В противном случае в порядке).
  • Для MASM/TASM требуется word ptr , когда размер операнда не подразумевается другим операндом. (В противном случае в порядке).

Каждый из них задыхается от другого синтаксиса.

ПРЕДУПРЕЖДЕНИЕ: Это очень странная область без каких-либо стандартов ИСО или легкодоступных таблиц BNF; и я не специалист по прохождению через минные поля проприетарного синтаксиса MASM.

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

В общем случае выражение оператора PTR принудительно обрабатывается как указатель указанного типа:

.DATA num DWORD 0 .CODE mov ax, WORD PTR ; Load a word-size value from a DWORD

Я думаю, что существуют также специфические требования для ассемблера (nasm/tasm/other asm), а использование "байтового ptr" более переносимо.

Также проверьте раздел 4.2.16 в книга из Индии и разделы 8.12.3 (и 8.11.3 "Типы конфликтов") в "Программе программирования языка программирования".

ОБНОВЛЕНИЕ: спасибо Фрэнку Котлеру, похоже, что NASM "использует вариант синтаксиса сборки Intel" (wiki), который не включает операцию PTR.

UPDATE1: существует оригинальная "ASM86 LANGUAGE REFERENCE MANUAL" от Intel, 1981-1983, оператор PTR определен на стр. 4-15

Оператор PTR

Синтаксис: введите имя PTR

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

MOV WORD PTR , 5 ;set word pointed to by BX = 5 INC DS:BYTE PTR 10 ;increment byte at offset 10 ;from DS

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

MOV CL, BYTE PTR AWORD ;get first byte MOV CL, BYTE PTR AWORD + 1 ;get second byte

Значения полей:

type Это поле может иметь одно из следующих значений: BYTE, WORD, DWORD, QWORD, TBYTE, NEAR, FAR

name Это поле может быть: 1. Имя переменной. 2. Имя метки. 3. Адрес или регистр. 4. Целое число, которое представляет смещение.

UPDATE2: Благодаря Uni из Stuttgart битрейдер! Существует оригинальное руководство MACRO-86 от Microsoft (1981). Страница 3-7:

Оператор PTR может использоваться другим способом для сохранения байта при использовании прямых ссылок. Если вы определили FOO в качестве постоянной константы, вы можете ввести оператор:

MOV ,FOO

Вы можете обратиться к FOO как к байту немедленно. В этом случае вы можете ввести любой из операторов (они эквивалентны):

MOV BYTE PTR ,FOO MOV ,BYTE PTR FOO

Эти утверждения указывают MACRO-86, что FOO является байтом немедленно. Создается меньшая команда.

И страница 3-16:

Операторы переопределения

Эти операторы используются для переопределения сегмента, смещения, типа или расстояния между переменными и метками.

Указатель (PTR)

PTR

Оператор PTR переопределяет тип (BYTE, WORD, DWORD) или расстояние (NEAR, FAR) операнда.

- новый атрибут; новый тип или новое расстояние.

- это операнд, атрибут которого должен быть переопределен.

Самое важное и частое использование для PTR заключается в том, чтобы убедиться, что MACRO-86 понимает, какой атрибут должен иметь выражение. Это особенно верно для атрибута type. Всякий раз, когда вы размещаете ссылки в своей программе, PTR очищает расстояние или тип выражения. Таким образом, вы можете избежать фазовых ошибок.

Второе использование PTR заключается в доступе к данным по типу, отличному от типа в определении переменной. Чаще всего это происходит в структурах. Если структура определена как WORD, но вы хотите получить доступ к элементу в виде байта, PTR является оператором для этого. Однако гораздо более простой способ - ввести второй оператор, который также определяет структуру в байтах. Это устраняет необходимость использования PTR для каждой ссылки на структуру. См. Директиву LABEL в разделе 4.2.1 "Директивы памяти".

CALL WORD PTR MOV BYTE PTR ARRAY, (something) ADD BYTE PTR FOO,9

После прочтения этого и поиска некоторых определений синтаксиса из этих документов я считаю, что запись PTR является обязательной. Использование mov BYTE , 0 неверно в соответствии с руководством MACRO-86.

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

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

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

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

  1. Нам понадобится компилятор Си, который поддерживает современный стандарт. Можно воспользоваться онлайн компилятором на сайте ideone.com .
  2. Так же нам нужен декомпилятор, опять же, можно воспользоваться онлайн декомпилятором на сайте godbolt.org .
  3. Можно так же взять компилятор для ассемблера, который есть на ideone по ссылке выше.
Почему у нас все онлайн? Потому что это удобно для разрешения спорных ситуаций из-за различных версий и операционных систем. Компиляторов много, декомпиляторов так же хватает, не хотелось бы в дискуссии учитывать особенности каждого.

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

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

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

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

Int main(void) { 5 + 3; return 0; }
Ничем не будет отличаться от:

Int main(void) { return 0; }
Поэтому придется писать таким образом, чтобы при декомпиляции мы, все же, увидели превращение нашего кода во что-то осмысленное, поэтому примеры могут выглядеть, как минимум странно.

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

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

Gcc source.c -O0 -m32 -o source

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

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

Int main(void) { register int a = 1; //записываем в регистровую переменную 1 return a; //возвращаем значение из регистровой переменной }
Итак, следующий код соответствует этому:

Push ebp mov ebp, esp push ebx mov ebx, 1 mov eax, ebx pop ebx pop ebp ret
Первые две строчки соответствую прологу функции (точнее три, но третью хочу пояснить сейчас), и мы их разберем в статье о функциях. Сейчас просто не обращайте на них внимание, тоже самое касается последних 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, при условии, что эта строка выделена цветом.

Стек

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

Int main(void) { int a = 1; //записываем в переменную 1 int b = a + 5; //прибавим к "a" 5 и сораним в "b" return b; //возвращаем значение из переменной }
ASM:

Push ebp mov ebp, esp sub esp, 16 mov DWORD PTR , 1 mov eax, DWORD PTR add eax, 5 mov DWORD PTR , eax mov eax, DWORD PTR leave ret
Опять же, пропустим верхние 3 строчки и нижние 2. Теперь у нас переменная а локальная, следовательно память ей выделяется на стеке. Поэтому мы видим следующую магию: DWORD PTR , что же она означает? 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. Думаю, это не нуждается в подробных объяснениях.

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

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

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

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

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

ASM:
mov DWORD PTR , 2
Компилятор сам отбросил дробную часть за ненадобностью.

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

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

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

Int a = 1; int b = a * 2;
mov DWORD PTR , 1 mov eax, DWORD PTR add eax, eax mov DWORD PTR , eax
Интересно, не правда ли? Компилятор решил не пользоваться операцией умножения, а просто сложил два числа, что и есть - умножить на 2. (Я уже не буду подробно описывать эти строки, вы должны понять их, исходя из предыдущего материала)

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

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

Int a = 1; int b = a * 3;
ASM

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

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

Mov DWORD PTR , 1 mov eax, DWORD PTR sal eax, 2 mov DWORD PTR , eax mov eax, 0
Итак, у нас новая инструкция sal , что же она делает? Это двоичный сдвиг влево. Эквивалентно следующему коду в Си:

Int a = 1; int b = a << 2;
Для тех, кто не очень понимает, как работает этот оператор:

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

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

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

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

Int a = 2; int b = a / 2;
Если вы ожидаете вычитания, то увы - нет. Компилятор будет выдавать более изощренные методы. Операция «деление» еще медленнее умножения, поэтому компилятор будет также выкручиваться:

Mov DWORD PTR , 2 mov eax, DWORD PTR mov edx, eax shr edx, 31 add eax, edx sar eax mov DWORD PTR , eax
Следует сказать, что для этого кода я выбрал компилятор существенно более поздней версии (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:

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

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

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

Заключение

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

Арифметические операции - ADD, SUB, MUL, DIV. Многие опкоды делают вычисления. Вы можете узнать многие из них по их названиям: add (addition - добавление), sub (substraction - вычитание), mul (multiply - умножение), div (divide - деление).

Опкод add имеет следующий синтаксис:

Add приемник, источник

Выполняет вычисление: приемник = приемник + источник.

Имеются также другие формы:

приемник источник пример
регистр регистр add ecx, edx
регистр память add ecx, dword ptr / add ecx,
регистр значение add eax, 102
память значение add dword ptr , 80
память регистр add dword ptr , edx

Эта команда очень проста. Она добавляет значение источника к значение приемника и помещает результат в приемник. Другие математические команды:

Sub приемник, источник (приемник = приемник - источник)
mul множимое, множитель (множимое = множимое * множитель)
div делитель (eax = eax / делитель, edx = остаток)

Поскольку регистры могут содержать только целочисленные значения (то есть числа, не, с плавающей запятой), результат деления разбит на частное и остаток. Теперь, в зависимости от размера источника, частное сохраняется в eax, а остаток в edx:
* = Например: если dx = 2030h, а ax = 0040h, dx: ax = 20300040h. Dx:ax - значение dword, где dx представляет старшее word, а ax - младшее. Edx:eax - значение quadword (64 бита), где старшее dword в edx и младшее в eax.

Источник операции деления может быть:

  1. 8-бит регистр (al, ah, cl,...)
  2. 16-бит регистр (ax, dx, ...)
  3. 32-бит регистр (eax, edx, ecx...)
  4. 8-бит значение из памяти (byte ptr )
  5. 16-бит значение из памяти (word ptr )
  6. 6a 32-бит значение памяти (dword ptr )

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

Логические операции с битами - OR, XOR, AND, NOT. Эти команды работают с приемником и источником, исключение команда "NOT". Каждый бит в приемнике сравнивается с тем же самым битом в источнике, и в зависимости от команды, 0 или 1 помещается в бит приемника:

AND (логическое И) устанавливает бит результата в 1, если оба бита, бит источника и бит приемника установлены в 1.
OR (логическое ИЛИ) устанавливает бит результата в 1, если один из битов, бит источника или бит приемника установлен в 1.
XOR (НЕ ИЛИ) устанавливает бит результата в 1, если бит источника отличается от бита приемника.
NOT инвертирует бит источника.

Пример:

Mov ax, 3406d
mov dx, 13EAh
xor ax, dx

Ax = 3406 (десятичное), в двоичном - 0000110101001110.

Dx = 13EA (шестнадцатиричное), в двоичном - 0001001111101010.

Выполнение операции XOR на этими битами:

Источник = 0001001111101010 (dx)

Приемник = 0000110101001110 (ax)

Результат = 0001111010100101 (новое значение в ax)

Новое значение в ax, после выполнения команды - 0001111010100101 (7845 - в десятичном, 1EA5 - в шестнадцатиричном).

Другой пример:

Mov ecx, FFFF0000h
not ecx

FFFF0000 в двоичном это -
Если вы выполните инверсию каждого бита, то получите:
, в шестнадцатиричном это 0000FFFF
Значит после операции NOT, ecx будет содержать 0000FFFFh.

Увеличение/Уменьшение - INC/DEC. Есть 2 очень простые команды, DEC и INC. Эти команды увеличивают или уменьшают содержимое памяти или регистра на единицу. Просто поместите:

Inc регистр; регистр = регистр + 1
dec регистр; регистр = регистр - 1
inc dword ptr ; значение в будет увеличено на 1.
dec dword ptr ; значение в будет уменьшено на 1.

Ещё одна команда сравнения - test. Команда Test выполняет операцию AND (логическое И) с двумя операндами и в зависимости от результата устанавливает или сбрасывает соответствующие флаги. Результат не сохраняется. Test используется для проверки бит, например в регистре:

Test eax, 100b
jnz смещение

Команда jnz выполнит переход, если в регистре eax третий бит справа - установлен. Очень часто комманду test используют для проверки, равен ли регистр нулю:

Test ecx, ecx
jz смещение

Команда jz выполнит переход, если ecx = 0.

Ничего не делающая команда - nop. Эта команда не делает абсолютно ничего (пустая команда). Она только занимает пространство и время. Используется для резервирования места в сегменте кода или организации программной задержки.

Обмен значениями - XCHG. Команда XCHG также весьма проста. Назначение: обмен двух значений между регистрами или между регистрами и памятью:

Mov eax , 237h
mov ecx, 978h
xchg eax, ecx
в результате:
eax = 978h
ecx = 237h

Вот и конец урока. Надеюсь, этот не был скучным. Следующий урок расскажет вам про подпрограммы.

Питер, 2003. - 629 c.
Скачать (прямая ссылка): assembler2003.djvu Предыдущая 1 .. 40 > .. >> Следующая

О Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением (рис. 5.10). Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far. Что означают эти значения, вы узнаете далее на этом уроке. Например:

d wrd dd 0 * * *

пюу al.byte ptr d_wrd+l ;пересылка второго байта из двойного

;словаПоясним этот фрагмент программы. Переменная djwrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всему значению переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой mov al. d_wrd+l, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

ЧТип!-(ptr)-Выражение -

Рис. 5.10. Синтаксис оператора переопределения типа

О Оператор переопределения сегмента: (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: «имя сегментного регистра», «имя сегмента» из соответствующей директивы SEGMENT или «имя группы» (рис. 5.11).

Этот момент очень важен, поэтому поясним его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов - кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра es и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое es на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра і р. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд - это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать, - по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.

А что же с сегментом стека? Посмотрите урок 2 там, где мы описывали назначение регистров общего назначения. В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд - выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда, используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин «по умолчанию»? Вспомните «рефлексы», о которых мы говорили на уроке 1. Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поЛя префикса машинной команды (см. формат команд в уроке* 2), Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс. Префикс представляет собой однобайтовую величину, численное значе-ниє которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. По ходу обсуждения материала книги мы познакомимся с большинством возможных префиксов. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs: і р. А вот сегменты стека и данных - можно. Для этого и предназначен оператор «:». Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента. Например:

jmp metl ;обход обязателен, иначе поле ind будет трактоваться;как очередная команда ind db 5 ;описание поля данных в сегменте команд

mov al,cs:ind !переопределение сегмента позволяет работать

;с данными, определенными внутри сегмента кода

Продолжим перечисление операторов.

О Оператор именования типа структуры. (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении. Подробно мы обсудим этот оператор, когда введем понятие сложных типов данных на уроке 12.

О Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения (рис. 5.12), в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.