Ассемблер под Windows для чайников. Часть 9

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

Обычно ассемблер начинают изучать именно с теории. Но без представления возможностей практического применения тех или иных команд такое обучение, скорее всего, покажется скучным и малопонятным. Добавьте к этому сложность предмета программирования самого по себе, и получите вполне ожидаемый результат — потерю желания изучать ассемблер вообще. Надеюсь, что вы уже имеете определенное положительное мнение об ассемблере и твердо решили идти до конца. Если же вы все еще не верите в свои силы, то попытаюсь вас успокоить: основных команд в ассемблере немногим более сотни, учитывая то, что многие из них — синонимы (близкие или одинаковые по значению) или антонимы (прямо противоположные). Команд, которые необходимо запомнить для каждодневного использования — вообще не более двух-трех десятков. Причем запоминать их все и сразу вам не обязательно. Просто сохраните эту статью, и всегда сможете быстро вспомнить название и назначение той или иной команды. Не забывайте, что данный цикл статей опирается на синтаксис, используемый в компиляторе FASM.

Прежде всего, придется помучить вас маленьким, но очень важным моментом — регистром флагов. По общим правилам изучения ассемблера с этим регистром принято знакомиться на первом или втором занятии, сразу же после знакомства с регистрами общего назначения и сегментными регистрами. Однако, дабы не сильно пугать вас сразу, я отложил знакомство с ним аж до сегодняшнего занятия, а сегментные регистры пока что вообще не трогал. Пишут же люди программы на языках высокого уровня, даже не подозревая о таких "мелочах", так зачем вам, все еще местами сомневающимся в собственных силах касательно освоения ассемблера, изначально забивать голову пугающей теорией? И все же азы надо знать, так что соберитесь: регистр флагов! Регистр флагов в 32-битных процессорах имеет размер 32 бита и называется EFLAGS. Как вы могли заметить, приставка "E" в названии 32-битного регистра обычно свидетельствует о том, что он имеет 16-битное происхождение (64-битные регистры имеют приставку R). Значит, из соображений обратной совместимости со старыми 16-битными программами должен существовать и 16-битный предок — FLAGS. На его примере мы и начнем знакомство с данным регистром:


Регистр FLAGS
БитФлагОписание
0CFФлаг переноса (Carry flag)
11Зарезервирован
2PFФлаг четности (Parity flag)
30Зарезервирован
4AFВспомогательный флаг переноса (Auxiliary flag)
50Зарезервирован
6ZFФлаг нуля (Zero flag)
7SFФлаг знака (Sign flag)
8TFФлаг трассировки (Trace flag)
9IFФлаг разрешения прерываний (Interrupt enable flag)
10DFФлаг направления (Direction flag)
11OFФлаг переполнения (Overflow flag)
12,13IOPLУровень приоритета ввода-вывода (I/O privilege level)
14NTФлаг вложенной задачи (Nested Task)
150Зарезервирован


На данном этапе вам понадобится запомнить лишь 3-4 флага, поэтому не пугайтесь этой таблицы: ее цель — лишь получение вами общего представления о регистре флагов 16-битного процессора, который, тем не менее, сохранился и в 32-, и в 64-битных потомках.

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

ZF устанавливается (1), если результат операции равен нулю, иначе ZF сбрасывается (0). Главное — впоследствии не запутаться в том, что ZF=0, если результат отличен от ноля, но ZF=1 при нулевом результате.

SF отражает состояние старшего (знакового) бита результата. SF=1, если старший бит результата равен единице, и ZF=0 — если старший бит результата нулевой.

OF фиксирует факт потери старшего (знакового) бита результата при арифметических операциях. OF=1, если произошел перенос в старший бит результата или заем из старшего бита, ZF=0 при отсутствии переноса или заема.

PF устанавливается (1), если младшие 8 битов результата содержат четное число единиц.

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

Команды обмена данными: MOV, XCHG.

MOV приемник,источник
Копирует байт(byte)/слово(word)/двойное слово(dword) из операнда источник в операнд приемник. Позволяет копировать данные из одного регистра общего назначения (РОН) в другой, из РОН в память, из памяти в РОН. Командой MOV нельзя напрямую переслать данные из одной области памяти в другую — для этого придется использовать в качестве посредника один из РОН. Копирование данных в таком случае осуществляется за две команды: сначала из исходной области памяти в РОН, а затем — из РОН в целевую область памяти. При помощи команды MOV также можно помещать в регистр или память непосредственное значение, копировать содержимое сегментного регистра в РОН или память, содержимое РОН или памяти в сегментный регистр, содержимое регистра управления или отладки в РОН, содержимое РОН в регистр управления или регистр отладки. Команда MOV может быть обработана компилятором только если размеры источника и приемника совпадают. Ниже следуют примеры возможных вариантов использования команды MOV:

