assembler_na_primeraxbazovyy_kurs_rudolf_marek
.pdfГлава 5. Управляющие конструкции
ние адресу, заданному оригинальным значением ЕАХ, что могло бы вызвать сбой нашей программы, а то и всей операционной системы. То же самое произойдет, если мы забудем написать команду RET: процессор продолжит выполнение с того места, где заканчивается наша подпрограмма, и рано или поздно совершит недопустимое действие.
Мы можем упростить нашу подпрограмму add_it, полностью отказавшись от инструкций POP и PUSH:
a d d _ i t :
mov |
ecx,eax |
;копируем значение |
ЕАХ |
(первый параметр) в ЕСХ |
|
add |
ecx,ebx |
;добавляем |
к нему |
ЕВХ |
(второй параметр), |
|
|
;результат |
будет сохранен в регистре ЕСХ |
||
r e t |
|
; возвращаемся |
|
|
|
Команды INT и IRET: вызываем прерывание
Вернемся к теме прерываний. Прерыванием называется такое событие, когда процессор приостанавливает нормальное выполнение программы и начинает выполнять другую программу, предназначенную для обработки прерывания. Закончив обработку прерывания, он возвращается к выполнению приоста новленной программы.
Во второй главе было сказано, что все прерывания делятся на две группы: программные и аппаратные. Программные прерывания порождаются по ко манде INT. Программные прерывания можно рассматривать как «прерывания по требованию», например, когда вы вызываете подпрограмму операционной системы для вывода строки символов. В случае с программным прерыванием вы сами определяете, какое прерывание будет вызвано в тот или иной момент. Команде INT нужно передать всего один 8-битный операнд, который задает номер нужного прерывания.
INT op
Аппаратные прерывания вызываются аппаратными средствами компьютера, подключенными к общей шине (ISA или PCI). Устройство, запрашивающее прерывание, генерирует так называемый запрос на прерывание (IRQ, interrupt requests). Всего существует 16 аппаратных запросов на прерывание, поскольку только 16 проводников в шине ISA выделено для этой цели. Запрос на пре рывание направляется контроллеру прерываний, который, в свою очередь, за прашивает микропроцессор. Вместе с запросом он передает процессору номер прерывания. После запуска компьютера и загрузки операционной системы DOS, IRQ 0 (системный таймер) соответствует прерыванию 8 (часы).
Когда процессор получает номер прерывания, он помещает в стек контекст выполняемой в данный момент программы, подобно тому, как человек кладет в книгу закладку, чтобы не забыть, с какого места продолжить чтение книги. Роль закладки играют значения CS, (Е)1Р и регистр флагов.
73
Ассемблер на примерах. Базовый курс
Теперь процессор будет выполнять другую программу — обработчик пре рывания. Адрес этой программы называется вектором прерывания. Векторы прерывания хранятся в таблице векторов прерываний, находящейся в памяти. Таблицу прерываний можно представить себе как массив адресов подпро грамм, в котором индекс массива соответствует номеру прерывания.
После того, как процессор определит адрес обработчика прерывания, он за пишет его в пару CS и (Е)1Р. Следующая выполненная команда будет первой командой обработчика прерывания.
В десятой главе, посвященной программированию в DOS, мы опишем функции 21-го (0x21) прерывания, которое генерируется следующей инструкцией:
i n t 0x21 |
; вызов DOS |
Возврат из обработчика прерывания осуществляется с помощью команды IRET, которая восстанавливает исходные значения (E)IP, CS и флагов из стека. Формат команды:
IRET
Давайте разберемся, как вызывается прерывание на примере 21-го преры вания (рис. 5.9). Мы считаем, что процессор работает в реальном режиме с 16-битной адресацией.
Рис. 5.9. Загрузка в CS и IP новых значений
Прежде всего процессор находит номер прерывания. Программные преры вания вызываются инструкцией INT, которая содержит номер прерывания в своем операнде. Следующий шаг — сохранение контекста программы в стеке. Продемонстрируем этот процесс с помощью команд ассемблера:
pushf |
/сохраняем значения регистра признаков в стеке |
|
|
push cs |
;сохраняем регистр CS в стеке |
74
|
Глава 5. Управляющие конструкции |
push ip |
;эта команда недопустима. Мы написали ее с |
|
демонстрационной целью. Реализовать ее можно так: |
|
call here |
|
here: |
Последний шаг — это безусловный переход на адрес, прочитанный из таблицы прерываний по номеру запрошенного прерывания: JMP far.
Здесь мы слегка забегаем вперед: пока просто примите, что таблица векто ров прерываний находится в самом начале адресуемой памяти, то есть по адресу 0x0000:0x0000. Каждый из векторов прерывания занимает четыре байта. Первые два байта определяют новое значение IP (то есть смещение), а оставшиеся два — новое значение сегментного регистра CS. Вектор пре рывания номер 0x21 хранится по адресу 0х0000:(0х21*4), поэтому его вызов можно записать так:
jmp far [0x21*4] |
; эта инструкция переходит на указанный |
|
|
;адрес, предполагаем, что DS=0, поэтому |
|
|
;мы можем адресовать память с адреса |
|
|
;0х0000: |
0x0000) |
Команду INT можно реализовать самостоятельно с помощью команд PUSHF и CALL far:
pushf |
;сохраняем флаги в стеке |
call far [0x21*4] |
; теперь сохраняем CS и IP |
|
;и выполняем "jump" |
Программные прерывания часто используются как шлюз для вызова функций операционной системы. Подробнее о них мы поговорим в главах, посвященных отдельным операционным системам.
75
ева 6 Прочие команды
Изменение регистра признаков напрямую
Команда XCHG — меняем местами операнды
Команда LEA — не только вычисление адреса
Команды для работы со строками
Команды ввода/вывода (I/O)
Сдвиг и ротация
Псевдокоманды
Советы по использованию команд
Ассемблер на примерах. Базовый курс
В этой главе мы рассмотрим наиболее часто используемые команды системы команд процессора х86. С помощью этих команд вы сможете написать до статочно сложные программы для решения самых разнообразных задач.
6.1. Изменение регистра признаков напрямую
Несколько команд позволяют непосредственно модифицировать флаги реги стра признаков. Мы рассмотрим четыре команды, которые изменяют флаги IF и ID, то есть флаг прерывания и флаг направления.
Команды СИ и STI
Команды CLI (Clear Interrupt) и STI (Set Interrupt) сбрасывают и устанав ливают флаг прерывания IF. Иными словами, при их помощи вы можете за претить или разрешить аппаратные прерывания. Если этот флаг установлен (1), аппаратные прерывания разрешены. Команда CLI сбрасывает флаг (0) — запрещает аппаратные прерывания. Потом вы должны снова разрешить их, выполнив команду STI:
c l i |
;отключить прерывания — использовать только в DOS! |
. . . |
;теперь мы никем не будем потревожены и можем |
|
/выполнять какие-то действия, например, изменять |
|
;таблицу прерываний |
s t i |
;включаем прерывания снова |
Команды STD и CLD
Команды STD и CLD модифицируют значение флага DF. Этим флагом поль зуется группа команд для обработки строк, поэтому подробнее о нем мы по говорим в следующей главе. Команда CLD сбрасывает этот флаг (что означает отсчет вверх), a STD устанавливает его (отсчет в обратном порядке).
77
Ассемблер на примерах. Базовый курс
Формат команд простой:
STD
CLD
6.2. Команда XCHG — меняем местами операнды
Очень часто нам нужно поменять местами значения двух регистров. Конечно, можно это сделать, используя третий, временный, регистр, но намного проще использовать инструкцию XCHG (exchange — обмен), которая как раз для этого и предназначена.
XCHG o l , o 2
Подобно инструкции MOV, она имеет два операнда — ol и о2. Каждый из них может быть 8-, 16- и 32-разрядным, и только один из них может находиться в памяти — требования такие же, как у команды MOV.
xchg еах,еах |
/меняем местами значения ЕАХ и ЕАХ. |
|
|
|
;Так реализован NOP — пустой оператор |
xchg ebx,ecx |
;меняем местами значения ЕВХ и ЕСХ |
|
xchg |
al,ah |
;меняем местами значения AL и АН |
xchg |
dl,ah |
;меняем местами значения DL и АН |
xchg byte [variable],cl ;меняем местами один байт памяти и CL
6.3. Команда LEA —
не только вычисление адреса
Имя этой команды — это сокращение от «Load Effective Address», то есть «загрузить эффективный адрес». Она вычисляет эффективный адрес второго операнда и сохраняет его в первом операнде (который может быть только регистром). Синтаксис этой команды требует, чтобы второй операнд был за ключен в квадратные скобки, но фактически она не адресует память.
LEA o l , [o2]
LEA полезна в тех случаях, когда мы не собираемся обращаться к памяти, а адрес нужен нам для другой цели:
lea edi, [ebx*4+ecx] ;загружает в регистр |
EDI адрес, |
;вычисленный как EDI |
= ЕВХ*4+ЕСХ |
Значение, вычисленное командой LEA, не обязательно рассматривать как адрес: ее можно использовать для целочисленных арифметических вычисле ний. Несколько примеров «экзотического» использования LEA будет при ведено в главе, посвященной оптимизации.
78
Глава 6. Прочие команды
6.4. Команды для работы со строками
Строкой в языке ассемблера называется последовательность символов (бай тов), заканчивающаяся нулевым байтом (точно так же, как в языке С).
0x43 0x6F 0x6D 0x70 0x75 0x74 0x65 0x72 0x00
C o m p u t e r / 0
Рис. 6.1. Строка заканчивается нулевым байтом
Система команд х86-совместимых процессоров содержит несколько команд, облегчающих манипуляции со строками. Каждую из них можно заменить не сколькими элементарными командами, но, как и в случае с командой LOOP, эти команды сделают вашу программу короче и понятнее.
В зависимости от размера операнда команды для обработки строк делятся на три группы. Первая группа предназначена для работы с 8-битными операн дами, то есть с отдельными символами. Имена этих команд заканчиваются на «В» (byte). Имена команд второй группы, предназначенных для работы с 16-битными операндами, заканчиваются символом «W» (word). Третья группа команд работает с 32-битными операндами, и имена их заканчиваются на «D» (double word).
Все команды, обсуждаемые в этом параграфе, работают с фиксированными операндами и не имеют аргументов. Порядок отсчета байтов в строке во время работы этих команд зависит от флага направления (DF).
Команды STOSx — запись строки в память
Под единым обозначением STOSx (STOre String) скрываются три команды:
•STOSB
•STOSW
• STOSD
Команда STOSB копирует содержимое регистра AL в ячейку памяти, адрес которой находится в паре регистров ES:(E)DI, и уменьшает или увеличивает (в зависимости от флага DF) на единицу значение регистра (E)DI, чтобы при готовиться к копированию AL в следующую ячейку. Если DF=0, то (E)DI будет увеличен на 1, в противном случае — уменьшен на 1. Какой регистр будет использоваться — DI или EDI — зависит от режима процессора.
Вторая инструкция, STOSW, работает аналогично, но данные берутся из регистра АХ, a (E)DI уменьшается/увеличивается на 2. STOSD копирует со держимое ЕАХ, a E(DI) уменьшает/увеличивает на 4.
79
Ассемблер на примерах. Базовый курс
e ld |
;сбрасываем DF, направление будет вверх |
stosw |
;сохраняем АХ в ES:[DI] или ES:[EDI] (зависит от |
|
;режима процессора) и увеличиваем (E)DI на 2 |
Команды LODSx — чтение строки из памяти
Под единым обозначением LODSx (LOaD String) скрываются три команды:
• LODSB •LODSW •LODSD
Действие этих команд противоположно командам предыдущей группы: они копируют порцию данных из памяти в регистр AL, АХ и ЕАХ. Адрес нужной ячейки памяти берется из пары DS:(E)SI. Если флаг DF равен нулю, то ре гистр SI будет увеличен на 1/2/4 (В, W, D), в противном случае — уменьшен на 1/2/4.
Команды CMPSx — сравнение строк
Под единым обозначением CMPSx (CoMPare String) скрываются три команды:
•CMPSB
•CMPSW
•CMPSD
Команда CMPSB сравнивает байт, находящийся по адресу ES:(E)DI, с байтом по адресу DS:(E)SI и изменяет значения регистров SI и DI в зависимости от флага DF. Команды CMPSB и CMPSD сравнивают не байты, а слова и двойные слова соответственно, увеличивая или уменьшая регистры SI и DI на размер порции данных (2 или 4).
Команды SCASx — поиск символа в строке
Под единым обозначением SCASx (SCAn String) скрываются три команды:
•SCASB •SCASW
•SCASD
Команды SCASB/W/D сравнивают значения регистра AL/AX/EAX со значе нием в памяти по адресу [ES:(E)DI]. Регистр (E)DI изменяется в зависимости от флага DF.
Команды REP и REPZ — повторение следующей команды
Команда REP (Repeat) облегчает обработку строк произвольной длины. Это так называемая префиксная команда: ее нельзя использовать отдельно,
80
Глава 6. Прочие команды
а только в паре с какой-нибудь другой командой. Она работает подобно команде LOOP: повторяет следующую за ней команду до тех пор, пока зна чение в регистре (Е)СХ не станет равно нулю. Регистр (Е)СХ уменьшается на единицу при каждой итерации. Чаще всего команда REP применяется в паре с MOVS или STOS:
r e p movsb
Или: r e p
s t o s b
с к о п и р о в а т ь первые (Е)СХ б а й т о в из |
D S : ( E ) S I в |
|
E S : ( E ) D I . Это ассемблерный а н а л о г |
|
|
С-функции memcpy() |
|
|
с к о п и р о в а т ь (Е)СХ |
р а з з н а ч е н и е AL в |
E S : ( E ) D I . |
:Это ассемблерный |
а н а л о г С-функции m e m s e t ( ) |
|
Команда REPZ (синоним REPE), подобно команде LOOPZ, позволяет уточнить условие. Следующая итерация выполняется тогда, когда не только (Е)СХ не равен нулю, но и флаг ZF не установлен. Второе условие может быть инвертировано командой REPNZ (синоним REPNE).
Эти команды часто используются вместе с SCAS или CMPS:
r e p z s c a s b Или: r e p z cmpsb
; п о в т о р я е м SCASB
; п о в т о р я е м CMPSB
Теперь вы уже знаете достаточно, чтобы реализовать на языке ассемблера функцию подсчета символов в строке, которая в языке С называется strlenQ.
Рис. 6.2. Блок-схема функции strlen()
Ассемблер на примерах. Базовый курс
Функции нужен только один аргумент — адрес начала строки, который нужно сохранить в ES:(E)DI. Результат функции (количество символов строки + нулевой символ) будет записан в ЕСХ. Указатель ES:(E)DI будет указывать на байт, следующий за последним (нулевым) символом строки. Код функции приведен в листинге 6.1.
Листинг 6.1. Код функции strlen()
strlen: push eax xor ecx,ecx
xor eax,eax dec ecx
eld
repne scasb neg ecx
pop eax ret
помещаем регистр EAX в стек
сбрасываем ЕСХ (ЕСХ=0), то же самое можно сделать так: mov ecx,О
EAX = 0
ЕСХ = ЕСХ - 1. Получаем OxFFFFFFFF - максимальная Длина строки
DF = 0, будем двигаться вперед, а не назад ищем нулевой байт отрицание ЕСХ (дополнительный код)
даст количество выполненных итераций восстанавливаем исходный ЕАХ выходим из подпрограммы
Рис. 6.3. Состояние регистров ES:(E)DI перед вызовом strlen и после ее выполнения
Вы можете легко адаптировать программу для работы только с 16-битными регистрами: просто удалите «Е» в имени каждого регистра.
Мы уже написали функцию для подсчета символов в строке, но до сих пор не знаем, как разместить строку в памяти. Пока будем считать, что в ES:(E)DI уже загружен правильный адрес начала строки. Мы вызываем нашу подпро грамму с помощью CALL, а после ее выполнения в регистре ЕСХ получим количество символов в строке.
call strlen |
;вызываем strlen |
82
