Файл: 2005 Рудольф Марек. Ассемблер на примерах. Базовый курс. .pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 26.10.2023

Просмотров: 147

Скачиваний: 12

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
60
Глава 5. Управляющие конструкции Подобно командами, для работы с числами со знаком служит другой набор команд условного перехода. Причина этого в том, что проверяемое условие состоит из значений других флагов. Адрес назначения команды условного перехода должен лежать в пределах
128 байтов все они реализуют переходы короткого типа. Если вам нужно прогуляться за пределы 128 байтов, то вы должны в инструкции условного перехода указать адрес, по которому будет находиться команда jmp, которая и выполнит дальнейший переход jz far_jump ; если ZF = 1, перейти к far_jump
; несколько команд far_jump: jmp f a r f i n i s h ; "дальний" переход Теперь рассмотрим, как реализовать конструкцию IF-THEN на языке ассемблера. В нашем простом случае мы перейдем к метке i f_three, если регистр АХ содержит значение 3. Прежде всего мы должны проверить, есть ли в регистре АХ тройка. Для этого используем команду СМР: страх сравниваем АХ с 3 Для проверки равенства применим команду JZ, как показано в таблице команд условного перехода jz i s _ t h r e e переходит к " i s _ t h r e e " , если АХ = 3 Обратите внимание, что для проверки на равенство используются одинаковые команды (JZ — равно и JNZ — неравно) для чисел со знаком и для беззнаковых чисел. Если АХ = 3, то команда jz выполнит переход к метке i s _ t h r e e , в противном случае будет продолжено выполнение программы со следующей за jz команды. Следующий пример показывает беззнаковое сравнение CL и AL. Если оба значения равны, тов регистр BL помещается значение 1, если AL больше, чем CL, то BL=2, а если AL меньше CL, то BL=3. cmp a l , c l сравниваем AL и CL jz w r i t e _ l переходим к w r i t e _ l , если AL = CL cmp a l , c l сравниваем AL и CL ja w r i t e _ 2 переходим к w r i t e _ 2 , если AL > CL mov Ы , 3 последний случай — сразу загружаем 3 в BL end_if: просто метка, символизирующая конец IF w r i t e _ l : метка w r i t e _ l mov b l , l ;BL = 1 jmp end_if переходим к end_if write_2: метка write_2 mov bl,2 ;BL = 2 jmp end_if переходим к end_if
61
Ассемблер на примерах. Базовый курс Рис. 5.3. Структурная схема нашей программы В нашей программе мы использовали безусловный переход (jmp end_if), чтобы вернуть управление на исходную позицию. Это не лучшее решение нам придется выполнить еще один безусловный переход перед меткой w r i t e _ l , а то наша программа зациклится. Адрес назначения понятен — следующая после последнего jmp end_if команда. Вот так выглядит улучшенный фрагмент кода сразу устанавливаем BL = 1 сравниваем AL и CL переходим вконец программы,
;BL = 2 сравниваем AL и CL переходим вконец программы,
;BL = 3 конец программы mov Ы , 1 cmp a l , c l j e end_if mov Ы , 2 cmp a l , c l j a end_if mov Ы , 3 end_i г -. если AL
CL если AL > CL Новый пример короче, нонет предела совершенству, и мы можем его еще улучшить. Инструкция MOV не изменяет регистр флагов, поэтому в дальнейшем сравнении нет надобности mov Ы , 1 cmp a l , c l je end__if mov b l , 2 j a end_if mov Ы , 3 end i f :
BL = 1 сравниваем AL и CL переходим вконец программы,
BL = 2 переходим вконец программы,
BL = 3 конец программы если AL
CL если AL > CL
62