mov bx,ax ;копировать содержимое РОН в РОН
mov [char],al ;РОН в память
mov bl,[char] ;память в РОН
mov dl,32 ;значение в РОН
mov [char],32 ;значение в память
mov ax,ds ;сегментный регистр в РОН
mov [bx],ds ;сегментный регистр в память
mov ds,ax ;РОН в сегментный регистр
mov ds,[bx] ;память в сегментный регистр
mov eax,cr0 ;регистр управления в РОН
mov cr3,ebx ;РОН в регистр управления

XCHG операнд1,операнд2
Используется для двунаправленной пересылки данных между операндами. Размер обоих операндов может быть байтом, словом или двойным словом, но оба операнда должны быть одинакового размера. Команда XCHG помещает содержимое первого операнда во второй, а второго — в первый. Один из операндов всегда должен быть РОН, а другой может быть областью памяти или также РОН.

xchg ax,bx ;обменять содержимое РОН и РОН
xchg al,[char] ;обменять содержимое РОН и памяти

Обмен данными через стек: PUSH, POP.

Стек — область памяти, специально выделяемая каждой программе для временного хранения промежуточных данных. Обычно адреса в памяти растут от ноля к максимальному адресу. В стеке все наоборот: он растет от дна (максимальный адрес сегмента стека) к нолю. Для того, чтобы поместить данные в стек, применяется команда PUSH, ее синтаксис следующий:

PUSH источник
Эта команда уменьшает указатель на текущий кадр стека (регистр ESP) и копирует содержимое операнда-источника по адресу вершины стека, содержащемуся в ESP. В роли операнда может выступать РОН, память, сегментный регистр, значение размером в слово или двойное слово. Если в качестве операнда указано непосредственное значение, то в 16-битном режиме оно по умолчанию воспринимается компилятором как слово, а в 32-битном — как двойное слово. Мнемоники PUSHW и PUSHD указывают компилятору, что значение необходимо сохранить как слово или как двойное слово соответственно независимо от режима, в котором работает компилятор. Если за командой PUSH следуют несколько операндов, разделенных пробелами, то они будут обработаны компилятором как последовательность из нескольких команд PUSH с этими операндами по отдельности. PUSHA сохраняет в стеке содержимое всех восьми регистров общего назначения. Примеры использования команды PUSH:

push ax ;сохранить РОН
push es ;сохранить сегментный регистр
pushw [bx] ;сохранить память
push 1000h ;сохранить значение
push ebx,esi,edi ;сохранить по очереди три регистра
pusha ;сохранить все 8 РОН

POP приемник
Команда POP копирует слово или двойное слово, содержащееся на вершине стека, в указанный операнд-приемник, затем увеличивает ESP так, чтобы он указывал на новую вершину стека. Операндом может быть РОН, память, сегментный регистр. Эта команда предназначена для извлечения из стека данных, сохраненных командой PUSH. Следует помнить, что извлекаются данные в обратном порядке, так что, если вы сохранили в стеке EAX, EBX и потом ECX, то извлекать надо сперва ECX, затем EBX и EAX. Команда POP является полной противоположностью команды PUSH, поэтому мнемоники POPW, POPD и POPA работают по аналогии с описанными выше мнемониками PUSH, но выполняют обратные действия.

Примеры:
pop bx ;восстановить РОН
pop ds ;восстановить сегментный регистр
popw [si] ;восстановить память
pop edi,esi,ebx ;восстановить по очереди три регистра
popa ;восстановить все 8 РОН

Арифметические команды.

INC операнд

Команда увеличивает значение операнда на единицу. Операнд может быть РОН или памятью. Размер операнда — байт, слово или двойное слово. Примеры:
inc ax ;увеличить значение в регистре
inc byte[bx] ;увеличить значение в памяти

