- •Аппаратно-ориентированное программирование
- •Ббк 32.973.73
- •Удк 681.3 ббк 32.973.73ф 73
- •1. Основы программирования на ассемблере
- •1.1. Принципы построения ассемблерных программ
- •1.2. Понятие архитектуры компьютера
- •1.3. Регистры программиста в ia32
- •1.4. Описание сегментной структуры программы
- •2. Простейшие средства ассемблера
- •2.1. Средства описания данных
- •2.2. Обращения к функциям ос посредством прерываний
- •2.3. Средства преобразования в исполняемый файл
- •2.4. Управление строками при выводе и ввод данных
- •2.5. Простейшие способы адресации
- •3. Архитектурные элементы для построения программ
- •3.1. Организация условных переходов
- •3.2. Средства организации циклов
- •3.3. Особенности команд умножения и деления
- •3.4. Организация процедур
- •3.5. Неарифметические операции над кодами
- •4. Использование неэлементарных способов адресации
- •4.1. Косвенно-регистровая адресация
- •4.2. Использование индексной адресации данных
- •4.3. Базовая и индексно базовая адресации
- •4.4. Адресация с масштабированием
- •5. Взаимосвязи программных единиц
- •5.1. Многомодульная разработка программ
- •5.2. Использование библиотек объектных модулей
- •5.3. Организация стекового кадра подпрограммы
- •5.4. Программный доступ к системным функциям Win32
- •5.5. Особенности использования объектных файлов формата coff
- •5.6. Стандартный доступ к системным функциям Unix
- •6. Вспомогательные средства базовой архитектуры
- •6.1. Использование строковых команд пересылки
- •6.2. Применение строковых команд сравнения
- •7. Использование ассемблерных отладчиков
- •7.1. Особенности отладчика gdb для программ в Linux
- •7.2. Отладчики текстового режима для Windows
- •Библиографический список
- •Оглавление
3. Архитектурные элементы для построения программ
3.1. Организация условных переходов
Команды условных переходов предназначены для реализации действий, которые в языках высокого уровня записываются условными операторами. Напомним, что в алголоподобных языках условные операторы имеют вид
IF условие THEN оператор
(в зависимости от языка ключевое слово THEN может отсутствовать, а условие быть заключено в скобки, но эти синтаксические особенности не имеют для нашего рассмотрения какого-либо значения). Существенным же является то, что в общем случае составная часть условного оператора условие может иметь достаточно сложную структуру и требовать для своего логического вычисления немало машинных команд. Поэтому вложить конструкцию условного оператора в какую-либо машинную команду оказывается принципиально невозможным, а приходится разбивать эту конструкцию на большее число команд.
Разработчики машинной архитектуры еще на самой заре вычислительной техники стали применять следующее принципиальное техническое решение. Содержательный компонент условие вычисляется даже в простейших случаях отдельной командой, а собственно выбор выполняется другой командой по зафиксированному результату вычисления условия. При этом осуществляется переход или не переход к командам оператора, находящийся внутри условной конструкции. Иначе говоря, условный оператор языков высокого уровня даже в простейшем случае реализуется не одной, а по крайней мере двумя командами.
Специализированной командой для вычисления условия является команда
CMP операнд1, операнд2
название которой восходит к слову compare и которая сравнивает значения операндов операнд1 и операнд2. Результат такого сравнения автоматически фиксируется в специальном регистре, обозначаемом в рассматриваемой архитектуре как EFLAGS (регистр флагов). Это 32-битный регистр, но основное значение имеет его младшая 16-битная половина, обозначаемая просто FLAGS. В этом регистре отдельными битами отображается ситуация равенства нулю результата, его отрицательности, появления переполнения при выполнении некоторых операций (биты ZF, NF, CF) и т.п. Для разветвления действия программы по значениям отдельных установленных бит в этом регистре имеются специальные команды, но они используются достаточно редко, чтобы начинать с их изучения.
Общей мнемоникой команды разветвления является
Jcc метка
где сс обозначает мнемонический код условия (condition code). Для кодов условия имеет множество допустимых вариантов, основные из которых приведены в следующей таблице.
Таблица 3.1.
Команды условных переходов
Мнемокод |
Условие перехода |
Математическое отношение |
Примечание |
JE |
равно |
= |
знаковое |
JNE |
не равно |
|
знаковое |
JL |
меньше |
< |
знаковое |
JG |
больше |
> |
знаковое |
JLE |
меньше или равно |
<= |
знаковое |
JGE |
больше или равно |
>= |
знаковое |
JA |
выше (above) |
> |
беззнаковое |
JB |
ниже (below) |
< |
беззнаковое |
JC |
по флагу CF переноса |
|
|
JNC |
если CF не установлен |
|
|
Команды условного перехода для знакового сравнения соответствуют использованию предварительно сформированного условия на основе значений модификатора типа signed, а беззнакового - на основе модификатора unsigned. Сами мнемоники построены, исходя из начальных букв английских слов equal (равно), less (меньше), greater (больше), not (нет). Кроме перечисленных в таблице мнемоник имеются еще JNL, JNG, JNA, JNB, JAE, JBE, отвечающие, соответственно, условиям не меньше, не больше, не выше, не ниже, выше или равно, ниже или равно. Причем мнемоники в парах JGE и JNL, JLE и JNG, JAE и JNB, JBE и JNA обозначают одинаковые по действиям команды.
Действия всех перечисленных команд заключается в том, что в момент их выполнения проверяется содержимое регистра флагов EFLAGS и, если совокупность этих флагов отвечает мнемонике команды, то происходит переход выполнения на указанную в команде метку. В противном случае выполняется следующая по порядку следования в памяти команда.
Для знакомых с программированием на языке Бейсик действия команд условного перехода можно представлять как реализацию оператора Бейсика:
IF условие THEN GOTO метка
где условие предварительно вычисляется предыдущими командами и временно запоминается в служебном регистре EFLAGS.
Заметим, что при программировании на ассемблере в подавляющем большинстве случаев нет никакой необходимости задумываться о текущих и устанавливаемых значениях флажков условия, но достаточно мыслить в терминах условий, записываемых мнемониками в командах условных переходов. Практически при написании очередной команды Jcc нужно просто записывать в мнемонике, по какому условию требуется переходить по метке в этой команде.
Сделаем пояснения по использованию беззнаковых команд условного перехода. Они предназначены, в частности, для сравнения адресов на предмет, который из них находится в памяти раньше или позже. Дело в том, что если старший бит второго из адресов будет иметь единичное значение, а первого адреса - нулевое значение, то знаковые команды сравнения рассматривают такую ситуацию, как сравнение двух чисел, из которых первое положительно, а второе отрицательно. В результате такого непродуманного сравнения получится, что более дальний адрес будет истолкован как меньший (отрицательный адрес!). Аналогичная ситуация возникает, когда сравниваются порядковые номера символов, рассматриваемых как однобайтовые величины. При этом оказывается, что символы, находящиеся в старшей половине таблицы кодов с формальной арифметической стороны, учитывающей знак, считаются, предшествующими.
Совершенно не обязательно для формирования условия с целью условного перехода использовать команду сравнения. Арифметические и логические команды, в качестве побочного эффекта, переустанавливают флажки в регистре EFLAGS. В частности, это делают команды сложения и вычитания. Поэтому часто достаточно использовать команды условного перехода непосредственно после тех арифметических или логических, которые формируют значение, на основе которого должно приниматься решение о разветвлении.
Например, для вычисления модуля разности двух переменных a и b (описанных в сегменте данных как двойные слова, т.е. переменные типа long или int для 32-битной архитектуры) с результатом в регистре eax можно использовать следующую последовательность команд
MOV eax, [a]
SUB eax, [b]
JGE meta
NEG eax
meta:
Здесь мы встретились с двумя новыми командами: командой вычитания SUB и командой NEG изменения знака числа на противоположный. Первая из них чрезвычайно похожа на команду ADD, но выполняет не сложение, а вычитание. Мнемоника второй восходит к английскому слову negative, а сама команда имеет только один операнд, который перед выполнением содержит исходное значение для выполнения операции, а после выполнения команды этот операнд содержит уже значение с противоположным знаком (реализуется оператор вида N = -N из языка Си). В последней команде в качестве операнда поэтому оказывается возможным использовать только регистровую адресацию или задавать с помощью адресации место в памяти, где собственно и находится преобразуемое значение.
Команды условного перехода в архитектуре Intel имеют одну «хитрую» особенность, унаследованную от первых очень примитивных микропроцессоров. Она состоит в том, что простейшая форма записи команд условного перехода порождает машинные команды, неспособные осуществлять переход далее чем на 127 байтов вперед и далее чем на 128 байтов назад! (Связано это с очень экономным кодированием относительного места перехода, размещаемого всего в одном байте.) Как следствие этого, попытки указать условный переход далее указанного диапазона вызывают ошибку со следующим текстом сообщения о ней: "short jump is out of range". В таких случаях программисту необходимо явно указать использование другого варианта построения команды условного перехода. Это указание заключается в добавлении модификатора NEAR перед меткой, задаваемой в качестве операнда. В общем случае это приводит к записи вида
Jcc NEAR метка
Построенные таким образом команды содержат в своем коде на три байта - для 32-битной архитектуры - или на один байт - для 16-битной архитектуры - больше чем простейшая форма, называемая короткой. В настоящее время подобное удлинение команд и, как следствие, всей программы, в подавляющем большинстве применений оказывается совершенно несущественным.
Заметим, что с помощью команд условного перехода можно организовывать разветвления не по двум направлениям, как в обычных операторах if языков Паскаль и Си, но и в большем числе направлений. Например, фрагмент
. . . формирование условия
JL metk1
JG metka
JE metk2
обеспечивает переходы на одну из меток mek1, metk2 и metka. Правда, такие ситуации достаточно редки, так как на команду, помещенную непосредственно за приведенными командами, ни при каком из использованных условий не происходит переход, если только за ними не поставить одну из перечисленных меток. Последний вариант избыточен, так как проще в данном случае опустить соответствующую команду условного перехода и команда, следующая за командами фрагмента, будет выполняться автоматически при невыполнении других условных переходов в данном наборе. Иной вариант построения этого фрагмента есть
. . . формирование условия
JL metk1
JG metka
; сюда попадаем при выполнении условия равенства
; и метку, например metk2, ставить совершенно излишне
Практически это тоже разветвление на три направления, но неявное в записи, так как третье направление равносильно продолжению последовательности без явных переходов.
Кроме команд условного перехода в архитектуре имеется (совершенно необходимая) команда безусловного перехода, изображаемая мнемокодом JMP. Она при записи на ассемблере имеет вид
JMP метка
После выполнения этой команды следующей выполняется не та, которая расположена за данной командой JMP, а та команда, которая в программе обозначена меткой метка. Причем этот переход выполняется всегда - без использования каких-либо условий. Первейшее применение этой команды направлено на реализацию условных операторов языков высокого уровня, когда условный оператор имеет общую форму вида
IF условие THEN оператор1 ELSE оператор2.
Разворачивание такого условного оператора в последовательность команд имеет общую структуру вида
. . . формирование условия
Jncc метка_оп2
. . . команды оператора1
JMP метка_за
метка_оп2:
. . . команды оператора2
метка_за:
где условное обозначение ncc мнемоники условного перехода обозначает условие, противоположное для исходного условия условие в операторе высокого уровня. Например, если исходное условие есть равно, то используется переход по мнемонике JNE, если исходное условие есть "не больше", то используется мнемоника JG и т.п. Основная цель такого приема в том, чтобы при выполнение исходного условия команда Jncc метка_оп2 не осуществляла переход на метку, с которой начинается выполнение оператора оператор2, а выполнялись следующие по записи команды, которые реализуют оператор оператор1. В конце же последовательности команд, которые реализуют оператор оператор1, должен выполняться "скачок", чтобы не попасть на выполнение команд оператора оператор2, а продолжить выполнение программы далее за пределами рассматриваемой условной конструкции.
Далее в листинге 3.1.1 приведена более содержательная программа для Linux, которая демонстрирует простое использование различных переходов для решение практических задач. Эта программа осуществляет ввод строки текста и сравнение в нем пятого и шестого символа. По результатам сравнения выдается одно из двух сообщений - равны символы или нет. Причем при вводе строки меньшей, чем из шести символов, выдается сообщение о невозможности выполнения задачи.
GLOBAL _start
SEGMENT .text
_start:
;--- read(1, buf, 80) == <3>(ebx, ecx, edx)
mov eax,3 ; N function=read
mov ebx,0 ; 0 handle=0 (stdin)
mov ecx, buf ; address of buf
mov edx,80 ; number of byte
int 80h
cmp eax, 7
jl wrno
mov al, [buf+4]
cmp al, [buf+5]
jne wrne
mov ecx, mese
mov edx, lenmese
;--- write(1, mese, lenmese) == <4>(ebx, ecx, edx)
write: mov eax,4 ; N function=write
mov ebx,1 ; N handle=1 (stdout)
int 80h
mov eax,1
int 80h ; function=exit Linux
wrne: mov ecx, mesne
mov edx, lenmesne
jmp write
wrno: mov ecx, mesno
mov edx, lenmesno
jmp write
SEGMENT .data
buf times 80 db 0 ; или resb 80; но тогда будет предупреждение
mesno db 'Длина текста меньше 6'
lenmesno equ $-mesno
mese db 'Пятый и шестой символы совпадают'
lenmese equ $-mese
mesne db 'Пятый символ отличается от шестого';
lenmesne equ $-mesne
Листинг 3.1.1. Ввод текста и сравнение в нем 5-го и 6-го символов
Напомним, что число символов, введенных системной функцией write, возвращается в регистре eax, (причем выдается число символов, включая завершающий символ перевода строки), поэтому после завершения ввода текста содержимое этого регистра сравнивается с числом 7 посредством команды
cmp eax, 7
за которой следует команда условного перехода JL wrno. В месте программы, отмеченной меткой wrno, выполняется вывод сообщения о недостаточной длине введенного текста. Для чего подготавливается содержимое регистров ecx и edx, в которые помещается адрес текста сообщения и длина этого сообщения, соответственно. После этой подготовки командой JMP write задается принудительный переход на метку write. Начиная со строки, отмеченной меткой write, продолжается подготовка к вызову системной функции write. С этой целью необходимые значения заносятся в регистры eax и ebx (значение 4 - номер системной функции и значение 1 - хэндла стандартного вывода). Далее следует собственно обращение к системной функции (командой INT 80H), за которой записаны команды обращения к системной функции exit завершения выполнения программы.
Если длина введенного текста достаточна для решения поставленной задачи, то рассмотренного выше перехода командой JL wrno не происходит, а выполняются помещенные за ней команды
mov al, [buf+4]
cmp al, [buf+5]
которые переносят в регистр al содержимое пятого символа текста (в математической системе отсчета индексов от единичного), а затем сравнивая содержимое этого регистра с шестым символом введенного текста.
При несовпадении сравниваемых значений следующая команда jne wrne выполняет переход на метку wrne, а при совпадении перехода не происходит и выполняются последующие команды:
mov ecx, mese
mov edx, lenmese
которые заносят в регистры ecx и edx информацию об адресе и длине сообщения, которое должно быть выдано в этой ситуации.
За этими командами непосредственно следует команда с меткой write, которая начинает уже рассмотренную нами последовательность команд, обеспечивающая вывод подготовленного сообщения. Если же выше произошел переход на метку wrne, то, начиная с нее, подготавливается выдача сообщения о неравенстве рассматриваемых символов и опять же осуществляется принудительный переход на метку write для выдачи сообщения.
В этой программе продемонстрирована также замечательная возможность, предоставляемая служебным метасимволом $. Метасимвол $ обозначает относительный адрес начала строки, в которой он встречается. Поэтому в последовательности
mes db 'Текст'
lenmes equ $-mes
директива EQU вычисляет значение выражения, которое задает разность между относительным адресом текущей строки (а это относительный адрес того места, где закончился текст из предыдущей строки) и относительным адресом, который соответствует в памяти имени mes, которое обозначает отметку начала текста. Тем самым результат дает, сколько байтов занимает данный текст.
Здесь неявно используется тот факт, что обычное применение имени (как отметки в памяти, где размещается именованный объект) дает относительный адрес размещения именованного объекта. (Поэтому применение имени для обозначения содержимого именованного объекта в NASM использует дополнительные символы квадратных скобок.)
Следует запомнить, что правильный результат для длины текста, формируемой рассмотренным образом (в символической константе lenmes нашего примера), получается только, если указанная директива EQU размещается непосредственно за директивой определения текста и в ее выражении безошибочно используется имя именно этого текста. (Последняя часть замечания тривиальна, так как ошибочная запись и должна приводить к ошибочным результатам, но как раз такая ошибка оказывается очень характерной для практической работы, когда обе директивы определения строятся, исходя из строк, скопированных из другого места программы.)
Заметим, что как демонстрирует рассмотренная программа, команды условных переходов позволяют использовать не только фрагменты программы, расположенные далее, но и фрагменты, расположенные ранее команды условных переходов. Используя эти команды вместе с командами безусловного перехода, теоретически можно строить программы, продолжение которых размещается не далее по тексту, а значительно раньше последнего фрагмента или даже начала программы. Хотя такой непоследовательности обычно стараются избежать, отсюда непосредственно видно, какими мощными средствами, в сравнении с языками высокого уровня, обладает действительная архитектура процессора и, следовательно, насколько более сложные программы можно писать с помощью ассемблеров.