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

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

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

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

Добавлен: 26.10.2023

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

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

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Рудольф Марек АССЕМБЛЕР на примерах Базовый курс Наука и Техника, Санкт-Петербург
2005
Рудольф Марек. Ассемблер на примерах. Базовый курс. —
СПб: Наука и Техника, 2005. — 240 сил Серия Просто о сложном Эта книга представляет собой великолепное практическое руководство по основам программирования на языке ассемблера. Изложение сопровождается большим количеством подробно откомментированных примеров, что способствует наилучшему пониманию и усвоению материала. Доходчиво объясняются все основные вопросы программирования на этом языке. Вы узнаете, как писать ассемблерные программы под разные операционные системы
(Windows, DOS, Linux), как создавать резидентные программы, как писать ассемблерные вставки в программы на языках высокого уровня и многое другое. Попутно вам будут разъяснены основные моменты работы процессора, операционных систем, управления памятью и взаимодействия программ с аппаратными устройствами ПК - то есть все то, без знания чего нельзя обойтись при программировании на языке низкого уровня, которыми является ассемблер. Книга написана доступным языком. Лучший выбор для начинающих. Русское издание под редакцией Финкова МВ. и Березкиной О.И.
Copyright © Computer Press 2004 Uiime se programovat vjazyce Assembler pro PC by Rudolf Marek, ISBN: 80-722-6843-0.
All rights reserved Контактные телефоны издательства
llllllll llllllllli I llll lllll II
( 8 1 2 ) 5 6 7
-
7 0
"
2 5
. 567-70-26
I (044)516-38-66 Официальный сайт www.nit.com.ru
© Перевод на русский языки У "7 т li t. Наука и Техника, 2005
© Издание нерусском языке,
ISBN 5-94387-232-9 оформление, Наука и Техника, 2005 0 0 0 Наука и Техника. Лицензия №000350 от 23 декабря 1999 года.
198097, г. Санкт-Петербург, ул. Маршала Говорова, д. 29. Подписано в печать 08.08.05. Формат 70x100 1/16. Бумага газетная. Печать офсетная. Объем 15 пл. Тираж 5000 экз. Заказ № 293 Отпечатано с готовых диапозитивов в ОАО Техническая книга
190005, Санкт-Петербург, Измайловский пр, 29
Содержание Введение 10 Глава 1. Базовые системы счисления и термины 11
1.1. Системы счисления и преобразования между ними 12 1.2. Типы данных. Их представление в компьютере 15 Глава 2. Введение в семейство процессоров х 19
2.1. О компьютерах 20 2.2. История процессоров х 22 2.3. Процессоры и их регистры общая информация 23 2.4. Процессор 80386 25 Регистры общего назначения 25 Индексные регистры 27 Сегментные регистры 27 Регистры состояния и управления 27 2.5 Прерывания 28 Глава 3. Анатомия команд и как они выполняются процессором 30
3.1. Как команды выполняются процессором 31 3.2. Операнды 33 3.3. Адресация памяти 34 3.4. Команды языка ассемблера 35 Глава 4. Основные команды языка ассемблера 36
4.1. Команда MOV 37 4.2. «Остроконечники» и «тупоконечники» 39 4.3. Арифметические команды 40 4.3.1. Инструкции сложения ADD и вычитания SUB 41 4.3.2. Команды инкрементирования INC и декрементирования DEC 43 4.3.3. Отрицательные числа — целые числа со знаком 44 4.3.4. Команды для работы с отрицательными числами 46 Команда NEG 46 Команда CBW 46 Команда CWD 47 Команда CDQ 47 Команда CWDE 47 4.3.5. Целочисленное умножение и деление 48 Команды MUL и IMUL 48 Команды DIV и IDIV 50 4.4. Логические команды 51 Команда AND 51 Команда OR 52 Команда XOR 52 Команда NOT 53 Массивы битов (разрядные матрицы) 53
Глава 5. Управляющие конструкции
55 5.1. Последовательное выполнение команд 56 5.2. Конструкция «IF THEN» — выбор пути 57 5.2.1. Команды СМР и TEST 57 5.2.2. Команда безусловного перехода — JMP 58 5.2.3. Условные переходы — Jx 59 5.3. Итерационные конструкции — циклы 63 Цикл со счетчиком с помощью конструкций IF и GOTO 63
LOOP — сложная команда, простая запись цикла 65 Цикл со счетчиком и дополнительным условием. Команды LOOPZ и LOOPNZ 66 5.4. Команды обработки стека 67 Что такое стеки как он работает 67 Команды PUSH и POP: втолкнуть и вытолкнуть 68 Команды PUSHA/POPA и PUSHAD/POPAD: толкаем все регистры общего назначения 70 Команды PUSHF/POPF и PUSHFD/POPFD: толкаем регистр признаков 71 Команды CALL и RET: организуем подпрограмму 71 Команды INT и IRET: вызываем прерывание 73 Глава 6. Прочие команды 76
6.1. Изменение регистра признаков напрямую 77 Команды CLI и STI 77 Команды STD и CLD 77 6.2. Команда XCHG — меняем местами операнды 78 6.3. Команда LEA — не только вычисление адреса 78 6.4. Команды для работы со строками 79 Команды STOSx — запись строки в память 79 Команды LODSx — чтение строки из памяти 80 Команды CMPSx — сравнение строк 80 Команды SCASx — поиск символа в строке 80 Команды REP и REPZ — повторение следующей команды 80 6.5. Команды ввода/вывода (I/O) 84 Команды IN и OUT — обмен данными с периферией 84 Организация задержки. Команда NOP 86 6.6. Сдвиги ротация 86 Команды SHR и SHL — сдвиг беззнаковых чисел 87 Команды SAL и SAR — сдвиг чисел со знаком 89 Команды RCR и RCL — ротация через флаг переноса 89 Команды ROR и ROL — ротация с выносом во флаг переноса 90 6.7. Псевдокоманды 90 Псевдокоманды DB, DW и DD — определение констант 90 Псевдокоманды RESB, RESWn RESD — объявление переменных . . 91 Псевдокоманда TIMES — повторение следующей псевдокоманды .. 91 Псевдокоманда INCBIN — подключение двоичного файла 92 Псевдокоманда EQU — вычисление константных выражений 92 Оператор SEG — смена сегмента 93 6.8. Советы по использованию команд 93 Директива ALIGN — выравнивание данных в памяти 93 Загрузка значения в регистр 94 Оптимизируем арифметику 94 Операции сравнения 95 Разное 96
6
к Глава 7. Полезные фрагменты кода
97
7.1. Простые примеры 98 Сложение двух переменных 98 Сложение двух элементов массива 99 Суммируем элементы массива 99 Чет и нечет 100 Перестановка битов в числе 101 Проверка делимости числа нацело 101 7.2. Преобразование числа в строку 102 7.3. Преобразование строки в число 107 Глава 8. Операционная система 111
8.1. Эволюция операционных систем 112 8.2. Распределение процессорного времени. Процессы 113 Процессы 113 Планирование процессов 114 Состояния процессов 114 Стратегия планирования процессов 115 8.3. Управление памятью 116 Простое распределение памяти 116
Свопинг (swapping) — организация подкачки 117 Виртуальная память и страничный обмен 117 8.4. Файловые системы 120 Структура файловой системы 120 Доступ к файлу 121 Физическая структура диска 122 Логические диски 123 8.5. Загрузка системы 123 Этапы загрузки 123 Немного о том, что такое BIOS 124 Глава 9. Компилятор NASM 125
9.1. Предложения языка ассемблера 126 9.2. Выражения 126 9.3. Локальные метки 127 9.4. Препроцессор NASM 128
Однострочные макросы — %define, %undef 128 Сложные макросы — %macro %endmacro 129 Объявление макроса — %assign 130 Условная компиляция — %if 130 Определен ли макрос Директивы %ifdef, %infndef 131 Вставка файла — %include 131 9.5. Директивы Ассемблера 131 Директива BITS — указание режима процессора 132 Директивы SECTION и SEGMENT — задание структуры программы. 132 Директивы EXTERN, GLOBAL и COMMON — обмен данными с другими рограммными модулями 134 Директива CPU — компиляция программы для выполнения на определенном процессоре 134 Директива ORG — указание адреса загрузки 134 9.6. Формат выходного файла 135 Создание выходного файла компиляция и компоновка 135
7
Формат bin — готовый исполняемый файл 136 Формат OMF — объектный файл для 16-битного режима 136 Формат Win32 — объектный файл для 32-битного режима 137 Форматы aout и aoutb — старейший формат для UNIX 137 Формат coff — наследник a.out 138 Формат elf — основной формат в мире UNIX 138 Символическая информация 138 Глава 10. Программирование в DOS 139 10.1. Адресация памяти в реальном режиме 140 10.2. Организация памяти в DOS 142 10.3. Расширение памяти свыше 1 MB 143 10.4. Типы исполняемых файлов в DOS 144 10.5. Основные системные вызовы 146 Немедленное завершение программы 146 Вывод строки. Пишем программу «Hello, World!» 147 Ввод с клавиатуры 148 10.6. Файловые операции ввода-вывода 153 Открытие файла 153 Закрытие файла 154 Чтение из файла 154 Запись в файл 155
Открытие/создание файла 158 Поиск позиции в файле (SEEK) 160 Другие функции для работы с файлами 161 Длинные имена файлов и работа сними. Работа с каталогами 163 Создание и удаление каталога (MKDIR, RMDIR) 163 Смена текущего каталога (CHDIR) 163 Получение текущего каталога (GETCWD) 163 10.8. Управление памятью 165 Изменение размера блока памяти 165 Выделение памяти 166 Освобождение памяти 166 10.9. Аргументы командной строки 166 10.10. Коды ошибок 167 10.11. Отладка 168 10.11.1. Что такое отладка программы и зачем она нужна 168 10.11.2. Отладчик grdb.exe 169 Методика использования 169 Основные команды отладчика grdb 172 Пример разработки и отладки программы 172 10.12. Резидентные программы 180 10.13. Свободные источники информации 185 Глава 1 1 . Программирование в Windows 186
11.1. Введение 187 11.2. Родные приложения 187 11.2.1. Системные вызовы API 187 11.2.2. Программа «Hello, World!» с кнопкой под Windows 188 11.3. Программная совместимость 190 11.4. Запуск приложений под Windows 190 11.5. Свободные источники информации 190
8
Глава 12. Программирование в Linux 191
12.1. Введение 192 12.2. Структура памяти процесса 193 12.3. Передача параметров командной строки и переменных окружения 194 12.4. Вызов операционной системы 194 12.5. Коды ошибок 195 12.6. страницы 195 12.7. Программа «Hello, World!» под Linux 197 12.8. Облегчим себе работу утилиты Asmutils 199 12.9. Макросы Asmutils 200 12.10. Операции файлового ввода/вывода (I/O) 201 Открытие файла 201 Закрытие файла 202 Чтение из файла 202 Запись в файл 203 Поиск позиции в файле 206 Другие функции для работы с файлами 207 12.11. Работа с каталогами 209 Создание и удаление каталога (MKDIR, RMDIR) 209 Смена текущего каталога (CHDIR) 210 Определение текущего каталога (GETCWD) 210 12.12. Ввод с клавиатуры. Изменение поведения потока стандартного ввода. Системный вызов IOCTL 210 12.13. Распределение памяти 211 12.14. Отладка. Отладчик ALD 212 12.15. Ассемблер GAS 215 12.16. Свободные источники информации. 216 12.17. Ключи командной строки компилятора 216 Глава 13. Компоновка — стыковка ассемблерных программ с программами, написанными на языках высокого уровня 217
13.1. Передача аргументов 218 13.2. Что такое стек-фрейм? 219 13.2.1. Стек-фрейм в языке С (32-битная версия) 220 13.2.2. Стек-фрейм в языке С (16-битная версия) 223 13.3. Компоновка с С-программой 224 13.4. Компоновка с программой 226 Глава 14. Заключение 229 Глава 15. Часто используемые команды 230
Введение Эта книга — начальный курс и практическое руководство по программированию на языке ассемблера для процессоров серии х — самых распространенных в современных ПК. Она предназначена для студентов и старшеклассников, которые хотят познакомиться с языком программирования низкого уровня, позволяющим писать компактные, быстрые и эффективные программы, взаимодействующие с аппаратным обеспечением компьютера напрямую, минуя любую операционную систему. Книга содержит подробные объяснения и множество практических примеров. Последовательное изложение предмета дает читателю ясное понимание того, как язык ассемблера связан с физической архитектурой компьютера, как ассемблер работает с регистрами процессора, как реализовать основные программные конструкции, как скомпилировать и запустить законченную программу, как из ассемблерной программы вызывать операционную систему и обращаться к файловой системе. Вместе с автором читатель проходит шаг за шагом отрешения типовых, независящих от платформы, задач к написанию практически полезных программ, работающих в среде DOS, Windows и Linux, а также узнает, как скомпоновать подпрограммы на языке ассемблера с подпрограммами, написанными на языках высокого уровня. Книга написана простым, доступным языком, а все примеры программ тщательно прокомментированы. Особое внимание обращено наследующие вопросы
• Архитектура процессора, функции операционной системы, машинный код и символическое представление команд и адресов
• Различные системы счисления и перевод чисел из одной в другую
• Основные и сложные команды
• Управляющие конструкции и их реализация
• Готовые фрагменты кода, выполняющие самые типичные задачи Использование свободно распространяемого компилятора Netwide
Assembler (NASM);
• Практическое программирование в среде DOS, Windows и Linux;
• Написание ассемблерных вставок в программы на языках высокого уровня Си Паскаль. Автор книги — деятельный разработчик свободного программного обеспечения, соавтор самого маленького в мире веб-сервера размером в 514 байт, участник разработки пакета Asmutils и создатель программного модуля для проигрывателя MPlayer.
10
Базовые системы счисления и термины Системы счисления и преобразования между ними Типы данных. Их представление в компьютере
Архитектура компьютера тесно связана с двоичной системой счисления, которая состоит всего из двух цифр — 0 и 1. С технической точки зрения, двоичная система сосновой) идеально подходит для компьютеров, поскольку обе цифры могут отображать два состояния — включено (1) и выключено (0). Как мы вскоре увидим, большие числа становятся огромными в двоичной системе, поэтому для более удобного представления чисел были разработаны восьмеричная и шестнадцатеричная системы счисления (с основами 8 и 16 соответственно. Обе системы легко преобразуются в двоичную систему, но позволяют записывать числа более компактно.
1.1. Системы счисления и преобразования между ними Различные системы счисления отличаются не только базовым набором чисел, но и основными концепциями, которые лежат в их основе. Взять, например, систему счисления, которая использовалась древними римлянами она довольно трудна для восприятия, в ней очень сложно производить вычисления и невозможно представить 0. Данная система неудобна даже для человека, не говоря ужо том, чтобы научить компьютер понимать ее. Десятичная система, которую мы используем всю жизнь, относится к классу так называемых позиционных систем, в которых число А может быть представлено в виде А = а V + а а + ... + a,*z'+a *z° Здесь a n
— это цифры числа, a Z — основание системы счисления, в нашем случае — 10. Например, число 1234 можно представить так
1234 = 1*10 3
+ 2*10 2
+ 3*10' + 4*10° Вес каждой цифры определяется позицией цифры в числе и равен степени основания, соответствующей ее позиции.
12
Глава 1. Базовые системы счисления и термины При работе с различными системами счисления мы будем записывать само число в скобках, аза скобками — основание системы. Например, если написать просто число 1100, тоне понятно, в какой системе оно записано — это может быть одна тысяча сто, а может быть 12, если число записано в двоичной системе. А если представить число в виде (1100)
2
, то сразу все становится на свои места число записано в двоичной системе. Кстати, двоичная система тоже является позиционной, поэтому число 1100 в двоичной системе мы можем представить так
(1100), = 1*2 3
+ 1*2 2
+ 0*2' + 0*2° После сложения 8+4 мы получим, что (1100)
2
равно 12. Как видите, все точно также, как и с десятичной системой. Обратите внимание, что для представления числа 12 в двоичной системе использованы только четыре разряда. Наибольшее число, которое можно записать четырьмя двоичными цифрами, равно 15, потому что (Ш = I*
8
+ I*
4
+ I*
2
+ 1*' = 15. Давайте рассмотрим первые 16 чисел Десятичное число
0
1
2
3
4
5
6
7 Двоичное число
0
1
10
11
100
101
110
111 Десятичное число
8
9
10
11
12
13
14
15 Двоичное число
1000
1001
1010
1011
1100
1101
1110
1111 Числа растут равномерно, и нетрудно предположить, что 16 будет представлено в двоичной системе как (Восьмеричная система счисления (по основанию 8) состоит из большего количества цифр — из восьми (от 0 до 7). Преобразование из этой системы в десятичную систему полностью аналогично преобразованию из двоичной системы, например
(77)
8
= 7*8' + 7*8° - 63 Восьмеричная система счисления использовалась в очень популярных ранее 8- битньгх компьютерах ATARI, ZX Spectrum и др. Позже она была заменена шестнадцатеричной системой, которая также будет рассмотрена в этой книге. В шестнадцатеричной системе цифрами представлены только первые 10 чисел, а для представления остальных 5 чисел используются символы A-F: А = 10, ВСЕ Ассемблер на примерах. Базовый курс Представим, как изменяется наш возраст в шестнадцатеричной системе вы получили свой паспорт влети стали совершеннолетним влет. Для шестнадцатеричной системы сохраняются те же принципы преобразования Число (524D)
16
= 5*16 3
+ 2*16 2
+ 4*16» + 13*16° =
= 20 480 + 512 + 64 + 13 = 21 069 Число (DEAD)
16
ш 13*16 3
+ 14*16 2
+ 10*16' + 13*16° = 57 005 Число (DEADBEEF)
16
= 13*16 7
+ 14*16 6
+ 10*16 5
+ 13*16 4
+
+ 11*16 3
+ 14*16 2
+ 14*16' + 15*16° = 3 735 928 559 Число (С • 12*16 3
+ 0*16 2
+ 0*16' + 1 = 49 153 Итак, мы научились преобразовывать любое число, представленное в двоичной, восьмеричной и шестнадцатеричной системах, в десятичную систему. А теперь займемся обратным преобразованием — из десятичной системы в систему с основанием п. Для обратного преобразования мы должны делить наше число на пи записывать остатки отделения до тех пор, пока частное от предыдущего деления не станет равно 0. Например, преобразуем 14 в двоичную систему
14/2 = 7 остаток 0 7/2 = 3 остаток 1 3/2 = 1 остаток 1 1/2 = 0 остаток 1 Мы завершили процесс деления, когда последнее частное стало равно 0. Теперь запишем все остатки подряд от последнего к первому, и мы получим число в двоичной системе — (Рассмотрим еще один пример — преобразование числа 13 в двоичную систему
13/2 - 6 остаток 1 6/2 = 3 остаток 0 3/2 = 1 остаток 1 1/2 = 0 остаток 1 Как ив прошлом случае, мы делили до тех пор, пока частное не стало равно
0. Если записать остатки снизу вверх, мы получим двоичное число (А теперь потренируемся с шестнадцатеричной системой — преобразуем число
123456 в эту систему
123456/16 = 7716 остаток 0 7716/16 = 482 остаток 4 14
Глава 1. Базовые системы счисления и термины
482/16 = 30 остаток 2 30/16 = 1 остаток 14 = Е
1/16 = 0 остаток 1 После записи всех остатков получим, что число 123 456 = (1Е240)
16
Запись со скобками и нижним индексом в тексте программы неудобна, поэтому мы будем использовать следующие обозначения для записи чисел в различных системах счисления
• Запись шестнадцатиричного числа начинается с Ох или $0 либо заканчивается символом «h». Если первая цифра шестнадцатеричного числа — символ A-F, то перед таким числом нужно обязательно написать 0, чтобы компилятор понял, что передним число, а не идентификатор, например, ODEADh. Таким образом, записи 0x1234, $01234 и 01234h представляют число
(1234)
16
• Десятичные числа могут записываться без изменений либо они заканчиваться постфиксом «d». Например, 1234 и 1234d представляют число ш Двоичные цифры должны заканчиваться постфиксом Ь, например,
ПООЬ — это (1100),.
• Восьмеричные цифры заканчиваются на «q»: 12q — это (Далее в этой книге шестнадцатеричные числа мы будем записывать в виде х, двоичные — Ь, а десятичные — без изменений. В вашем собственном коде основание системы счисления (постфикс «d» или «h») лучше указывать явно, потому что одни ассемблеры рассматривают число без приставок как десятичное, а другие — как шестнадцатеричное.
  1   2   3   4   5   6   7   8   9   ...   20

1.2. Типы данных. Их представление в компьютере Основной и неделимой единицей данных является бит Слово «bit» — это сокращение от «binary digit» — двоичная цифра Бит может принимать два значения — 0 и 1 — ложь или истина, выключено или включено На логике двух состояний основаны все логические цепи компьютеров, поэтому поговорим о бите более подробно. Двоичное число содержит столько битов, сколько двоичных цифр в егоза bbписи,b поэтому диапазон допустимых значений выводится из количества разрядов (цифр, отведенных для числа. Возьмем положительное целое двоичное число, состоящее из четырех битов оно может выражать 2 4
или шестнадцать различных значений.
15
Ассемблер на примерах. Базовый курс Биты (разряды) двоичного числа нумеруются справа налево, от наименее значимого до наиболее значимого Нумерация начинается с 0. Самый правый бит числа — это бит с номером 0 (первый бит Этот бит называется битом
(Least Significant Bit наименее значимый бит Подобно этому самый левый бит называется битом (Most Significant Bit наиболее значимый бит. Биты могут объединяться в группы, группа из четырех битов называется полубайтом (nibble). Компьютер не работает с отдельными битами, обычно он оперирует группами битов, например, группа из восьми битов образует базовый тип данных, который называется байтом Восемь битов в байте — это не закон природы, а количество, произвольно выбранное разработчиками IBM, создававшими первые компьютеры. Большие группы битов называются словом (word) или двойным словом
(dword — double word). Относительно совместимых компьютеров мы можем сказать следующее
1 байт = 8 бит
1 слово (word) = 2 байта = 16 бит
1 двойное слово (dword) = 4 байта = 32 бит Один байт — это наименьшее количество данных, которое может быть прочитано из памяти или записано в нее, поэтому каждый байт памяти имеет индивидуальный адрес Байт может содержать число в диапазоне 0 — 255 (то есть 2 8
— 256 различных чисел. В большинстве случаев этого значения недостаточно, поэтому используется следующая единица данных — слово. Слово может содержать число в диапазоне 0 — 65 535 (то есть 2 16
= 65 536 различив значений. Двойное слово имеет диапазон значений 0 — 4 294 967 295 (2 32
Щ
4 294 967 296 значений.
Давным-давно, еще во времена первых компьютеров, емкость носителе информации представлялась в байтах. Со временем технологии усовершен-'
0 - 1
(0 - 0x1)
0 - 4294967295
(0 - OxFFFFFFFF)
31 16 15 0 Двойное слово
(dword) РИС. 1.1. ОТ бита до двойного слова

16
Глава 1. Базовые системы счисления и термины ствовались, цены на память падали, а требования к обработке информации возрастали. Поэтому выражать емкость в байтах стало неудобно. Решили, что емкость будет выражаться в килобайтах (KB, Kb, Кб или просто к. Нов отличие от международной системы SI, приставка кило означает не
1000, а 1024. Почему именно 1024? Поскольку все в компьютере было завязано на двоичной системе, для простоты любое значимое число должно было выражаться как степень двойки. 1024 — это 2 10
. Следующие приставки — М (мегабайт, MB, Mb, Мб), G (гигабайт, GB, Гб), Т (терабайт, ТВ, ТБ) и Р (петабайт, РВ, ПБ) — вычисляются умножением 1024 на предыдущее значение, например, 1 Кб = 1024, значит, 1 Мб = 1 Кб * 1024 = 1024 * 1024 - 1 048 576 байт. Думаю, с этим все понятно, давайте вернемся к типам данных. В языках программирования высокого уровня есть специальные типы данных, позволяющие хранить символы и строки. В языке ассемблера таких типов данных нет. Вместо них для представления одного символа используется байта для представления строки — группа последовательных байтов. Тут все просто. Каждое значение байта соответствует одному из символов таблицы (American Standard Code for Information Interchange). Первые
128 символов — управляющие символы, латинские буквы, цифры — одинаковы для всех компьютеров и операционных систем. Давайте рассмотрим таблицу кодов ASCII (рис. 1.2). Шестнадцатеричные цифры в заголовках строки столбцов таблицы представляют числовые значения отдельных символов. Например, координаты заглавной латинской буквы Аи. Сложив эти значения, получим 0x41 то есть 65 в десятичной системе) — код символа 'А' в коде. Печатаемые символы в таблице начинаются с кода 0x20 (или 32d). Символы с кодом ниже 32 представляют так называемые управляющие символы. Наиболее известные из них — это ОхА или LF — перевод строки, и OxD —
CR — возврат каретки. Важность управляющих символов CR и LF обусловлена тем, что они обозначают конец строки — тот символ, который в языке программирования С обозначается как п. К сожалению, в различных операционных системах он представляется по-разному: например, в Windows (ион представляется двумя символами (CR, LF — OxD, ОхА), а в операционной системе UNIX для обозначения конца строки используется всего один символ (LF — ОхА). Символы с кодами от 128 дои выше стали жертвами различных стандартов и кодировок. Обычно они содержат национальные символы, например, у нас это будут символы русского алфавита и, возможно, некоторые символы псевдографики, в Чехии — символы чешского алфавита и т.д. Следует отметить, что для русского языка используются кодировки СР 866 (в DOS) и
СР 1251 (Windows).
17
Ассемблер на примерах. Базовый курс РИС. 1.2. Таблица кодов ASCII
18
Введение в семейство процессоров х О компьютерах. История процессоров х Процессоры и их регистры общая информация Процессор 80386 Прерывания
Мы начинаем знакомиться с языком ассемблера. Это язык программирования низкого уровня, то есть максимально приближенный к железу — аппаратному обеспечению компьютера. Для каждого процессора характерен свой уникальный набор действий, которые процессор способен выполнить, поэтому языки ассемблера разных процессоров отличаются друг от друга. Например, если процессор не умеет выполнять умножение, тов его языке ассемблера н будет отдельной команды умножить, а перемножать числа программисту придется при помощи нескольких команд сложения. Собственно говоря, язык ассемблера — это всего лишь ориентированная н человека форма записи инструкций процессора (которые называются такж машинным языком, асам ассемблер — это программа, переводящая символические имена команд в машинные коды. Вот почему, прежде чем приступать к изучению команд языка ассемблера, нам нужно побольше узнать о процессоре, для которого этот язык предназначен. О компьютерах. Первым популярным компьютером стал компьютер ENIAC (Electron'
Numerical Integrator And Calculator), построенный из электронных ламп предназначенный для решения дифференциальных уравнений. Программирование этого компьютера, которое заключалось в переключении тумблеров было очень трудоемким процессом. Следующим компьютером после ENIAC был не столь популярный EDVA
(Electronic Discrete Variable Automatic Computer), построенный в 1946 г Принципы, заложенные в этом компьютере, используются и посей день э машина, подобно современным компьютерам, хранила заложенную програ му в памяти. Концепция компьютера EDVAC, разработанная американски ученым венгерского происхождения Джоном фон Нейманом, основывала наследующих принципах
1. Компьютер должен состоять из следующих модулей управляющий бло контроллер, арифметический блок, память, блоки ввода/вывода.
20
Глава 2. Введение в семейство процессоров х 2. Строение компьютера не должно зависеть от решаемой задачи (это как раз относится к ENIAC), программа должна храниться в памяти.
3. Инструкции и их операнды (то есть данные) должны также храниться в той же памяти (гарвардская концепция компьютеров, основанная на концепции фон Неймана, предполагала отдельную память для программы и данных.
4. Память делится на ячейки одинакового размера, порядковый номер ячейки считается ее адресом (1 ячейка эквивалентна 1 байту.
5. Программа состоит из серии элементарных инструкций, которые обычно не содержат значения операнда (указывается только его адрес, поэтому программа не зависит от обрабатываемых данных (это уже прототип переменных. Инструкции выполняются одна за другой, в том порядке, в котором они находятся в памяти (к слову, современные микропроцессоры позволяют параллельное выполнение нескольких инструкций.
6. Для изменения порядка выполнения инструкций используются инструкции условного или безусловного (jump) перехода.
7. Инструкции и данные (то есть операнды, результаты или адреса) представляются в виде двоичных сигналов ив двоичной системе счисления. Оказалось, что концепция фон Неймана настолько мощна и универсальна, что она до сих пор используется в современных компьютерах. Однако продвижение компьютера в наши дома потребовало долгого времени — почти сорока лет. В 1950-ых годах был изобретен транзистор, который заменил большие, склонные ко всяким сбоям, вакуумные лампы. Со временем размер транзисторов уменьшился, но самым большим их компонентом оставался корпус. Решение было простым разместить много транзисторов водном корпусе. Так появились интегральные микросхемы (чипы. Компьютеры, построенные Данные Инструкции Рис. 2.1. Концепция фон Неймана
21
Ассемблер на примерах. Базовый курс на интегральных микросхемах, стали намного меньше в размере, однако они все еще занимали целые комнаты. К тому же эти компьютеры были очень чувствительны к окружающей среде без кондиционера они не работали. В конце 1970-ых интегральные микросхемы упали в цене до такой степени, что стали доступны рядовым потребителям. Почему же люди в то время не покупали персональные компьютеры Потому что их не было в продаже Люди покупали микросхемы и собирали простые восьмиразрядные компьютеры в порядке хобби — так, в знаменитом гараже, началась история фирмы Apple. За Apple последовали другие компании, начавшие производство восьмиразрядных компьютеров, которые можно было подключить к обычному телевизору и играть или заняться их программированием. В 1981 году лидером рынка универсальных ЭВМ — компанией IBM — был выпущен персональный компьютер IBM PC XT. Это был полноценный персональный компьютер — с монитором, клавиатурой и системным блоком. Компьютер IBM PC XT был оборудован разрядным микропроцессором
Intel 8088. Эта модель стала началом огромной серии персональных компьютеров (PC, Personal Computer), которые производились вплоть до нашего времени.
2.2. История процессоров х История первых разрядных процессоров классах, была начата компанией Intel в 1978 году. Чипы того времени работали на частоте 5, 8 или
10 МГц и благодаря разрядной шине адреса позволяли адресовать 1 Мб оперативной памяти. В то время были популярны 8-битные компьютеры, поэтому Intel разработала другой чип — 8088, который был аппаратно и программно совместим с 8086, но оснащен только разрядной шиной. В 1982 году Intel представила процессор 80286, который был обратно совместим с обеими предыдущими моделями, но использовал более широкую, разрядную, шину адреса. Этот процессор позволял адресовать 16 Мб оперативной памяти. Кроме расширенного набора команд (появилось несколько новых команд, данный процессор мог работать в двух режимах — реальном и защищенном. Защищенный режим обеспечивал механизмы страничной организации памяти, прав доступа и переключения задач, которые необходимы для любой многозадачной операционной системы. Реальный режим использовался для обратной совместимости с предыдущими моделями х. Четыре года спустя, в 1986 году, Intel выпустила процессору которого обе шины (шина данных и шина адреса) были разрядными. В тоже время был выпущен процессор 80386 SX, который был во всем идентичен 80386 DX,
22
Глава 2. Введение в семейство процессоров х но только с разрядной внешней шиной данных. Оба процессора работали на частоте 20, 25 или 33 МГц. Процессор 80386 не имел интегрированного математического сопроцессора, математический сопроцессор поставлялся в виде отдельного чипа — 80387. В 1989 году было выпущено следующее поколение микропроцессоров Intel —
80486DX, 80486DX/2 и 80486DX/4, которые отличались только рабочей частотой. Выпущенная тогда же версия 80486SX, в отличие от 80486DX, поставлялась без математического сопроцессора. Новые возможности интеграции позволили разместить на чипе 8 Кб кэш-памяти. В 1993 году был выпущен первый чип под названием Pentium. С него началась новая линия чипов, которые не только используются сейчас, но и все еще могут выполнять программы, написанные 20 лет назад для процессора 8086. Процессоры, совместимые с х, выпускались не только компанией Intel, но также и другими компаниями AMD, Cyrix, NEC, IBM. Мы более подробно рассмотрим 80386, который сточки зрения программирования полностью совместим даже с самыми современными процессорами.
2.3. Процессоры и их регистры общая информация Поговорим о внутреннем строении процессора Процессор — это кремниевая плата или подложка с логическими цепями, состоящими из транзисторов, скрытая в пластмассовом корпусе, снабженном контактными ножками (выводами, pin). Большинство ножек процессора подключено к шинам — шине адреса, шине данных и шине управления, связывающим чип процессора с остальной частью компьютера. Остальные ножки служат для подачи питания на сам чип. Каждая шина состоит из группы проводников, которые выполняют определенную функцию. Пункт 7 концепции фон Неймана говорит ИНСТРУКЦИИ И ДАННЫЕ ТО ЕСТЬ ОПЕРАНДЫ, РЕЗУЛЬТАТЫ ИЛИ АДРЕСА) ПРЕДСТАВЛЯЮТСЯ В ВИДЕ ДВОИЧНЫХ СИГНАЛОВ ИВ ДВОИЧНОЙ СИСТЕМЕ СЧИСЛЕНИЯ. Это означает, что один проводник шины компьютера может нести один бит. Значение этого бита (1 или 0) определяется уровнем напряжения в проводнике. Значит, процессор с одной разрядной шиной и одной разрядной должен иметь 24 (16 и 8) ножки, соединенные с различными проводниками. Например, при передаче числа 27 (00011011 в двоичной системе) поразрядной шине проводник, по которому передается самый правый бит
(LSB), покажет логический уровень 1, следующий провод также покажет 1, следующий — 0 и т.д.
23
Ассемблер на примерах. Базовый курс Пока мы сказали, что процессор состоит из логических контуров. Эти цепи реализуют все модули, из которых процессор должен состоять согласно концепции фон Неймана: контроллер, арифметико-логическое устройство (АЛУ) и регистры. Контроллер управляет получением инструкций из памяти и их декодированием. Контроллер не обрабатывает инструкцию после декодирования он просто передает ее по внутренней шине управления к другим модулям, которые выполняют необходимое действие.
Арифметико-логическое устройство (АЛУ) выполняет арифметические и логические действия над данными. Для более простых процессоров достаточно АЛУ, умеющего выполнять операции отрицания и сложения, поскольку другие арифметические действия (вычитание, умножение и целочисленное деление) могут быть сведены к этим операциям. Другая, логическая, часть АЛУ выполняет основные логические действия над данными, например, логическое сложение и умножение (ИЛИ, И, а также исключительное ИЛИ. Еще одна функция АЛУ, которую выполняет устройство циклического сдвига (barrel-shifter), заключается в сдвигах битов влево и вправо. Для выполнения процессором инструкции необходимо намного меньше времени, чем для чтения этой инструкции из памяти. Чтобы сократить время ожидания памяти, процессор снабжен временным хранилищем инструкций и
24
Глава 2. Введение в семейство процессоров х данных — регистрами Размер регистра — несколько байтов, но зато доступ к регистрам осуществляется почти мгновенно. Среди регистров обязательно должны присутствовать следующие группы регистры общего назначения, регистры состояния и счетчики. Регистры общего назначения содержат рабочие данные, полученные из памяти. Регистры состояния содержат текущее состояние процессора (или состояние АЛУ. Последняя группа — это счетчики. Согласно теории фон Неймана, должен быть хотя бы один регистр из этой группы — счетчик команд, содержащий адрес следующей инструкции. Как все это работает, мы расскажем в следующей главе.
2.4. Процессор 80386 Микропроцессор 80386 полностью разрядный, что означает, что он может работать с 4 Гб оперативной памяти (2 32
байтов. Поскольку шина данных также разрядная, процессор может обрабатывать и хранить в своих регистрах число шириной вбита (тип данных int в большинстве реализаций языка С как раз разрядный. Чтобы научиться программировать на языке ассемблера, мы должны знать имена регистров (рис. 2.3) и общий принцип работы команд. Сами команды обсуждаются в следующих главах. Регистры общего назначения Сначала рассмотрим регистры общего назначения. Они называются ЕАХ,
ЕВХ, ЕСХ и EDX (Аккумулятор, База, Счетчики Данные. Кроме названий, они больше ничем другим не отличаются друг от друга, поэтому рассмотрим только первый регистр — ЕАХ (рис. 2.4). Процессор 80386 обратно совместим с процессором 80286, регистры которого разрядные. Как же 80386 может выполнять команды, предназначенные для регистров меньшего размера Регистр ЕАХ может быть разделен на две части — разрядный регистр АХ (который также присутствует в 80286) и верхние 16 битов, которые никак не называются. В свою очередь, регистр АХ может быть разделен (не только в 80386, но ив) на два 8-битных регистра — АН и AL. Если мы заносим в регистр ЕАХ значение 0x12345678, то регистр АХ будет содержать значение 0x5678 (0x56 в АН ива значение 0x1234 будет помещено в верхнюю часть регистра ЕАХ. Младшие регистры других регистров общего назначения называются по такому же принципу ЕВХ содержит ВХ, который, в свою очередь, содержит
ВН и BL и т.д.
25
Ассемблер на примерах. Базовый курс
26
Глава 2. Введение в семейство процессоров х Индексные регистры К регистрам общего назначения иногда относят и индексные регистры процессора 80386 — ESI, EDI и ЕВР (или SI, DI и ВР для разрядных действий. Обычно эти регистры используются для адресации памяти обращения к массивам, индексирования и т.д. Отсюда их имена индекс источника (Source
Index), индекс приемника (Destination Index), указатель базы (Base Pointer). Но хранить в них только адреса совсем необязательно регистры ESI, EDI и ЕВР могут содержать произвольные данные. Эти регистры программно доступны, то есть их содержание может быть изменено программистом. Другие регистры лучше руками не трогать. У регистров ESI, EDI и ЕВР существуют только в разрядная и разрядная версии. Сегментные регистры Эту группу регистров можно отнести к регистрам состояния. Регистры из этой группы используются при вычислении реального адреса (адреса, который будет передан на шину адреса. Процесс вычисления реального адреса зависит от режима процессора (реальный или защищенный) и будет рассмотрен в следующих главах. Сегментные регистры только разрядные, такие же, как в 80286. Названия этих регистров соответствуют выполняемым функциям CS (Code
Segment, сегмент кода) вместе с EIP (IP) определяют адрес памяти, откуда нужно прочитать следующую инструкцию аналогично регистр SS (Stack
Segment, сегмент стека) в паре с ESP (SS:SP) указывают на вершину стека. Сегментные регистры DS, ES, FS, и GS (Data, Extra, F и G сегменты) используются для адресации данных в памяти. Регистры состояния и управления Регистр ESP (SP) — это указатель памяти, который указывает на вершину стека (х86-совместимые процессоры не имеют аппаратного стека. О стеке мы поговорим в следующих главах. Также программно не может быть изменен регистр EIP (IP, Instruction Pointer) — указатель команд. Этот регистр указывает на инструкцию, которая будет выполнена следующей. Значение этого регистра изменяется непосредственно контроллером процессора согласно инструкциям, полученным из памяти. Нам осталось рассмотреть только регистр флагов (иногда его называют регистром признаков) — EFLAGS. Он состоит из одноразрядных флагов, отображающих в основном текущее состояние арифметико-логического устройства. В наших программах мы будем использовать все 32 флага, а пока рассмотрим только самые важные из них
1   2   3   4   5   6   7   8   9   ...   20

27
Ассемблер на примерах. Базовый курс
• Признак нуля ZF (Zero Flag) — 1, если результат предыдущей операции равен нулю.
• Признак знака SF (Sign Flag) — 1, если результат предыдущей операции отрицательный.
• Признак переполнения OF (Overflow Flag) — 1, если при выполнении предыдущей операции произошло переполнение (overflow), то есть результат операции больше, чем зарезервированная для него память.
• Признак переноса CF (Carry Flag) — 1, если бит был перенесен и стал битом более высокого порядка (об этом мы поговорим в четвертой главе, когда будем рассматривать арифметические операции.
• Признак прерывания IF (Interrupt Flag) — 1, если прерывания процессора разрешены.
• Признак направления DF (Direction Flag) — используется для обработки строк, мы рассмотрим подробнее этот регистр в шестой главе. Другие регистры процессора относятся к работе в защищенном режиме, описание принципов которого выходит за рамки этой книги. Если 80386 процессор оснащен математическим сопроцессором 80387 (это отдельный чип на вашей материнской плате, он будет быстрее обрабатывать числа с плавающей точкой. Современным процессорам отдельный математический процессор ненужен он находится внутри процессора. Раньше вы могли немного сэкономить и купить компьютер без математического сопроцессора — его наличие было необязательно, и компьютер мог работать без него. Если математический процессор не был установлен, его функции эмулировались основным процессором, так что производительность операций над числами с плавающей точкой была очень низкой. Примечание. Когда мы будем говорить сразу о 16- и разрядных регистрах, то мы будем использовать сокращение (Е)АХ) — вместо АХ и ЕАХ.
2.5. Прерывания Событие прерывания состоит в том, что процессор прекращает выполнять инструкции программы в нормальной последовательности, а вместо этого начинает выполнять другую программу, предназначенную для обработки этого события. После окончания обработки прерывания процессор продолжит выполнение прерванной программы. Давайте рассмотрим пример. Я сижу за столом и читаю книгу. Сточки зрения компьютера, я выполняю процесс чтения книги. Внезапно звонит телефон —
28
Глава 2. Введение в семейство процессоров х я прерываю чтение, кладу в книгу закладку (на языке процессора это называется сохранить контекст) и беру трубку. Теперь я обрабатываю телефонный звонок. Закончив разговор, я возвращаюсь к чтению книги. Найти последнее прочитанное место помогает та самая закладка. Процессоры семействах и совместимые сними могут порождать 256 прерываний. Адреса всех 256 функций обработки прерываний (так называемые векторы прерываний) хранятся в специальной таблице векторов прерываний. Прерывания могут быть программными и аппаратными. Аппаратные прерывания происходят по запросу периферийных устройств и называются IRQ (Interrupt Requests). Архитектура шины ISA ограничивает их число до 16 (IRQ0 — IRQ15). К аппаратным прерываниям относятся также специальные прерывания, которые генерирует сам процессор. Такие прерывания используются для обработки исключительных ситуаций — неверный операнд, неизвестная команда, переполнение и другие непредвиденные операции, когда процессор сбит столку и не знает, что делать. Эти прерывания имеют свои обозначения и никак не относятся к зарезервированным для периферии прерываниям
IRQ0-IRQ15. Все аппаратные прерывания можно разделить на две группы прерывания, которые можно игнорировать (замаскировать) и те, которые игнорировать нельзя. Первые называются маскируемыми (maskable), а вторые — немаски­
руемыми (non-maskable). Аппаратные прерывания могут быть отключены путем установки флага IF регистра признаков в 0. Единственное прерывание, которое отключить нельзя — это NMI, немаскируемое прерывание, генери­
рующееся при сбое памяти, сбоев питании процессора и подобных форс- мажорных обстоятельствах. Программные прерывания генерируются с помощью специальной команды в телепрограммы, то есть их порождает программист. Обычно программные прерывания используются для общения вашей программы с операционной системой.
29
Анатомия команд и как они выполняются процессором Как команды выполняются процессором Операнды Адресация памяти Команды языка ассемблера

3.1. Как команды выполняются процессором Команда микропроцессора — это команда, которая выполняет требуемое действие над данными или изменяет внутреннее состояние процессора. Существует две основные архитектуры процессоров. Первая называется RISC
(Reduced Instruction Set Computer) — компьютер с уменьшенным набором команд. Архитектура RISC названа в честь первого компьютера с уменьшенным набором команд — RISC I. Идея этой архитектуры основывается на том, что процессор большую часть времени тратит на выполнение ограниченного числа инструкций (например, переходов или команд присваивания, а остальные команды используются редко. Разработчики архитектуры создали облегченный процессор. Благодаря упрощенной внутренней логике (меньшему числу команд, менее сложным логическим контурам, значительно сократилось время выполнения отдельных команд и увеличилась общая производительность. Архитектура RISC подобна архитектуре общения с собакой — она знает всего несколько командно выполняет их очень быстро. Вторая архитектура имеет сложную систему команд, она называется CISC
(Complex Instruction Set Computer) — компьютер со сложной системой команд. Архитектура CISC подразумевает использование сложных инструкций, которые можно разделить на более простые. Все х86-совместимые процессоры принадлежат к архитектуре CISC. Давайте рассмотрим команду загрузить число 0x1234 в регистр АХ. На языке ассемблера она записывается очень просто — MOV АХ, 0x1234. К настоящему моменту вы уже знаете, что каждая команда представляется в виде двоичного числа (пункт 7 концепции фон Неймана). Ее числовое представление называется машинным кодом. Команда MOV АХ, 0x1234 на машинном языке может быть записана так
31
Ассемблер на примерах. Базовый курс
0x11хх: предыдущая команда
0x1111: 0хВ8, 0x34, 0x12 0x1114: следующие команды Мы поместили команду по адресу 0x1111. Следующая команда начинается тремя байтами дальше, значит, под команду с операндами отведено 3 байта. Второй и третий байты содержат операнды команды MOV. А что такое
0хВ8? После преобразования 0хВ8 в двоичную систему мы получим значение
ЮШОООЬ. Первая часть — 1011 — и есть код команды MOV. Встретив код 1011, контроллер понимает, что передним именно MOV. Следующий разряд (1) означает, что операнды будут разрядными. Три последние цифры определяют регистр назначения. Три нуля соответствуют регистру АХ (или AL, если предыдущий бит был равен 0, указывая таким образом, что операнды будут разрядными. Чтобы декодировать команды, контроллер должен сначала прочитать их из памяти. Предположим, что процессор только что закончил выполнять предшествующую команду, и IP (указатель команд) содержит значение 0x1111. Прежде чем приступить к обработке следующей команды, процессор посмотрит на шину управления, чтобы проверить, требуются ли аппаратные прерывания. Если запроса на прерывание не поступало, то процессор загружает значение, сохраненное по адресу 0x1111 (в нашем случае — это 0хВ8), в свой внутренний командный) регистр. Он декодирует это значение так, как показано выше, и понимает, что нужно загрузить в регистр АХ разрядное число — два следующих байта, находящиеся по адресами (они содержат наше число, 0x1234). Теперь процессор должен получить из памяти эти два байта. Для этого процессор посылает соответствующие команды в шину и ожидает возвращения по шине данных значения из памяти. Получив эти два байта, процессор запишет их в регистр АХ. Затем процессор увеличит значение в регистре IP на 3 (наша команда занимает 3 байта, снова проверит наличие запросов на прерывание и, если таких нет, загрузит один байт по адресу 0x1114 и продолжит выполнять программу. Если запрос на прерывание поступил, процессор проверит его типа также значение флага IF. Если флаг сброшен (0), процессор проигнорирует прерывание если же флаг установлен (1), то процессор сохранит текущий контекст и начнет выполнять первую инструкцию обработчика прерывания, загрузив ее из таблицы векторов прерываний. К счастью, нам не придется записывать команды в машинном коде, поскольку ассемблер разрешает использовать их символические имена. Но перед тем как углубиться в команды, поговорим об их операндах.
32
Глава 3. Анатомия команд и как они выполняются процессором
3.2. Операнды Данные, которые обрабатываются командами, называются операндами. Операнды в языке ассемблера записываются непосредственно после команды если их несколько, то через запятую. Одни команды вообще не имеют никаких операндов, другие имеют один или два операнда. В качестве операнда можно указать непосредственное значение (например,
0x123), имя регистра или ссылку на ячейку памяти (так называемые косвенные операнды. Что же касается разрядности, имеются разрядные, разрядные, и разрядные операнды. Почти каждая команда требует, чтобы операнды были одинакового размера (разрядности. Команда MOV АХ, 0x1234 имеет два операнда операнд регистра и непосредственное значение, и оба они 16-бит­
ные. Последний тип операнда — косвенный тип — адресует данные, находящиеся в памяти, получает их из памяти и использует в качестве значения. Узнать этот операнд очень просто — по наличию в записи квадратных скобок. Адресация памяти будет рассмотрена в следующем параграфе. В документации по Ассемблеру различные форматы операндов представлены следующими аббревиатурами
• операнд — любой разрядный регистр общего назначения
• regl6-onepaHfl — любой разрядный регистр общего назначения
• reg32-onepaHfl — любой разрядный регистр общего назначения
• m операнд может находиться в памяти
• imm8 — непосредственное разрядное значение
• imml6 — непосредственное разрядное значение
• imm32 — непосредственное разрядное значение
• segreg — операнд должен быть сегментным регистром. Допускаются неоднозначные типы операндов, например reg8/imm8-onepaHfl может быть любым 8-битным регистром общего назначения или любым
8-битным непосредственным значением. Иногда размер операнда определяется только по последнему типу, например, следующая запись аналогична предыдущей R/imm8-onepaHfl
может быть любым регистром (имеется ввиду 8-битный регистр) общего назначения или разрядным значением.
2 Зак. 293
33
Ассемблер на примерах. Базовый курс
3.3. Адресация памяти Мы уже знаем, что адрес, как и сама команда, — это число. Чтобы не запоминать адреса всех переменных, используемых в программе, этим адресам присваивают символические обозначения, которые называются переменными иногда их также называют указателями. При использовании косвенного операнда адрес в памяти, по которому находится нужное значение, записывается в квадратных скобках адрес. Если мы используем указатель, то есть символическое представление адреса, например, [ESI], тов листинге машинного кода мы увидим, что указатель был заменен реальным значением адреса. Можно также указать точный адрес памяти, например, [0x594F]. Чаще всего мы будем адресовать память по значению адреса, занесенному в регистр процессора. Чтобы записать такой косвенный операнд, нужно просто написать имя регистра в квадратных скобках. Например, если адрес загружен в регистр ESI, вы можете получить данные, расположенные поэтому адресу, используя выражение [ESI]. Теперь рассмотрим фрагмент программы, в которой регистр ESI содержит адрес первого элемента (нумерация начинается св массиве байтов. Как получить доступ, например, ко второму элементу (элементу, адрес которого на 1 байт больше) массива Процессор поддерживает сложные способы адресации, которые очень нам пригодятся в дальнейшем. В нашем случае, чтобы получить доступ ко второму элементу массива, нужно записать косвенный операнд [ESI + 1]. Имеются даже более сложные типы адресации адрес + ЕВХ + 4]. В этом случае процессор складывает адрес значение 4 и значение, содержащееся в регистре ЕВХ. Результат этого выражения называется эффективным адресом
(ЕА, Effective Address) и используется в качестве адреса, по которому фактически находится операнд (мы пока не рассматриваем сегментные регистры. При вычислении эффективного адреса процессор 80386 также позволяет умножать один член выражения на константу, являющуюся степенью двойки адрес + ЕВХ * 4]. Корректным считается даже следующее сумасшедшее выражение
[ число- б + ЕВХ * 8 + E S I ] На практике мы будем довольствоваться только одним регистром [ESI] или суммой регистра и константы, например, [ESI + 4]. В зависимости от режима процессора, мы можем использовать любой разрядный или разрядный регистр общего назначения [ЕАХ], [ЕВХ],... ЕВР. Процессор предыдущего поколения 80286 позволял записывать адрес в виде суммы содержимого регистра и константы только для регистров ВР, SI, DI, и ВХ.
34
Глава 3. Анатомия команд и как они выполняются процессором Выше мы упомянули, что в адресации памяти участвуют сегментные регистры. Их функция зависит от режима процессора. Каждый способ адресации предполагает, что при вычислении реального (фактического) адреса используется сегментный регистр по умолчанию. Сменить регистр по умолчанию можно так
ES:[ESI] Некоторые ассемблеры требуют указания регистра внутри скобок
[ES:ESI] В наших примерах мы будем считать, что все сегментные регистры содержат одно и тоже значение, поэтому мы не будем использовать их при адресации.
3.4. Команды языка ассемблера Когда мы знаем, что такое операнд, давайте рассмотрим, как описываются команды языка ассемблера. Общий формат такой имя_команды подсказка операнды В следующих главах мы поговорим об отдельных командах и выполняемых ими функциях. Операнды мы только что рассмотрели, осталась одна темная лошадка — подсказка. Необязательная подсказка указывает компилятору требуемый размер операнда. Ее значением может быть слово BYTE (8-битный операнд, WORD (16-битный) или DWORD (32-битный). Представим инициализацию некоторой переменной нулем, то есть запись нулей по адресу переменной. Подсказка сообщит компилятору размер операнда, то есть сколько именно нулевых байтов должно быть записано поэтому адресу. Пока мы не знаем правильной инструкции для записи значения, поэтому будем считать, что записать значение можно так mov dword [ 0x12345678 ],0 записывает 4 нулевых байта, начиная с адреса 0x12 345678 mov word [ 0x12345678 ],0 записывает 2 нулевых байта, начиная с адреса 0x12345678 mov b y t e [ 0x12345678 ],0 записывает 1 нулевой байт по адресу 0x12345 678 Примечание. В языке ассемблера точка с запятой является символом начала комментария. Первая инструкция последовательно запишет 4 нулевых байта, начиная с адреса 0x12345678. Вторая инструкция запишет только два нулевых байта, поскольку размер операнда — слово. Последняя инструкция запишет только один байт (в битах 00000000) в ячейку с адресом 0x12345678.
35
Основные команды языка ассемблера Команда MOV
«Остроконечники» и «тупоконечник Арифметические команды Логические команды Ассемблер на примерах. Базовый курс
В этой главе рассмотрены основные команды процессоров семействах, которые являются фундаментом и простых, и сложных программ, написанных на языке ассемблера. Мы не только опишем синтаксис командно и приведем несколько практических примеров, которые пригодятся вам при написании собственных программ.
4.1. Команда MOV Прежде чем изменять каким-либо образом наши данные, давайте научимся их сохранять копировать из регистра в память и обратно. Ведь прежде чем оперировать данными в регистрах, их сначала туда надо поместить. Команда MOV, хоть название ее и происходит от слова «move» (перемещать, на самом деле не перемещает, а копирует значение из источника в приемник
MOV приемник, источник Рассмотрим несколько примеров применения команды MOV: mov ax,[number] mov [number],bx mov b x , e x mov a l , 1 mov d h , с 1 mov e s i , e d i mov word [number] заносим значение переменной number в регистр АХ загрузить значение регистра ВХ в переменную number занести в регистр ВХ значение регистра СХ занести в регистр AL значение 1 занести в регистр DH значение регистра CL копировать значение регистра EDI в регистр ESI сохранить 16-битное значение 1 в переменную "number" Процессоры семействах позволяют использовать в командах только один косвенный аргумент. Следующая команда копирования значения, находящегося по адресу number_one, в область памяти с адресом number_two, недопустима

mov [number_two], [number_one] НЕПРАВИЛЬНО
37
Ассемблер на примерах. Базовый курс Чтобы скопировать значение из одной области памяти в другую, нужно использовать промежуточный регистр mov ax, [number_one] загружаем в АХ 16-битное значение "number_one" mov [number_two], ах а затем копируем его в переменную
;"number_two" Оба операнда команды MOV должны быть одного размера mov ах, bl НЕПРАВИЛЬНО - Операнды разных размеров. Для копирования значения BL в регистр АХ мы должны расширить диапазон, то есть скопировать весь ВХ в АХ, а затем загрузить 0 в АХ mov ах, Ьх загружаем ВХ в АХ mov ah, 0 сбрасываем" верхнюю часть АХ — записываем в нее О Регистр АН является верхней 8-битной частью регистра АХ. После выполнения команды MOV ах, Ьх регистр АН будет содержать значение верхней части регистра ВХ, то есть значение регистра ВН. Номы не можем быть уверены, что
ВН содержит 0, поэтому мы должны загрузить 0 в АН — команда MOV ah, О сбрасывает значение регистра АН. В результате мы расширили 8-битное значение, ранее содержащееся в регистре BL, до 16 битов. Новое, 16-битное, значение будет находиться в регистре АХ. Можно поступить и наоборот сначала сбросить весь АХ, а затем загрузить
BL в младшую часть АХ (AL): mov ах АН = 0 , AL = О mov al, bl заносим в AL значение BL Точно также можно скопировать 16-битное значение в 32-битный регистр. Для полноты картины приведем список всех допустимых форматов команды
MOV — как в официальной документации
MOV r / m 8 , r e g 8
MOV r / m l 6 , r e g l 6
MOV r/m32,reg32
MOV r e g 8 , r / m 8
MOV r e g l 6 , r / m l 6
MOV reg32,r/m32
MOV reg8,imm8
MOV r e g l 6 , i m m l 6
MOV reg32,imm32
MOV r/m8,imm8
MOV r/ml6,imml6
MOV r/m32,imm32 '
1   2   3   4   5   6   7   8   9   ...   20

38
Глава 4. Основные команды языка ассемблера
4.2. «Остроконечники» и «тупоконечники» Сейчас мы немного отклонимся от обсуждения команд. Предположим, что вы хотите проверить, было ли значение 0x12345678, которое содержалось в регистре ЕВР, корректно загружено в разрядную переменную counter. Следующий фрагмент кода заносит значение 0x12345678 в переменную co­
u n t e r : mov e b p , 0x12345678 загружаем в ЕВР значение 0x12345678 mov [ c o u n t e r ] , ebp сохраняем значение ЕВР в переменную " c o u n t e r " (счетчик) Для того чтобы проверить, загружено значение или нет, нужно воспользоваться отладчиком. Понимаю, что вы пока не знаете ни того, как откомпилировать программу, ни того, как загрузить ее в отладчик, но давайте представим, что это уже сделано за вас. Как будет выглядеть откомпилированная программа в отладчике Отладчик преобразует все команды из машинного кода назад в язык ассемблера. Все точно также, как мы написали, только с небольшим отличием везде, где мы использовали символьное имя переменной, оно будет заменено реальным адресом переменной, например А BD78563412 mov ebp, 0x12345678 0804808F ' 892DC0900408 mov dword [ + 0x80490c0] , ebp Первая колонка — это реальный адрес команды в памяти, вторая — машинный код, в который превратилась команда после компиляции. После этого мы видим символьное представление машинной команды в мнемокодах ассемблера. Символическое имя нашей переменной counter было заменено адресом этой переменной в памяти (0х80490с0). Перед выполнением первой команды, mov ebp, 0x12 345 67 8, регистры процессора содержали следующие значения еах = 0x00000000 ebx = 0x00000000 есх = 0x00000000 edx = 0x00000000 esp = 0xBFFFF910 ebp = 0x00000000 e s i = 0x00000000 edi = 0x00000000 ds = 0x0000002B es = 0x0000002B fs = 0x00000000 gs = 0x00000000 ss = 0x0000002B cs = 0x00000023 eip = 0x0804808A eflags = 0x00200346
Flags: PF ZF TF IF ID После выполнения первой команды значение регистра ЕВР было заменено значением 0x12345678. Если просмотреть дамп памяти по адресу нашей переменной (0х80490с0), то мы увидим следующее
Dumping 64 b y t e s of memory s t a r t i n g at 0x080490C0 in hex
080490C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
39
Ассемблер на примерах. Базовый курс Когда будет выполнена вторая команда MOV, значение 0x12345678 будет записано в память по адресу 0х80490с0 и дамп памяти покажет другой результат
Dumping 64 b y t e s of memory s t a r t i n g at 0x080490C0 in hex
080490C0: 78 56 34 12 00 00 00 00 00 00 00 00 00 00 00 00 xV4 Требуемое значение (0x12345678) действительно было записано по адресу
0х80490с0, но почему-то задом наперед. Дело в том, что все х86-процессоры относятся к классу LITTLE_ENDIAN, то есть хранят байты слова или двойного слова в порядке от наименее значимого к наиболее значимому («little-end-first», младшим байтом вперед. Процессоры BIG_ENDIAN (например, Motorola) поступают наоборот размещают наиболее значимый байт по меньшему адресу
(«big-end-first», старшим байтом вперед. Примечание. Любопытно, что термины LITTLEENDIAN и BIG_ENDIAN — это непросто сокращения они происходят от названий соперничавших в Лилипутии партий «остроконечников» и «тупоконечников» из Путешествий Гулливера» Джонатана Свифта, отсюда и название этого пункта. «Остроконечники» и «тупоконечники» у Свифта кушали яйца с разных концов, одни — с острого, другие — с тупого. В результате спора по поводу того, как правильнее, развязалась война, а дальше ... если не помните, возьмите книжку и почитайте. Порядок следования байтов в слове (двойном слове) учитывается не только при хранении, но и при передаче данных. Если у вас есть небольшой опыт программирования, возможно, вы сталкивались с «остроконечниками и тупоконечниками» при разработке сетевых приложений, когда некоторые структуры данных приходилось преобразовывать к тупоконечному виду при помощи специальных функций (htonl, htons, ntohl, ntohs). Когда переменная counter будет прочитана обратно в регистр, там окажется оригинальное значение, то есть 0x12345678.
4.3. Арифметические команды Рассмотренная выше команда MOV относится к группе команд перемещения значений, другие команды из которой мы рассмотрим позже. А сейчас перейдем к следующей группе чаще всего используемых команд — арифметическим операциям. Процессор 80386 не содержит математического сопроцессора, поэтому мы рассмотрим только целочисленную арифметику, которая полностью поддерживается процессором 80386. Каждая арифметическая команда изменяет регистр признаков.
40
Глава 4. Основные команды языка ассемблера
4.3.1. Инструкции сложения ADD и вычитания SUB Начнем с самого простого — сложения (ADD) и вычитания (SUB). Команда
ADD требует двух операндов, как и команда MOV:
ADD o l , о Команда ADD складывает оба операнда и записывает результат в ol, предыдущее значение которого теряется. Точно также работает команда вычитания — SUB:
SUB o l , o2 Результат, ol-o2, будет сохранен в ol, исходное значение ol будет потеряно. Теперь рассмотрим несколько примеров mov ax, 8 ; заносим в АХ число 8 mov сх, 6 заносим в СХ число 6 mov dx, сх копируем СХ в DX, DX = б add dx, ax ;DX = DX + АХ Поскольку мы хотим избежать уничтожения (то есть перезаписи результатом) исходных значений и хотим сохранить оба значения — АХ и СХ, мы копируем оригинальное значение СХ в DX, а затем добавляем АХ к DX. Команда ADD сохранит результат DX + АХ в регистре DX, а исходные значения АХ и СХ останутся нетронутыми. Рассмотрим еще примеры использования инструкций ADD и SUB: add e a x , 8 sub e c x , e b p add b y t e [number] sub word [number], 4 add dword [number], 4 sub b y t e [number], a l sub a h , a l
EAX = EAX + 8
ECX = ECX - EBP добавляем значение 4 к переменной number размером в 1 байт диапазон значений 0-255) number = number — 4 размером в 2 байта диапазон значений 0-65535) добавляем значение 00000004 к "number" вычитаем значение регистра AL из "number" вычитаем AL из АН, результат помещаем в АН Что произойдет, если сначала занести в AL (разрядный регистр) наибольшее допустимое значение (255), а затем добавить к нему 8? mov a l , 255 ; заносим в AL значение 255, то есть OxFF add a l , 8 ; добавляем 8 В результате в регистре AL мы получим значение 7.
41
Ассемблер на примерах. Базовый курс Рис. 4.1. Сложение 255 (OxFF) + 8 Номы ведь ожидали 0x107 (263 в десятичном виде. Что случилось В регистре AL может поместиться только разрядное число (максимальное значение — 255). Девятый, потерянный, бит скрыт в регистре признаков, а именно в флаге CF — признак переноса. Признак переноса используется в арифметических командах при работе с большими диапазонами чисел, чем могут поддерживать регистры. Полезны для этого команды ADC (Add With
Carry — сложение с переносом) и SBB (Subtract With Borrow — вычитание с займом
ADC o l , o2 ; o l = Ol + о + CF
SBB o l , o2 ; o l = o l - о - CF Эти команды работают также, как ADD и SUB, но соответственно добавляют или вычитают флаг переноса CF. В контексте арифметических операций очень часто используются так называемые пары регистров. Пара — это два регистра, использующихся для хранения одного числа. Часто используется пара EDX:EAX (или DX:AX) — обычно приумножении. Регистр АХ хранит младшие 16 битов числа, a DX — старшие 16 битов. Таким способом даже древний 80286 может обрабатывать разрядные числа, хотя у него нет ни одного разрядного регистра. Пример пара DX:AX содержит значение OxFFFF (АХ = OxFFFF, DX = 0). Добавим 8 к этой паре и запишем результат обратно в DX:AX: mov a x , O x f f f f ;AX = OxFFFF mov d x , 0 ;DX = 0 a d d a x , 8 ;AX = AX + 8 a d c d x , 0 ; добавляем Оспе рено сом к DX Первая команда ADD добавит 8 к регистру АХ. Полностью результат не помещается в АХ, поэтому его старший бит переходит в CF. Вторая команда добавит к DX значение 0 и значение CF. После выполнения ADC флаг CF будет добавлен к DX (DX теперь равен 1). Результат сложения OxFFFF и 8 (0x10007) будет помещен в пару DX:AX
(DX=1,AX=0007).
42
Глава 4. Основные команды языка ассемблера Рис. 4.2. Сложение чисел OxFFFF и 0x0008 и их сохранение в регистрах Процессор 80386 может работать с разрядными числами напрямую — безо всяких переносов mov e a x , O x f f f f ;ЕАХ = OxFFFF a d d e a x , 8 ;ЕАХ = ЕАХ + 8 Рис. 4.3. Использование разрядных регистров процессора 80386 После выполнения этих инструкций в ЕАХ мы получим разрядное значение
0x10007. Для работы с разрядными числами мы можем использовать пару
EDX:EAX — точно также, как мы использовали пару DX:AX.
4.3.2. Команды инкрементирования INC и декрементирования DEC Эти команды предназначены для инкрементирования и декрементирования. Команда INC добавляет, a DEC вычитает единицу из единственного операнда. Допустимые типы операнда — такие же, как у команд ADD и SUB, а формат команд таков
INC Ol
DEC Ol
; o l = o l + 1
; o l = o l - 1 ВНИМАНИЕ Ни одна из этих инструкций не изменяет флаг CF. О значении этого факта и следствиях из него, а также о том, как безопасно (без потери
43
Ассемблер на примерах. Базовый курс данных) использовать эти команды, будет сказано в главе, посвященной оптимизации. add a l , l inc a l Увеличение на единицу значения регистра AL выглядит следующим образом
;AL = AL + 1
;AL = AL + 1 Увеличение на единицу значения 16-битной переменной number: inc word [number] мы должны указать размер переменной — word
4.3.3. Отрицательные числа — целые числа со знаком Отрицательные целые числа в ПК представлены в так называемом дополнительном коде. Дополнительный код можно представить себе как отображение некоторого диапазона, включающего положительные и отрицательные целые числа, на другой диапазон, содержащий только положительные целые числа. Рассмотрим код дополнения одного байта. Один байт может содержать числа в диапазоне от 0 до 255. Код дополнения заменяет этот диапазон другим — от -128 до 127. Диапазон от 0 до 127 отображается сам на себя, а отрицательным числам сопоставляется диапазон от
128 до 255: числу -1 соответствует число 255 (OxFF), -2 — 254 (OxFE) и т.д. Число -50 будет представлено как 206. Обратите внимание самый старший бит отрицательного числа всегда устанавливается в 1 — так можно определить, что число отрицательное. Процесс отображения отрицательных чисел в дополнительный код иногда называют маппингом
(mapping). Дополнительный код может быть расширен до 2 байтов (от 0 до 65535). Он будет охватывать диапазон чисел от -32768 до 32767. Если дополнительный код расширить до 4 байтов, то получим диапазон от -2 147 483 648 до 2 147 483 647. Во многих языках программирования именно этот диапазон используется для код целочисленного типа данных (integer). Пример преобразуем числа 4, -4,386, -8000 ив дополнительный код, считая, что целевой диапазон — 16 бит (2 байта. Прежде всего, выясним, сколько чисел может поместиться в 16 разрядов. Это очень просто возведем 2 в степень 16.
2 16
= 65 536, что соответствует диапазону от 0 до 65 535. Теперь определим границы
65 536 / 2 = 32 768. Итак, мы получили диапазон от -32 768 до 32 767 (0 — это положительное число.
256(0x100)
206(0хСЕ) Интерпретация числа
1—
128 Дополнительный код для 1 байта Рис. 4.4. Дополнительный код для 1 байта
44
Глава 4. Основные команды языка ассемблера Первое число, 4, попадает в диапазон <0, 32 767>, поэтому оно отображается само на себя — в целевом диапазоне это будет тоже число 4. Число -4 — отрицательное, оно находится в диапазоне <-32 768, 0>. В целевом диапазоне оно будет представлено как 65 536 — 4 = 65 532. Число 386 останется само собой. Число -8 000 — отрицательное, в результате отображения получается
65 536 — 8 000 = 57 536 — это и будет число -8 000 в дополнительном коде. И, наконец, число 45 000 не может быть представлено в дополнительном коде, поскольку оно выходит за пределы диапазона. Выполнять арифметические операции над отрицательными числами в дополнительном коде можно при помощи обычных команд ADD и SUB. Рассмотрим, как это происходит, на примере суммы чисел -6 ив дополнительном коде из
2 байтов. Число 7 будет отображено само в себя, а число -6 будет представлено числом 65 536 — 6 = 65 530 (OxFFFA). Что получится, если мы сложим два эти числа (7 и 65 530)? Попробуем решить эту задачу на языке ассемблера mov ax,OxFFFA ;AX = - 6 , то есть или OxFFFA mov dx,7 ;DX = 7 add a x , d x ;AX = AX + DX Мы получим результат 65 530 + 7 = 65 537 = 0x10001, который не помещается в регистре АХ, поэтому будет установлен флаг переноса. Но если мы его проигнорируем, то оставшееся в АХ значение будет правильным результатом Механизм дополнительного кода ввели именно для того, чтобы при сложении и вычитании отрицательных чисел не приходилось выполнять дополнительных действий. Теперь давайте сложим два отрицательных числа. Ассемблер NASM позволяет указывать отрицательные числа непосредственно, поэтому нам ненужно преобразовывать их вручную в дополнительный код mov ax, -6 АХ = -6 mov dx,- 6 ;DX = - 6 add ax,dx ;AX = AX + DX Результат 0xFFF4 (установлен также флаг CF, номы его игнорируем. В десятичной системе 0xFFF4 = 65 524. В дополнительном коде мы получим правильный результат -12 (65 536 — 65 524 = 12). Отрицательные числа также могут использоваться при адресации памяти. Пусть регистр ВХ содержит адреса нам нужен адрес предыдущего байта, номы не хотим изменять значение регистра ВХ (предполагается, что процессор находится в реальном режиме mov ах Ь х - 1 ] поместить в АХ значение по адресу на единицу меньшему, чем хранится в ВХ Значение -1 будет преобразовано в OxFFFF, и инструкция будет выглядеть так MOV AX, [BX+OxFFFF]. При вычислении адреса не учитывается флаг
CF, поэтому мы получим адрес, на единицу меньший.
45
Ассемблер на примерах. Базовый курс
4.3.4. Команды для работы с отрицательными числами Команда NEG Система команд процессора 80386 включает в себя несколько команд, предназначенных для работы с целыми числами со знаком. Первая из них — команда
NEG (negation, отрицание
NEG r/m8
NEG Г/Ш16
NEG Г/Ш32 Используя NEG, вы можете преобразовывать положительное целое число в отрицательное и наоборот. Инструкция NEG имеет только один операнд, который может быть регистром или адресом памяти. Размер операнда — любой 8, 16 или 32 бита. пед еах изменяет знак числа, сохраненного в ЕАХ пед Ы тоже самое, но используется 8-битный
; регистр Ы пед b y t e [number] изменяет знак 8-битной переменной number Расширение диапазона целого беззнакового числа делалось просто мы просто копировали число в больший регистра расширенное место заполняли нулями. При работе с целыми числами со знаком мы должны заполнить это место старшим битом преобразуемого числа. Так мы можем сохранять положительные и отрицательные числа при расширении их диапазона. Расширение диапазона числа со знаком называется знаковым расширением. Процессор имеет несколько специальных команд, предназначенных для знакового расширения. Эти команды не имеют операндов, они выполняют действия над фиксированными регистрами. Команда CBW Команда CBW копирует седьмой (старший) бит регистра AL в регистр АН, расширяя таким образом оригинальное значение регистра AL в значение со знаком регистра АХ (значение АН становится равно 0x00 или OxFF = 11111111b, в зависимости от старшего бита AL). Сложно Ничего, скоро рассмотрим пару примеров, и все станет на свои места.
15 АХ о
I АН ! AL I
15 8 7 О Рис 4.5. Знаковое расширение с помощью инструкции CBW
46
Глава 4. Основные команды языка ассемблера Команда CWD Команда CWD копирует старший бит АХ в регистр DX, расширяя таким образом оригинальное значение АХ в пару регистров со знаком DX:AX. Рис. 4.6. Знаковое расширение с помощью инструкции CWD Команда CDQ Команда CDQ копирует старший бит ЕАХ в регистр EDX, расширяя таким образом оригинальное значение ЕАХ в пару регистров со знаком EDX:EAX. Команда CWDE Команда CWDE копирует старший бит АХ в верхнюю часть (старшую часть)
ЕАХ, расширяя таким образом оригинальное значение АХ в двойное слово со знаком, которое будет помещено в регистр ЕАХ. Рис 4.7. Знаковое расширение с помощью инструкции CWDE Рассмотрим пару примеров mov a l , -l ;AL = -1 (или OxFF) cbw знаковое расширение навесь АХ После выполнения команды CBW АХ будет содержать значение OxFFFF, то есть -1. Единица (1) старшего разряда заполнила все биты АН, и мы получили знаковое расширение AL навесь регистр АХ.
47
Ассемблер на примерах. Базовый курс mov ax, 4 АХ = 4 cwd выполним знаковое расширение в DX Первая команда заносит в регистр АХ значение 4. Вторая команда, CWD, производит знаковое расширение АХ в пару DX:AX. Оригинальное значение
DX заменяется новым значением — старшим битом регистра АХ, который в этом случае равен 0. В результате мы получили 0 в регистре DX. Иногда команда CWD полезна для очищения регистра DX, когда АХ содержит положительное значение, то есть значение, меньшее 0x8000.
4.3.5. Целочисленное умножение и деление Давайте познакомимся с оставшимися целочисленными операциями умножением и делением. Первое арифметическое действие выполняется командой
MUL, а второе — командой DIV. Дополнительный код делает возможным сложение и вычитание целых чисел со знаком и без знака с помощью одних и тех же команд ADD и SUB. Но к умножению и делению это не относится для умножения и деления чисел со знаком служат отдельные команды — IMUL и IDIV. Операнды этих инструкций такие же, как у MUL и DIV. Операции умножения и деления имеют свою специфику. В результате умножения двух чисел мы можем получить число, диапазон которого будет в два раза превышать диапазон операндов. Деление целых чисел — это операция целочисленная, поэтому в результате образуются два значения частное и остаток. С целью упрощения реализации команд умножения и деления эти команды спроектированы так, что один из операндов и результат находятся в фиксированном регистре, а второй операнд указывается программистом. Подобно командами, команды MUL, DIV, IMUL, IDIV изменяют регистр признаков. Команды MUL и IMUL Команда MUL может быть записана в трех различных форматах — в зависимости от операнда
MUL r/m8
MUL r/ml 6
MUL r/m32 В разрядной форме операнд может быть любым 8-битным регистром или адресом памяти. Второй операнд всегда хранится в AL. Результат (произведение) будет записан в регистр АХ.
1   2   3   4   5   6   7   8   9   ...   20

48
Глава 4. Основные команды языка ассемблера
(r/m8) * AL -> АХ В разрядной форме операнд может быть любым 16-битным регистром или адресом памяти. Второй операнд всегда хранится в АХ. Результат сохраняется в паре DX:AX.
(r/ml6) * АХ -> DX:AX В разрядной форме второй операнд находится в регистре ЕАХ, а результат записывается в пару EDX.EAX.
(r/m32) * ЕАХ -> EDX:ЕАХ Рассмотрим несколько примеров. Пример 1: умножить значения, сохраненные в регистрах ВН и CL, результат сохранить в регистр АХ mov a l , bh ;AL = ВН — сначала заносим в AL второй операнд mul cl АХ = AL * CL — умножаем его на CL Результат будет сохранен в регистре АХ. Пример вычислить 486 2
, результат сохранить в DX:AX: mov ax, 486 ; АХ = 486 mul ах ; АХАХ Пример 2: вычислить диаметр по радиусу, сохраненному в 8-битной переменной r a d i u s l , результат записать в 16-битную переменную d i a m e t e r l : mov a l , 2 ; AL = 2 mul byte [ r a d i u s l ] ; AX = r a d i u s * 2 mov [diameterl],ax ; diameter <- AX Вы можете спросить, почему результат разрядного умножения сохранен в паре DX:AX, а не в каком-то разрядном регистре Причина — совместимость с предыдущими разрядными процессорами, у которых не было разрядных регистров. Команда IMUL умножает целые числа со знаком и может использовать один, два или три операнда. Когда указан один операнд, то поведение IMUL будет таким же, как и команды MUL, просто она будет работать с числами со знаком. Если указано два операнда, то инструкция IMUL умножит первый операнд на второй и сохранит результат в первом операнде, поэтому первый операнд всегда должен быть регистром. Второй операнд может быть регистром, непосредственным значением или адресом памяти. imul e d x , e c x ;EDX = EDX * ECX imul e b x , [ s t h i n g ] умножает разрядную переменную
; " s t h i n g " на ЕВХ, результат будет сохранен в ЕВХ
49
Ассемблер на примерах. Базовый курс imul есх,6 ; ЕСХ = ЕСХ * 6 Если указано три операнда, то команда IMUL перемножит второй и третий операнды, а результат сохранит в первый операнд. Первый операнд — только регистр, второй может быть любого типа, а третий должен быть только непосредственным значением imul edx,ecx,7 imul ebx,[sthing],9 imul ecx,edx,ll
EDX = ECX * 7 умножаем переменную "sthing" на 9, результат будет сохранен EBX
ЕСХ = EDX * 11 Команды DIV и IDIV Подобно команде MUL, команда DIV может быть представлена в трех различных форматах в зависимости от типа операнда
- DIV r/m8
DIV r/ml 6
DIV r/m32 Операнд служит делителем, а делимое находится в фиксированном месте (как в случае с MUL). В 8-битной форме переменный операнд (делитель) может быть любым 8-битным регистром или адресом памяти. Делимое содержится в АХ. Результат сохраняется так частное — в AL, остаток — в АН. АХ / (r/m8) -> AL, остаток -> АН В 16-битной форме операнд может быть любым 16-битным регистром или адресом памяти. Второй операнд всегда находится в паре DX:AX. Результат сохраняется в паре DX:AX (DX — остаток, АХ — частное.
DX:AX / (r/ml6) -> АХ, остаток -> DX В разрядной форме делимое находится в паре EDX:EAX, а результат записывается в пару EDX:EAX (частное в ЕАХ, остаток в EDX).
EDX:EAX / (r/m32) -> ЕАХ, остаток -> EDX Команда IDIV используется для деления чисел со знаком, синтаксис ее такой же, как у команды DIV. Рассмотрим несколько примеров. Пример 1: разделить 13 на 2, частное сохранить в BL, а остаток в — ВН: mov ах mov c l , 2 div c l mov bx,ax АХ = 13
CL = 2 делим на CL ожидаемый результат находится в АХ, копируем в ВХ
50
Глава 4. Основные команды языка ассемблера Пример 2: -вычислить радиус по диаметру, значение которого сохранено в 16-битной переменной diameterl, результат записать в radiusl, а остаток проигнорировать. mov a x , [ d i a m e t e r l j mov Ы , 2 div Ы mov [ r a d i u s l ] , a l
AX = d i a m e t e r l загружаем делитель 2 делим сохраняем результат. Логические команды К логическим операциям относятся логическое умножение (И, AND), логическое сложение (ИЛИ, OR), исключающее ИЛИ (XOR) и отрицание (NOT). Все эти инструкции изменяют регистр признаков. Команда AND Команда AND выполняет логическое умножение двух операндов — ol и о. Результат сохраняется в операнде ol. Типы операндов такие же, как у команды ADD: операнды могут быть 8-, 16- или 32-битными регистрами, адресами памяти или непосредственными значениями.
AND o l , o2 Таблица истинности для оператора AND приведена ниже (табл. 4.1). Таблица истинности для оператора AND Таблица 4.1 А
0
0
1
1
b
0
1
0
1
a
AND b
0
0
0
1 Следующий пример вычисляет логическое И логической единицы и логического нуля (1 AND 0). mov a l , 1 mov b l , 0 and a l . b l
AL = one
BL = z e r o
AL = AL and BL = 0 Тот же самый примерно записанный более компактно

mov a l , 1 ;AL = one and a l , 0 ; AL = AL and 0 = 1 and 0 = 0 51
Ассемблер на примерах. Базовый курс Команда OR Команда OR выполняет логическое сложение двух операндов — ol и о. Результат сохраняется в операнде ol. Типы операндов такие же, как у команды AND.
OR o l , o2 Таблица истинности для оператора OR приведена ниже (табл. 4.2). Таблица истинности для оператора OR Таблица 4.2 А
0
0
1
1
b
0
1
0
1
a
OR b
0
1
1
1 Дополнительные примеры использования логических команд будут приведены в последнем пункте данной главы. А пока рассмотрим простой пример установки наименее значимого бита (первый справа) переменной mask в 1. or byte [mask],1 Команда XOR Вычисляет так называемое исключающее ИЛИ операндов ol и о. Результат сохраняется во. Типы операндов такие же, как у предыдущих инструкций. Формат команды
XOR o l , о Таблица истинности для оператора XOR приведена ниже (табл. 4.3). Таблица истинности для оператора XOR Таблица 4.3 а

0
0
1
1
b
0
1
0
1
a
XOR b
0
1
1
0 Исключающее ИЛИ обратимо выражение (х хог у) хог у) снова возвратит х. mov al,0x55 ; AL = 0x55 хог al,0xAA ;AL = AL xor OxAA xor al,0xAA возвращаем в AL исходное значение - 0x55 52
Глава 4. Основные команды языка ассемблера Команда NOT Используется для инверсии отдельных битов единственного операнда, который может быть регистром или памятью. Соответственно команда может быть записана в трех различных форматах
NOT r/m8
NOT r/ml 6
NOT r/m32 Таблица истинности для оператора NOT приведена ниже (табл. 4.4). Таблица истинности для оператора NOT Таблица 4.4 А
0
1
NOT a
1
0 Следующий пример демонстрирует различие между операциями N O T и
NEG:
mov al,00000010b ;AL = 2 mov bl,al ;BL = 2 not al после этой операции мы получим
;11111101b = OxFD (-3) neg bl ;a после этой операции результат будет другим 11111110 = OxFE (-2) Массивы битов (разрядные матрицы) Любое число можно записать в двоичной системе в виде последовательности нулей и единиц. Например, любое разрядное число состоит из 16 двоичных цифр — 0 и 1. Мы можем использовать одно число для хранения шестнадцати различных состояний — флагов. Нам ненужно тратить место на хранение 16 различных переменных, ведь для описания состояния (включено/выключено) вполне достаточно 1 бита Переменная, используемая для хранения флагов, называется разрядной матрицей или массивом битов.
Высокоуровневые языки программирования также используют разрядные матрицы, например, при хранении набора значений перечисления, для экономии памяти. Мы уже знакомы с одной разрядной матрицей, которая очень часто используется в программировании — это регистр признаков микропроцессора. Мы будем очень часто сталкиваться с разрядными матрицами при программировании различных устройств видеоадаптера, звуковой платы и т.д. В этом случае изменение одного бита в разрядной матрице может изменить режим работы устройства.
53
Ассемблер на примерах. Базовый курс Для изменения значения отдельных битов в матрице служат логические операции. Первый операнд задает разрядную матрицу, с которой мы будем работать, а второй операнд задает так называемую маску, используемую для выбора отдельных битов. Для установки определенных битов массива в единицу (все остальные биты при этом должны остаться без изменения) применяется команда OR. В качестве маски возьмите двоичное число, в котором единицы стоят на месте тех битов, которые вы хотите установить в массиве. Например, если вы хотите установить первый и последний биты массива, вы должны использовать маску 10000001. Все остальные биты останутся нетронутыми, поскольку 0 OR
X всегда возвращает X. Чтобы сбросить некоторые биты (установить их значение в 0), возьмите в качестве маски число, в котором нули стоят на месте тех битов, которые вы хотите сбросить, а единицы — во всех остальных позициях, а потом используйте команду AND. Поскольку 1 AND X всегда возвращает X, мы сбросим только необходимые нам биты. Рассмотрим несколько примеров. Пример В регистре AL загружен массив битов. Нужно установить все нечетные позиции в 1. Предыдущее состояние массива неизвестно. or a l , 10101010b маска устанавливает все нечетные биты в 1 Пример В массиве битов, хранящемся в регистре AL, сбросить й и й биты, все остальные оставить без изменения. Исходное состояние массива также неизвестно. and a l , 01111110b каждая 1 в маске сохраняет битв ее позиции С помощью XOR также можно изменять значения битов, не зная предыдущего состояния. Для этого в маске установите 1 для каждого бита, который вы хотите инвертировать (0 станет 1, а 1 станет 0), а для всех оставшихся битов установите 0. Если мы выполним XOR дважды, то получим исходное значение. Такое поведение операции XOR позволяет использовать эту команду для простого шифрования к каждому байту шифруемых данных применяется XOR с постоянной маской (ключом, а для дешифровки тот же ключ применяется
(XOR) к шифрованным данным.
54
Управляющие конструкции Последовательное выполнение команд Конструкция «IF THEN» выбор пути Итерационные конструкции — циклы Команды обработки стека Ассемблер на примерах. Базовый курс
Программа любой сложности на любом языке программирования может быть написана при помощи всего трех управляющих структур линейной, условия и цикла. В этой главе мы рассмотрим эти три краеугольных камня программирования и реализацию их в языке ассемблера, а в конце главы вы узнаете о стеке, который нужен для использования подпрограмм.
5.1. Последовательное выполнение команд Последовательная обработка знакома нам еще с концепции фон Неймана. Каждая программа состоит из одной или нескольких последовательностей отдельных элементарных команд. Последовательность здесь означает участок программы, где команды выполняются одна за другой, без любых переходов. В более широком контексте языка программирования высокого уровня можно рассматривать целую программу как последовательность, состоящую как из элементарных команд, таки из управляющих конструкций — условных и итерационных. Если программа не содержит других конструкций, кроме последовательности элементарных команд, она называется линейной.
, j . Инструкция 1 Инструкция 2 Рис. 5.1. Последовательная обработка команд
56
Глава 5. Управляющие конструкции
5.2. Конструкция «IF THEN» — выбор пути В языках программирования высокого уровня конструкция выбора известна как оператор IF-THEN. Эта конструкция позволяет выбрать следующее действие из нескольких возможных вариантов в зависимости от выполнения определенного условия. В языке ассемблера механизм выбора реализован посрздством команд сравнения, условного и безусловного переходов. Рис. 5.2. Выбор следующей команды
5 . 2 . 1 . Команды СМР и TEST Команды СМР и TEST используются для сравнения двух операндов. Операндами могут быть как регистры, таки адреса памяти, размер операнда — 8,
16 или 32 бита.
СМР o l ,
о Команда СМР — это сокращение от «compare», сравнить. Она работает подобно SUB: операнд о вычитается из ol. Результат нигде не сохраняется, команда просто изменяет регистр признаков. Команда СМР может использоваться как для сравнения целых беззнаковых чисел, таки для сравнения чисел со знаком. Команда TEST работает подобно СМР, но вместо вычитания она вычисляет поразрядное И операндов. Результат инструкции — измененные флаги регистра признаков. Мы можем использовать TEST для проверки значений отдельных битов в массиве битов. Проиллюстрируем эти команды несколькими примерами страх сравниваем АХ со значением 4 стр d l , a h сравниваем DL с АН страх сравниваем переменную " d i a m e t e r l " с АХ
57
Ассемблер на примерах. Базовый курс cmp a x , [ d i a m e t e r l ] сравниваем АХ с переменной "diameterl" стр еах,есх сравниваем регистры ЕАХ и ЕСХ t e s t ax, 00000100b проверяем значение второго третьего справа) бита
5.2.2. Команда безусловного перехода — JMP Самый простой способ изменить последовательность выполнения команд заключается в использовании команды jmp — так называемой команды безусловного перехода. Она перезаписывает указатель команд (регистр IP или
CS), что заставляет процессор переключиться на выполнение команды по указанному адресу. Формат команды таков
JMP [тип_перехода] операнд Команда JMP — аналог конструкции GOTO, которая используется в высо­
коуровневых языках программирования. Название команды объясняет ее действие, а именно «jump», переход. Команде нужно передать один обязательный операнд — адрес в памяти, с которого процессор должен продолжить выполнение программы. Операнд может быть указан явно (непосредственное значение адреса) или быть регистром общего назначения, в который загружен требуемый адрес. Но новичкам я никогда не рекомендовал бы это делать язык ассемблера, подобно языкам программирования высокого уровня, позволяет обозначить адрес назначения при помощи метки. В зависимости от расстояния переходы бывают трех типов короткие
(short), ближние (near) и дальние (far). Тип перехода задается необязательным параметром инструкции jmp. Если тип не задан, по умолчанию используется тип near. Максимальная длина короткого перехода (то есть максимальное расстояние между текущими целевым адресом) ограничена. Второй байт инструкции операнд) содержит только одноразрядное значение, поэтому целевой адрес может быть в пределах от -128 до 127 байтов. При переходе выполняется знаковое расширение разрядного значения и его добавление к текущему значению ЕР. Длина ближнего перехода (near) зависит только от режима процессора. В реальном режиме меняется только значение IP, поэтому мы можем путешествовать только в пределах одного сегмента (то есть в пределах 64 Кб); в защищенном режиме используется EIP, поэтому целевой адрес может быть где угодно в пределах 4 Гб адресного пространства. Переход типа far модифицирует кроме IP еще и сегментный регистр CS, который используется при вычислении фактического адреса памяти. Поэтому команда перехода должна содержать новое значение CS.
58
Глава 5. Управляющие конструкции Сейчас мы совершим дальний переход от предмета нашего рассмотрения и поговорим о метках в языке ассемблера. Вкратце, метка — это идентификатор, заканчивающийся двоеточием. Вовремя компиляции он будет заменен точным адресом согласно его позиции в программе. Рассмотрим следующий фрагмент кода mov ах АХ = 4 new_loop : метка new_loop mov bx, ах копируем АХ в ВХ Чтобы перейти к метке new__loop из другого места программы, используйте команду jmp new_loop переходим к new_loop После выполнения этой команды выполнение программы продолжится сметки. Если вам нужно сначала написать инструкцию перехода и только потом определить метку, нет проблем компилятор обрабатывает текст программы в несколько проходов и понимает такие забегания вперед jmp s t a r t переход наметка"
;какие-то команды s t a r t : метка " s t a r t " jmp f i n i s h переход на " f i n i s h " Теперь давайте вернемся к различным типам переходов даже если мы новички, мы все равно будем изредка их использовать. Короткий переход полезен в ситуации, где метка назначения находится в пределах 128 байтов. Поскольку команда короткого перехода занимает 2 байта, команда ближнего перехода занимает 3 байта, а дальнего — 5 байтов, мы можем сэкономить байт или три. Если вы не можете оценить правильное расстояние, все равно можете попробовать указать s h o r t — в крайнем случае, компилятор выдаст ошибку n e a r _ l a b e l : метка " n e a r _ l a b e l " несколько команд jmp short near_label переходим к "near_label"
5.2.3. Условные переходы — Jx Другой способ изменения последовательности выполнения команд заключается в использовании команды условного перехода. В языке ассемблера имеется множество команд условного перехода, и большинство из них вам нужно знать — иначе вы не сможете написать даже
59
Ассемблер на примерах. Базовый курс средненькую программку. Имена этих команд различаются в зависимости от условия перехода. Условие состоит из значений одного или нескольких флагов в регистре признаков. Работают эти команды одинаково если условие истинно, выполняется переход на указанную метку, если нетто процессор продолжит выполнять программу со следующей команды. Общий формат команд условного перехода следующий
Jx метка_назначения Рассмотрим наиболее часто встречающиеся команды jz i s _ t r u e переходит к i s _ t r u e , если флаг ZF = 1 jc i s _ t r u e переходит к i s _ t r u e , если флаг CF = 1 js i s _ t r u e переходит к i s _ t r u e , если флаг SF = 1 jo i s _ t r u e переходит к i s _ t r u e , если флаг переполнения
;OF = 1 Любое условие может быть инвертировано, например jnz is_true переходит к is_true, если флаг ZF = О Также образованы имена команд JNC, JNS и JNO. Рассмотрим сводную таблицу команд условного перехода в зависимости от условия, которое проверяет процессор (чтобы не писать для перехода, будем использовать сокращение jump) (см. табл. 5.1). Сводная таблица команд условного перехода Таблица 5.1 Инструкции для беззнаковых чисел Инструкции для чисел со знаком о1==о2 о1=о2
JE(JZ)
Jump, если равно
Jump, если 0
JE(JZ)
Jump, если равно
Jump, если 0 о1!=о2 о Ко, если неравно, если не 0
JNE(JNZ)
Jump, если неравно, если неО о1>о2
JA(JNBE)
Jump, если больше
Jump, если не меньше или равно
JG(JNLE)
Jump, если больше
Jump, если не меньше или равно о Ко, если меньше
Jump, если не больше или равно
JL(JNGE)
Jump, если меньше
Jump, если не больше или равно о1=<о2
JNA(JBE)
Jump, если не больше
Jump, если меньше или равно
JNG(JLE)
Jump, если не больше
Jump, если меньше или равно о1>=о2
JNB(JAE)
Jump, если не меньше
Jump, если больше или равно
JNL(JGE)
Jump, если не меньше
Jump, если больше или равно Впервой строке таблицы указано условие перехода. Во второй строке показаны соответствующие команды условного перехода (в скобках — их дополнительные названия. Чтобы лучше запомнить имена команд, запомните несколько английских слов equal — равно, above — больше, below — ниже, zero — ноль, greater — больше, less — меньше. Таким образом, JE — Jump if Equal (Переход, если Равно, JNE — Jump if Not Equal (Переход, если Не Равно, JA — Jump if Above (Переход, если больше) и т.д.
1   2   3   4   5   6   7   8   9   ...   20