ADD приемник,источник
Складывает оба операнда и помещает результат в приемник. Если результат превысил размер приемника, устанавливает флаг переноса (CF). Размером операндов может быть байт, слово или двойное слово. Приемник может быть РОН или памятью. Источник может быть РОН или значением. Источник может также быть памятью при условии, что приемник является регистром. Примеры:
add ax,bx ;прибавить к регистру регистр
add ax,[si] ;прибавить к регистру память
add [di],al ;прибавить к памяти регистр
add al,48 ;прибавить к регистру значение
add [char],48 ;прибавить к памяти значение

ADC приемник,источник
Складывает оба операнда и добавляет единицу в случае, если флаг переноса установлен. Помещает результат в приемник. Правила для операндов те же, что и у ADD. Команда ADD в связке с ADC может использоваться для сложения чисел, не помещающихся целиком в регистр процессора.

DEC операнд
Команда уменьшает значение операнда на единицу. Правила для операнда те же, что и у INC.

SUB приемник,источник
Вычитает источник из приемника, помещает результат в приемник. Если источник был больше приемника, то устанавливается флаг переноса CF. Если источник был равен приемнику (результат 0), то устанавливается флаг ноля ZF. Правила для операндов те же, что и у ADD.

SBB приемник,источник
Вычитает источник из приемника, вычитает единицу в случае, если флаг переноса установлен. Помещает результат в приемник. Правила для операндов те же, что и у ADD. Команда SUB в связке с SBB может использоваться для вычитания чисел, не помещающихся целиком в регистр процессора.

CMP приемник,источник
Команда осуществляет сравнение приемника с источником способом вычитания источника из приемника, но, в отличие от SUB, результат никуда не сохраняет. По установленным командой флагам можно отследить результат такого сравнения и выполнить условный переход (JZ, JO, JC и т.п.).

NEG приемник
Изменяет знак операнда на противоположный, вычитая операнд из нуля. На практике команда применяется не только для смены знака, но и для вычитания из константы. Допустим, нам надо вычесть содержимое AX из 300. Очень хочется написать: sub 300,ax, но команда SUB не допускает возможности вычитания из непосредственного значения, потому что приемник должен являться РОН или памятью. Значит, мы могли бы предварительно поместить значение 300 в какой-то РОН, а затем вычесть из него AX, однако более простым вариантом с точки зрения процессора будет такой:
neg ax
add ax,300

Мы прибавили 300 к отрицательному значению ax, что по законам математики дает такой же результат, что и вычитание ax из 300.

XADD приемник,источник
Еще одна редкая, но иногда очень полезная команда. Она похожа на ADD, только перед тем, как поместить сумму операндов в приемник, производит обмен значениями между операндами (как команда XCHG). Эта команда одним махом выполняет сразу 2 действия, а значит, может помочь сэкономить процессорное время.
Все вышеперечисленные арифметические операции изменяют флаги SF, ZF, PF, OF в соответствии с результатом. Команды MUL(умножение) и DIV(деление) были достаточно подробно описаны в предыдущей статье, поэтому не будем повторяться, и идем дальше.

Преобразование типов

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

Команды преобразования со знаковым расширением без операндов:
CBW преобразовывает байт, содержащийся в регистре AL, в слово, помещаемое в регистр AX.
CWD преобразовывает слово, содержащееся в регистре AX, в двойное слово, помещаемое в регистры DX:AX. Старшая часть значения разместится в DX, а младшая — в AX.
CWDE преобразовывает слово, содержащееся в регистре AX, в двойное слово, помещаемое в регистр EAX.
CDQ преобразовывает двойное слово, содержащееся в EAX, в учетверенное слово, помещаемое в регистры EDX:EAX.
Еще раз напомню, что все перечисленные преобразования по сути своей — лишь распространение значения старшего (знакового) бита исходного операнда на все биты добавляемой части. Эти команды работают с конкретными регистрами и поэтому не имеют операндов.

MOVSX приемник,источник
Преобразовывает с учетом знакового расширения байт в слово или двойное слово; слово — в двойное слово. Операнд-источник может быть памятью или РОН, приемник всегда должен быть РОН.

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

Десятичная арифметика
Десятичные числа могут быть представлены в так называемом двоично-десятичном коде (Binary Coded Decimal — BCD). Этот способ предполагает хранение каждой десятичной цифры в четырех битах. Различают два формата хранения BCD-чисел:

— упакованный, когда каждый байт (8 бит) может содержать две десятичные цифры (по 4 бита каждая). В таком случае каждый байт содержит десятичное число в диапазоне от 00 до 99.