Глава 5. Управляющие конструкции Если подытожить, то мы только что записали на Ассемблере следующую конструкцию языка С if (al == cl) Ы = 1 e l s e if (al > cl) Ы = 2 e l s e Ы = 3;
5.3. Итерационные конструкции — циклы Последней управляющей конструкцией, которую мы рассмотрим, будет итерация, или цикл Циклом называется многократное повторение последовательности команд до наступления указанного условия. Рис 5.4. Цикл в программе В языках программирования высокого уровня известно много разновидностей циклов, в том числе
• цикл со счетчиком (цикл FOR), повторяющийся заранее заданное количество раз
• цикл с условием (цикл WHILE), повторяющийся до тех пор, пока условие истинно
• цикл с инверсным условием (цикл UNTIL), повторяющийся до тех пор, пока условие не станет истинным. Цикл со счетчиком с помощью конструкций IF и GOTO Давайте попробуем написать цикл с пустым телом (то есть внутри цикла не будут выполняться никакие команды. Первое, с чем нужно разобраться — это где разместить переменную управления циклом, то есть счетчик. Счетчик нужен для того, чтобы цикл выполнялся не бесконечно, а определенное количество раз. Команда сравнения позволяет хранить счетчик либо в памяти, либо в каком-то регистре общего назначения.
63
Ассемблер на примерах. Базовый курс Рассмотрим символическую структуру пустого цикла FOR на псевдоязыке:
FOR_START: ; начало инициализация счетчика
FOR_LOOP: метка цикла
. . . тело цикла (пустое) увеличиваем счетчик проверяем счетчик переходим на начало цикла или выходим из цикла
FOR_FINISH: конец цикла В нашем примере тело цикла должно повторяться 10 раз. Сначала мы инициализируем счетчик. Затем выполняем тело цикла (в нашем случае пустое, после этого увеличиваем счетчик на 1. Проверяем если счетчик меньше 10, то начинаем опять выполнять тело цикла, если же счетчик равен 10, то мы выходим из цикла.
1=1+1
IF I < 10 THEN
GOTO FOR_LOOP А теперь запишем нашу схему на языке ассемблера. Мы уже знаем, как реализовать на языке ассемблера конструкции IF и GOTO, из которых можно построить цикл FOR. В качестве счетчика (псевдопеременной I) мы будем использовать регистр ЕСХ: for_start: mov ecx,0 for_loop: инициализируем счетчик ЕСХ метка для перехода назад
= 0 64

Глава 5. Управляющие конструкции
. . . тело цикла i n c есх увеличиваем ЕСХ на 1 стр е с х , 1 0 сравниваем ЕСХ с 10 j n z f o r _ l o o p если неравно, переход на f o r _ l o o p f o r _ f i n i s h : если ЕСХ = 10, выходим Рассмотрим другую версию цикла FOR. Она работает также, как предыдущая, но счетчик мы будем хранить не в регистре, а в памяти, в переменной I. f o r _ s t a r t : mov dword [i],0 переменная типа dword 1 = 0 for_loop: метка для перехода назад
... тело цикла i n c dword [ i ] увеличиваем i на 1 cmp dword [ i ] , 1 0 сравниваем i с 10 j n z f o r _ l o o p если неравно, переход на f o r _ l o o p f o r _ f i n i s h : если равно, выходим Вторая версия будет работать медленнее, поскольку счетчик хранится в памяти, время доступа к которой существенно больше, чем время доступа к регистрам. В заключение давайте рассмотрим еще одну версию цикла, использующую команду DEC и команду проверки флага ZF вместо команды сравнения СМР. Принцип работы следующий устанавливаем счетчик (ЕСХ=10), выполняем тело цикла, уменьшаем счетчик на 1. Если ZF установлен, значит, в ЕСХ находится 0 и нам нужно прекратить выполнение цикла f o r _ s t a r t : mov е с х , 1 0 ;ЕСХ = 1 0 f o r _ l o o p : метка для перехода назад
. . . ; тело цикла. dec есх уменьшаем ЕСХ на 1 j n z f o r _ l o o p если не 0, переходим на f o r _ l o o p f o r _ f i n i s h : если 0, выходим из цикла Мы только что записали на языке ассемблера следующую конструкцию языка С for ( i = 0 ; i < 10;i++) {}
LOOP — сложная команда, простая запись цикла В главе, посвященной процессору 80386, мы упомянули, что х86-совмести- мые чипы используют архитектуру CISC (Компьютер со сложным набором команд, то есть имеют полную систему команд. Другими словами, в составе системы команд имеются сложные команды, которые могут заменить ряд простых. Причем здесь циклы Если у вас процессор, то вам ненужно ж. 293 65
Ассемблер на примерах. Базовый курс реализовать цикл самостоятельно — для организации цикла можно использовать команду LOOP:
LOOP метка Подобно команде MUL, команда LOOP работает с двумя операндами. Первый операнд фиксирован, и мы не можем его указать явно. Это значение регистра
ЕСХ (или СХ). Второй — это адрес целевой метки цикла. Инструкция LOOP уменьшает значение регистра ЕСХ (СХ) на единицу и, если результат неравен Ото она переходит на указанную метку. Метка должна быть в пределах 128 байтов (короткий тип перехода. Перепишем наш простой д
цикл FOR с использованием команды LOOP:
f o r _ s t a r t : mov ex, 10 ;CX = 10 — 10 итераций for_loop: метка для возврата назад
... тело цикла loop for_loop уменьшаем СХ, если не 0, переходим к for_loop for_finish: выход из цикла Как видите, код стал еще более компактным. Цикл со счетчиком и дополнительным условием. Команды LOOPZ и LOOPNZ Команда LOOPZ позволяет организовать цикл с проверкой дополнительного условия Например, мы можем уточнить условие из предыдущего примера цикл нужно выполнить, как и раньше, не более 10 разно только при условии, что регистр ВХ содержит значение 3. Как только значение в регистре ВХ изменится, цикл нужно прервать.
LOOPZ метка
LOOPNZ метка Команда LOOPZ уточняет условие перехода следующим образом переход на указанную метку произойдет, если СХ не содержит нуля ив тоже время флаг ZF равен единице. Другое имя этой команды — LOOPE. Следующий фрагмент кода показывает пример цикла с дополнительным условием for_start: mov cx,10 fОГ_1оор:
СХ = 10 метка для возврата назад тело цикла FOR где-то здесь изменяется регистр ВХ
66

Глава 5. Управляющие конструкции стр Ьх,3 loopz f o r _ l o o p f o r f i n i s h :
;ВХ равен 3?
;СХ=СХ-1; если СХоО, И если ВХ=3, переход к for_locp если СХ = 0 или если ВХ о 3, выходим Команда LOOPNZ работает аналогично, но дополнительное условие противоположно переход будет выполнен только если СХ (ЕСХ) неравен ив тоже время ZF равен 0. Другое имя этой команды — LOOPNE.
5.4. Команды обработки стека При программировании очень часто возникает потребность временно сохранять содержимое регистров процессора или какого-то адреса памяти, чтобы через некоторое время восстановить исходные значения. Язык ассемблера удовлетворяет эту потребность набором команд для работы со специальной областью памяти, которая называется стеком. Что такое стеки как он работает Давайте разберемся, как работает стек. Все мы знаем, что такое очередь. Приходит первый клиент, пока его обслуживают, подходят еще два клиента. Когда первый клиент обслужен, начнут обслуживать второго клиента, затем третьего — итак далее. Принцип заключается в том, что первым будет обслужен тот, кто пришел первым. Такой тип очереди называется FIFO (First
In — First Out) — первым пришел, первым вышел.
67
Ассемблер на примерах. Базовый курс Существует и другой тип очереди — LIFO (Last In — First Out) — последним пришел, первым вышел. Понимаю, что сточки зрения обычного человека это какая-то неправильная очередь, нов жизни мы сталкиваемся с ней не реже, чем с первой. Например, мы собираемся куда-то поехать и укладываем вещи. Когда мы откроем сумку, вверху окажутся вещи, которые мы положили последними. Рис 5.7. Очередь LIFO Стек работает по принципу LIFO. Данные, помещенные в стек последними, будут первыми вытолкнуты из стека. В совместимых компьютерах нет аппаратного стека, поэтому данные стека хранятся в памяти. Вершина стека представлена парой SS:SP (SS:ESP) — сегмент стека (Stack Segment) и указатель вершины стека (Stack Pointer). Стек растет в памяти вниз, то есть новая порция данных записывается по меньшему адресу. Впрочем, точный адрес данных внутри стека не имеет для нас значения, потому что любая операция над стеком имеет дело сего вершиной, на которую всегда указывает регистр SP (ESP). Стек может содержать
16- или 32-битные данные. Микропроцессор имеет две команды для работы со стеком — PUSH и POP. Команды PUSH и POP: втолкнуть и вытолкнуть Команда PUSH позволяет поместить в стек содержимое любого 16- или
32-битного регистра или ячейки памяти Формат команды следующий
PUSH ol
68

Глава 5. Управляющие конструкции Пример использования push eax поместить ЕАХ в стек Мы можем сами реализовать команду PUSH с помощью следующей пары команд sub e s p , 4 уменьшаем ESP на 4 (ЕАХ - 4-байтный регистр) mov [ s s : e s p ] , e a x сохраняем ЕАХ в стеке В общем виде (с использованием оператора sizeof, позаимствованного из языков высокого уровня) команда push ol может быть записана на псевдо­
языке так
( E ) S P = ( E ) S P - s i z e o f ( o l ) ol -> S S : [ ( E ) S P ] Другая команда POP, записывает в свой операнд значение вершины стека последнее сохраненное в стеке значение Тип операнда должен быть таким же, как у инструкции PUSH (другими словами, если вы поместили в стек разрядный регистр, извлечение из стека должно происходить тоже в 32- разрядный регистр. Команду POP можно реализовать с помощью команд MOV и ADD: mov e a x , [ s s : e s p ] помещаем в ЕАХ вершину стека add e s p , 4 удаляем" последнее значение типа dword в стеке Рассмотрим несколько примеров push eax сохранить значение регистра ЕАХ в стеке push e s i сохранить значение регистра ESI в стеке pop eax извлечь данные из стека в ЕАХ pop e s i извлечь данные из стека в ESI В результате выполнения этих команд мы поменяем местами значение регистров ЕАХ и ESI: сначала помещаем в стек значение ЕАХ, затем — ESI, после этого извлекаем из стека последнее сохраненное значение (бывшее значение регистра ESI) в регистр ЕАХ, после этого в стеке останется бывшее значение
ЕАХ, которое мы записываем в ESI. Для обеспечения обратной совместимости с процессорами предыдущих поколений 16-битные регистры тоже можно поместить в стеках АХ = 0x1234 mov bx,0x567 8 ;ВХ = 0x5 67 8 push ax сохранить значение регистра АХ в стеке push bx сохранить значение регистра ВХ в стеке
. . . изменяем значения регистров pop bx извлекаем вершину стека в ВХ
69
Ассемблер на примерах. Базовый курс Рис. 5.8. Изменение содержимого стека До выполнения первой команды PUSH вершина стека содержала значение
0x0000. На вершину стека указывает пара SS:SP. Допустим, что SP содержит адрес OxFFFE. После выполнения PUSH AX указатель стека был уменьшен на 2 и принял значение OxFFFC, и поэтому адресу (в новую вершину стека) было записано значение 0x1234. Вторая команда, PUSH BX, также уменьшила значение SP на 2 (OxFFFA) и записала в новую вершину стека значение
0x5678. Команда POP BX удалила значение 0x5678 из стека и сохранила его в регистре ВХ, а указатель стека увеличила на 2. Он стал равен OxFFFC, ив вершине стека оказалось значение 0x1234. Помните, что 8-битные регистры сохранять в стеке нельзя. Нельзя и поместить в стек регистр IP (EIP) непосредственно, при помощи команд PUSH/POP: это делается по-другому, и чуть позже вы узнаете, как именно. Команды PUSHA/POPA и PUSHAD/POPAD: толкаем все регистры общего назначения Иногда полезно сохранить в стеке значения сразу всех регистров общего назначения. Для этого используется команда PUSHA, а для извлечения из стека значений всех регистров служит команда РОРА. Команды PUSHA и РОРА помещают в стеки извлекают из него все разрядные регистры. Операндов у этих команд нет. Поскольку команды PUSHA и РОРА разрабатывались для предшественника процессора 80386, они не могут сохранять значений 32-битных регистров (они просто не подозревают об их существовании. Для сохранения и восстановления значений расширенных регистров служат команды PUSHAD и POPAD.
70