— неупакованный, когда каждый байт содержит лишь одну десятичную цифру в младших четырех битах, а старшие четыре бита имеют нулевое значение. Диапазон представления неупакованного BCD-числа в отдельно взятом байте составляет от 0 до 9.

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

DAA корректирует результат сложения двух упакованных BCD-чисел в регистре AL. Эта команда должна следовать за командой сложения (ADD или ADC), если в сложении участвовали два упакованных BCD, и результат находится в AL. Если откорректированный результат превысит 99, то будет установлен флаг CF, а в AL останутся лишь две младшие цифры. Пример:
mov al,49h
mov bl,52h
add al,bl
daa

В данном примере наглядно показано, что упакованные BCD можно записывать просто как шестнадцатеричные числа (буковка h в конце числа означает HEX), только без использования символов ABCDEF. Когда будет произведено сложение 49h и 52h, результат будет неверным. Точнее, он будет верным, но лишь применительно к шестнадцатеричным числам, потому что процессор, складывая эти числа, считает их не упакованными десятичными, а стандартными двоичными или шестнадцатеричными, если хотите. Однако команда DAA все расставляет по своим местам, и в результате в AL получается 01 (две младшие цифры от 101), а установленный флаг CF позволяет определить, что единичка для следующего третьего разряда "в уме".

DAS работает аналогично DAA, только используется для корректировки результата вычитания. У команды также отсутствует операнд, потому что действие производится над регистром AL. Флаг CF (если установлен) указывает на то, что вычитаемое оказалось больше уменьшаемого, и необходимо это дело обработать, например, уменьшением третьего разряда на единицу.

AAA предназначена для корректировки результата сложения НЕупакованных BCD-чисел размером в байт, то есть одноразрядных. Операнды отсутствуют, действие производится над регистром AL. Если результат превышает 9, то в AL помещается лишь младший разряд, а AH увеличивается на единицу, и устанавливается флаг CF.

AAS аналогична AAA за исключением того, что применяется для корректировки вычитания неупакованных одноразрядных BCD-чисел. При необходимости заема устанавливается флаг CF, а содержимое AH уменьшается на единицу.

AAM корректирует результат умножения двух неупакованных BCD, находящийся в AL. Данная команда просто делит содержимое AL на 10 и помещает частное в AH, а остаток — в AL. Таким образом в AX помещается двухразрядное неупакованное BCD. Стандартная версия команды не имеет операндов, однако существует расширенная версия AAM, в которой в роли операнда выступает непосредственное значение — база, на которую будет производиться деление. Так что, выполнив, к примеру, команду AAM 5, вы поделите нацело содержимое AL, получив частное в AH, а остаток — в AL. Очень удобный способ для деления нацело небольших (до 8 бит) чисел.

AAD подготавливает к делению неупакованное BCD-число, находящееся в AX. Команда просто добавляет к AL содержимое AH, умноженное на 10, потом AH обнуляется. Несмотря на свое основное назначение, команда отлично справляется не только с подготовкой к делению, но и с простым преобразованием неупакованного двузначного BCD в двоичный эквивалент. Также команда может оказать неоценимую помощь в преобразовании символьного кода цифр сразу в двоичный код. Например, в таблице ASCII цифра "1" имеет код 31h, цифра "2" — 32h и т.д. Значит, для того, чтобы из символьного кода получить неупакованное BCD, нам достаточно обнулить старший шестнадцатеричный разряд (старшие 4 бита) ASCII-кода. Поместим число 15 в символьном виде в AX и переведем его в двоичное значение:
mov AX,3135h
and AX,0F0Fh ;обнуляем левые половинки байтов
aad

После выполнения этих команд в AX у нас будет 0Fh, то есть число 15 в нормальном двоичном (шестнадцатеричном) виде, привычном для процессора. В расширенном варианте команды AAD есть возможность указать в качестве операнда непосредственное значение, на которое будет умножаться AH, прежде чем прибавиться к AL. Это дает еще один вариант использования команды: быстрое умножение небольших чисел.

Что ж, на сегодня, думаю, с вас достаточно. Очень сложная тема, хотя, надеюсь, и весьма интересная. Примеров программ в газете на этот раз не будет, но вы всегда можете найти их на форуме: сайт

BarMentaLisk, SASecurity gr., q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 30 за 2008 год в рубрике программирование

©1997-2024 Компьютерная газета