Ассемблер на примерах. Базовый курс

233

Upload: krittoss-kriogen

Post on 29-Jul-2015

1.636 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Ассемблер на примерах. Базовый курс
Page 2: Ассемблер на примерах. Базовый курс

Rudolf Marek

Ucime se programovat V jazyce Assembler pro PC

Computer Press Brno

Page 3: Ассемблер на примерах. Базовый курс
Page 4: Ассемблер на примерах. Базовый курс

Рудольф Марек

АССЕМБЛЕР на примерах

Б а з о в ы й к у р с

иТ Наука и Техника, Санкт-Петербург

2005

Page 5: Ассемблер на примерах. Базовый курс

Рудольф Марек. Ассемблер на примерах. Базовый курс. — СПб: Наука и Техника, 2005. — 240 с: ил.

ISBN 5-94387-232-9

Серия «Просто о сложном»

Эта книга представляет собой великолепное практическое руководство по основам программирования на языке ассемблера. Изложение сопровождается большим количеством подробно откомментированных примеров, что способствует наилучшему пониманию и усвоению материала. Доходчиво объясняются все основные вопросы программирования на этом языке.

Вы узнаете, как писать ассемблерные программы под разные операционные системы (Windows, DOS, Linux), как создавать резидентные программы, как писать ассемблерные вставки в программы на языках высокого уровня и многое другое. Попутно вам будут разъяснены основные моменты работы процессора, операционных систем, управления памятью и взаимодействия программ с аппаратными устройствами ПК - то есть все то, без знания чего нельзя обойтись при программировании на языке низкого уровня, которым и является ассемблер.

Книга написана доступным языком. Лучший выбор для начинающих.

Русское издание под редакцией Финкова М.В. и Березкиной О.И.

Copyright © Computer Press 2004 Ucime se programovat vjazyce Assembler pro PC by Rudolf Marek, ISBN: 80-722-6843-0.

All rights reserved

9 ' i 7 9 5 9 4 3 l l 8 7 2 3 2 6 ISBN 5-94387-232-9

Контактные телефоны издательства (812) 567-70-25, 567-70-26 (044)516-38-66

Официальный сайт www.nit.com.ru

© Перевод на русский язык. Наука и Техника, 2005

© Издание на русском языке, оформление, Наука и Техника, 2005

0 0 0 «Наука и Техника». Лицензия №000350 от 23 декабря 1999 года.

198097, г. Санкт-Петербург, ул. Маршала Говорова, д. Подписано в печать 08.08.05. Формат 70x100 1/16. Бумага газетная. Печать офсетная. Объем 15 п. л.

Тираж 5000 экз. Заказ № 293

29.

О-гаечатано с готовых диапозитивов в ОАО «Техническая книга» 190005, Санкт-Петербург, Измайловский пр., 29

Page 6: Ассемблер на примерах. Базовый курс

Содержание

Введение 10

Глава 1. Базовые системы счисления и термины 11

1.1. Системы счисления и преобразования между ними 12 1.2. Типы данных. Их представление в компьютере 15

Глава 2. Введение в семейство процессоров х8б 19

2.1 .0 компьютерах 20 2.2. История процессоров х86 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

Page 7: Ассемблер на примерах. Базовый курс

Глава 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 и OLD 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, RESW и RESD — объявление переменных . . 91 Псевдокоманда TIMES — повторение следующей псевдокоманды . . 91 Псевдокоманда INCBIN — подключение двоичного файла . . . . . . . . 92 Псевдокоманда EQU — вычисление константных выражений 92 Оператор SEG — смена сегмента 93

6.8. Советы по использованию команд 93 Директива ALIGN — выравнивание данных в памяти 93 Загрузка значения в регистр 94 Оптимизируем арифметику 94 Операции сравнения 95 Разное 96

Page 8: Ассемблер на примерах. Базовый курс

Глава 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

Page 9: Ассемблер на примерах. Базовый курс

Формат bin ~ готовый исполняемый файл 136 Формат OMF — объектный файл для 16-битного режима 136 Формат Win32 — объектный файл для 32-битного режима 137 Форматы aout и aoutb — старейший формат для UNIX 137 Формат coff — наследник а.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 Длинные имена файлов и работа с ними 162

10.7. Работа с каталогами 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

Глава 11 . Программирование в Windows 186

11.1. Введение 187 11.2. «Родные» Windows-приложения 187

11.2.1. Системные вызовы API 187 11.2.2. Программа «Hello, World!» с кнопкой под Windows 188

11.3. Программная совместимость 190 11.4. Запуск DOS-приложений под Windows 190 11.5. Свободные источники информации 190

8

Page 10: Ассемблер на примерах. Базовый курс

Глава 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. Облегчим себе работу: утилиты Asmutiis 199 12.9. Макросы Asmutiis 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. Компоновка с Pascal-программой 226

Глава 14. Заключение 229

Глава 15. Часто используемые команды 230

Page 11: Ассемблер на примерах. Базовый курс

Введение Эта книга — начальный курс и практическое руководство по програм­

мированию на языке ассемблера для процессоров серии х86 — самых распространенных в современных ПК. Она предназначена для студентов и старшеклассников, которые хотят познакомиться с языком программирования низкого уровня, П03В0ЛЯЮ1ДИМ писать компактные, быстрые и эффективные программы, взаимодействующие с аппаратным обеспечением компьютера напрямую, минуя любую операционную систему.

Книга содержит подробные объяснения и множество практических при­меров.

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

Вместе с автором читатель проходит шаг за шагом от решения типовых, не зависящих от платформы, задач к написанию практически полезных программ, работающих в среде DOS, Windows и Linux, а также узнает, как скомпоновать подпрограммы на языке ассемблера с подпрограммами, написанными на языках высокого уровня.

Книга написана простым, доступным языком, а все примеры программ тщательно прокомментированы.

Особое внимание обращено на следующие вопросы: • Архитектура процессора, функции операционной системы, машинный код

и символическое представление команд и адресов; • Различные системы счисления и перевод чисел из одной в другую; • Основные и сложные команды; • Управляющие конструкции и их реализация; • Готовые фрагменты кода, выполняющие самые типичные задачи; •Использование свободно распространяемого компилятора Netwide

Assembler (NASM); • Практическое программирование в среде DOS, Windows и Linux; • Написание ассемблерных вставок в программы на языках высокого уровня

(С и Паскаль). Автор книги — деятельный разработчик свободного программного обес­

печения, соавтор самого маленького в мире веб-сервера размером в 514 байт, участник разработки пакета Asmutils и создатель программного модуля для Linux-проигрывателя MPlayer.

10

Page 12: Ассемблер на примерах. Базовый курс

jnsiBc Базовые системы счисления и термины

Системы счисления и преобразования между ними

Типы данных. Их представление в компьютере

Page 13: Ассемблер на примерах. Базовый курс

Архитектура компьютера тесно связана с двоичной системой счисления, кото­рая состоит всего из двух цифр — О и 1. С технической точки зрения, двоичная система (с основой 2) идеально подходит для компьютеров, поскольку обе цифры могут отображать два состояния — включено (1) и выключено (0). Как мы вскоре увидим, «большие» числа становятся огромными в двоичной системе, поэтому для более удобного представления чисел были разработаны восьмеричная и шестнадцатеричная системы счисления (с основами 8 и 16 соответственно). Обе системы легко преобразуются в двоичную систему, но позволяют записывать числа более компактно.

1.1. Системы счисления и преобразования между ними

Различные системы счисления отличаются не только базовым набором чисел, но и основными концепциями, которые лежат в их основе. Взять, например, систему счисления, которая использовалась древними римлянами: она до­вольно трудна для восприятия, в ней очень сложно производить вычисления и невозможно представить 0. Данная система неудобна даже для человека, не говоря уж о том, чтобы научить компьютер «понимать» ее. Десятичная система, которую мы используем всю жизнь, относится к классу так называемых позиционных систем, в которых число А может быть пред­ставлено в виде:

А = а *z" -h а *z"-i + ... -h а *z4a *z n n-1 1 0

Здесь a — это цифры числа, a Z — основание системы счисления, в нашем случае — 10. Например, число 1234 можно представить так:

1234 = 1*10 + 2*10- -h 3*10 -h 4*100 «Вес» каждой цифры определяется позицией цифры в числе и равен степени основания, соответствующей ее позиции.

12

Page 14: Ассемблер на примерах. Базовый курс

Глава 1. Базовые системы счисления и термины

При работе с различными системами счисления мы будем записывать само число в скобках, а за скобками — основание системы. Например, если напи­сать просто число 1100, то не понятно, в какой системе оно записано — это может быть одна тысяча сто, а может быть 12, если число записано в двоичной системе. А если представить число в виде (1100)2, ^^ сразу все становится на свои места: число записано в двоичной системе. Кстати, двоичная система тоже является позиционной, поэтому число 1100 в двоичной системе мы можем представить так:

(1100), = 1*2 + 1*22 + 0*21 + 0*20

После сложения 8+4 мы получрш, что (1100)2 Р^^^о 12. Как видите, все точно так же, как и с десятичной системой. Обратите внимание, что для представ­ления числа 12 в двоичной системе использованы только четыре разряда. Нартбольшее число, которое можно записать четырьмя двоичными цифрами, равно 15, потому что (1111)2 = 1*" + 1*" + 1*' + 1* = 15. Давайте рассмотрим первые 16 чисел:

Десятичное число

1 ° 1 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 будет представ­лено в двоичной системе как (10000)2-Восьмеричная система счисления (по основанию 8) состоит из большего количества цифр — из восьми (от О до 7). Преобразование из этой системы в десятичную систему полностью аналогично преобразованию из двоичной системы, например:

(77), = 7*8 + 7*8« = 63 Восьмеричная система счисления использовалась в очень популярных ранее 8-битных компьютерах ATARI, ZX Spectrum и др. Позже она была заменена шест-надцатеричной системой, которая также будет рассмотрена в этой книге. В шестнадцатеричной системе цифрами представлены только первые 10 чисел, а для представления остальных 5 чисел используются символы A-F:

А = 10, В = И, С = 12, D = 13, Е = 14, F = 15

13

Page 15: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Представим, как изменяется наш возраст в шестнадцатеричной системе: вы получили свой паспорт в 10 лет и стали совершеннолетним в 12 лет. Для шестнадцатеричной системы сохраняются те же принципы преобразования:

Число (524D)^, = 5*16^ + 2*16^ + 4*16^ + 13*16^ = = 20 480 + 512 + 64 + 13 = 21 069

Число (DEAD)j^ = 13*16^ + 14*16^ + 10*16^ 4- 13*16° = 57 005

Число (DEADBEEF)j, = 13*16^ + 14*16^ + 10*16^ 4- 13*16^ + 4-11*16- + 14*162 + 14*16^ 4- 15*16° = 3 735 928 559

Число (COOl) , = 12*163 4- 0*162 4- 0*16 4- 1 = 49 153 Итак, мы научились преобразовывать любое число, представленное в двоич­ной, восьмеричной и шестнадцатеричной системах, в десятичную систему. А теперь займемся обратным преобразованием — из десятичной системы в систему с основанием п. Для обратного преобразования мы должны делить наше число на п и записывать остатки от деления до тех пор, пока частное от предыдущего деления не станет равно 0. Например, преобразуем 14 в двоичную систему:

14/2 = 7 остаток О 7/2 = 3 остаток 1 3/2 = 1 остаток 1 1/2 = О остаток 1

Мы завершили процесс деления, когда последнее частное стало равно 0. Теперь запишем все остатки подряд от последнего к первому, и мы получим число в двоичной системе —• (1110)2-Рассмотрим еще один пример — преобразование числа 13 в двоичную систему:

13/2 = 6 остаток 1 6/2 = 3 остаток О 3/2 = 1 остаток 1 1/2 = 0 остаток 1

Как и в прошлом случае, мы делили до тех пор, пока частное не стало равно 0. Если записать остатки снизу вверх, мы получим двоичное число (1101)^. А теперь потренируемся с шестнадцатеричной системой — преобразуем число 123456 в эту систему:

123456/16 = 7716 остаток О 7716/16 = 482 остаток 4

14

Page 16: Ассемблер на примерах. Базовый курс

Глава 1. Базовые системы счисления и термины

482/16 = 30 остаток 2 30/16 = 1 остаток 14 = Е 1/16 = О остаток 1

После записи всех остатков получим, что число 123 456 = (lE240)j^. Запись со скобками и нижним индексом в тексте программы неудобна, по­этому мы будем использовать следующие обозначения для записи чисел в различных системах счисления:

• Запись шестнадцатеричного числа начинается с Ох или $0 либо заканчи­вается символом «h». Если первая цифра шестнадцатеричного числа — символ A-F, то перед таким числом нужно обязательно написать О, чтобы компилятор понял, что перед ним число, а не идентификатор, например, ODEADh. Таким образом, записи 0x1234, $01234 и 01234h представляют число (1234),,.

• Десятичные числа могут записываться без изменений либо они закан­чиваться постфиксом «d». Например, 1234 и 1234d представляют число (1234),„.

• Двоичные цифры должны заканчиваться постфиксом «Ь», например, 1100b —это (1100),.

• Восьмеричные цифры заканчиваются на «q»: 12q — это (12) . Далее в этой книге шестнадцатеричные числа мы будем записывать в виде «Ох...», двоичные — «...Ь», а десятичные — без изменений. В вашем соб­ственном коде основание системы счисления (постфикс «d» или «h») лучше указывать явно, потому что одни ассемблеры рассматривают число без при­ставок как десятичное, а другие — как шестнадцатеричное.

1.2. Типы данных. Их представление в компьютере

Основной и неделимой единицей данных является бит. Слово «bit» — это сокращение от «binary digit» — двоичная цифра. Бит может принимать два значения — О и 1 — ложь или истина, выключено или включено. На логике двух состояний основаны все логические цепи компьютеров, поэтому пого­ворим о бите более подробно. Двоичное число содержит столько битов, сколько двоичных цифр в его за­писи, поэтому диапазон допустимых значений выводится из количества раз­рядов (цифр), отведенных для числа. Возьмем положительное целое двоичное число, состоящее из четырех битов: оно может выражать 1^ или шестнадцать различных значений.

15

Page 17: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Биты (разряды) двоичного числа нумеруются справа налево, от наименее зна­чимого до наиболее значимого. Нумерация начинается с 0. Самый правый бит числа — это бит с номером О (первый бит). Этот бит называется LSB-битом {Least Significant Bit — наименее значимый бит). Подобно этому самый левый бит называется MSB-битом {Most Significant Bit — наиболее значимый бит). Биты могут объединяться в группы, группа из четырех битов называется по­лубайтом (nibble). Компьютер не работает с отдельными битами, обычно он оперирует группами битов, например, группа из восьми битов образует базо­вый тип данных, который называется байтом. Восемь битов в байте — это не закон природы, а количество, произвольно выбранное разработчиками IBM, создававшими первые компьютеры. Большие группы битов называются словом (word) или двойным словом (dword — double word). Относительно PC-совместимых компьютеров мы можем сказать следующее:

1 байт = 8 бит 1 слово (word) ~ 2 байта = 16 бит 1 двойное слово (dword) = 4 байта ~ 32 бит

Один байт — это наименьшее количество данных, которое может быть про­читано из памяти или записано в нее, поэтому каждый байт памяти имеет индивидуальный адрес. Байт может содержать число в диапазоне О — 255 (то есть 2'" — 256 различных чисел). В большинстве случаев этого значения недо­статочно, поэтому используется следующая единица данных — слово. Слово может содержать число в диапазоне 0 — 65 535 (то есть 2' = 65 536 различных значений). Двойное слово имеет диапазон значений О — 4 294 967 295 (2 ^ = 4 294 967 296 значений). Давным-давно, еще во времена первых компьютеров, емкость носителей информации представлялась в байтах. Со временем технологии усовершен-

0 - 1 (0-0x1)

0 - 2 5 5 (О - OxFF)

О - 65535 (О - OxFFFF)

О - 4294967295 (О - OxFFFFFFFF)

7 6 5 4 3 2 L. Bai

^

•^

Бит]

1 0 1 iT(byte)

15 8 7 0 Слово

^ (word)^

1 1 1 1

31 16 15 О Двойное слово

(dword)

Рис. 1.1. От бита до двойного слова

16

Page 18: Ассемблер на примерах. Базовый курс

Глава 1. Базовые системы счисления и термины

ствовались, цены на память падали, а требования к обработке информации возрастали. Поэтому выражать емкость в байтах стало неудобно. Решили, что емкость будет выражаться в килобайтах (KB, Kb, Кб или просто к). Но, в отличие от международной системы SI, приставка «кило» означает не 1000, а 1024. Почему именно 1024? Поскольку все в компьютере было завя­зано на двоичной системе, для простоты любое значимое число должно было выражаться как степень двойки. 1024 — это 2'^. Следующие приставки — М (мегабайт, MB, Mb, Мб), G (гигабайт, GB, Гб), Т (терабайт, ТВ, ТБ) и Р (петабайт, РВ, ПБ) — вычисляются умножением 1024 на предыдущее значе­ние, например, 1 Кб = 1024, значит, 1 Мб = 1 Кб * 1024 - 1024 * 1024 = 1 048 576 байт. Думаю, с этим все понятно, давайте вернемся к типам данных. В языках программирования высокого уровня есть специальные типы дан­ных, позволяющие хранить символы и строки. В языке ассемблера таких типов данных нет. Вместо них для представления одного символа использу­ется байт, а для представления строки — группа последовательных байтов. Тут все просто. Каждое значение байта соответствует одному из символов ASCII-таблицы (American Standard Code for Information Interchange). Первые 128 символов — управляющие символы, латинские буквы, цифры — одинаковы для всех компьютеров и операционных систем. Давайте рассмотрим таблицу кодов ASCII (рис. 1.2). Шестнадцатеричные цифры в заголовках строк и столбцов таблицы пред­ставляют числовые значения отдельных символов. Например, координаты заглавной латинской буквы А — 40 и 01. Сложив эти значения, получим 0x41 (то есть 65 в десятичной системе) — код символа 'А' в ASCII-коде. Печатаемые символы в ASCII-таблице начинаются с кода 0x20 (или 32d). Сим­волы с кодом ниже 32 представляют так называемые управляющие символы. Наиболее известные из них — это ОхА или LF — перевод строки, и OxD — CR — возврат каретки. Важность управляющих символов CR и LF обусловлена тем, что они обо­значают конец строки — тот символ, который в языке программирования С обозначается как \п. К сожалению, в различных операционных системах он представляется по-разному: например, в Windows (и DOS) он представляется двумя символами (CR, LF — OxD, ОхА), а в операционной системе UNIX для обозначения конца строки используется всего один символ (LF — ОхА). Символы с кодами от 128 до 256 и выше стали «жертвами» различных стан­дартов и кодировок. Обычно они содержат национальные символы, например, у нас это будут символы русского алфавита и, возможно, некоторые символы псевдографики, в Чехии — символы чешского алфавита и т.д. Следует от­метить, что для русского языка используются кодировки СР 866 (в DOS) и СР 1251 (Windows).

17

Page 19: Ассемблер на примерах. Базовый курс

Ассемблер на примера>

00

10

20 30

40

50 60

70

80 90

АО

ВО СО

DO

ЕО F0

0

16

3Z 0 48 (Of) 64 Р 80

96

Р 112

128 Е 144

160

176 L

192 1 208 а 224

240

1 1 i

17 1 33 1 49 А

Q 81 а 97

q 113 й 129

145 i 161

177 ± 193 Т 209

Р 225 ± 241

2

2

18

34 2 50 В 66 R 82 b 98 Г

114 е 130

146 6 162

178 Т 194 Т 210 Г 226 >

242

3 V 3 1

19

35 3 51 С 67 S 83 С 99 S

115 а. 131 6 147 й 163

1 179

195 1

211

227 <

243

.. Базовый

4 • •

4

20

36 4 52 D 68 Т 34 d 100 t

116 о. 132 6 148 п 164

180

196 L

212

228

f 244

ь 4» г

21 % 37 5 S3 Е 69 и 85 е 101 U 117 а 133 6 149 Н 165

181 •f 197 Г

213 а 229 J

245

курс

в 1 А 6

22

38 6 54 F 70 V 86 f

102 V 118 а 134 й 150 а

166

1 182 198 1Г

214 М-230

246

7 • 7

23 1

39 7 55 G 71 W 87 9 103 W 119 ч 135 u 151 о

167 1 183

1 199 4 215

231

247

8

8

т 24 ( 40 8 56 н 72 X 88 h 104 X

120 ё 136 У 152

С 168 1 184

200

216 Ф 232 о

248

9 О 9 4. 25 ) 41 9 57 1 73 Y 89 1

105

У 121 ё 137 О 153

г-

169

185 ff

201 J 217 е

233

249

А

10

26

4 2

58 J 74 Z 90 J

106 z

122 e 133 и 154 —1

170

186 JL 202 Г

218 Q 234

250

В

11

27 + 43

59 К 75 [ 91 к

107 £

123 i

139

< 155

171

1 187

203

1 219 5

235

251

1 С 12

28

44 < 60 L 76

92 1

108

1 124

140 £ 156

172

J 188

1 204 • 220

CO

236

252

D

13

29

45

61

M 77

] 93

m 109

} 125 i 141

157

i 173

J 189

205

1 221 Ф 237

253

E 14

30

46 > 62 N 78 A

94 n 110

126 A 142 Б. 158 •e: 174 J 190 JL ir 206 1 222 238 • 254

F

15

31 / 47 ? 63 0 79

95 0 111 П 127 A 143 f 159

175 1 191 J, 207 • 223 n 239 D 255

Рис. 1.2. Таблица кодов ASCII

18

Page 20: Ассемблер на примерах. Базовый курс

ГЛ( Введение в семейство процессоров х86

о компьютерах...

История процессоров х86

Процессоры и их регистры: общая информация

Процессор 80386

Прерывания

Page 21: Ассемблер на примерах. Базовый курс

Мы начинаем знакомиться с языком ассемблера. Это язык программирования низкого уровня, то есть максимально приближенный к «железу» — аппарат­ному обеспечению компьютера. Для каждого процессора характерен свой уникальный набор действий, которые процессор способен выполнить, поэтому языки ассемблера разных процессоров отличаются друг от друга. Например, если процессор не умеет выполнять умножение, то в его языке ассемблера не будет отдельной команды «умножить», а перемножать числа программисту придется при помощи нескольких команд сложения. Собственно говоря, язык ассемблера — это всего лишь ориентированная на человека форма записи инструкций процессора (которые называются также машинным языком), а сам ассемблер — это программа, переводящая симво­лические имена команд в машинные коды. Вот почему, прежде чем приступать к изучению команд языка ассемблера, нам нужно побольше узнать о процессоре, для которого этот язык предна­значен.

2.1. О компьютерах... Первым популярным компьютером стал компьютер ENIAC (Electronic Numerical Integrator And Calculator), построенный из электронных ламп и предназначенный для решения дифференциальных уравнений. Программи­рование этого компьютера, которое заключалось в переключении тумблеров, было очень трудоемким процессом. Следующим компьютером после ENIAC был не столь популярный EDVAC (Electronic Discrete Variable Automatic Computer), построенный в 1946 г. Принципы, заложенные в этом компьютере, используются и по сей день: эта машина, подобно современным компьютерам, хранила заложенную програм­му в памяти. Концепция компьютера EDVAC, разработанная американским ученым венгерского происхождения Джоном фон Нейманом, основывалась на следуюш.их принципах:

1. Компьютер должен состоять из следующих модулей: управляющий блок (контроллер), арифметический блок, память, блоки ввода/вывода.

20

Page 22: Ассемблер на примерах. Базовый курс

Глава 2. Введение в семейство процессоров х86

2. Строение компьютера не должно зависеть от решаемой задачи (это как раз относится к ENIAC), программа должна храниться в памяти.

3. Инструкции и их операнды (то есть данные) должны также храниться в той же памяти (гарвардская концепция компьютеров, основанная на концепции фон Неймана, предполагала отдельную память для про­граммы и данных).

4. Память делится на ячейки одинакового размера, порядковый номер ячейки считается ее адресом (1 ячейка эквивалентна 1 байту).

5. Программа состоит из серии элементарных инструкций, которые обыч­но не содержат значения операнда (указывается только его адрес), поэтому программа не зависит от обрабатываемых данных (это уже прототип переменных). Инструкции выполняются одна за другой, в том порядке, в котором они находятся в памяти (к слову, современные микропроцессоры позволяют параллельное выполнение нескольких инструкций).

6. Для изменения порядка выполнения инструкций используются инструк­ции условного или безусловного Oump) перехода.

7. Инструкции и данные (то есть операнды, результаты или адреса) пред­ставляются в виде двоичных сигналов и в двоичной системе счисле­ния.

Оказалось, что концепция фон Неймана настолько мощна и универсальна, что она до сих пор используется в современных компьютерах. Однако продвижение компьютера в наши дома потребовало долгого времени — почти сорока лет. В 1950-ых годах был изобретен транзистор, который заменил большие, склонные ко всяким сбоям, вакуумные лампы. Со временем размер транзисторов уменьшился, но самым большим их компонентом оставался кор­пус. Решение было простым: разместить много транзисторов в одном корпусе. Так появились интегральные микросхемы (чипы). Компьютеры, построенные

Блок ввода

^ г

Память

^ — , J . — •

1 1

Контроллер

А А

4- — L — >

Блок вывода

А

У

\

г Арифметичес-ко-логическое

устройство

— Данные - Инструкции

Рис. 2.1. Концепция фон Неймана

21

Page 23: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

на интегральных микросхемах, стали намного меньше в размере, однако они все еще занимали целые комнаты. К тому же эти компьютеры были очень чувствительны к окружающей среде: без кондиционера они не работали. В конце 1970-ых интегральные микросхемы упали в цене до такой степени, что стали доступны рядовым потребителям. Почему же люди в то время не покупали персональные компьютеры? Потому что их не было в продаже! Люди покупали микросхемы и собирали простые восьмиразрядные компьютеры в порядке хобби — так, в знаменитом гараже, началась история фирмы Apple. За Apple последовшш другие компании, начавшие производство восьмиразряд­ных компьютеров, которые можно было подключить к обычному телевизору и играть или заняться их программированием. В 1981 году лидером рынка универсальных ЭВМ — компанией IBM — был выпущен персональный компьютер IBM PC XT. Это был полноценный пер­сональный компьютер — с монитором, клавиатурой и системным блоком. Компьютер IBM PC XT был оборудован 8-разрядным микропроцессором Intel 8088. Эта модель стала началом огромной серии персональных компью­теров (PC, Personal Computer), которые производились вплоть до нашего времени.

2.2. История процессоров х86 История первых 16-разрядных процессоров класса х86, 8086, была начата компанией Intel в 1978 году. Чипы того времени работали на частоте 5, 8 или 10 МГц и благодаря 20-разрядной шине адреса позволяли адресовать 1 Мб оперативной памяти. В то время были популярны 8-битные компьютеры, поэтому Intel разработала другой чип — 8088, который был аппаратно и программно совместим с 8086, но оснащен только 8-разрядной шиной. В 1982 году Intel представила процессор 80286, который был обратно совме­стим с обеими предыдущими моделями, но использовал более «широкую», 24-разрядную, шину адреса. Этот процессор позволял адресовать 16 Мб опе­ративной памяти. Кроме расширенного набора команд (появилось несколько новых команд), данный процессор мог работать в двух режимах — реальном и защищенном. Защищенный режим обеспечивал механизмы страничной организации па­мяти, прав доступа и переключения задач, которые необходимы для любой многозадачной операционной системы. Реальный режим использовался для обратной совместимости с предыдущими моделями х86. Четыре года спустя, в 1986 году, Intel выпустила процессор 80386 DX, у которо­го обе шины (шина данных и шина адреса) были 32-разрядными. В то же время был выпущен процессор 80386 SX, который был во всем идентичен 80386 DX,

22

Page 24: Ассемблер на примерах. Базовый курс

Глава 2. Введение в семейство процессоров х86

но только с 16-разрядной внешней шиной данных. Оба процессора работали на частоте 20, 25 или 33 МГц. Процессор 80386 не имел интегрированного математического сопроцессора, математический сопроцессор поставлялся в виде отдельного чипа — 80387. В 1989 году было выпущено следующее поколение микропроцессоров Intel — 80486DX, 80486DX/2 и 80486DX/4, которые отличались только рабочей часто­той. Выпущенная тогда же версия 80486SX, в отличие от 80486DX, постав­лялась без математического сопроцессора. Новые возможности интеграции позволили разместить на чипе 8 Кб кэш-памяти. В 1993 году был выпущен первый чип под названием Pentium. С него началась новая линия чипов, которые не только используются сейчас, но и все еще могут выполнять программы, написанные 20 лет назад для процессора 8086. Процессоры, совместимые с х86, выпускались не только компанией Intel, но также и другими компаниями: AMD, Cyrix, NEC, IBM. Мы более подробно рассмотрим 80386, который с точки зрения программирования полностью совместим даже с самыми современными процессорами.

2.3. Процессоры и их регистры: общая информация

Поговорим о внутреннем строении процессора. Процессор — это кремниевая плата или «подложка» с логическими цепями, состоящими из транзисторов, скрытая в пластмассовом корпусе, снабженном контактными ножками (вы­водами, pin). Большинство ножек процессора подключено к шинам — шине адреса, шине данных и шине управления, связывающим чип процессора с остальной частью компьютера. Остальные ножки служат для подачи питания на сам чип. Каждая шина состоит из группы проводников, которые выполняют определенную функцию. Пункт 7 концепции фон Неймана говорит: ИНСТРУКЦИИ И ДАННЫЕ (ТО ЕСТЬ ОПЕРАНДЫ, РЕЗУЛЬТАТЫ ИЛИ АДРЕСА) ПРЕДСТАВЛЯ­ЮТСЯ В ВИДЕ ДВОИЧНЫХ СИГНАЛОВ И В ДВОИЧНОЙ СИСТЕМЕ СЧИСЛЕНИЯ. Это означает, что один проводник шины компьютера может «нести» один бит. Значение этого бита (1 или 0) определяется уровнем напряжения в проводнике. Значит, процессор с одной 16-разрядной шиной и одной 8-раз­рядной должен иметь 24 (16 и 8) ножки, соединенные с различными прово­дниками. Например, при передаче числа 27 (00011011 в двоичной системе) по 8-разрядной шине проводник, по которому передается самый правый бит (LSB), покажет логический уровень 1, следуюш;ий провод также покажет 1, следуюш^ий — О и т.д.

23

Page 25: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Пока мы сказали, что процессор состоит из логических контуров. Эти цепи реализуют все модули, из которых процессор должен состоять согласно кон­цепции фон Неймана: контроллер, арифметико-логическое устройство (АЛУ) и регистры. Контроллер управляет получением инструкций из памяти и их декодировани­ем. Контроллер не обрабатывает инструкцию: после декодирования он про­сто передает ее по внутренней гаине управления к другим модулям, которые выполняют необходимое действие. Арифметико-логическое устройство (АЛУ) выполняет арифметические и ло­гические действия над данными. Для более простых процессоров достаточно АЛУ, умеющего выполнять операции отрицания и сложения, поскольку другие арифметические действия (вычитание, умножение и целочисленное деление) могут быть сведены к этим операциям. Другая, логическая, часть АЛУ выполняет основные логические действия над данными, например, логическое сложение и умножение (ИЛИ, И), а также исключительное ИЛИ. Еще одна функция АЛУ, которую выполняет устройство циклического сдвига (barrel-shifter), заключается в сдвигах битов влево и вправо. Для выполнения процессором инструкции необходимо намного меньше вре­мени, чем для чтения этой инструкции из памяти. Чтобы сократить время ожидания памяти, процессор снабжен временным хранилищем инструкций и

Шина данных

АЛУ

Регистр адреса

Регистр команд

Декодер команд

н—н reg

Регистры

Контрол­лер

Шина адреса | | Системная шина

Рис. 2.2. Структура микропроцессора

24

Page 26: Ассемблер на примерах. Базовый курс

Глава 2. Введение в семейство процессоров х86

данных — регистрами. Размер регистра — несколько байтов, но зато доступ к регистрам осуществляется почти мгновенно. Среди регистров обязательно должны присутствовать следующие группы: регистры общего назначения, регистры состояния и счетчики. Регистры обще­го назначения содержат рабочие данные, полученные из памяти. Регистры состояния содержат текущее состояние процессора (или состояние АЛУ). Последняя группа — это счетчики. Согласно теории фон Неймана, должен быть хотя бы один регистр из этой группы — счетчик команд, содержащий адрес следующей инструкции. Как все это работает, мы расскажем в следу­ющей главе.

2.4. Процессор 80386 Микропроцессор 80386 полностью 32-разрядный, что означает, что он может работать с 4 Гб оперативной памяти (2 ^ байтов). Поскольку шина данных также 32-разрядная, процессор может обрабатывать и хранить в своих реги­страх число «шириной» в 32 бита (тип данных int в большинстве реализаций языка С как раз 32-разрядный). Чтобы научиться программировать на языке ассемблера, мы должны знать имена регистров (рис. 2.3) и общий принцип работы команд. Сами команды обсуждаются в следующих главах.

Регистры общего назначения

Сначала рассмотрим регистры общего назначения. Они называются ЕАХ, ЕВХ, ЕСХ и EDX (Аккумулятор, База, Счетчик и Данные). Кроме названий, они больше ничем другим не отличаются друг от друга, поэтому рассмотрим только первый регистр — ЕАХ (рис. 2.4). Процессор 80386 обратно совместим с процессором 80286, регистры которого 16-разрядные. Как же 80386 может выполнять команды, предназначенные для регистров меньшего размера? Регистр ЕАХ может быть разделен на две части — 16-разрядный регистр АХ (который также присутствует в 80286) и верхние 16 битов, которые никак не называются. В свою очередь, регистр АХ может быть разделен (не только в 80386, но и в 80286) на два 8-битных регистра — АН и AL. Если мы заносим в регистр ЕАХ значение 0x12345678, то регистр АХ будет содержать значение 0x5678 (0x56 в АН и 0x78 в AL), а значение 0x1234 будет помещено в верхнюю часть регистра ЕАХ. «Младшие» регистры других регистров общего назначения называются по такому же принципу: ЕВХ содержит ВХ, который, в свою очередь, содержит ВН и BL и т.д.

25

Page 27: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

ЕАХ 31

ЕВХ

ЕСХ

31

31

EDX

ESI

EDI

ЕВР

ESP

EIP

31

31

31

31

31

Регистры 80386

АХ 1 АН 1 AL

16 15 8 7 О

вх 1 ВН ( BL 1

> сх 1

он 1 CL 1

ее X 0) т 03 I

16 15 8 7 О " S

О

О)

О 16 15 8 7 О

DX 1 DH 1 DL

16 15 8 7 О

SI 16 15

DI > 2

16 15

ВР

I

16 15

SP 16 15

IP 31 16 15

Флаги

0

С

15

15

15

15

15

DS

ES

FS

GS

SS

CS

0

0

0

0

0

2 0)

о

15

CRO 31

CR1 31

03 л X -О

> 1 и О) с О 313029 2 1 О /

Рис. 2.3. Основные регистры процессора 80386

ЕАХ "АХ'

АН 1 AL 31 16 15 8 7 о

Рис. 2.4. Регистр ЕАХ

26

Page 28: Ассемблер на примерах. Базовый курс

Глава 2. Введение в семейство процессоров х86

Индексные регистры

К регистрам общего назначения иногда относят и индексные регистры про­цессора 80386 —• ESI, EDI и ЕВР (или SI, DI и ВР для 16-разрядных действий). Обычно эти регистры используются для адресации памяти: обращения к массивам, индексирования и т.д. Отсюда их имена: индекс источника (Source Index), индекс приемника (Destination Index), указатель базы (Base Pointer). Но хранить в них только адреса совсем необязательно: регистры ESI, EDI и ЕВР могут содержать произвольные данные. Эти регистры программно до­ступны, то есть их содержание может быть изменено программистом. Другие регистры лучше «руками не трогать». У регистров ESI, EDI и ЕВР существуют только в 16-разрядная и 32-разряд­ная версии.

Сегментные регистры

Эту группу регистров можно отнести к регистрам состояния. Регистры из этой группы используются при вычислении реального адреса (адреса, который будет передан на шину адреса). Процесс вычисления реального адреса зави­сит от режима процессора (реальный или защищенный) и будет рассмотрен в следующих главах. Сегментные регистры только 16-разрядные, такие же, как в 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 флага, а пока рассмотрим только самые важные из них:

27

Page 29: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

• Признак нуля 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- и 32-разрядных регистрах, то мы будем использовать сокращение (Е)АХ) — вместо АХ и ЕАХ.

2.5. Прерывания Событие прерывания состоит в том, что процессор прекращает выполнять инструкции программы в нормальной последовательности, а вместо этого начинает выполнять другую программу, предназначенную для обработки этого события. После окончания обработки прерывания процессор продолжит выполнение прерванной программы. Давайте рассмотрим пример. Я сижу за столом и читаю книгу. С точки зрения компьютера, я выполняю процесс чтения книги. Внезапно звонит телефон —

28

Page 30: Ассемблер на примерах. Базовый курс

Глава 2. Введение в семейство процессоров х86

я прерываю чтение, кладу в книгу закладку (на языке процессора это назы­вается «сохранить контекст») и беру трубку. Теперь я «обрабатываю» теле­фонный звонок. Закончив разговор, я возвращаюсь к чтению книги. Найти последнее прочитанное место помогает та самая закладка. Процессоры семейства х86 и совместимые с ними могут порождать 256 преры­ваний. Адреса всех 256 функций обработки прерываний (так называемые век­торы прерываний) хранятся в специальной таблице векторов прерываний. Прерывания могут быть программными и аппаратными. Аппаратные прерывания происходят по запросу периферийных устройств и называются IRQ (Interrupt Requests). Архитектура шины ISA ограничивает их число до 16 (IRQO — IRQ15). К аппаратным прерываниям относятся также специальные прерывания, которые генерирует сам процессор. Такие прерывания используются для обработки «исключительных ситуаций» — неверный операнд, неизвестная команда, переполнение и другие непредвиденные операции, когда процессор сбит с толку и не знает, что делать. Эти прерывания имеют свои обозначения и никак не относятся к зарезервированным для периферии прерываниям IRQ0-IRQ15. Все аппаратные прерывания можно разделить на две группы: прерывания, которые можно игнорировать («замаскировать») и те, которые игнорировать нельзя. Первые называются маскируемыми (maskable), а вторые — немаски­руемыми (non-maskable). Аппаратные прерывания могут быть отключены путем установки флага IF регистра признаков в 0. Единственное прерывание, которое отключить нельзя —- это NMI, немаскируемое прерывание, генери­рующееся при сбое памяти, сбое в питании процессора и подобных форс-мажорных обстоятельствах. Программные прерывания генерируются с помощью специальной команды в теле программы, то есть их порождает программист. Обычно программные прерывания используются для «общения» вашей программы с операционной системой.

29

Page 31: Ассемблер на примерах. Базовый курс

Глава 3 Анатомия команд и как они выполняются процессором

Как команды выполняются процессором

Операнды

Адресация памяти

Команды языка ассемблера

Page 32: Ассемблер на примерах. Базовый курс

3.1. Как команды выполняются процессором

Команда микропроцессора — это команда, которая выполняет требуемое действие над данными или изменяет внутреннее состояние процессора. Суш,ествует две основные архитектуры процессоров. Первая называется RISC (Reduced Instruction Set Computer) — компьютер с уменьшенным набором ко­манд. Архитектура RISC названа в честь первого компьютера с уменьшенным набором команд — RISC I. Идея этой архитектуры основывается на том, что процессор большую часть времени тратит на выполнение ограниченного числа инструкций (например, переходов или команд присваивания), а остальные команды используются редко. Разработчики RISC-архитектуры создали «облегченный» процессор. Благодаря упрошенной внутренней логике (меньшему числу команд, менее сложным логическим контурам), значительно сократилось время выполнения отдельных команд и увеличилась общая производительность. Архитектура RISC подобна «архитектуре общения» с собакой — она знает всего несколько команд, но выполняет их очень быстро. Вторая архитектура имеет сложную систему команд, она называется CISC (Complex Instruction Set Computer) — компьютер со сложной системой ко­манд. Архитектура CISC подразумевает использование сложных инструкций, которые можно разделить на более простые. Все х86-совместимые процессоры принадлежат к архитектуре CISC. Давайте рассмотрим команду «загрузить число 0x1234 в регистр АХ». На языке ассемблера она записывается очень просто — MOV АХ, 0x1234= К на­стоящему моменту вы уже знаете, что каждая команда представляется в виде двоичного числа (пункт 7 концепции фон Неймана). Ее числовое представ­ление называется машинным кодом. Команда MOV АХ, 0x1234 на машинном языке может быть записана так:

31

Page 33: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

0x11хх: предыдущая команда 0х1111:0хВ8, 0x34, 0x12 0x1114: следующие команды

Мы поместили команду по адресу 0x1111. Следующая команда начинается тремя байтами дальше, значит, под команду с операндами отведено 3 бай­та. Второй и третий байты содержат операнды команды MOV. А что такое 0хВ8? После преобразования 0хВ8 в двоичную систему мы получим значение 10111000b. Первая часть — 1011 — и есть код команды MOV. Встретив код 1011, кон­троллер «понимает», что перед ним — именно MOV. Следующий разряд (1) означает, что операнды будут 16-разрядными. Три последние цифры опреде­ляют регистр назначения. Три нуля соответствуют регистру АХ (или AL, если предыдущий бит был равен О, указывая таким образом, что операнды будут 8-разрядными). Чтобы декодировать команды, контроллер должен сначала прочитать их из памяти. Предположим, что процессор только что закончил выполнять пред­шествующую команду, и IP (указатель команд) содержит значение 0x1111. Прежде чем приступить к обработке следующей команды, процессор «по­смотрит» на шину управления, чтобы проверить, требуются ли аппаратные прерывания. Если запроса на прерывание не поступало, то процессор загружает значение, сохраненное по адресу 0x1111 (в нашем случае — это 0хВ8), в свой внутренний (командный) регистр. Он декодирует это значение так, как показано выше, и «понимает», что нужно загрузить в регистр АХ 16-разрядное число —- два следующих байта, находящиеся по адресам 0x1112 и 0x1113 (они содержат наше число, 0x1234). Теперь процессор должен получить из памяти эти два байта. Для этого процессор посылает соответствующие команды в шину и ожидает возвращения по шине данных значения из памяти. Получив эти два байта, процессор запишет их в регистр АХ. Затем процессор увеличит значение в регистре IP на 3 (наша команда занимает 3 байта), снова проверит наличие запросов на прерывание и, если таких нет, загрузит один байт по адресу 0x1114 и продолжит выполнять программу. Если запрос на прерывание поступил, процессор проверит его тип, а также значение флага IF. Если флаг сброшен (0), процессор проигнорирует преры­вание; если же флаг установлен (1), то процессор сохранит текущий контекст и начнет выполнять первую инструкцию обработчика прерывания, загрузив ее из таблицы векторов прерываний. К счастью, нам не придется записывать команды в машинном коде, поскольку ассемблер разрешает использовать их символические имена. Но перед тем как углубиться в команды, поговорим об их операндах.

32

Page 34: Ассемблер на примерах. Базовый курс

Глава 3. Анатомия команд и как они выполняются процессором

3.2. Операнды Данные, которые обрабатываются командами, называются операндами. Опе­ранды в языке ассемблера записываются непосредственно после команды; если их несколько, то через запятую. Одни команды вообще не имеют никаких операндов, другие имеют один или два операнда. В качестве операнда можно указать непосредственное значение (например, 0x123), имя регистра или ссылку на ячейку памяти (так называемые косвен­ные операнды). Что же касается разрядности, имеются 32-разрядные, 16-разрядные, и 8-раз­рядные операнды. Почти каждая команда требует, чтобы операнды были одинакового размера (разрядности). Команда MOV АХ, 0x1234 имеет два операнда: операнд регистра и непосредственное значение, и оба они 16-бит­ные. Последний тип операнда — косвенный тип — адресует данные, находящиеся в памяти, получает их из памяти и использует в качестве значения. Узнать этот операнд очень просто — по наличию в записи квадратных скобок. Адресация памяти будет рассмотрена в следующем параграфе. В документации по Ассемблеру различные форматы операндов представлены следующими аббревиатурами:

• reg8-oпepaнд — любой 8-разрядный регистр общего назначения; • regl6-oпepaнд — любой 16-разрядный регистр общего назначения; • reg32-oпepaнд — любой 32-разрядный регистр общего назначения; • m — операнд может находиться в памяти; • immS — непосредственное 8-разрядное значение; • imml6 — непосредственное 16-разрядное значение; • imm32 — непосредственное 32-разрядное значение; • segreg — операнд должен быть сегментным регистром.

Допускаются неоднозначные типы операндов, например: reg8/imm8-oпepaнд может быть любым 8-битным регистром общего назначения или любым 8-битным непосредственным значением. Иногда размер операнда определяется только по последнему типу, например, следующая запись аналогична предыдущей: К/1тт8-операнд может быть любым регистром (имеется в виду 8-битный регистр) общего назначения или 8-разрядным значением.

33

Page 35: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

3.3. Адресация памяти Мы уже знаем, что адрес, как и сама команда, — это число. Чтобы не запо­минать адреса всех «переменных», используемых в программе, этим адресам присваивают символические обозначения, которые называются переменными (иногда их также называют указателями). При использовании косвенного операнда адрес в памяти, по которому на­ходится нужное значение, записывается в квадратных скобках: [адрес]. Если мы используем указатель, то есть символическое представление адреса, на­пример, [ESI], то в листинге машинного кода мы увидим, что указатель был заменен реальным значением адреса. Можно также указать точный адрес памяти, например, [0x594F]. Чаще всего мы будем адресовать память по значению адреса, занесенному в регистр процессора. Чтобы записать такой косвенный операнд, нужно просто написать имя регистра в квадратных скобках. Например, если адрес загружен в регистр ESI, вы можете получить данные, расположенные по этому адресу, используя выражение [ESI]. Теперь рассмотрим фрагмент программы, в которой регистр ESI содержит адрес первого элемента (нумерация начинается с 0) в массиве байтов. Как получить доступ, например, ко второму элементу (элементу, адрес которого на 1 байт больше) массива? Процессор поддерживает сложные способы адре­сации, которые очень нам пригодятся в дальнейшем. В нашем случае, чтобы получить доступ ко второму элементу массива, нужно записать косвенный операнд [ESI -I- 1]. Имеются даже более сложные типы адресации: [адрес 4- ЕВХ 4- 4]. В этом случае процессор складывает адрес, значение 4 и значение, содержащееся в регистре ЕВХ. Результат этого выражения называется эффективным адресом (ЕА, Effective Address) и используется в качестве адреса, по которому факти­чески находится операнд (мы пока не рассматриваем сегментные регистры). При вычислении эффективного адреса процессор 80386 также позволяет умножать один член выражения на константу, являющуюся степенью двойки: [адрес 4- ЕВХ * 4]. Корректным считается даже следующее «сумасшеди1ее» выражение:

[число - б + ЕВХ * 8 + E S I ]

На практике мы будем довольствоваться только одним регистром [ESI] или суммой регистра и константы, например, [ESI -1- 4]. В зависимости от режима процессора, мы можем использовать любой 16-разрядный или 32-разрядный регистр общего назначения [ЕАХ], [ЕВХ],... [ЕВР]. Процессор предыдущего поколения 80286 позволял записывать адрес в виде суммы содержимого регистра и константы только для регистров ВР, SI, D1, иВХ.

34

Page 36: Ассемблер на примерах. Базовый курс

Глава 3. Анатомия команд и как они выполняются процессором

Выше мы упомянули, что в адресации памяти участвуют сегментные регистры. Их функщгя зависит от режима процессора. Каждый способ адресации предполагает, что при вычислении реального (фактического) адреса используется сегментный регистр по умолчанию. Сменить регистр по умолчанию можно так:

E S : [ E S I ]

Некоторые ассемблеры требуют указания регистра внутри скобок: [ E S : E S I ]

В наших примерах мы будем считать, что все сегментные регистры содержат одно и то же значение, поэтому мы не будем использовать их при адреса­ции.

3.4. Команды языка ассемблера Когда мы знаем, что такое операнд, давайте рассмотрим, как описываются команды языка ассемблера. Общий формат такой:

имя_команды [подсказка] операнды В следующих главах мы поговорим об отдельных командах и выполняемых ими функциях. Операнды мы только что рассмотрели, осталась одна «темная лошадка» — подсказка. Необязательная подсказка указывает компилятору требуемый размер операнда. Ее значением может быть слово BYTE (8-битный операнд), WORD (16-битный) или DWORD (32-битный). Представим инициализацию некоторой «переменной» нулем, то есть за­пись нулей по адресу переменной. Подсказка сообщит компилятору размер операнда, то есть сколько именно нулевых байтов должно быть записано по этому адресу. Пока мы не знаем правильной инструкции для записи значения, поэтому будем считать, что записать значение можно так:

mov dword [ 0x12345678 ] , 0 ;записывает 4 нулевых байта , ;начиная с адреса 0x12 345 67 8

mov word [ 0x12345678 ] , 0 ;записывает 2 нулевых байта, ;начиная с адреса 0x12345678

mov b y t e [ 0x12345678 ] , 0 ;записывает 1 нулевой байт ;по адресу 0x12345678

Примечание.

В языке ассемблера точка с запятой является символом начала комментария.

Первая инструкция последовательно запишет 4 нулевых байта, начиная с адреса 0x12345678. Вторая инструкция запишет только два нулевых байта, поскольку размер операнда — слово. Последняя инструкция запишет только один байт (в битах: 00000000) в ячейку с адресом 0x12345678.

35

Page 37: Ассемблер на примерах. Базовый курс

Глава 4 Основные команды языка ассемблера

Команда MOV

«Остроконечники» и «тупоконечники»

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

Логические команды

БсГ

Page 38: Ассемблер на примерах. Базовый курс

в этой главе рассмотрены основные команды процессоров семейства х86, ко­торые являются фундаментом и простых, и сложных программ, написанных на языке ассемблера. Мы не только опишем синтаксис команд, но и приведем несколько практических примеров, которые пригодятся вам при написании собственных программ.

4.1. Команда MOV Прежде чем изменять каким-либо образом наши данные, давайте научимся их сохранять: копировать из регистра в память и обратно. Ведь прежде чем оперировать данными в регистрах, их сначала туда надо поместить. Команда MOV, хоть название ее и происходит от слова «move» (переме­щать), на самом деле не перемещает, а копирует значение из источника в приемник:

MOV приемник, источник Рассмотрим несколько примеров применения команды MOV:

mov ах,[number]

mov [number],bx

mov bx,ex

mov a l , 1 mov dli^cl

mov e s i , e d i

mov word [number] Д

заносим значение переменной number в регистр АХ загрузить значение регистра ВХ в переменную number занести в регистр ВХ значение регистра СХ занести в регистр AL значение 1 занести в регистр DH значение регистра CL копировать значение регистра EDI в регистр ESI сохранить 16-битное значение 1 в переменную "number''

Процессоры семейства х86 позволяют использовать в командах только один косвенный аргумент. Следующая команда копирования значения, находя­щегося по адресу number_one, в область памяти с адресом number_two, недопустима:

mov [number_two], [number_one] /НЕПРАВИЛЬНО!!!

37

Page 39: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Чтобы скопировать значение из одной области памяти в другую, нужно ис­пользовать промежуточный регистр:

mov ах , [number_one] /загружаем в АХ 16-битное /значение ' 'number_one"

mov [number_two], ах ;а затем копируем его в переменную ;"number_two"

Оба операнда команды MOV должны быть одного размера: mov ах , Ь1 ;НЕПРАВИЛЬНО! - Операнды разных

/размеров .

Для копирования значения BL в регистр АХ мы должны «расширить диапа­зон», то есть скопировать весь ВХ в АХ, а затем загрузить О в АХ:

mov ах , Ьх /загружаем ВХ в АХ mov ah , О ; ' 'сбрасываем" верхнюю часть

;АХ — записываем в нее О

Регистр АН является верхней 8-битной частью регистра АХ. После выполнения команды MOV ах, Ьх регистр АН будет содержать значение верхней части ре­гистра ВХ, то есть значение регистра ВН. Но мы не можем быть уверены, что ВН содержит О, поэтому мы должны загрузить О в АН — команда MOV ah, О «сбрасывает» значение регистра АН. В результате мы расширили 8-битное значение, ранее содержащееся в регистре BL, до 16 битов. Новое, 16-битное, значение будет находиться в регистре АХ. Можно поступить и наоборот: сначала сбросить весь АХ, а затем загрузить BL в младшую часть АХ (AL):

mov ах , О ;АН = О, AL = О mov a l , Ы ;заносим в AL значение BL

Точно так же можно скопировать 16-битное значение в 32-битный регистр. Для полноты картины приведем список всех допустимых форматов команды MOV — как в официальной документации: MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV

г/т8,; r/ml6 г/т32 гед8,: regis гед32 гед8 ,. regis гед32 г/тВ,: r/ml6 г/т32

regS ,regis ,reg32 r/mS ,r/ml6 ,r/m32 immS ,imml6 , imm32 immS ,immlS , imm32

38

Page 40: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

4.2. «Остроконечники» и «тупоконечники» Сейчас мы немного отклонимся от обсуждения команд. Предположим, что вы хотите проверить, было ли значение 0x12345678, которое содержалось в регистре ЕВР, корректно загружено в 32-разрядную переменную counter . Следуюгций фрагмент кода заносит значение 0x12345678 в переменную co­unter :

mov ebp , 0x12345678 ;загружаем в ЕВР значение 0x12345678 mov [ c o u n t e r ] , ebp ;сохраняем значение ЕВР

;в переменную "coun te r ' ' (счетчик)

Для того чтобы проверить, загружено значение или нет, нужно воспользовать­ся отладчиком. Понимаю, что вы пока не знаете ни того, как откомпилировать программу, ни того, как загрузить ее в отладчик, но давайте представим, что это уже сделано за вас. Как будет выглядеть откомпилированная программа в отладчике? Отладчик преобразует все команды из машинного кода назад в язык ассемблера. Все точно так же, как мы написали, только с небольшим отличием: везде, где мы использовали символьное имя переменной, оно будет заменено реальным адресом переменной, например:

0804808А BD78563412 mov ebp , 0x12345678 0804808F 892DC0900408 mov dword [ + 0x80490c0] , ebp

Первая колонка — это реальный адрес команды в памяти, вторая — машинный код, в который превратилась команда после компиляции. После этого мы ви­дим символьное представление машинной команды в мнемокодах ассемблера. Символическое имя нашей переменной counter было заменено адресом этой переменной в памяти (0х80490с0). Перед выполнением первой команды, mov ebp, 0x12345678, регистры про­цессора содержали следующие значения:

еах = 0x00000000 еЬх - 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 a t 0x080490C0 i n hex 080490C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

39

Page 41: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Когда будет выполнена вторая команда MOV, значение 0x12345678 будет записано в память по адресу 0х80490с0 и дамп памяти покажет другой ре­зультат:

Dumping 64 b y t e s of memory s t a r t i n g a t 0x080490C0 i n 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», старшим байтом вперед).

Примечание.

Любопытно, что термины LITTLE_ENDIAN и BIG_ENDIAN — это не просто сокращения: они происходят от названий соперничавших в Лилипутии партий «остроконечников» и «тупоконечников» из «Путешествий Гулливера» Джонатана Свифта, отсюда и название этого пункта. «Остроконечники» и «тупоконечники» у Свифта кушали яйца с разных концов, одни — с острого, другие — с тупого. В результате спора по поводу того, как правильнее, развязалась война, а дальше ... если не помните, возьмите книжку и почитайте.

Порядок следования байтов в слове (двойном слове) учитывается не только при хранении, но и при передаче данных. Если у вас есть небольшой опыт программирования, возможно, вы сталкивались с «остроконечниками и тупоконечниками» при разработке сетевых приложений, когда некоторые структуры данных приходилось преобразовывать к «тупоконечному» виду при помош,и специальных функций (htonl, htons, ntohl, ntohs). Когда переменная counter будет прочитана обратно в регистр, там окажется оригинальное значение, то есть 0x12345678.

4.3. Арифметические команды Рассмотренная выше команда MOV относится к группе команд перемеще­ния значений, другие команды из которой мы рассмотрим позже. А сейчас перейдем к следующей группе чаще всего используемых команд — арифме­тическим операциям. Процессор 80386 не содержит математического сопро­цессора, поэтому мы рассмотрим только целочисленную арифметику, которая полностью поддерживается процессором 80386. Каждая арифметическая команда изменяет регистр признаков.

40

Page 42: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

4.3.1. Инструкции сложения ADD и вычитания SUB Начнем с самого простого — сложения (ADD) и вычитания (SUB). Команда ADD требует двух операндов, как и команда MOV:

ADD о 1 , о2

Команда ADD складывает оба операнда и записывает результат в о1, предыдущее значение которого теряется. Точно так же работает команда вычитания — SUB:

SUB o l , о2 Результат, о1-о2, будет сохранен в о1, исходное значение о1 будет потеряно. Теперь рассмотрим несколько примеров:

mov ах , 8 /заносим в АХ число 8 mov СХ, б ; заносим в СХ число б mov dx, ex ; копируем СХ в DX, DX = б add dx, ах ;DX = DX + АХ

Поскольку мы хотим избежать уничтожения (то есть перезаписи результатом) исходных значений и хотим сохранить оба значения — АХ и СХ, мы копируем оригинальное значение СХ в DX, а затем добавляем АХ к DX. Команда ADD сохранит результат DX 4- АХ в регистре DX, а исходные значения АХ и СХ останутся нетронутыми. Рассмотрим еще примеры использования инструкций ADD и SUB:

add е а х , 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 ] i , a l

EAX = EAX + 8 ECX = ECX - EBP добавляем значение 4 к переменной number размером в 1 байт (диапазон значений 0-2 55) number = number — 4 размером в 2 байта (диапазон значений 0-6553 5) добавляем значение 00000004 к "number" вычитаем значение регистра AL из "number" вычитаем AL из АН, результат помещаем в АН

Что произойдет, если сначала занести в AL (8-разрядный регистр) наибольшее допустимое значение (255), а затем добавить к нему 8?

mov a l , 255 ;заносим в AL значение 255, то есть OxFF add a l , 8 ;добавляем 8

В результате в регистре AL мы получим значение 7.

41

Page 43: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Перенос OxFF

11111111

00001000

00000111

AL +

imm8

AL

Рис. 4.1. Сложение 255 (OxFF) + 8

Но мы ведь ожидали 0x107 (263 в десятичном виде). Что случилось? В регистре AL может поместиться только 8-разрядное число (максимальное значение — 255). Девятый, «потерянный», бит скрыт в регистре признаков, а именно в флаге CF — признак переноса. Признак переноса используется в арифметических командах при работе с большими диапазонами чисел, чем могут поддерживать регистры. Полезны для этого команды ADC (Add With Carry — сложение с переносом) и SBB (Subtract With Borrow — вычитание с займом): ADC о1, SBB о1,

о2 о2

;о1 = о1 + ;о1 - о1 -

о2 + CF - о2 - CF

Эти команды работают так же, как ADD и SUB, но соответственно добавляют или вычитают флаг переноса CF. В контексте арифметических операций очень часто используются так называе­мые пары регистров. Пара — это два регистра, использующихся для хранения одного числа. Часто используется пара EDX:EAX (или ЬХ:АХ) — обычно при умножении. Регистр АХ хранит младшие 16 битов числа, а DX — старшие 16 битов. Таким способом даже древний 80286 может обрабатывать 32-разрядные числа, хотя у него нет ни одного 32-разрядного регистра. Пример: пара DX:AX содержит значение OxFFFF (АХ = OxFFFF, DX = 0). Добавим 8 к этой паре и запишем результат обратно в DX:AX:

;АХ = OxFFFF ;DX = О ;АХ = АХ + 8 ; д о б а в л я е м О с п е р е н о с о м к DX

mov а х , O x f f f f mov d x , О a d d а х , 8 a d o d x , О

Первая команда ADD добавит 8 к регистру АХ. Полностью результат не по­мещается в АХ, поэтому его старший бит переходит в CF. Вторая команда добавит к DX значение О и значение CF. После выполнения ADC флаг CF будет добавлен к DX (DX теперь равен 1). Результат сложения OxFFFF и 8 (0x10007) будет помещен в пару DX:AX (DX=1, АХ=0007).

42

Page 44: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

DX

0x0000

Перенос

ш АХ

OxFFFF

ADD АХ, 8

0x0000

0x0008

0x0000

0x0001

0x0007

ADC DX, О

в 0x0007

Рис. 4.2. Сложение чисел OxFFFF и 0x0008 и их сохранение в регистрах

Процессор 80386 может работать с 32-разрядными числами напрямую — безо всяких переносов:

mov е а х , O x f f f f ;ЕАХ - OxFFFF a d d е а х , 8 ; ЕАХ = ЕАХ + 8

OxOOOOFFFF | ЕАХ

imm32 0x00000008

0x00010007 ЕАХ

Рис. 4.3. Использование 32-разрядных регистров процессора 80386

После выполнения этих инструкций в ЕАХ мы получим 32-разрядное значение 0x10007. Для работы с 64-разрядными числами мы можем использовать пару EDXiEAX — точно так же, как мы использовали пару DX:AX.

4.3.2. Команды инкрементирования INC и декрементирования DEC

Эти команды предназначены для инкрементирования и декрементирования. Команда INC добавляет, а DEC вычитает единицу из единственного операнда. Допустимые типы операнда — такие же, как у команд ADD и SUB, а формат команд таков:

INC о 1 DEC о 1

; о 1 - о 1 + 1 ; о 1 = о 1 - 1

ВНИМАНИЕ! Ни одна из этих инструкций не изменяет флаг CF. О значении этого факта и следствиях из него, а также о том, как безопасно (без потери

43

Page 45: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

данных) использовать эти команды, будет сказано в главе, посвященной оптимизации. Увеличение на единицу значения регистра AL выглядит следующим образом:

add a l , 1 inc a l

;AL = AL + 1 ;AL = AL + 1

Увеличение на единицу значения 16-битной переменной number: inc word [number] ;мы должны указать размер

/переменной — word

4.3.3. Отрицательные числа — целые числа со знаком Отрицательные целые числа в ПК представлены в так называемом дополни­тельном коде. Дополнительный код можно представить себе как отображение некоторого диапазона, включающего положительные и отрицательные целые числа, на другой диапазон, содержащий только положительные целые числа. Рассмотрим код дополнения одного байта. Один байт может содержать числа в диапазоне от О до 255. Код дополнения заменяет этот диапазон другим — от -128 до 127. Диапазон от О до 127 ото­бражается сам на себя, а отрицательным числам сопоставляется диапазон от 128 до 255: числу -1 соответствует число 255 (OxFF), -2 — 254 (OxFE) и т.д. Число -50 будет представлено как 206. Обратите внимание: самый старший бит отрицательного числа всегда устанавливается в 1 — так можно опреде­лить, что число отрицательное. Процесс отображения отрицательн1Лх чисел в дополнительный код иногда называют маппингом (mapping). Дополнительный код может быть расширен до 2 байтов (от О до 65535). Он будет охватывать диапазон чисел от -32768 до 32767. Если дополнительный код расширить до 4 байтов, то получим диапазон от -2 147 483 648 до 2 147 483 647. Во многих языках программирования именно этот диапазон используется для код целочисленного типа данных (integer). Пример: преобразуем числа 4, -4,386, -8000 и 45000 в дополнительный код, считая, что целевой диапазон — 16 бит (2 байта). Прежде всего, выясним, сколько чисел может поместиться в 16 разрядов. Это очень просто: возведем 2 в степень 16. 2 ^ = 65 536, что соответствует диапазону от О до 65 535. Теперь определим границы: 65 536 / 2 = 32 768. Итак, мы получили диапазон от -32 768 до 32 767 (О — это положительное число!).

256(0x100)

206(0хСЕ)

Интерпретация числа

—t— 128

Дополнительный код для 1 байта

Рис. 4.4. Дополнительный код для 1 байта

44

Page 46: Ассемблер на примерах. Базовый курс

Глава 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 и 7 в дополнительном коде из 2 байтов. Число 7 будет отображено само в себя, а число -6 будет представлено числом 65 536 — 6 = 65 530 (OxFFFA). Что получится, если мы сложим два эти числа (7 и 65 530)? Попробуем решить эту задачу на языке ассемблера:

mov ах,OxFFFA ;АХ = -6, то есть 65 53 0 или OxFFFA mov dx ,7 ;DX = 7 add a x , d x ;AX = AX + DX

Мы получим результат 65 530 -h 7 = 65 537 = 0x10001, который не помещается в регистре АХ, поэтому будет установлен флаг переноса. Но если мы его про­игнорируем, то оставшееся в АХ значение будет правильным результатом! Механизм дополнительного кода ввели именно для того, чтобы при сложении и вычитании отрицательных чисел не приходилось выполнять дополнительных действий. Теперь давайте сложим два отрицательных числа. Ассемблер NASM позволя­ет указывать отрицательные числа непосредственно, поэтому нам не нужно преобразовывать их вручную в дополнительный код:

mov ах , -б ;АХ = -б mov dx, - б ; DX = - б add a x , d x ;АХ = АХ + DX

Результат: 0xFFF4 (установлен также флаг CF, но мы его игнорируем). В десятичной системе 0xFFF4 = 65 524. В дополнительном коде мы получим правильный результат: -12 (65 536 — 65 524 = 12). Отрицательные числа также могут использоваться при адресации памяти. Пусть регистр ВХ содержит адрес, а нам нужен адрес предыдущего байта, но мы не хотим изменять значение регистра ВХ (предполагается, что процессор находится в реальном режиме):

mov а х , [ Ь х - 1 ] /поместить в АХ значение по адресу ;на единицу меньшему, чем хранится в ВХ

Значение -1 будет преобразовано в OxFFFF, и инструкция будет выглядеть так: MOV АХ, [BX-hOxFFFF]. При вычислении адреса не учитывается флаг CF, поэтому мы получим адрес, на единицу меньший.

45

Page 47: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

4.3.4. Команды для работы с отрицательными числами

Команда NEG

Система команд процессора 80386 включает в себя несколько команд, предна­значенных для работы с целыми числами со знаком. Первая из них — команда NEG (negation, отрицание): NEG г/т8 NEG r/ml6 NEG r/m32

Используя NEG, вы можете преобразовывать положительное целое число в отрицательное и наоборот. Инструкция NEG имеет только один операнд, который может быть регистром или адресом памяти. Размер операнда — лю­бой: 8, 16 или 32 бита.

neg еах ;изменяет знак числа, сохраненного в ЕАХ neg Ы ;то же самое, но используется 8-битный

;регистр Ы neg b y t e [number] /изменяет знак 8-битной переменной number

Расширение диапазона целого беззнакового числа делалось просто: мы про­сто копировали число в больший регистр, а расширенное «место» заполняли нулями. При работе с целыми числами со знаком мы должны заполнить это место старшим битом преобразуемого числа. Так мы можем сохранять поло­жительные и отрицательные числа при расширении их диапазона. Расширение диапазона числа со знаком называется знаковым расширением. Процессор имеет несколько специальных команд, предназначенных для знакового расширения. Эти команды не имеют операндов, они выполняют действия над фиксированными регистрами.

Команда CBW

Команда CBW копирует седьмой (старший) бит регистра AL в регистр АН, рас­ширяя таким образом оригинальное значение регистра AL в значение со знаком регистра АХ (значение АН становится равно 0x00 или OxFF = П И И lib, в за­висимости от старшего бита AL). Сложно? Ничего, скоро рассмотрим пару примеров, и все станет на свои места.

15 АХ о АН . AL

• I I I I 1 I 1 I

АН , AL I I I I I I I II I I И I 1} 15 8 7 О

Рис, 4.5. Знаковое расширение с помощью инструкции CBW

46

Page 48: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

Команда CWD

Команда CWD копирует старший бит АХ в регистр DX, расширяя таким об­разом оригинальное значение АХ в пару регистров со знаком ЬХ:АХ.

CWD DX АХ

DH I DL

DH DL

АН I AL

АН AL DX АХ

Рис. 4.6. Знаковое расширение с помощью инструкции CWD

Команда CDQ

Команда CDQ копирует старший бит ЕАХ в регистр EDX, расширяя таким об­разом оригинальное значение ЕАХ в пару регистров со знаком EDXiEAX.

Команда CWDE

Команда CWDE копирует старший бит АХ в верхнюю часть (старшую часть) ЕАХ, расширяя таким образом оригинальное значение АХ в двойное слово со знаком, которое будет помещено в регистр ЕАХ.

CWDE

1 1

г 1

АН 1 AL

т '

АН , AL 1

ЕАХ

ЕАХ

Рис. 4.7. Знаковое расширение с помощью инструкции CWDE

Рассмотрим пару примеров: mov a l , -1 ;AL = - 1 (или OxFF) cbw ; знаковое расширение на весь АХ

После выполнения команды CBW АХ будет содержать значение OxFFFF, то есть - 1 . Единица (1) старшего разряда заполнила все биты АН, и мы получили знаковое расширение AL на весь регистр АХ.

47

Page 49: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

mov а х , 4 ;АХ = 4 cwd ;выполним з н а к о в о е р а с ш и р е н и е в DX

Первая команда заносит в регистр АХ значение 4. Вторая команда, CWD, производит знаковое расгаирение АХ в пару DX:AX. Оригинальное значение DX заменяется новым значением — старшим битом регистра АХ, который в этом случае равен 0. В результате мы получили О в регистре DX. Иногда команда CWD полезна для очищения регистра DX, когда АХ содержит положительное значение, то есть значение, меньшее 0x8000.

4.3.5. Целочисленное умножение и деление Давайте познакомимся с оставшимися целочисленными операциями: умноже­нием и делением. Первое арифметическое действие выполняется командой MUL, а второе — командой DIV. Дополнительный код делает возможным сложение и вычитание целых чисел со знаком и без знака с помощью одних и тех же команд ADD и SUB. Но к умножению и делению это не относится: для умножения и деления чисел со знаком служат отдельные команды — IMUL и IDIV. Операнды этих инструк­ций такие же, как у MUL и DIV. Операции умножения и деления имеют свою специфику. В результате умно­жения двух чисел мы можем получить число, диапазон которого будет в два раза превышать диапазон операндов. Деление целых чисел — это операция целочисленная, поэтому в результате образуются два значения: частное и остаток. С целью упрощения реализации команд умножения и деления эти команды спроектированы так, что один из операндов и результат находятся в фикси­рованном регистре, а второй операнд указывается программистом. Подобно командам ADD и SUB, команды MUL, DIV, IMUL, IDIV изменяют регистр признаков.

Команды MUL и IMUL

Команда MUL может быть записана в трех различных форматах — в зави­симости от операнда:

MUL г/гп8 MUL г / m l б MUL г / т 3 2

В 8-разрядной форме операнд может быть любым 8-битным регистром или адресом памяти. Второй операнд всегда хранится в AL. Результат (произве­дение) будет записан в регистр АХ.

48

Page 50: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

( г / т 8 ) * AL -> АХ

В 16-разрядной форме операнд может быть любым 16-битным регистром или адресом памяти. Второй операнд всегда хранится в АХ. Результат сохраняется в паре DX:AX.

( r / m l 6 ) * АХ -> DX:AX

В 32-разрядной форме второй операнд находится в регистре ЕАХ, а результат записывается в пару EDX:EAX.

( г / т 3 2 ) * ЕАХ - > EDX:ЕАХ

Рассмотрим несколько примеров. Пример 1: умножить значения, сохраненные в регистрах ВН и CL, результат сохранить в регистр АХ:

mov a l , bh ;AL = ВН — с н а ч а л а з а н о с и м в AL в т о р о й о п е р а н д mul с 1 ;АХ = AL * CL — умножаем е г о на CL

Результат будет сохранен в регистре АХ. Пример: вычислить 486^ результат сохранить в DX:AX:

mov а х , 486 ; АХ = 486 mul а х ; АХ * АХ - > DX:AX

Пример 2: вычислить диаметр по радиусу, сохраненному в 8-битной перемен­ной r a d i u s l , результат записать в 16-битную переменную d iamete r l :

mov a l , 2 mul b y t e [ r a d i u s l ] mov [ d i a m e t e r l ] , a x

AL = 2 AX = r a d i u s * 2 d i a m e t e r < - AX

Вы можете спросить, почему результат 16-разрядного умножения сохранен в паре DX:AX, а не в каком-то 32-разрядном регистре? Причина — совме­стимость с предыдущими 16-разрядными процессорами, у которых не было 32-разрядных регистров. Команда IJVIUL умножает целые числа со знаком и может использовать один, два или три операнда. Когда указан один операнд, то поведение IMUL бу­дет таким же, как и команды MUL, просто она будет работать с числами со знаком. Если указано два операнда, то инструкция Ш и Ь умножит первый операнд на второй и сохранит результат в первом операнде, поэтому первый операнд всегда должен быть регистром. Второй операнд может быть регистром, не­посредственным значением или адресом памяти.

i m u l e d x , e c x i m u l e b x , [ s t l i i n g ]

EDX - EDX * ECX умножает 3 2-разрядную переменную ''stiling" на ЕВХ, результат будет сохранен в ЕВХ

49

Page 51: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

imul е с х , б ;ЕСХ - ЕСХ * б

Если указано три операнда, то команда IMUL перемножит второй и третий операнды, а результат сохранит в первый операнд. Первый операнд — только регистр, второй может быть любого типа, а третий должен быть только непо­средственным значением:

imul edx,ecx,l imul e b x Л s t h i n g ] , 9

imul ecx,edx,11

EDX = ECX * 7 умножаем переменную ' ' s t h i n g " на 9, результат будет сохранен ЕВХ ЕСХ = EDX * 11

Команды DIV и IDIV

Подобно команде MUL, команда DIV может быть представлена в трех раз­личных форматах в зависимости от типа операнда: DIV г/т8 DIV r/ml6 DIV r/m32

Операнд служит делителем, а делимое находится в фиксированном месте (как в случае с MUL). В 8-битной форме переменный операнд (делитель) может быть любым 8-битным регистром или адресом памяти. Делимое содержится в АХ. Результат сохраняется так: частное — в AL, остаток —- в АН.

АХ / (г/т8) -> AL, остаток -^ АН В 16-битной форме операнд может быть любым 16-битным регистром или адресом памяти. Второй операнд всегда находится в паре DX:AX. Результат сохраняется в паре DX:AX (DX — остаток, АХ — частное).

DX:AX / (r/ml6) ^ АХ, остаток -> DX В 32-разрядной форме делимое находится в паре EDX:EAX, а результат за­писывается в пару EDX:ЕАХ (частное в ЕАХ, остаток в EDX).

EDX:EAX / (r/m32) -^ ЕАХ, остаток -^ EDX Команда IDIV используется для деления чисел со знаком, синтаксис ее такой же, как у команды DIV. Рассмотрим несколько примеров. Пример 1: разделить 13 на 2, частное сохранить в BL, а остаток в — ВН:

mov ах Д З mov с 1 , 2 d i v с1 mov b x , а х

АХ - 13 CL - 2 делим на CL ожидаемый р е з у л ь т а т находится в АХ, копируем в ВХ

50

Page 52: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

Пример 2: вычислить радиус по диаметру, значение которого сохранено в 16-битной переменной diameterl, результат записать в radiusl, а остаток проигнорировать.

mov а х , [ d i a m e t e r l ] ;АХ = d i a m e t e r l mov Ы , 2 ;загружаем делитель 2 d i v Ы ;делим mov [radiusl],al ;сохраняем результат

4.4. Логические команды к логическим операциям относятся: логическое умножение (И, AND), логи­ческое сложение (ИЛИ, OR), исключаюпдее ИЛИ (XOR) и отрицание (NOT). Все эти инструкции изменяют регистр признаков.

Команда AND

Команда AND выполняет логическое умножение двух операндов — о1 и о2. Результат сохраняется в операнде о1. Типы операндов такие же, как у коман­ды ADD: операнды могут быть 8-, 16- или 32-битными регистрами, адресами памяти или непосредственными значениями.

AND о 1 , о2

Таблица истинности для оператора AND приведена ниже (табл. 4.1).

Таблица истинности для оператора AND Таблица 4.1

А

0

0

1

1

b

0

1

0

1

aANDb

0

0

0

1 1

Следующий пример вычисляет логическое И логической единицы и логиче­ского нуля (1 AND 0).

mov a l , 1 mov b l , 0 a n d a l , b l

AL = o n e BL = z e r o AL = AL a n d BL = 0

TOT же самый пример, но записанный более компактно: mov а 1 , 1 a n d a l , О

;AL =•- o n e ; AL = AL a n d 0 1 a n d 0

51

Page 53: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Команда OR

Команда OR выполняет логическое сложение двух операндов — о1 и о2. Результат сохраняется в операнде о1. Типы операндов такие же, как у ко­манды AND.

OR o l , о2

Таблица истинности для оператора OR приведена ниже (табл. 4.2).

Таблица истинности для оператора OR Таблица 4.2

А

1 ° 1 ° 1 1

1

b

0

1

0

1

aORb

0

1

1

1

Дополнительные примеры использования логических команд будут приведены в последнем пункте данной главы. А пока рассмотрим простой пример уста­новки наименее значимого бита (первый справа) переменной mask в 1.

or b y t e [mask] ,1

Команда XOR

Вычисляет так называемое «исключающее ИЛИ» операндов о1 и о2. Результат сохраняется в о1. Типы операндов такие же, как у предыдущих инструкций. Формат команды:

XOR о 1 , о2

Таблица истинности для оператора XOR приведена ниже (табл. 4.3).

Таблица истинности для оператора XOR Таблица 4.3

а

0

0

1

1

b

0

1

0

1

а XOR b

0

1

1

0 J

Исключающее ИЛИ обратимо: выражение ((х хог у) хог у) снова возвра­тит X. mov al,0x55 хог al,ОхАА хог al, ОхАА,

AL - 0x55 AL = AL хог ОхАА возвращаем в AL исходное значение — 0x55

52

Page 54: Ассемблер на примерах. Базовый курс

Глава 4. Основные команды языка ассемблера

Команда NOT

Используется для инверсии отдельных битов единственного операнда, ко­торый может быть регистром или памятью. Соответственно команда может быть записана в трех различных форматах: NOT r/m8 NOT г/ml б NOT r/m32

Таблица истинности для оператора NOT приведена ниже (табл. 4.4).

Таблица истинности для оператора NOT Таблица 4.4

А

0

1

NOT а

1

0

Следующий пример демонстрирует различие между операциями NOT и NEG: mov al,00000010b mov bl,al not al

neg Ы

AL = 2 BL = 2 после этой операции мы получим 11111101b - OxFD (-3) а после этой операции результат будет другим: 11111110 = OxFE (-2)

Массивы битов (разрядные матрицы)

Любое число можно записать в двоичной системе в виде последовательности нулей и единиц. Например, любое 16-разрядное число состоит из 16 двоичных цифр — О и 1. Мы можем использовать одно число для хранения шестнадцати различных состояний — флагов. Нам не нужно тратить место на хранение 16 различных переменных, ведь для описания состояния (включено/выключено) вполне достаточно 1 бита. Переменная, используемая для хранения флагов, называется разрядной матрицей или массивом битов.

Высокоуровневые языки программирования также используют разрядные матрицы, например, при хранении набора значений перечисления, для эко­номии памяти. Мы уже знакомы с одной разрядной матрицей, которая очень часто используется в программировании — это регистр признаков микропро­цессора. Мы будем очень часто сталкиваться с разрядными матрицами при программировании различных устройств: видеоадаптера, звуковой платы и т.д. В этом случае изменение одного бита в разрядной матрице может изменить режим работы устройства.

53

Page 55: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Для изменения значения отдельных битов в матрице служат логические операции. Первый операнд задает разрядную матрицу, с которой мы будем работать, а второй операнд задает так называемую маску, используемую для выбора отдельных битов. Для установки определенных битов массива в единицу (все остальные биты при этом должны остаться без изменения) применяется команда OR. В каче­стве маски возьмите двоичное число, в котором единицы стоят на месте тех битов, которые вы хотите установить в массиве. Например, если вы хотите установить первый и последний биты массива, вы должны использовать ма­ску 10000001. Все остальные биты останутся нетронутыми, поскольку О OR X всегда возвраш^ает X. Чтобы сбросить некоторые биты (установить их значение в 0), возьмите в качестве маски число, в котором нули стоят на месте тех битов, которые вы хотите сбросить, а единицы — во всех остальных позициях, а потом исполь­зуйте команду AND. Поскольку 1 AND X всегда возвращает X, мы сбросим только необходимые нам биты. Рассмотрим несколько примеров. Пример. В регистре AL загружен массив битов. Нужно установить все не­

четные позиции в 1. Предыдущее состояние массива неизвестно. or a l , 10101010b ;маска устанавливает все нечетные биты в 1

Пример. В массиве битов, хранящемся в регистре AL, сбросить 0-й и 7-й биты, все остальные оставить без изменения. Исходное состояние массива также неизвестно.

and a l , 01111110b ;каждая 1 в маске сохраняет бит ;в ее позиции

С помощью XOR также можно изменять значения битов, не зная предыдуще­го состояния. Для этого в маске установите 1 для каждого бита, который вы хотите инвертировать (О станет 1, а 1 станет 0), а для всех оставшихся битов установите 0. Если мы выполним XOR дважды, то получим исходное значение. Такое поведение операции XOR позволяет использовать эту команду для про­стого шифрования: к каждому байту шифруемых данных применяется XOR с постоянной маской (ключом), а для дешифровки тот же ключ применяется (XOR) к шифрованным данным.

54

Page 56: Ассемблер на примерах. Базовый курс

Гл; Управляющие конструкции

Последовательное выполнение команд

Конструкция «IF THEN» выбор пути

Итерационные конструкции — циклы

Команды обработки стека

Page 57: Ассемблер на примерах. Базовый курс

Программа любой сложности на любом языке программирования может быть написана при помощи всего трех управляющих структур: линейной, условия и цикла. В этой главе мы рассмотрим эти три краеугольных камня програм­мирования и реализацию их в языке ассемблера, а в конце главы вы узнаете о стеке, который нужен для использования подпрограмм.

5.1. Последовательное выполнение команд Последовательная обработка знакома нам еще с концепции фон Неймана. Каждая программа состоит из одной или нескольких последовательностей отдельных элементарньис команд. Последовательность здесь означает участок программы, где команды выполняются одна за другой, без любых переходов. В более широком контексте языка программирования высокого уровня мож­но рассматривать целую программу как последовательность, состоящую как из элементарных команд, так и из управляющих конструкций — условных и итерационных. Если программа не содержит других конструкций, кроме последовательности элементарных команд, она называется линейной.

Инструкция 1

Инструкция 2

Рис. 5.1. Последовательная обработка команд

56

Page 58: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

5.2. Конструкция «IF THEN» — выбор пути в языках программирования высокого уровня конструкция выбора извест­на как оператор IF-THEN. Эта конструкция позволяет выбрать следующее действие из нескольких возможных вариантов в зависимости от выполнения определенного условия. В языке ассемблера механизм выбора реализован посредством команд сравнения, условного и безусловного переходов.

Да Условие 1

Да

Нет

Команда 1

Условие 2 Нет

Команда 2

Команда 3

Рис, 5.2. Выбор следующей команды

5 . 2 . 1 . Команды СМР и TEST Команды СМР и TEST используются для сравнения двух операндов. Операн­дами могут быть как регистры, так и адреса памяти, размер операнда — 8, 16 или 32 бита.

СМР о 1 , о2

Команда СМР — это сокращение от «compare», «сравнить». Она работает подобно SUB: операнд о2 вычитается из о1. Результат нигде не сохраняется, команда просто изменяет регистр признаков. Команда СМР может исполь­зоваться как для сравнения целых беззнаковых чисел, так и для сравнения чисел со знаком. Команда TEST работает подобно СМР, но вместо вычитания она вычисля­ет поразрядное И операндов. Результат инструкции — измененные флаги регистра признаков. Мы можем использовать TEST для проверки значений отдельных битов в массиве битов. Проиллюстрируем эти команды несколькими примерами:

стр ах , 4 ; сравниваем АХ со значением 4 стр d l , a h ;сравниваем DL с АН стр [ d i a m e t e r l ] , а х ;сравниваем переменную " d i a m e t e r l " с АХ

57

Page 59: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

стр а х , [ d i a m e t e r 1 ] /сравниваем АХ с переменной " d i a m e t e r l " cmp е а х , е с х ;сравниваем регистры ЕАХ и ЕСХ t e s t ах , 00000100b ;проверяем значение второх'о

; ( т р е т ь е г о справа) бита

5.2.2. Команда безусловного перехода — JMP Самый простой способ изменить последовательность выполнения команд заключается в использовании команды jmp — так называемой команды без­условного перехода. Она перезаписывает указатель команд (регистр IP или CS), что заставляет процессор «переключиться» на выполнение команды по указанному адресу. Формат команды таков:

JMP [тип_перехода] операнд Команда JMP — аналог конструкции GOTO, которая используется в высо­коуровневых языках программирования. Название команды объясняет ее действие, а именно «jump», «переход». Команде нужно передать один обяза­тельный операнд — адрес в памяти, с которого процессор должен продолжить выполнение программы. Операнд может быть указан явно (непосредственное значение адреса) или быть регистром общего назначения, в который загружен требуемый адрес. Но новичкам я никогда не рекомендовал бы это делать: язык ассемблера, подобно языкам программирования высокого уровня, позволяет обозначить адрес назначения при помощи метки. В зависимости от «расстояния» переходы бывают трех типов: короткие (short), ближние (near) и дальние (far). Тип перехода задается необязательным параметром инструкции jmp. Если тип не задан, по умолчанию используется тип near. Максимальная «длина» короткого перехода (то есть максимальное расстояние между текущим и целевым адресом) ограничена. Второй байт инструкции (операнд) содержит только одно 8-разрядное значение, поэтому целевой адрес может быть в пределах от -128 до 127 байтов. При переходе выполняется знаковое расширение 8-разрядного значения и его добавление к текущему значению Е(1Р). «Длина» ближнего перехода (near) зависит только от режима процессора. В реальном режиме меняется только значение IP, поэтому мы можем «путеше­ствовать» только в пределах одного сегмента (то есть в пределах 64 Кб); в защищенном режиме используется EIP, поэтому целевой адрес может быть где угодно в пределах 4 Гб адресного пространства. Переход типа far модифицирует кроме IP еще и сегментный регистр CS, ко­торый используется при вычислении фактического адреса памяти. Поэтому команда перехода должна содержать новое значение CS.

58

Page 60: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

Сейчас мы совершим «дальний переход» от предмета нашего рассмотрения и поговорим о метках в языке ассемблера. Вкратце, метка — это идентифика­тор, заканчивающийся двоеточием. Во время компиляции он будет заменен точным адресом согласно его позиции в программе. Рассмотрим следующий фрагмент кода:

mov а х , 4 ; АХ == 4 new_loop: ;метка new_loop mov b x , ах ;копируем АХ в ВХ

Чтобы перейти к метке new_loop из другого места программы, используйте команду:

jmp new_loop ;переходим к new_loop После выполнения этой команды выполнение программы продолжится с метки new_loop. Если вам нужно сначала написать инструкцию перехода и только потом определить метку, нет проблем: компилятор обрабатывает текст программы в несколько проходов и понимает такие «забегания вперед»:

jmp s t a r t /переход на s t a r t

f i n i s h : ;метка ' ' f in ish"

. . . ;какие-то команды

s t a r t : ;метка ' ' s t a r t " jmp f i n i s h /переход на " f in i sh"

Теперь давайте вернемся к различным типам переходов: даже если мы нович­ки, мы все равно будем изредка их использовать. Короткий переход полезен в ситуации, где метка назначения находится в пределах 128 байтов. Поскольку команда короткого перехода занимает 2 байта, команда ближнего перехода занимает 3 байта, а дальнего — 5 байтов, мы можем сэкономить байт или три. Если вы не можете оценить правильное «расстояние», все равно можете по­пробовать указать shor t — в крайнем случае, компилятор выдаст ошибку:

nea r_ labe l : ;метка "near_label" . . . ;несколько команд

jmp short near_label ;переходим к "near_label"

5.2.3. Условные переходы — Jx Другой способ изменения последовательности выполнения команд заключа­ется в использовании команды условного перехода. В языке ассемблера имеется множество команд условного перехода, и боль­шинство из них вам нужно знать — иначе вы не сможете написать даже

59

Page 61: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

«средненькую» программку. Имена этих команд различаются в зависимости от условия перехода. Условие состоит из значений одного или нескольких флагов в регистре признаков. Работают эти команды одинаково: если условие истинно, выполняется переход на указанную метку, если нет, то процессор продолжит выполнять программу со следующей команды. Общий формат команд условного перехода следующий:

Jx метка_назначения Рассмотрим наиболее часто встречающиеся команды:

;переходит к i s _ t r u e , если флаг ZF = 1 ;переходит к i s _ t r u e , если флаг CF = 1 ;переходит к i s _ t r u e , если флаг SF = 1 ;переходит к i s _ t r u e , ;0F = 1

jz i s _ t r u e jc i s _ t r u e j s i s _ t r u e jo i s _ t r u e если флаг переполнения

Любое условие может быть инвертировано, например: jnz i s_ t rue ;переходит к i s _ t r u e , если флаг ZF = О

Так же образованы имена команд JNC, JNS и JNO. Рассмотрим сводную таблицу команд условного перехода в зависимости от условия, которое проверяет процессор (чтобы не писать «для перехода», будем использовать сокращение jump) (см. табл. 5.1).

Сводная таблица команд условного перехода

Инструкции для беззнаковых чисел

Инструкции для чисел со знаком

о1==о2 о1=о2

JE(JZ)

Jump, если равно

Jump, если 0

JE(JZ)

Jump, если равно

Jump, если 0

о1!=о2 о1<>о2

JNE(JNZ)

Jump, если не равно

Jump, если не 0

JNE(JNZ)

Jump, если не равно

Jump, если неО

о1>о2

JA(JNBE)

Jump, если больше

Jump, если не меньше или равно

JG(JNLE)

Jump, если больше

Jump, если не меньше или равно

о1<о2

JB(JNAE)

Jump, если меньше

Jump, если не больше или равно

JL(JNGE)

Jump, если меньше

Jump, если не больше или равно

о1=<о2

JNA(JBE)

Jump, если не больше Jump, если меньше или

равно

JNGCJLE)

Jump, если не больше Jump, если меньше или

равно

Таблица 5.1

о1>=о2

JNB(JAE)

Jump, если не меньше Jump, если больше или

равно

JNL(JGE) 1 Jump, если не меньше Jump, если больше или

равно

В первой строке таблицы указано условие перехода. Во второй строке по­казаны соответствующие команды условного перехода (в скобках — их до­полнительные названия). Чтобы лучше запомнить имена команд, запомните несколько английских слов: equal — равно, above — больше, below — ниже, zero — ноль, greater — больше, less — меньше. Таким образом, JE — Jump if Equal (Переход, если Равно), JNE — Jump if Not Equal (Переход, если Не Равно), JA — Jump if Above (Переход, если больше) и т.д.

60

Page 62: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

Подобно командам MUL и DIV, для работы с числами со знаком служит дру­гой набор команд условного перехода. Причина этого в том, что проверяемое условие состоит из значений других флагов. Адрес назначения команды условного перехода должен лежать в пределах 128 байтов: все они реализуют переходы короткого типа. Если вам нужно «прогуляться» за пределы 128 байтов, то вы должны в инструкции условного перехода указать адрес, по которому будет находиться команда jmp, которая и выполнит дальнейший переход:

jz far_jump ; если ZF = 1, перейти к far_jump ; несколько команд

far_jump: jmp far f i n i sh ; ''дальний" переход

Теперь рассмотрим, как реализовать конструкцию IF-THEN на языке ассем­блера. В нашем простом случае мы перейдем к метке i f_ th ree , если регистр АХ содержит значение 3. Прежде всего мы должны проверить, есть ли в регистре АХ тройка. Для этого используем команду СМР:

стр ах,3 ;сравниваем АХ с 3 Для проверки равенства применим команду JZ, как показано в таблице ко­манд условного перехода:

jz i s_ th r ee /переходит к " i s _ t h r e e " , если АХ = 3 Обратите внимание, что для проверки на равенство используются одина­ковые команды (JZ — равно и JNZ — не равно) для чисел со знаком и для беззнаковых чисел. Если АХ = 3, то команда jz выполнит переход к метке i s _ t h r e e , в противном случае будет продолжено выполнение программы со следующей за jz команды. Следующий пример показывает беззнаковое сравнение CL и AL. Если оба значения равны, то в регистр BL помещается значение 1, если AL больше, чем CL, то BL=2, а если AL меньше CL, то BL=3.

cmp a l , c l ;сравниваем AL и CL j z w r i t e _ l ;переходим к w r i t e _ l , если AL - CL cmp a l , c l ;сравниваем AL и CL j a w r i t e _ 2 ; переходим к w r i t e _ 2 , если AL > CL mov b l , 3 ;последний случай - сразу загружаем 3 в BL end_ i f : /просто метка, символизирующая конец IF

w r i t e _ l : ;метка w r i t e _ l mov Ы Д ;BL = 1 jmp end_if /переходим к end_if write_2: ;метка write_2 mov bl,2 ;BL = 2 jmp end_if ;переходим к end_if

61

Page 63: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

I Начало J

/ Конец )4 '

Рис. 5.3. Структурная схема нашей программы

В нашей программе мы использовали безусловный переход (jmp end_if), что­бы вернуть управление на исходную позицию. Это не лучшее решение: нам придется выполнить еш;е один безусловный переход перед меткой w r i t e ^ l , а то наша программа «зациклится». Адрес назначения понятен — следую­щая после последнего jmp end_if команда. Вот так выглядит улучшенный фрагмент кода:

mov Ь 1 Д ; сразу устанавливаем BL = 1 сшр a l , c l ; сравниваем AL и CL j e end_i f /переходим в конец программы, mov b l , 2 ;BL = 2 cmp al,cl ;сравниваем AL и CL ja end_„if ; переходим в конец программы, mov bl,3 ;BL - 3 end_ i f : /конец программы

если AL = CL

если AL > CL

Новый пример короче, но нет предела совершенству, и мы можем его еще улучшить. Инструкция MOV не изменяет регистр флагов, поэтому в дальней­шем сравнении нет надобности:

mov b l , 1 cmp а 1 , с 1 j e end_i f mov b l , 2 j a end_i f mov b l , 3 end if:

BL = 1 сравниваем AL и CL переходим в конец программы, BL = 2 переходим в конец программы, BL = 3

если AL

если AL

CL

CL

конец программы

62

Page 64: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

Если подытожить, то мы только что записали на Ассемблере следующую конструкцию языка С:

if (al =- cl) bl = 1 e l s e i f (al > cl) bl = 2 e l s e bl = 3;

5.3. Итерационные конструкции — циклы Последней управляющей конструкцией, которую мы рассмотрим, будет итера­ция, или цикл. Циклом называется многократное повторение последователь^ ности команд до наступления указанного условия.

Нет

Действие

Рис. 5.4. Цикл в программе

В языках программирования высокого уровня известно много разновидностей циклов, в том числе:

• цикл со счетчиком (цикл FOR), повторяющийся заранее заданное ко­личество раз;

• цикл с условием (цикл WHILE), повторяющийся до тех пор, пока условие истинно;

• цикл с инверсным условием (цикл UNTIL), повторяющийся до тех пор, пока условие не станет истинным.

Цикл со счетчиком с помощью конструкций IF и GOTO

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

63

Page 65: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Рассмотрим символическую структуру пустого цикла FOR на псевдоязыке: FOR_START: 1 = 0 FOR_LOOP:

I-I + l IF I < 10 THEN GOTO FOR_LOOP

FOR_FINISH:

;начало /инициализация счетчика ;метка цикла ;тело цикла (пустое) ;увеличиваем счетчик ;проверяем счетчик ;переходим на начало цикла или выходим ;из цикла ;конец цикла

В нашем примере тело цикла должно повторяться 10 раз. Сначала мы ини­циализируем счетчик. Затем выполняем тело цикла (в нашем случае пустое), после этого увеличиваем счетчик на 1. Проверяем: если счетчик меньше 10, то начинаем опять выполнять тело цикла, если же счетчик равен 10, то мы выходим из цикла.

( Начало )

т

1 = 1

^ г

Тело цикла

т

1 = 1 + 1

^ К 10 : Да

Нет

Г Конец j

Рис. 5.5. Структурная схема цикла FOR

А теперь запишем нашу схему на языке ассемблера. Мы уже знаем, как ре­ализовать на языке ассемблера конструкции IF и GOTO, из которых можно построить цикл FOR. В качестве счетчика (псевдопеременной I) мы будем использовать регистр ЕСХ: for_start: mov есх,О for_loop:

/инициализируем счетчик ЕСХ ;метка для перехода назад

64

Page 66: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

. . . ; тело цикла i n c есх ;увеличиваем ЕСХ на 1 стр есх ДО /сравниваем ЕСХ с 10 jnz f o r _ l o o p ; если не равно , переход на 1:ог_1оор f o r _ f i n i s h : ;если ЕСХ = 10, выходим

Рассмотрим другую версию цикла FOR. Она работает так же, как предыдущая, но счетчик мы будем хранить не в регистре, а в памяти, в переменной I.

f o r _ s t a r t : mov dword [i],0 ;переменР1ая типа dword 1 = 0 for_loop: ;метка для перехода назад ... ;тело цикла inc dword [i] /увеличиваем i на 1 cmp dword [1 ]Д0 ; сравниваем i с 10 jnz for__loop ;если не равно, переход на for_loop fo r_ f in i sh : ;если равно, выходим

Вторая версия будет работать медленнее, поскольку счетчик хранится в памяти, время доступа к которой существенно больше, чем время доступа к регистрам. В заключение давайте рассмотрим еще одну версию цикла, использующую команду DEC и команду проверки флага ZF вместо команды сравнения СМР. Принцип работы следующий: устанавливаем счетчик (ЕСХ-Ю), выполняем тело цикла, уменыиаем счетчик на 1. Если ZF установлен, значит, в ЕСХ на­ходится О и нам нужно прекратить выполнение цикла: for_start: mov есх,10 ;ЕСХ =10 fог_1оор: ;метка для перехода назад ... ;тело цикла dec есх ;уменьшаем ЕСХ на 1 jnz f o r _ l o o p ; если не О, переходим на for_„loop f o r _ f i n i s h : ;если О, выходим из цикла

Мы только что записали на языке ассемблера следующую конструкцию языка С:

fo r ( i = 0 ; i < 10; i++) {}

LOOP — сложная команда, простая запись цикла

В главе, посвященной процессору 80386, мы упомянули, что х86-совмести-мые чипы используют архитектуру CISC (Компьютер со сложным набором команд), то есть имеют полную систему команд. Другими словами, в составе системы команд имеются сложные команды, которые могут заменить ряд простых. При чем здесь циклы? Если у вас CISC-процессор, то вам не нужно

65

Page 67: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

реализовать цикл самостоятельно — для организации цикла можно исполь­зовать команду LOOP:

LOOP метка

Подобно команде MUL, команда LOOP работает с двумя операндами. Первый операнд фиксирован, и мы не можем его указать явно. Это значение регистра ЕСХ (или СХ). Второй — это адрес целевой метки цикла. Инструкция LOOP уменьшает значение регистра ЕСХ (СХ) на единицу и, если результат не равен О, то она переходит на указанную метку. Метка должна быть в пределах 128 байтов (короткий тип перехода). Перепишем наш простой цикл FOR с использованием команды LOOP:

f o r _ s t a r t : mov схДО ;СХ = 10 — 10 итераций for_loop: ;метка для возврата назад ... ;тело цикла loop for_loop ;уменьшаем СХ, если не О, переходим

;к for_loop for_f in i sh : ;выход из цикла

Как видите, код стал еш;е более компактным.

Цикл со счетчиком и дополнительным условием. Команды LOOPZ и LOOPNZ

Команда LOOPZ позволяет организовать цикл с проверкой дополнительного условия. Например, мы можем уточнить условие из предыдущего примера: цикл нужно выполнить, как и раньше, не более 10 раз, но только при условии, что регистр ВХ содержит значение 3. Как только значение в регистре ВХ из­менится, цикл нужно прервать. LOOPZ метка LOOPNZ метка

Команда LOOPZ уточняет условие перехода следующим образом: переход на указанную метку произойдет, если СХ не содержит нуля и в то же время флаг ZF равен единице. Другое имя этой команды — LOOPE. Следующий фрагмент кода показывает пример цикла с дополнительным условием: for_start: mov СХ,10 for_loop:

СХ - 10 метка для возврата назад тело цикла FOR

где-то здесь изменяется регистр ВХ

66

Page 68: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

стр Ьх ,3 loopz f o r _ l o o p

fo r f i n i s h :

ВХ равен 3? СХ=СХ-1; если СХоО, и если ВХ=3 , переход к f o r _ l o o p если СХ = О или если ВХ о 3 , выходим

Команда LOOPNZ работает аналогично, но дополнительное условие противо­положно: переход будет выполнен только если СХ (ЕСХ) не равен О и в то же время ZF равен 0. Другое имя этой команды — LOOPNE.

5.4. Команды обработки стека При программировании очень часто возникает потребность временно сохра­нять содержимое регистров процессора или какого-то адреса памяти, чтобы через некоторое время восстановить исходные значения. Язык ассемблера удовлетворяет эту потребность набором команд для работы со специальной областью памяти, которая называется стеком.

Что такое стек и как он работает?

Давайте разберемся, как работает стек. Все мы знаем, что такое очередь. Приходит первый клиент, пока его обслуживают, подходят еще два клиента. Когда первый клиент обслужен, начнут обслуживать второго клиента, затем третьего — и так далее. Принцип заключается в том, что первым будет об­служен тот, кто пришел первым. Такой тип очереди называется FIFO (First In — First Out) — первым пригаел, первым вышел.

Выход FIFO Вход

&

\(Ь\^Ы 1 |(b|0|Q|G^| |(b|0|Q|C?| |(bH|Q|(?|

\^М(?\ 1

е

Рис. 5.6. Очередь FIFO

67

Page 69: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Существует и другой тип очереди — LIFO (Last In — First Out) — последним пришел, первым вышел. Понимаю, что с точки зрения обычного человека это какая-то неправильная очередь, но в жизни мы сталкиваемся с ней не реже, чем с первой. Например, мы собираемся куда-то поехать и укладываем вещи. Когда мы откроем сумку, вверху окажутся вещи, которые мы положили по­следними.

LIFO Вход/Выход

(bUD

GkDK? (bkDC? (bUDQ (?

(bUD Рис. 5.7. Очередь LIFO

Стек работает по принципу LIFO. Данные, помещенные в стек последними, будут первыми «вытолкнуты» из стека. В РС-совместимых компьютерах нет аппаратного стека, поэтому данные стека хранятся в памяти. Вершина стека представлена парой SS:SP (SS:ESP) — сегмент стека (Stack Segment) и указатель вершины стека (Stack Pointer). Стек растет в памяти «вниз», то есть новая порция данных записывается по меньшему адресу. Впрочем, точный адрес данных внутри стека не имеет для нас значения, потому что любая операция над стеком имеет дело с его вер­шиной, на которую всегда указывает регистр SP (ESP). Стек может содержать 16- или 32-битные данные. Микропроцессор имеет две команды для работы со стеком — PUSH и POP.

Команды PUSH и POP: втолкнуть и вытолкнуть

Команда PUSH позволяет поместить в стек содержимое любого 16- или 32-битного регистра или ячейки памяти. Формат команды следующий:

PUSH о1

68

Page 70: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

Пример использования: push еах /поместить ЕАХ в стек

Мы можем сами реализовать команду PUSH с помощью следующей пары команд:

sub e s p , 4 ;уменьшаем ESP на 4 (ЕАХ — 4-байтный ;регистр)

mov [ s s : e s p ] , e a x ;сохраняем ЕАХ в стеке В общем виде (с использованием оператора sizeof, «позаимствованного» из языков высокого уровня) команда push о1 может быть записана на псевдо­языке так:

( E ) S P = ( E ) S P - s i z e o f ( o l ) o l -> SS : [ (E)SP]

Другая команда, POP, записывает в свой операнд значение вершины стека (последнее сохраненное в стеке значение). Тип операнда должен быть таким же, как у инструкции PUSH (другими словами, если вы поместили в стек 32-разрядный регистр, извлечение из стека должно происходить тоже в 32-разрядный регистр).

Команду POP можно реализовать с помощью команд MOV и ADD: mov e a x , [ s s : e s p ] /помещаем в ЕАХ вершину стека add e s p , 4 ;"удаляем" последнее значение

;типа dword в стеке

Рассмотрим несколько примеров: push еах ;сохранить значение регистра ЕАХ в стеке push e s i ;сохранить значение регистра ESI в стеке

pop еах ;извлечь данные из стека в ЕАХ pop e s i ;извлечь данные из стека в ESI

В результате выполнения этих команд мы поменяем местами значение реги­стров ЕАХ и ESI: сначала помещаем в стек значение ЕАХ, затем — ESI, после этого извлекаем из стека последнее сохраненное значение (бывшее значение регистра ESI) в регистр ЕАХ, после этого в стеке останется бывшее значение ЕАХ, которое мы записываем в ESI.

Для обеспечения обратной совместимости с процессорами предыдущих по­колений 16-битные регистры тоже можно поместить в стек. mov ах,0x1234 ;АХ - 0x1234 mov bx,0x5 67 8 ;ВХ - 0x5 67 8 push ах /сохранить значение регистра АХ в стеке push bx /сохранить значение регистра ВХ в стеке ... /изменяем значения регистров pop bx /извлекаем вершину стека в ВХ

69

Page 71: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Конец стека

OxFOOO

Чистая память

(значения не заданы)

SP=OxFFFE

?? I ??

O O i O O

OxFOOO

PUSH АХ

SP=OxFFFC

OxFFFE

0

0

??

34

00

П l°l

,??

\'^\ lOoJ

OxFOOO

PUSH BX

SP--OxFFFA

OxFFFC

OxFFFE

0

0

78

34

00

П l°l

56

12

lOoJ

OxFOOO

POPBX

SP=OxFFFC

OxFFFE

[rfol 0 | 0 J

00 1 00

3 4 | 1 2 |

00 1 00 1 AX=1234 BX = 5678

AX=1234 BX = 5678

AX=1234 BX=5678

AX=1234 BX = 5678

Рис. 5.8. Изменение содержимого стека

До выполнения первой команды PUSH вершина стека содержала значение 0x0000. На вершину стека указывает пара SS:SP. Допустим, что SP содержит адрес OxFFFE. После выполнения PUSH АХ указатель стека был уменьшен на 2 и принял значение OxFFFC, и по этому адресу (в новую вершину стека) было записано значение 0x1234. Вторая команда, PUSH BX, также уменьши­ла значение SP на 2 (OxFFFA) и записала в новую вершину стека значение 0x5678. Команда POP BX удалила значение 0x5678 из стека и сохранила его в регистре ВХ, а указатель стека увеличила на 2. Он стал равен OxFFFC, и в вершине стека оказалось значение 0x1234. Помните, что 8-битные регистры сохранять в стеке нельзя. Нельзя и поместить в стек регистр IP (EIP) непосредственно, при помош;и команд PUSH/POP: это делается по-другому, и чуть позже вы узнаете, как именно.

Команды PUSHA/РОРАи PUSHAD/POPAD: «толкаем» все регистры общего назначения

Иногда полезно сохранить в стеке значения сразу всех регистров общего на­значения. Для этого используется команда PUSHA, а для извлечения из стека значений всех регистров служит команда РОРА. Команды PUSHA и РОРА помещают в стек и извлекают из него все 16-разрядные регистры. Операндов у этих команд нет. Поскольку команды PUSHA и РОРА разрабатывались для предшественника процессора 80386, они не могут сохранять значений 32-битных регистров (они просто не подозревают об их существовании). Для сохранения и восстановле­ния значений расширенных регистров служат команды PUSHAD и POPAD.

70

Page 72: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

Регистры помещаются в стек в следующем порядке (сверху вниз): ( Е ) А Х , ( Е ) С Х , ( E ) D X , ( Е ) В Х , ( E ) S P , ( Е ) В Р , ( E ) S I , ( E ) D I

Рассмотрим небольшой пример: pusha /поместить в стек все регистры общего назначения

;некоторые действия, модифицирующие ;значения регистров

рора ;восстанавливаем все регистры

Команды P U S H F / P O P F M PUSHFD/POPFD: «толкаем» регистр признаков

Рассмотренные четыре команды не заботились о помещении в стек регистра признаков. В 16-битных процессорах и регистр признаков был 16-битным, по­этому для помещения в стек флагов и восстановления из него использовались команды PUSHF и POPF. Для новых процессоров, где регистр признаков 32-битный, нужно использовать 32-битные версии этих команд — PUSHFD и POPFD. Ни одна из рассмотренных до сих пор операций не изменяет старшие 16 битов регистра флагов, поэтому для практических целей будет достаточно команд PUSHF и POPF.

стр ах,Ьх ; сравниваем АХ и ВХ pushf ;помещаем результат сравнения в стек . . . ;выполняем операции, изменяющие флаги add d i , 4 ;например, сложение popf /восстанавливаем флаги jz equal ; если АХ = ВХ, переходим на ' 'equal"

Команды CALL и RET: организуем подпрограмму

Ни одна серьезная программа не обходится без подпрограмм. Основное на­значение подпрограмм — сокращение кода основной программы: одни и те же инструкции не нужно писать несколько раз — их можно объединить в подпрограммы и вызывать по мере необходимости. Для вызова подпрограммы используется команда CALL, а для возврата из подпрограммы в основную программу — RET. Формат обеих команд таков:

CALL тип_вызова операнды RET

Команде CALL нужно передать всего один операнд — адрес начала подпро­граммы. Это может быть непосредственное значение, содержимое регистра, памяти или метка. В отличие от JMP, при выполнении команды CALL первым

71

Page 73: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

делом сохраняется в стеке значение регистра IP (EIP). Передача управления на указанный адрес называется вызовом подпрограммы. Как и команде JMP, команде CALL можно указать «размер шага». По умол­чанию используется near. Когда происходит вызов типа fa r , сегментный регистр CS также сохраняется в стеке вместе с IP (EIP). Возврат из подпрограммы выполняется с помощью команды RET, которая выталкивает из стека его вершину в IP (EIP). После этого процессор про­должит выполнение инструкций, находящихся в основной программе после команды CALL. Если подпрограмма вызывалась по команде CALL far, то для возврата из нее нужно восстановить не только IP (EIP), но и CS: следует использовать команду RETF, а не RET. Существует еще более сложный способ возврата из подпрограммы: команда RETF или RET может принимать непосредственный операнд, указывающий, сколько порций данных нужно вытолкнуть из стека вслед за IP (EIP) и CS. Этот вариант мы рассмотрим в 13 главе, когда будем говорить об объедине­нии в одной программе фрагментов кода на ассемблере и на языках высокого уровня, а сейчас перейдем к практике. Напишем подпрограмму, которая складывает значения ЕАХ и ЕВХ, а ре­зультат помещает в ЕСХ, не обращая внимания на переполнение. Значения ЕАХ и ЕВХ после возвращения из подпрограммы должны сохраниться не­изменными. Назовем нашу подпрограмму add_„it. Прежде всего мы должны получить аргументы из основной программы. В высокоуровневых языках для передачи аргументов используется стек, но мы упростим себе задачу и используем ре­гистры. Операция ADD изменяет значение своего операнда, поэтому первым делом сохраним его в стеке:

add_i t : push еах /сохраняем значение ЕАХ в стек add eax,ebx ;ЕАХ = ЕАХ + ЕВХ mov есх,еах ;копируем значение из ЕАХ в ЕСХ pop еах ;восстанавливаем оригинальное значение ЕАХ ret ; возвращаемся

Теперь вызовем нашу подпрограмму add_it с аргументами 4 и 8: mov е а х , 4 mov ebx ,8 c a l l add i t

ЕАХ - 4 ЕВХ - 8 вызываем add_.it Результат будет в регистре ЕСХ

Что если мы забудем восстановить оригинальное значение ЕАХ (забудем написать команду pop еах)? Команда RET попыталась бы передать управле-

72

Page 74: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

ние адресу, заданному оригинальным значением ЕАХ, что могло бы вызвать сбой нашей программы, а то и всей операционной системы. То же самое произойдет, если мы забудем написать команду RET: процессор продолжит выполнение с того места, где заканчивается наша подпрограмма, и рано или поздно совершит недопустимое действие. Мы можем упростить нашу подпрограмму add_i t , полностью отказавшись от инструкций POP и PUSH:

a d d _ i t : mov Gcx,eax /копируем значение ЕАХ (первый параметр) в ЕСХ add e c x , e b x /добавляем к нему ЕВХ (второй параметр) ,

/ ре зультат будет сохранен в регистре ЕСХ r e t /возвращаемся

Команды INT и IRET: вызываем прерывание

Вернемся к теме прерываний. Прерыванием называется такое событие, когда процессор приостанавливает нормальное выполнение программы и начинает выполнять другую программу, предназначенную для обработки прерывания. Закончив обработку прерывания, он возвращается к выполнению приоста­новленной программы. Во второй главе было сказано, что все прерывания делятся на две группы: программные и аппаратные. Программные прерывания порождаются по ко­манде ШТ. Программные прерывания можно рассматривать как «прерывания по требованию», например, когда вы вызываете подпрограмму операционной системы для вывода строки символов. В случае с программным прерыванием вы сами определяете, какое прерывание будет вызвано в тот или иной момент. Команде INT нужно передать всего один 8-битный операнд, который задает номер нужного прерывания.

INT ор

Аппаратные прерывания вызываются аппаратными средствами компьютера, подключенными к общей шине (ISA или PCI). Устройство, запрашивающее прерывание, генерирует так называемый запрос на прерывание (IRQ, interrupt requests). Всего существует 16 аппаратных запросов на прерывание, поскольку только 16 проводников в шине ISA выделено для этой цели. Запрос на пре­рывание направляется контроллеру прерываний, который, в свою очередь, за­прашивает микропроцессор. Вместе с запросом он передает процессору номер прерывания. После запуска компьютера и загрузки операционной системы DOS, IRQ О (системный таймер) соответствует прерыванию 8 (часы). Когда процессор получает номер прерывания, он помещает в стек контекст выполняемой в данный момент программы, подобно тому, как человек кладет в книгу закладку, чтобы не забыть, с какого места продолжить чтение книги. Роль закладки играют значения CS, (Е)1Р и регистр флагов.

73

Page 75: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Теперь процессор будет выполнять другую программу — обработчик пре­рывания. Адрес этой программы называется вектором прерывания. Векторы прерывания хранятся в таблице векторов прерываний, находящейся в памяти. Таблицу прерываний можно представить себе как массив адресов подпро­грамм, в котором индекс массива соответствует номеру прерывания.

После того, как процессор определит адрес обработчика прерывания, он за­пишет его в пару CS и (Е)1Р. Следующая выполненная команда будет первой командой обработчика прерывания. В десятой главе, посвященной программированию в DOS, мы опишем функции 21-го (0x21) прерывания, которое генерируется следующей инструкцией:

i n t 0x21 вызов DOS Возврат из обработчика прерывания осуществляется с помощью команды IRET, которая восстанавливает исходные значения (Е)1Р, CS и флагов из стека. Формат команды:

IRET

Давайте разберемся, как вызывается прерывание на примере 21-го преры­вания (рис. 5.9). Мы считаем, что процессор работает в реальном режиме с 16-битной адресацией.

0000:0000 Таблица векторов прерывания

IN 1 U X ^ l

0000:0x84(0x21*4)

1 п

INTOO INT01 ^ \ / N

смещение сегмент

смещение сегмент

смещение сегмент

Рис. 5.9. Загрузка в CS и IP новых значений

Прежде всего процессор находит номер прерывания. Программные преры­вания вызываются инструкцией INT, которая содержит номер прерывания в своем операнде. Следующий шаг — сохранение контекста программы в стеке. Продемонстрируем этот процесс с помощью команд ассемблера:

pushf ;сохраняем значения регистра признаков в стеке push CS ; сохраняем регистр CS в стеке

74

Page 76: Ассемблер на примерах. Базовый курс

Глава 5. Управляющие конструкции

push i p ; э т а команда недопустима. Мы написали ее с /демонстрационной целью. Реализовать ее можно так : ; са11 h e r e ; h e r e :

Последний шаг — это безусловный переход на адрес, прочитанный из таблицы прерываний по номеру запрошенного прерывания: JMP far. Здесь мы слегка забегаем вперед: пока просто примите, что таблица векто­ров прерываний находится в самом начале адресуемой памяти, то есть по адресу 0x0000:0x0000. Каждый из векторов прерывания занимает четыре байта. Первые два байта определяют новое значение IP (то есть смещение), а оставшиеся два — новое значение сегментного регистра CS. Вектор пре­рывания номер 0x21 хранится по адресу 0х0000:(0х21*4), поэтому его вызов можно записать так:

jmp f a r [0x21*4] / э т а инструкция переходит на указанный ; а д р е с , предполагаем, что DS=0, поэтому ;мы можем адресовать память с адреса ;ОхОООО: 0x0000)

Команду INT можно реализовать самостоятельно с помощью команд PUSHF и CALL far:

pushf /сохраняем флаги в стеке c a l l f a r [0x21*4] / теперь сохраняем CS и IP

/И выполняем ''jump''

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

75

Page 77: Ассемблер на примерах. Базовый курс

Глава 6 Прочие команды

Изменение регистра признаков напрямую

Команда XCHG — меняем местами операнды

Команда LEA — не только вычисление адреса

Команды для работы со строками

Команды ввода/вывода (I/O)

Сдвиг и ротация

Псевдокоманды

Советы по использованию команд

АПСг?^Л1^ГОО vV'l Г1П!1?Л6Г '>ч.

Page 78: Ассемблер на примерах. Базовый курс

в этой главе мы рассмотрим наиболее часто используемые команды системы команд процессора х86. С помощью этих команд вы сможете написать до­статочно сложные программы для решения самых разнообразных задач.

6.1. Изменение регистра признаков напрямую

Несколько команд позволяют непосредственно модифицировать флаги реги­стра признаков. Мы рассмотрим четыре команды, которые изменяют флаги IF и ID, то есть флаг прерывания и флаг направления.

Команды CLi и STI

Команды CLI (Clear Interrupt) и STI (Set Interrupt) сбрасывают и устанав­ливают флаг прерывания IF. Иными словами, при их помощи вы можете за­претить или разрешить аппаратные прерывания. Если этот флаг установлен (1), аппаратные прерывания разрешены. Команда CLI сбрасывает флаг (0) — запрещает аппаратные прерывания. Потом вы должны снова разрешить их, выполнив команду STI:

c l i ;отключить прерывания - использовать только в DOS! . . . ; теперь мы никем не будем потревожены и можем

/ВЫПОЛНЯТЬ какие -то действия, например, изменять /таблицу прерываний

s t i /включаем прерывания снова

Команды STD и CLD

Команды STD и CLD модифицируют значение флага DF. Этим флагом поль­зуется группа команд для обработки строк, поэтому подробнее о нем мы по­говорим в следующей главе. Команда CLD сбрасывает этот флаг (что означает отсчет вверх), а STD устанавливает его (отсчет в обратном порядке).

77

Page 79: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Формат команд простой: STD CLD

6.2- Команда XCHG — меняем местами операнды

Очень часто нам нужно поменять местами значения двух регистров. Конечно, можно это сделать, используя третий, временный, регистр, но намного проще использовать инструкцию XCHG (exchange — обмен), которая как раз для этого и предназначена.

XCHG о 1 , о 2

Подобно инструкции MOV, она имеет два операнда — о1 и о2. Каждый из них может быть 8-, 16- и 32-разрядным, и только один из них может находиться в памяти — требования такие же, как у команды MOV.

xchg е а х , е а х /меняем местами значения ЕАХ и ЕАХ. ;Так реализован NOP — пустой оператор

xchg e b x , e c x ;меняем местами значения ЕВХ и ЕСХ xchg a l , a h ;меняем местами значения AL и АН xchg d l , a h ;меняем местами значения DL и АН xchg by te [ v a r i a b l e ] ,с1 /меняем местами один байт памяти и CL

6.3. Команда LEA — не только вычисление адреса

Имя этой команды — это сокращение от «Load Effective Address», то есть «загрузить эффективный адрес». Она вычисляет эффективный адрес второго операнда и сохраняет его в первом операнде (который может быть только регистром). Синтаксис этой команды требует, чтобы второй операнд был за­ключен в квадратные скобки, но фактически она не адресует память.

LEA о 1 , [о2]

LEA полезна в тех случаях, когда мы не собираемся обращаться к памяти, а адрес нужен нам для другой цели:

l e a e d i , [ebx*4+ecx] ; загружает в регистр EDI адрес , ; вычисленный как EDI = ЕВХ' 4+ЕСХ

Значение, вычисленное командой LEA, не обязательно рассматривать как адрес: ее можно использовать для целочисленных арифметических вычисле­ний. Несколько примеров «экзотического» использования LEA будет при­ведено в главе, посвященной оптимизации.

78

Page 80: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

6.4. Команды для работы со строками Строкой в языке ассемблера называется последовательность символов (бай­тов), заканчивающаяся нулевым байтом (точно так же, как в языке С).

Гох43 0x6F 0x6D 0x70 0x75 0x74 0x65 0x72 0x00 C o m p u t e r / 0

Рис. 6.1. Строка заканчивается нулевым байтом

Система команд х86-совместимых процессоров содержит несколько команд, облегчающих манипуляции со строками. Каждую из них можно заменить не­сколькими элементарными командами, но, как и в случае с командой LOOP, эти команды сделают вашу программу короче и понятнее. В зависимости от размера операнда команды для обработки строк делятся на три группы. Первая группа предназначена для работы с 8-битными операн­дами, то есть с отдельными символами. Имена этих команд заканчиваются на «В» (byte). Имена команд второй группы, предназначенных для работы с 16-битными операндами, заканчиваются символом «W» (word). Третья группа команд работает с 32-битными операндами, и имена их заканчиваются на «D» (double word). Все команды, обсуждаемые в этом параграфе, работают с фиксированными операндами и не имеют аргументов. Порядок отсчета байтов в строке во время работы этих команд зависит от флага направления (DF).

Команды STOSx •— запись строки в память

Под единым обозначением STOSx (STOre String) скрываются три команды: •STOSB •STOSW •STOSD

Команда STOSB копирует содержимое регистра AL в ячейку памяти, адрес которой находится в паре регистров ES:(E)DI, и уменьшает или увеличивает (в зависимости от флага DF) на единицу значение регистра (E)DI, чтобы при­готовиться к копированию AL в следующую ячейку. Если DF==0, то (E)DI будет увеличен на 1, в противном случае — уменьшен на 1. Какой регистр будет использоваться —- D1 или EDI — зависит от режима процессора. Вторая инструкция, STOSW, работает аналогично, но данные берутся из регистра АХ, а (E)DI уменьшается/увеличивается на 2. STOSD копирует со­держимое ЕАХ, а E(DI) уменьшает/увеличивает на 4.

79

Page 81: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

clcl ; сбрасываем DF, направление будет вверх stosw /сохраняем АХ в ES:[DI] или ES:[EDI] (зависит от

/режима процессора) и увеличиваем (E)DI на 2

Команды LODSx — чтение строки из памяти

Под единым обозначением LODSx (LOaD String) скрываются три команды: •LODSB •LODSW •LODSD

Действие этих команд протршоположно командам предыдущей группы: они копируют порцию данных из памяти в регистр AL, АХ и ЕАХ. Адрес нужной ячейки памяти берется из пары DS:(E)SI. Если флаг DF равен нулю, то ре­гистр SI будет увеличен на 1/2/4 (В, W, D), в противном случае — уменьшен на 1/2/4.

Команды CMPSx — сравнение строк

Под единым обозначением CMPSx (СоМРаге String) скрываются три команды: • CMPSB • CMPSW • CMPSD

Команда CMPSB сравнивает байт, находящийся по адресу ES:(E)DI, с байтом по адресу DS:(E)SI и изменяет значения регистров SI и DI в зависимости от флага DF. Команды CMPSB и CMPSD сравнивают не байты, а слова и двойные слова соответственно, увеличивая или уменьгаая регистры SI и DI на размер порции данных (2 или 4).

Команды SCASx — поиск символа в строке

Под единым обозначением SCASx (SCAn String) скрываются три команды: • SCASB •SCASW • SCASD

Команды SCASB/W/D сравнивают значения регистра AL/AX/EAX со значе­нием в памяти по адресу [ES:(E)DI]. Регистр (E)DI изменяется в зависимости от флага DF.

Команды REP и REPZ — повторение следующей команды

Команда REP (Repeat) облегчает обработку строк произвольной длины. Это так называемая префиксная команда: ее нельзя использовать отдельно,

80

Page 82: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

а только в паре с какой-нибудь другой командой. Она работает подобно команде LOOP: повторяет следующую за ней команду до тех пор, пока зна­чение в регистре (Е)СХ не станет равно нулю. Регистр (Е)СХ уменьшается на единицу при каждой итерации. Чаще всего команда REP применяется в паре с MOVS или STOS:

r e p movsb

Или: гер s t o s b

скопировать первые {Е)СХ байтов из DS:(E)SI в ES: (E)DI . Это ассемблерный аналог С-функции memcpy()

;скопировать (Е)СХ раз значение AL в ES : (E)DI . ;Это ассемблерный аналог С-функции memset()

Команда REPZ (синоним REPE), подобно команде LOOPZ, позволяет уточнить условие. Следующая итерация выполняется тогда, когда не только (Е)СХ не равен нулю, но и флаг ZF не установлен. Второе условие может быть инвертировано командой REPNZ (синоним REPNE). Эти команды часто используются вместе с SCAS или CMPS:

repz scasb Или: repz cmpsb

;повторяем SCASB

;повторяем CMPSB

Теперь вы уже знаете достаточно, чтобы реализовать на языке ассемблера функцию подсчета символов в строке, которая в языке С называется strlen().

I Начало j

[ Конец j

Следующий I Перемещаем указатель на следующую позицию

Рис. 6.2. Блок-схема функции strlen()

81

Page 83: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Функции нужен только один аргумент — адрес начала строки, который нужно сохранить в ES:(E)DL Результат функции (количество символов строки + нулевой символ) будет записан в ЕСХ. Указатель ES:(E)D1 будет указывать на байт, следующий за последним (нулевым) символом строки. Код функции приведен в листинге 6.L

Листинг 6.1. Код фу|Я1|1И1И|: т 1ШайД strlen: push еах хог есх,есх

хог еах,еах dec есх

eld repne scasb neg есх

pop еах ret

помещаем регистр ЕАХ в стек сбрасываем ЕСХ (ЕСХ=0), то же самое можно сделать так: mov есх,О ЕАХ = О ЕСХ = ЕСХ - 1. Получаем OxFFFFFFFF -максимальная длина строки DF = О, будем двигаться вперед, а не назад ищем нулевой байт отрицание ЕСХ (дополнительный код) даст количество выполненных итераций восстанавливаем исходный ЕАХ выходим из подпрограммы

Вызов стека

ES:(E)DI

[м 0 я н а д п и с ь _ • « ES:(E)DI

ЕСХ 0x11

• • /0

Рис. 6.3. Состояние регистров ES:(E)DI перед вызовом strlen и после ее выполнения

Вы можете легко адаптировать программу для работы только с 16-битными регистрами: просто удалите «Е» в имени каждого регистра. Мы уже написали функцию для подсчета символов в строке, но до сих пор не знаем, как разместить строку в памяти. Пока будем считать, что в ES:(E)DI уже загружен правильный адрес начала строки. Мы вызываем нашу подпро­грамму с помощью CALL, а после ее выполнения в регистре ЕСХ получим количество символов в строке.

c a l l s t r l e n ;вызываем s t r l e n

82

Page 84: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Еще одна очень нужная функция — это функция сравнения двух строк, ко­торая в языке С называется strcmp().

( Начало j

С Конец >

Нет

Рис. 6.4. Блок-схема функции strcmp

Подпрограмма сравнивает две строки: адрес первой сохранен в ES:(E)DI, а адрес второй — в DS:(E)SI. Если строки одинаковы, то после завершения под­программы ЕСХ будет содержать О, а если нет, то в ЕСХ будет количество пер­вых одинаковых символов. Код нашей strcmp () приведен в листинге 6.2.

Листинг 6.2,, Код функции s t i ^ ^ ^ ^ j strcmp: push edx push edi call strlen miov edx, eex mov edi,esi ;push ds ;push ds ;pop es

call strlen ;pop ds cmp ecx,edx jae .length_ok

помещаем EDX в стек помещаем EDI в стек вычисляем длину первой строки сохраняем длину первой строки в EDX ED1 = ESI сохраняем DS в стек еще раз а теперь загружаем его в ES (ES - DS)

теперь вычисляем длину второй строки восстанавливаем исходный DS какая строка длиннее? переходим, если длина первой строки (ЕСХ) больше или равна длине второй

83

Page 85: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

mov ecx.edx . length_ok: pop edi e ld repe cmpsb

pop edx ret

;будем использовать более длинную строку

; восстанавливаем исходный EDI ;DF - О ;повторяем сравнение, пока не встретим два /разных символа или пока ЕСХ не станет ;равен О ; восстанавливаем исходное значение EDX ;конец

Наша функция strcmp будет работать только тогда, когда значения сегментных регистров DS и ES равны. Корректнее было бы раскомментировать заком­ментированные команды — наша функция смогла бы работать со строками, расположенными в разных сегментах. Однако в большинстве программ сег­ментные регистры равны, поэтому нам не нужно учитывать их разницу. На рис. 6.5 вы можете видеть содержимое регистров после сравнения не­одинаковых строк.

L1 ES:EDI

ES:EDI

/О EI DS:ESI

DS:ESI ^-ECX = OxA

Рис. 6.5. Содержимое регистров до и после вызова strcmp

^

6.5. Команды ввода/вывода (I/O) Периферийные устройства используют так называемые шлюзы ввода/вывода — обычно их называют портами ввода/вывода. С помощью портов ваша про­грамма (или сам процессор) может «общаться» с тем или иным периферийным устройством. Для «общения» с периферийным или другим устройством на аппаратном уровне используются команды IN и OUT.

Команды IN и OUT — обмен данными с периферией

Команда IN позволяет получить от устройства, а OUT — передать устройству порцию данных в размере байта, слова или двойного слова.

IN a l , dx OUT dx , a l IN ax , dx OUT dx , ax IN eax , dx OUT dx , eax IN a l , iininS OUT immS, a l IN ax , immS OUT iiranS, ax

84

Page 86: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Команда IN читает данные из порта ввода/вывода, номер которого содержится в регистре DX, и помещает результат в регистр AL/AX/EAX. Другие регистры, кроме AL/AX/EAX и DX, использовать нельзя. Команда OUT отправляет данные в порт. Типы ее операндов такие же, как у IN, но обратите внимание: операнды указываются в обратном порядке. Диапазоны портов ввода/вывода, которые связаны с различными устройства­ми, перечислены в табл. 6.1.

Диапазоны портов ввода/вывода

1 0000-001f :dma1

1 0020-003f:pic1

0040-005f: timer

0060-006f:keyboard

1 0070-007f: rtc

0080-008f : dma page reg

OOaO-OObf: pic2

OOcO-OOdf: dma2

1 OOfO-OOff: fpu

1 0170-0177 :ide1

1 01f0-01f7:ide0

0213-0213 : isapnp read

0220-022f: soundblaster

1 0290-0297 :w8378Id

1 0376-0376 :ide1

03c0-03df:vga+

1 03f2-03f5 : floppy

1 03f6-03f6 : ideO

03f7-03f7: floppy DIR

03f8-03ff: lirc_serial

0a79-0a79 : isapnp write

0cf8-0cff:PCIconf1

4000-403f: Intel Corp. 82371AB/EB/MB PIIX4 ACPI

5000-501f : Intel Corp. 82371AB/EB/MB PIIX4 ACPI

eOOO-eOlf: Intel Corp. 82371AB/EB/MB PIIX4 USB

fOOO-fOOf: Intel Corp. 82371 AB/EB/MB PIIX4 IDE

Таблица 6.1

Первый контроллер DMA (Direct Memory Access)

Первый аппаратный контроллер прерываний

Системный таймер

Клавиатура

Часы реального времени (RTC, real time clock)

DMA page register

Второй аппаратный контроллер прерываний

Второй DMA-контроллер

Математический сопроцессор

Второй ЮЕ-контроллер (Secondary)

Первый ЮЕ-контроллер (Primary)

Интерфейс РпР (plug-and-play) шины ISA

Звуковая плата

Аппаратный мониторинг температуры и напряжения

Второй IDE-контроллер (продолжение)

Видеоадаптер

Дисковод для гибких дисков

Первый IDE-контроллер (продолжение)

Дисковод для гибких дисков (продолжение)

Последовательный порт

Интерфейс РпР (plug-and-play) шины ISA (продолжение)

Первый конфигурационный регистр шины PCI

Чипсет ACPI

Чипсет ACPI

Чипсет USB

Чипсет контроллера дисков

85

Page 87: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Рассмотрение функций отдельных регистров периферийных устройств выходит далеко за рамки этой книги. Вот просто пример:

i n a l , 0 x 6 0 ;чтение значения из порта с номером ОхбО ; ( э т о скан-код последней нажатой клавиши)

Организация задержки. Команда NOP

Название этой команды звучит как «No Operation», то есть это ничего не делающая, «пустая» инструкция. пор ;ничего не делать

Для чего же нужна такая команда? Когда программа работает с портами вво­да/вывода, между операциями чтения и записи нужно делать паузы. Причина ясна: периферийные устройства работают медленнее процессора, и им нужно время, чтобы обработать запрос. Для организации задержки и используют NOP. Выполнение этой команды занимает один цикл процессора — маловато, поэтому чаще задержку организуют при помощи безусловного перехода на следующую команду: jmp short delayl delayl:

Поскольку команда короткого перехода занимает 2 байта, тот же переход на следующую команду можно записать так:

jmp s h o r t $+2 ;перейти на 2 байта вперед

Знак доллара означает текущий адрес. Новые процессоры снабжены схемой предсказания переходов, то есть еще до самого перехода процессор будет знать, что ему нужно сделать. Предска­занный переход будет выполнен за меньшее число циклов процессора, чем непредсказанный, поэтому на новых процессорах задержку с помощью коман­ды JMP организовать нельзя. Лучшим способом «подождать» периферийное устройство будет запись произвольного значения в «безопасный» порт — это диагностический порт с номером 0x80: out 0x80,al ; это просто задержка

6.6. Сдвиг и ротация Операции поразрядного сдвига перемещают отдельные биты в байте, слове или двойном слове. Сдвиг может быть влево или вправо. При сдвиге вправо на одну позицию крайний правый (младший) бит теряется, а крайний левый замещается нулем. При сдвиге влево теряется крайний левый (старший) бит, а крайний правый замещается нулем. При сдвиге на несколько позиций «выталкивается» указанное количество битов, а «освободившиеся» биты замещаются нулями.

86

Page 88: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Ротация (также называемая циклическим сдвигом) не выталкивает биты, а возвращает их на место «освободившихся». В результате ротации вправо крайний правый бит встанет на место крайнего левого, а остальные биты будут сдвинуты на одну позицию вправо. Где это может использоваться, вы поймете из описания самих команд.

Команды SHR и SHL — сдвиг беззнаковых чисел Команды SHR и SHL поразрядно сдвигают беззнаковые целые числа вправо и влево соответственно. Это самый быстрый способ умножить или разделить целое число на степень двойки. Представим число 5 в двоичной системе — 0101b. После умножения на 2 мы получим 10, в двоичной системе это число 01010b. Сравнивая два двоичных числа, вы, наверное, заметили, как можно быстро получить из числа 5 число 10: с помощью сдвига на один бит влево, добавив один нуль справа. Точно так же можно умножить число на любую степень двух. Например, вместо умножения на 16 (2 в степени 4) можно сдвинуть исходное число на 4 бита влево.

° i ° i ° i ° i ° i ° i ^ ° i :

Г' °i°i°i°i°iS°i%°

Р\лс. 6.6. Умножение 5 на 2 методом поразрядного сдвига влево

Деление на степень двойки выполняется по такому же принципу, только вместо поразрядного сдвига влево нужно использовать сдвиг вправо. Команде SHL нужно передать два операнда:

SHL о 1 , о2

Первый должен быть регистром или адресом памяти, который нужно сдвинуть. Второй операнд определяет число позиций, на которое нужно сдвинуть. Чаще всего это непосредственное значение. Можно использовать в качестве второ­го операнда и регистр, но только CL — это касается всех операций сдвига и ротации. Сдвиг возможен не более чем на 32 позиции, поэтому принимается в расчет не весь второй операнд, а остаток от его деления на 32. Старший «вытолкнутый» бит сохраняется в флаге переноса CF, а младший бит заменяется нулем. Кроме флага CF используется флаг знака (SF) и флаг

87

Page 89: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

переполнения (OF). За этими флагами нужно следить, выполняя действия над числами со знаком, чтобы избежать превращения положительного числа в от­рицательное и наоборот (в этом случае флаги SF и OF устанавливаются в 1). То же самое, что и SHL, только биты сдвигаются вправо:

SHR о 1 , о2

Младший бит перемещается в CF, а старший заменяется нулем. Принцип работы SHR показан на рис. 6.7.

о-ЕЕЕЗ^гВЕН-а SHR SF

Рис. 6.7. Как работает SHR

А теперь рассмотрим несколько примеров, которые помогут вам лучше усво­ить прочитанное. Пример: используя SHR, разделим АХ на 16, не обращая внимания на пере­полнение:

s h r ах , 4 ; сдвигаем АХ на 4 бита вправо

Пример: подсчитаем количество двоичных единиц в АХ и сохраним результат B B L .

АХ — 16-разрядный регистр, поэтому мы должны сдвинуть его влево или вправо 16 раз. После каждого сдвига мы анализируем флаг переноса CF, в который попал вытолкнутый бит, что очень легко сделать с помощью инструк­ции JC. Если CF равен единице, то увеличиваем BL.

mov Ы,0 ;инициализируем счетчик единиц BL=0 mov ex Д б ; СХ = 1 б r e p e a t : s h r а х Д ; сдвигаем на 1 бит вправо, младший бит

/попадает в CF j n c not_one ;если это О, пропускаем следующую команду i n c Ы ;иначе — увеличиваем BL на 1 n o t _ o n e : loop r e p e a t ;повторить 16 раз

После выполнения этого фрагмента кода регистр BL будет содержать коли­чество единиц регистра АХ, а сам регистр АХ будет содержать 0.

88

Page 90: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Команды SAL и SAR — сдвиг чисел со знаком

Команды SAL и SAR используются для поразрядного сдвига целых чисел со знаком (арифметического сдвига). Команда SAL — это сдвиг влево, а команда SAR — вправо. Формат команд таков:

SAL о 1 , о2 SAR о 1 , о2

Команда SAR сдвигает все биты, кроме старшего, означающего знак числа — этот бит сохраняется. Младший бит, как обычно, вытесняется в CF. Операнды обеих инструкций такие же, как у SHL и SHR.

Ф SAR SF

Рис. 6.8. Как работает SAR

Команды RCR и RCL — ротация через флаг переноса

Эти команды выполняют циклический поразрядный сдвиг (ротацию). RCR действует точно так же, как SHR, но вместо нуля в старший бит первого операнда заносится предыдущее содержихмое CF. Исходный младший бит вытесняется в CF. Команда RCL работает подобно RCR, только в обратном направлении. Формат команд таков:

RCR о 1 , о2 RCL о 1 , о2

RCR SF

Рис. 6.9. Как работает RCR

444H-H-4-4 4-R1 RCL SF

Рис. 6.10. Как работает RCL

89

Page 91: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Команды ROR и ROL — ротация с выносом во флаг переноса

Эти команды выполняют другой вариант циклического сдвига: ROR сначала копирует младший бит первого операнда в его старший бит, а потом заносит его в CF; ROL работает в обратном направлении.

ROR о 1 , о2 ROL о 1 , о2

Операнды этих команд аналогичны операндам команд RCR и RCL.

^TTTW ROR SF

Рис, 6.11, Как работает ROR

6.7. Псевдокоманды Некоторые из команд, рассмотренных нами, могут работать с операндом, рас­положенным в памяти. Классический пример — команда MOV АХ, [number], загружающая в регистр АХ значение из области памяти, адрес которой пред­ставлен символическим обозначением «number». Но мы до сих пор не знаем, как связать символическое обозначение и реальный адрес в памяти. Как раз для этого и служат псевдокоманды. Предложения языка ассемблера делятся на команды и псевдокоманды (дирек­тивы). Команды ассемблера — это символические имена машинных команд, обработка их компилятором приводит к генерации машинного кода. Псев­докоманды же управляют работой самого компилятора. На одной и той же аппаратной архитектуре могут работать различные ассемблеры: их команды обязательно будут одинаковыми, но псевдокоманды могут быть разными.

Псевдокоманды DB, DW и DD — определение констант

Чаще всего используется псевдокоманда DB (define byte), позволяюш^ая определить числовые константы и строки. Рассмотрим несколько примеров:

db 0x55 db 0x55,0x56,0x57

db 'а',0x55

db 'Hello',13,10,'$'

один байт в шестнадцатеричном виде три последовательных байта: 0x55, 0x56, 0x57 можно записать символ в одинарных кавычках. Получится последовательность 0x61, 0x55 можно записать целую строку. Получится 0x48, 0x65, ОхбС, ОхбС, 0x6F, OxD, ОхА, 0x24

90

Page 92: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Для определения порции данных размера, кратного слову, служит директива DW (define word):

dw 0x1234 ;0х34 , 0x12 dw 'а.' ;0хб1, 0x00: второй байт заполняется нулями

Директива DD (define double word) задает значение порции данных размера, кратного двойному слову:

dd 0x12345678 ;0х78 0x56 0x34 0x12 dd 1.2 3 45 67е2 0 ; так определяются числа с плавающей точкой

А вот так определяется переменная, тот самый «number»: number dd 0x1 ;переменная number инициализирована

;значением 1

Переменная «number» теперь представляет адрес в памяти, по которому за­писано значение 0x00000001 длиной в двойное слово.

Псевдокоманды RESB, R E S W M R E S D — объявление переменных

Вторая группа псевдокоманд позволяет определить неинициализированные данные. Если в первом случае мы заносим данные в память, то сейчас мы просто укажем, сколько байтов нужно зарезервировать для будущих данных. Память для этих неинициализированных переменных распределяется дина­мически, а сами данные не содержатся в исполнимом файле. Грубо говоря, предыдущую группу инструкций удобно использовать для определения кон­стант, а эту группу — для переменных. Для резервирования памяти служат три директивы: RESB (резервирует байт), RESW (резервирует слово) и RESD (резервирует двойное слово). Аргументом этих псевдокоманд является количество резервируемых позиций:

resb 1 ;резервирует ;резервирует ;резервирует ;резервирует ;резервирует

'number'' ;резервирует 64 байта для ;переменной buffer

resb 2 resw 2 resd 1 number resd 1

buffer resb 64

1 2 4 4 4

байт байта байта байта байта

(2 слова)

для переменной

Некоторые ассемблеры поддерживают дополнительные директивы выделения памяти, но мы будем использовать ассемблер NASM, который их не поддер­живает. Поэтому мы ограничимся директивами RESx.

Псевдокоманда TIMES — повторение следующей псевдокоманды

Директива TIMES — это псевдокоманда префиксного типа, то есть она ис­пользуется только в паре с другой командой. Она повторяет последующую

91

Page 93: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

псевдокоманду указанное количество раз, подобно директиве DUP из ассем­блера Borland TASM. Применяется эта директива в тех случаях, когда нужно «забить» некоторую область памяти повторяющимся образцом. Следующий код определяет строку, состоящую из 64 повторяющихся «Hello»: many_hello: times 64 db 'Hello'

Первый аргумент, указывающий количество повторений, может быть и вы­ражением. Например, задачу «разместить строку в области памяти размером в 32 байта и заполнить пробелами оставшееся место» легко решить с помощью директивы TIMES:

b u f f e r db " H e l l o " ;определяем строку t i m e s 3 2 - ( $ - b u f f e r ) db ' ' ;определяем нужное

/количество пробелов

Выражение 32-($-buffer) возвратит значение 27, потому что $-buffer равно текущей позиции минус позиция начала строки, то есть 5. Вместе с TIMES можно использовать не только псевдокоманды, но и команды процессора:

t imes 5 inc еах ;5 раз выполнить INC ЕАХ

В результате будет сгенерирован код: inc еах inc еах inc еах inc еах inc еах

Псевдокоманда INCBIN — подключение двоичного файла

Эта директива будет полезна более опытным разработчикам. Она упаковывает графические или звуковые данные вместе с исполняемым файлом:

i n c b i n "sound.wav' ' ;упаковываем весь файл i n c b i n ' 'sound .wav" , 512 ;пропускаем первые 512 байтов i n c b i n " sound .wav ' \ 512,80 ;пропускаем первые 512 байтов и

;последние 80

Псевдокоманда EQU — вычисление константных выражений

Эта директива определяет константу, известную во время компиляции. В ка­честве значения константы можно указывать также константное выражение. Директиве EQU должно предшествовать символическое имя:

four EQU 4 /тривиальный пример. ;Позже я покажу и нетривиальные

92

Page 94: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Оператор SEG — смена сегмента

При создании больших программ для реального режима процессора нам нужно использовать для кода и данных несколько сегментов, чтобы обойти проблему 16-битной адресации. Пока будем считать, что сегмент — это часть адреса переменной. С помощью оператора SEG в сегментный регистр может быть загружен адрес сегмента, где физически расположена переменная:

mov a x , s e g coun te r ;поместить в АХ адрес сегмента , где ;размещена переменная c o u n t e r

mov e s , a x /поместить этот адрес в сегментный ; р е г и с т р . ; это можно сделать только косвенно

mov b x , c o u n t e r ; з а г р у з и т ь в ВХ адрес (смещение) ;переменной c o u n t e r . Теперь пара ES:ВХ ;содержит полный адрес переменной c o u n t e r

mov c x , e s : [ b x ] /копировать значение переменной ;в регистр СХ

В наших учебных программах на сегментные регистры можно не обращать внимания, потому что они содержат одно и то же значение. Поэтому оператор SEG нам пока не понадобится.

6.8. Советы по использованию команд Теперь, когда вы уже познакомились с основными командами, самое время поговорить об оптимизации кода программы. Сегодня, когда компьютеры очень быстры, а память стоит очень дешево, почти никто не считается ни со скоростью работы программы, ни с объемом занимаемой памяти. Но язык ассемблера часто применяется в особых ситуа­циях — в «узких местах», где быстродействие или малый размер программы особенно важны. Удачным подбором команд можно существенно сократить размер программы или увеличить ее быстродействие — правда, при этом по­страдает удобочитаемость. Ускорить работу программы иногда можно также путем правильного выделения памяти.

Директива ALIGN — выравнивание данных в памяти

Мы знаем, что процессоры работают с регистрами на порядок быстрее, чем с операндами, расположенными в памяти. Поэтому желательно выполнять все вычисления в регистрах, сохраняя в памяти только результат. При обработке больших массивов частые обращения к памяти неизбежны. Скорость доступа к памяти можно увеличить, если размещать данные по адресам, кратным степени двойки. Дело в том, что между памятью и про-

93

Page 95: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

цессором имеется система буферов памяти, недоступная программисту. Эти буферы содержат блоки чаще всего используемых данных из основной памяти или несохраненные данные. «Выравнивание» данных по некоторым адресам «освобождает» буферы, поэтому данные обрабатываются быстрее. К сожалению, каждый тип процессора предпочитает свой тип выравнивания. Мы можем сообщить компилятору требуемый тип директивой ALIGN. Ее аргумент — то число, по адресам, кратным которому, требуется размещать данные:

a l ign 4 /размещает данные по адресам, кратным 4 a l ign 16 /размещает данные по адресам, кратным 16

Загрузка значения в регистр Много обращений к памяти можно исключить, если отказаться от непосред­ственных операндов, что положительно скажется на быстродействии про­граммы. Например, как мы привыкли инициализировать счетчик? Командой MOV, которая копирует в регистр значение О? Намного рациональнее для этого использовать логическую функцию XOR (если ее аргументы одинаковы, результат равен 0):

хог еах,еах ;в машинном коде 0хЗЗ,0хС0 Эта команда будет выполнена быстрее и займет меньше памяти, чем

mov еах,0 ;в машинном коде 0хВ8,0,0,0,0 Но нужно помнить, что XOR изменяет регистр признаков, поэтому ее нужно использовать только в тех случаях, когда его изменение не имеет значения. Другой часто используемой парой инструкций являются:

хог еах,еах ;ЕАХ = О inc еах ;увеличиваем на 1

Этот фрагмент кода загружает в ЕАХ значение 1. Если использовать DEC вместо INC, в ЕАХ будет значение - 1 .

Оптимизируем арифметику Если вам нужно добавить к значению константу, то имейте в виду, что не­сколько команд INC будут выполнены быстрее, чем одна ADD. Таким образом, вместо команды

add еах,4 ;добавляем 4 к ЕАХ следует использовать четыре команды:

inc еах ; добавляем 1 к ЕАХ inc еах inc еах inc еах

94

Page 96: Ассемблер на примерах. Базовый курс

Глава 6. Прочие команды

Помните, что ни INC, ни DEC не устанавливают флаг переноса, поэтому не ис­пользуйте их в 64-разрядной арифметике, где используется перенос. Зато при вы­числении значения адреса вы можете смело использовать INC и DEC, поскольку арифметические операции над адресами никогда не требуют переноса. Вы уже знаете, что для быстрого умножения и деления можно использовать поразрядные сдвиги влево и право. Умножение можно выполнять также с помощью команды LEA, которая вычисляет эффективный адрес второго операнда. Вот несколько примеров:

l e a ebx Л ecx+Gdx'^4 + 0x500] ; загружаем в ЕВХ результат ;выражения ЕСХ + EDX*4 + 0x500

l e a еЬхЛеах+еах*4~1] ; ЕВХ = ЕАХ*5 - 1 l e a ebx , [ eax+eax*8] ; ЕВХ = ЕАХ -9 l e a e c x , [ e a x + e b x ] ;вычисляем ЕСХ = ЕАХ + ЕВХ

Операции сравнения

Очень часто нам нужно проверить какой-то регистр на наличие в нем 0. Со­всем не обязательно использовать для этого СМР, можно использовать OR или TEST. Таким образом, вместо команд

стр е а х , 0 ;ЕАХ равен О? j z i s _ z e r o ;да? Переходим к i s _ z e r o

МОЖНО написать следующее: or е а х , е а х ; э тот OR устанавливает тот же

;флаг (ZF), если результат равен О j z i s _ z e r o ;да? Переходим к i s _ z e r o

Если оба операнда команды OR имеют одно и то же значение, первый операнд сохраняется. Кроме того, команда изменяет регистр признаков: если результат операции OR нулевой, то нулевой признак (ZF) будет установлен в 1. Любая арифметическая команда устанавливает флаг нуля, поэтому совсем не обязательно использовать СМР для проверки на 0. Можно сразу исполь­зовать jz:

dec еах j z now_zero ;переход, если ЕАХ после декремента равен О

Иногда нужно узнать, содержит ли регистр отрицательное число. Для этого можно использовать команду TEST, которая вычисляет логическое И, но не сохраняет результат, а только устанавливает флаги регистра признаков. Флаг знака SF будет установлен в 1, если результат отрицательный, то есть если старший бит результата равен 1. Значит, мы можем выполнить TEST с двумя одинаковыми операндами: если число отрицательное, то флаг SF будет установлен (логическое И для двух отрицательных чисел даст 1 (1 AND 1 = 1) в старшем бите, то есть SF = 1):

95

Page 97: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

t e s t еах,еах ; вызываем TEST для двух одинаковых ;операндов

j s i s_negat ive ;переходим, если SF=1

Разное

Обычно вместо нескольких простых команд проще использовать одну слож­ную (например, LOOP или команды для манипуляций со строками). Очень часто такой подход и правильнее: сложные команды оптимизированы и могут выполнить ту же самую функцию быстрее, чем множество простых команд. Размер исполняемого файла может быть уменьшен, если оптимизировать пере­ходы. По умолчанию используется «средний шаг» — near, но если все цели находятся в пределах 128 байтов, используйте короткий тип перехода (short). Это позволит сэкономить один-три байта на каждом переходе.

96

Page 98: Ассемблер на примерах. Базовый курс

Глава 7 Полезные фрагмен ты кода

простые примеры

Преобразование числа в строку

Преобразование строки в число

Page 99: Ассемблер на примерах. Базовый курс

в этой главе мы представим несколько подпрограмм, которые можно исполь­зовать в большинстве ваших ассемблерных программ. Мы будем рассматривать только системно-независимый код, одинаково пригодный для любой опера­ционной системы. Программы, использующие особенности операционных систем, будут рассмотрены в следующих главах.

7.1. Простые примеры Начнем с нескольких простых примеров, которые помогут уменьшить «про­пасть» между языком высокого уровня и ассемблером.

Сложение двух переменных

Задача: сложить два 32-разрядных числа, содержащихся в переменных num-b e r l и number2, а результат сохранить в переменной r e s u l t . Сначала нужно выбрать регистры, куда будут загружены нужные значения. Затем нужно сложить эти регистры, а результат записать в переменную r e ­s u l t : mov еах,[number1] mov ebx,[number2] add eax,ebx mov [result],eax

квадратные скобки означают доступ к памяти ЕВХ = number2 ЕАХ - ЕАХ + ЕВХ сохраняем результат в переменной result

number1 dd 8

number2 dd 2

result dd 0

;определяем переменную numberl и /инициализируем 8 ;переменную number2 инициализируем ;значением 2 ;определяем переменную r e s u l t

Мы можем переписать программу, используя «на подхвате» регистр ЕАХ: mov e a x , [ n u m b e r l ] ; ЕАХ = ' 'numberl" add eax , [number2] ;EAX - EAX + number2 mov [ r e s u l t ] , e a x ;сохраняем р е з у л ь т а т в переменной r e s u l t

98

Page 100: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

Сложение двух элементов массива Задача: сложить два 32-битных числа. Регистр EDI содержит указатель на первое слагаемое, второе слагаемое расположено непосредственно за первым. Результат записать в EDX. Фактически мы имеем массив из двух 32-битных чисел, адрес которого задан указателем в регистре EDI. Каждый элемент массива занимает 4 байта, сле­довательно, второй элемент расположен по адресу, на 4 байта большему.

mov e d x , [ e d i ] add e d x , [ e d i + 4 "

/загружаем в EDX первый элемент массива /складываем его со вторым, результат — в EDX

Дополним программу загрузкой регистра EDI: mov e d i , n u m b e r s /загружаем в EDI адрес массива numbers . . . / к акие -то команды

mov e d x , [ e d i ] /загружаем в EDX первый элемент массива add e d x , [ e d i + 4 ] /прибавляем второй элемент

numbers dd 1

dd 2

/массив numbers инициализируется /значениями 1 и 2, поэтому результат /в EDX будет равен 3 /У второго элемента массива нет особого /имени

В следующем пункте мы расскажем, как работать с массивами в цикле.

Суммируем элементы массива

Задача: вычислить сумму массива 8-разрядных чисел, если в ESI загружен адрес первого элемента массива. Массив заканчивается нулевым байтом^ Сумма 8-разрядных чисел может оказаться числом большей разрядности, по­этому на всякий случай отведем для хранения результата 32-битный регистр. Элементы массива будут суммироваться до тех пор, пока не будет встречен нулевой байт.

mov esi,array mov ebx,0 mov eax,ebx again: mov al,[esi] inc esi add ebx,eax cmp a1,0 jnz again array db 1,2,3,4,

/загружаем в ESI начало массива /EBX = О /EAX = О

/загружаем в AL элемент массива /перемещаем указатель на след. элемент /ЕВХ = ЕВХ + ЕАХ /AL равен нулю? /переходим к again, если AL не О ,6,7,8,0 /инициализируем массив. /Сумма (ЕВХ) должна быть равна 3 6

99

Page 101: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Г Начало j

( Конец j

Рис. 7.7. Блок-схема алгоритма суммирования массива

Чет и нечет

Задача: определить, четное или нечетное значение содержит регистр АХ. Четное число отличается от нечетного тем, что его младший бит равен нулю. Используя SHR, мы можем сдвинуть этот бит в CF, а затем проверить этот бит, выполнив условный переход.

push ах /сохраняем исходное значение АХ в стеке s h r а х , 1 ;перемещаем младший бит АХ в CF pop ах /восстанавливаем оригинальное значение j c odd ;если CF = 1, переходим к odd even: ;действия , если число в в АХ — четное

odd: ; действия, если число в в АХ -- нечетное

Как обычно, мы можем переписать этот фрагмент кода проще: t e s t а 1 Д ; младший бит маски содержит 1, вызываем TEST j z even ;ZF (флаг нуля) установлен ,если результат t e s t

;равен О, о есть младший бит - О, то есть число ;четное

odd:

even : ; действия, если АХ — четное

Обратите внимание, что мы тестировали только AL, а не весь регистр АХ. Старшие биты АХ для проверки четности совершенно не нужны.

100

Page 102: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

Перестановка битов в числе

Задача: реверсируем порядок битов числа, сохраненного в AL, то есть пере­ставим младший бит на место старшего, второй справа — на место второго слева и т.д. Полученный результат сохраним в АН. Например, наше число равно 0x15, то есть 00010101b. После реверсирования мы получим его «зеркальное отображение»: 10101000b, то есть 0хА8. Как обычно, поставленную задачу можно решить несколькими способами. Можно «пройтись» по всем битам AL так, как показано в параграфе о 6Р1ТОВЫХ массивах, проверяя значение отдельных битов и устанавливая в соответствии с ним значения отдельных битов АН, но это не лучшее решение. Оптимальным в нашем случае будет использование инструкций сдвига и ро­тации. Например, используя SHR (как в предыдуш;ем примере), мы можем «вытолкнуть» один бит в CF (флаг переноса) и, используя RCL, переместить этот бит в младший бит операнда. Повторив это действие 8 раз, мы решим поставленную задачу.

mov с х , 8 ;наш счетчик СХ = 8 t h e l o o p : s h r а 1 Д /сдвигаем AL на 1 бит вправо, младший бит —

;в CF г с 1 а Ь Д ; сдвигаем АН на 1 бит влево , заменяем

;младший бит на CF loop t h e l o o p ;повторяем 8 раз

Проверка делимости числа нацело

Задача: определить, заканчивается ли десятичная запись числа цифрой нуль. Простого сравнения битов здесь недостаточно, мы должны разделить число на 10 (ОхА). Операция целочисленного деления помещает в регистр AL частное, а в регистр АН — остаток. Нам останется только сравнить остаток с нулем: если число делится нацело, то передадим управление на метку YES. Для определенности считаем, что наше число находится в регистре АХ:

mov bl,OxA ;BL = 10 - делитель d i v Ы ;делим АХ на BL стр ah,О ;остаток = 0 ? jz yes ;если да, перейти к YES по: ;если нет, продолжить

yes :

101

Page 103: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

7.2. Преобразование числа в строку Чтобы вывести число на экран, нужно преобразовать его в строку. В высоко­уровневых языках программирования это действие давно стало тривиальным: для преобразования числа в строку достаточно вызвать всего одну функцию. А вот на языке ассемблера эту подпрограмму еще нужно написать, чем мы и займемся в этом параграфе. Как бы мы решали эту задачу на С? Делили бы в цикле на 10 и записывали остатки, преобразуя их в символы цифр добавлением кода цифры О (см. та­блицу кодов ASCII, рис. 1.2). Цикл повторялся бы до тех пор, пока частное не стало бы нулем. Вот соответствующий код:

#includG < u n i s t d . h > vo id ma in (vo id ) { uns igned i n t number; cha r r e m a i n d e r ; number=12345678; w h i l e (number != 0) { r emainder = /* r emainder number /==10; p r i n t f ( " % c " , } }

(number % = number

10) mod

/* number = remainder ) ;

+ ' 0 ' ; 10 +

numbe] cha r (

r d i v '0 10

) * /

^

Да ^

r

Конец j

( Начало j

y^ Число ^ v 4 ^ равно 0>^

|нет

Делим на 10

1 Запись остатка

1 Рис. 7.2. Блок-схема алгоритма преобразования числа в строку

102

Page 104: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

Правда, программа выведет немного не то, что мы ожидали: наше число равно 12345678, но на экране мы увидим 87654321, — это потому что при делении мы получаем сначала младшие цифры, а потом старшие. Как теперь перевернуть полученную последовательность символов? При программировании на языке ассемблера нам доступен стек, куда можно сохранять наши остатки, а затем, вытолкнув их из стека, получить правильную последовательность. Мы оформим функцию преобразования числа в строку в виде подпрограммы, которую вы можете использовать в любой своей программе на языке ассем­блера. Первая проблема, которую предстоит решить — передача параметров. Мы знаем, что в высокоуровневых языках программирования параметры передаются через стек. Однако мы будем использовать более быстрый способ передачи параметров — через регистры процессора. В регистр ЕАХ мы занесем число, а после выполнения подпрограммы регистр EDI будет содержать адрес памяти (или указатель), по которому сохранен первый байт нашей строки. Сама строка будет заканчиваться нулевым байтом (как в С). Наша подпрограмма conver t состоит из двух циклов. Первый цикл соот­ветствует циклу while из предыдущего листинга — внутри него мы будем помещать остатки в стек, попутно преобразуя их в символы цифр, пока частное не сравняется с нулем. Второй цикл извлекает цифры из стека и записывает в указанную строку. Наша подпрограмма convert может выглядеть так, как показано в листинге 7.1.

Листинг 7,1. Программа преобразования *)| ШЩ1::Щ| (предварительный вариант) i J ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ H

convert: mov есх,О mov еЬхДО .divide: mov edx,О div ebx add edx , ' 0 ' push edx inc ecx cmp eax,0 jnz .divide

.reverse: pop eax

;ECX = 0 ;EBX = 010

EDX - 0 делим EAX на EBX, частное в EAX, остаток в EDX добавляем ASCII-код цифры О к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили? (частное равно О?) если нет, переходим к .divide иначе число уже преобразовано, цифры сохранены в стеке, ЕСХ содержит их количество 'выталкиваем цифру из стека

103

Page 105: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

mov [ e d i ] , add e d i , 1 dec ecx

al

cmp ecx,0 jnz . reverse ret

помещаем ее в строку перемещаем указатель на следующий символ уменьшаем счетчик цифр, оставшихся в стеке цифры кончились? Нет? Обрабатываем следующую Да? Возвращаемся

Эту программу я намеренно написал с недостатками. Попробуем ее оптими­зировать, руководствуясь предыдущей главой. Начнем с замены команды MOV ecx, О на более быструю — XOR есх, есх. Далее, вместо загрузки в ЕВХ 10 мы можем сбросить этот регистр (записать в него 0), а потом загрузить 10 в BL: так мы избавимся от использования «длинного» непосредственного операнда. Проверка ЕАХ на О может быть заменена инструкцией OR еах, еах (или TEST еах , еах). А две команды записи байта в строку можно заменить одной: вместо mov [edi],al add edi,1

напишем: s tosb

Эта команда делает то же самое, но занимает всего один байт. Последний цикл нашей функции можно переписать с использованием команды LOOP. В итоге функция будет выглядеть так, как показано в листинге 7.2.

Листинг 7.2. Программа преобразования числа в строЩ (промежуточный вариант) I

convert : хог есх,есх хог ebx,ebx mov bl ДО .d iv ide : хог edx,edx div ebx

add dl^O' push edx inc ecx or eax,eax jnz .divide

ECX - 0 EBX = 0 теперь EBX - 010

EDX = 0 делим ЕАХ на EBX, частное в ЕАХ, остаток в EDX добавляем ASCII-код цифры О к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили? (частное равно О?) если не О, переходим к .divide. Иначе число уже преобразовано, цифры сохранены в стеке, ЕСХ содержит их количество

104

Page 106: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

. reverse: pop еах stosb loop .reverse ret

выталкиваем цифру из стека записываем AL по адресу, содержащемуся в EDI, увеличиваем EDI на 1 ЕСХ=ЕСХ-1, переходим, если ЕСХ не равно О Да? Возвращаемся

Мы слегка улучшили программу, но она все еще работает неправильно. Не­обходимый нулевой байт не записывается в конец строки, поэтому нужно добавить дополнительную инструкцию:

MOV b y t e [ e d i ] , 0

Ее нужно вставить между LOOP и RET. Обратите внимание на слово byte — оно очень важно, поскольку сообщает компилятору о том, какого размера нуль нужно записать по адресу EDI. Са­мостоятельно он определить этого не может. Строку завершает нулевой байт, поэтому нуль размером в байт мы и запишем. Наша программа уничтожает исходное содержимое некоторых регистров (ЕАХ, ЕВХ, ЕСХ, EDX и EDI). Чтобы их сохранить, перед изменением этих регистров нужно поместить их значения в стек, а затем извлечь их оттуда в правильном порядке. Вызывать нашу подпрограмму нужно так:

mov еах ,0x12 3 45 67 8 ; з а г р у з и т ь в ЕАХ число, подлежащее ;преобразованию

mov e d i , b u f f ; з а г р у з и т ь в EDI адрес буфера для ; записи строки

c a l l coPxVert ; вызов подпрограммы

Число, которое мы хотим преобразовать, перед вызовом подпрограммы за­гружаем в регистр ЕАХ. Второй параметр — это указатель на адрес в памяти, куда будет записана наша строка. Указатель должен быть загружен в EDI или DI (в зависимости от режима процессора). Команда CALL вызывает нашу подпрограмму. В результате ее выполнения созданная строка будет записана по указанному адресу. Наша функция convert преобразует число только в десятичное представле­ние. Сейчас мы изменим ее так, чтобы получать число в шестнадцатеричной записи. Мы получаем десятичные цифры, прибавляя к остаткам ASCII-код цифры нуль. Это возможно, потому что символы цифр в ASCII-таблице расположены последовательно (рис.Ь2). Но если наши остатки получаются от деления на 16, то нам понадобятся цифры А — F, расположенные в ASCII-таблице тоже последовательно, но не вслед за цифрой 9. Решение простое: если остаток

105

Page 107: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

больше 9, то мы будем прибавлять к нему другую константу. Вместо добав­ления к остатку ASCII-кода нуля будем вызывать еще одну подпрограмму, HexDig i t :

HexDig i t :

cmp d l ДО j b . l e s s add d l , ' A ' - I O r e t . l e s s : o r d l , ' 0 ' r e t

;в DL ожидается число 0 -15 , ; нужно сопоставить ему шестнадцатеричьтую ;цифру /сравниваем DL с 10 ;переходим, если меньше ;10 превращается в 'А', 11 в 'В' и т.д. ;конец подпрограммы

;можно прибавлять и так ;конец подпрограммы

Модифицируя подпрограмму convert , не забудьте заменить делитель 10 на 0x10, то есть 16. А теперь обобщим нашу подпрограмму так, чтобы она преобразовывала число в строку в любой системе счисления. После того, как мы написали универ­сальную подпрограмму превращения остатка в N-ичную цифру, достаточно будет просто передавать основание системы счисления как еще один параметр. Все, что нужно сделать, — это удалить строки кода, загружающие делитель в регистр ЕВХ. Кроме того, для сохранения значений регистров общего на­значения мы добавим команды PUSHAD и POPAD. Окончательную версию подпрограммы, приведенную в листинге 7.3, мы будем использовать в других примерах:

Листинг 7,3, Профамма преобразования числа в строку (окончательный вариант)

NumToASCII

еах = 32-битное число еЬх - основание системы счисления edi = указатель на строку-результат Возвращает: заполненный буфер

NumToASCII: pushad

xor esi,esi convert_loop:

сохраняем все регистры общего назначения в стеке ESI - О: это счетчик цифр в стеке

106

Page 108: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

хог edx^edx div ebx

call HexDigit push edx inc esi test eax,eax jnz convert_loop eld

write_loop: pop eax stosb dec esi test esi,esi jnz write_loop mov byte [edi],0 popad

ret

EDX - 0 делим EAX на EBX , частное в EAX, остаток в EDX преобразуем в ASCII сохраняем EDX в стеке увеличиваем счетчик цифр в стеке все? (ЕАХ - 0) если не О, продолжаем сбрасываем флаг направления DF: запись вперед

/выталкиваем цифру из стека /записываем их в буфер по адресу ES:(E)DI /уменьшаем счетчик оставшихся цифр ;все? (ESI = 0) ;если не О, переходим к следующей цифре ;заканчиваем строку нулевым байтом /восстанавливаем исходные значения /регистров /все!!!

7.3. Преобразование строки в число Часто бывает нужно прочитать число, то есть извлечь его из строки. Высо­коуровневые языки программирования предоставляют ряд библиотечных функций для преобразования строки в число (readln, scanf), а мы напишем простенькую функцию такого назначения самостоятельно. Для преобразования одного символа в число мы напишем подпрограмму conver t_char , которая преобразует цифры '0'-'9' в числа 0-9, а символы 'А'-Т' и 'а'-Т в числа 10-15 (OxA-OxF). Единственным входным, он же выход­ной, параметром будет регистр AL, в который перед вызовом подпрограммы нужно будет загрузить один ASCII-символ. В результате работы подпрограммы в этом же регистре окажется числовое значение. Давайте рассмотрим эту подпрограмму. convert_char: sub al,'0' /вычитаем из символа ASCII-код нуля стр а1Д0 /если разность меньше 10, то была

/десятичная цифра — больше ничего не нужно /делать

jb done /команда JB — переход, если меньше /иначе — была буква

add al,'0' /AL = исходная буква and al,0x5f /приводим ее к верхнему регистру

107

Page 109: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

sub al,'A'-IO and al,OxOf

получаем диапазон от 10 вернуть нужно значение 0-15. Если буква больше F, то очищаем 4 старших бита AL

done : r e t ; возвраш;аемся

Наша программа пока не предусматривает никакой проверки корректности входных данных: мы просто предполагаем, что байт на входе представляет десятичную цифру либо латинскую букву в верхнем или нижнем регистре. Понятно, что против некорректного вызова она беззащитна. Сначала мы вычитаем из исходного символа ASCII-код цифры нуль. Если результат попадает в диапазон 0-9, то нужное число уже получено: пере­ходим к метке done; если нет, то считаем символ буквой. Благодаря хорошо продуманной таблице ASCII (рис. 1.2) код строчной буквы отличается от кода соответствуюш,ей ей заглавной только единицей в пятом бите (считая с нулево­го), поэтому для перевода буквы в верхний регистр достаточно сбросить этот бит (маска 0x5F). Следующий шаг — вычитанием сдвинуть значение в другой диапазон, получив из 'А' — ОхА, из Ъ ' — ОхВ и т.д. И, наконец, последняя операция AND ограничивает диапазон возвращаемых значений младшими четырьмя битами — числами от 0x00 до OxOF. Теперь подумаем, как реализовать функцию для преобразования чисел со знаком. Довольно непростая задача, но мы ее упростим. Договоримся, что запись отрицательных чисел, и только их, будет начинаться с минуса, и только одного. То есть если в начале строки стоит символ «минус», то, значит, перед нами отрицательное число. Преобразование отрицательного числа выполняется точно так же, как и по­ложительного, только в самом конце подпрограммы мы выполним операцию NEG (отрицание). Займемся разработкой самого алгоритма преобразования. В первой главе мы обсуждали представление любого числа в виде:

а = a *z" + a _j*z"~ -f ... -h a *z -f- а *7 (n — это количество цифр) Например, десятичное число 1234 может быть записано так: 1234 = 1*10 + 2*10^ + 3*10 -h 4*10^ Аналогично, шестнадцатеричное значение 0x524D может быть записано как:

(524D) ^ = 5*16 + 2*162 + 4*16^ + 13*16^ = 21 069 На первый взгляд кажется, что сама формула подсказывает алгоритм: пре­образуем каждую цифру в число по программе convert_char, умножаем на соответствующую степень основания, складываем и получаем в итоге нужное число. Но со второго взгляда становится видно, что степени основания мы не

108

Page 110: Ассемблер на примерах. Базовый курс

Глава 7. Полезные фрагменты кода

узнаем, пока не прочтем все цифры и по их количеству не определим порядок числа. Неуклюжим решением было бы сначала подсчитать цифры, а потом перейти к «очевидному» алгоритму, но мы поступим изящнее. Это же число 1234 может быть записано так:

1234 = ((((1)*10 + 2)*10 + 3)М0) + 4 Это означает, что мы можем умножить первую слева цифру на основание, прибавить вторую цифру, снова умножить на основание и т.д. Благодаря этому постепенному умножению нам не нужно заранее знать порядок пре­образуемого числа.

Г Начало j

Отрица-^S^ Нет тельное?.

Преобразовать цифру в число NEG

Сложить и умножить ( Конец р

Рис. 7.3. Блок-схема алгоритма преобразования строки в число

Окончательная версия подпрограммы преобразования строки в число при­ведена в листинге 7.4.

109

Page 111: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Листинг 7.4. Программа преобразования строки в число

ASCIIToNum

esi = указатель на строку, заканчивающуюся символом с кодом 0x0 есх = основание системы счисления Возвращает: еах = число

ASCIIToNum: push esi xor еах,еах xor ebx,ebx cmp byte [esi] , ' - ' jnz .next inc esi .next: lodsb or al,al j z .done call convert_char

imul ebx,ecx add ebx,eax jmp short .next .done: xchg ebx,eax pop esi cmp byte [esi] , ' - ' jz .negate ret .negate: neg eax ret

;сохраняем указатель в стеке ;ЕАХ = О ;ЕВХ = О: накопитель для числа ;число отрицательное? ;если нет, не пропускаем следующий ;символ ;пропускаем символ '- '

;читаем цифру в AL ;конец строки?

/преобразовать в число и сохранить ;его в AL ;умножить ЕВХ на ЕСХ, сохранить в ЕВХ ;сложить ;и повторить

/поместить накопленное число в ЕАХ /восстановить исходное значение ESI /результат должен быть отрицательным? /да, выполним отрицание /нет, положительным — все готово

/выполняем отрицание /все!!!

110

Page 112: Ассемблер на примерах. Базовый курс

Глава 8 Операционная система

Эволюция операционных систем

Распределение процессорного времени. Процессы

Управление памятью

Файловые системы

Загрузка системы

Page 113: Ассемблер на примерах. Базовый курс

Операционная система предоставляет интерфейс, то есть средства взаимо­действия, между прикладными программами и аппаратным обеспечением (оборудованием) компьютера. Она управляет системными ресурсами и рас­пределяет эти ресурсы между отдельными процессами.

8.1. Эволюция операционных систем История операционных систем началась в 1950-х годах. Первые компьютеры требовали постоянного внимания оператора: он вручную загружал програм­мы, написанные на перфокартах, и нажимал всевозможные кнопки на пульте управления, управляя вычислительным процессом. Простои такого компью­тера обходились очень дорого, поэтому первые операционные системы были разработаны для автоматического запуска следующей задачи после оконча­ния текущей. Часть операционной системы — так называемый монитор — управлял последовательным выполнением задач и позволял запускать их в автоматизированном пакетном режиме. Следующим этапом в развитии операционных систем стало создание специ­ального компонента операционной системы — ее ядра (1960-е годы). При­чина появления ядра была довольно простой. Периферийное оборудование компьютеров становилось все более разнообразным, и задача управления отдельными устройствами все более усложнялась. Ядро операционной систе­мы предоставило стандартный интерфейс для управления периферийными устройствами. При загрузке операционной системы ядро загружается в память, чтобы впо­следствии программы могли обращаться к периферийным устройствам через него. Постепенно в ядро добавлялись новые функции. С исторической точки зрения особенно интересна служба учета, позволившая владельцу компьютера выставлять счет пользователю в соответствии с затраченным на его задание машинным временем. Разработчики операционных систем старались более эффективно использовать ресурсы компьютера. Из очевидного соображения о том, что нерационально выполнять на компьютере только одно задание, если другое, использующее другие периферийные устройства, могло бы выполняться одновременно с ним, в 1964 году родилась концепция мультипрограммирования. 112

Page 114: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Мультипрограммирование поставило операционные системы перед новым вызовом: как распределить ресурсы компьютера, то есть процессор, память, периферийные устройства, между отдельными программами? В этой главе мы рассмотрим ответы на этот вопрос.

8.2. Распределение процессорного времени. Процессы

Одной из важнейших задач, решаемых операционной системой, является рас­пределение процессорного времени между отдельными программами.

Процессы

Попросту говоря, процесс — это загруженная в оперативную память про­грамма, которой выделено процессорное время. Каждый процесс, подобно человеку, от кого-то родился, но в отличие от человека у процесса родитель один — это запустивший его процесс. Родительский процесс еще называют предком. Всех потомков или «детей» процесса (то есть те процессы, которые он поро­дил) называют «дочерними» процессами. «Родословное дерево» называется иерархией процессов.

init — • xdm W Xfree86

xdm

предок ^—-•

предок

window maker

• потомок

<—-• потомок

— •

sshagent

xterm

Рис. 8.1. Иерархия процессов

Загрузившись, ядро операционной системы запускает первый процесс. В опе­рационных системах UNIX (Linux) он называется init. Этот процесс станет «прародителем» всех остальных процессов, протекаюгцих в системе. В опера­ционной системе DOS «прародителем» является командный интерпретатор COMMAND.COM. Дочерний процесс может унаследовать некоторые свойства или данные от своего родительского процесса. Процесс можно «убить» (kill), то есть за­вершить. Если убит родительский процесс, то его дочерние процессы либо «усыновляются» другим процессом (обычно init), либо тоже завершаются.

113

Page 115: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Планирование процессов

На самом деле процессы выполняются не параллельно, как если бы они были запущены на независимых компьютерах. Если у компьютера только один про­цессор, то процессы будут выполняться в так называемом псевдопараллельном режиме. Это означает, что каждый процесс разбивается на множество этапов. И этапы разных процессов по очереди получают кванты процессорного вре­мени — промежутки времени, в течение которого они монопольно занимают процессор. Список всех запущенных процессов, называемый очередью заданий, ведет ядро операционной системы. Специальная часть ядра — планировщик за­даний — выбирает задание из очереди, руководствуясь каким-то критерием, и «пробуждает» его, то есть выделяет ему квант времени и передает процесс диспетчеру процессов. Диспетчер процессов — это другая часть ядра, которая следит за процессом во время его выполнения. Диспетчер должен восстановить контекст процесса. Восстановление контек­ста процесса напоминает обработку прерывания, когда все регистры должны быть сохранены, а потом, после окончания обработки, восстановлены, чтобы прерванная программа смогла продолжить работу. После восстановления контекста ядро передает управление самому процессу. Затем, по прошествии кванта времени, выполняемый процесс прерывается и управление передается обратно ядру. Планировщик заданий выбирает следующий процесс, диспетчер процессов его запускает, и цикл повторяется.

Состояния процессов

Ядро операционной системы внимательно «наблюдает» за каждым процессом. Вся необходимая информация о процессе хранится в структуре, которая на­зывается блоком управления процессом (РСВ, process control block). В операционной системе UNIX процесс может находиться в одном из пяти различных состояний:

• Рождение — пассивное состояние, когда самого процесса еще нет, но уже готова структура для появления процесса.

• Готовность — пассивное состояние: процесс готов к выполнению, но процессорного времени ему пока не выделено.

• Выполнение — активное состояние, во время которого процесс обладает всеми необходимыми ему ресурсами. В этом состоянии процесс непо­средственно выполняется процессором.

• Ожидание — пассивное состояние, во время которого процесс заблокиро­ван, потому что не может быть выполнен: он ожидает какого-то события, например, ввода данных или освобождения нужного ему устройства.

• Смерть процесса — самого процесса уже нет, но может случиться, что его «место», то есть структура, осталось в списке процессов (процессы-зомби).

114

Page 116: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Выполнение

А 1

1 Т Готовность

А

Рождение (создание)

4—

т

—> Смерть (завершение)

Ожидание

Рис. 8.2. Жизненный цикл процесса

Процессы в DOS состояний не имеют, поскольку DOS — это однозадачная операционная система: процессор предоставлен в монопольное распоряжение только одного процесса. Ядро операционной системы хранит следующую информацию о процессе:

• Размещение в памяти. • Ресурсы процесса (открытые файлы, устройства и т.п.). • Контекст процесса. • Состояние процесса. • Имя процесса. • Идентификационный номер процесса (PID, Process ID). • Идентификационный номер родительского процесса. • Права доступа. • Текущий рабочий каталог. • Приоритет.

Стратегия планирования процессов

Пока мы не сказали ничего о том, как именно ядро (точнее, планировщик заданий) решает, какой процесс нужно запустить следующим. Простейшая стратегия планирования процессов называется кольцевой (Round Robin). Все процессы ставятся в очередь. Планировщик выбирает первый про­цесс и передает его диспетчеру. Через определенное время ядро прерывает первый процесс, перемещает его в конец очереди и выбирает следующий процесс. Другая стратегия распределения процессорного времени основана на при­оритетах. Каждому процессу в очереди назначен некоторый приоритет, и в соответствии с ним планировщик распределяет процессорное время — ста­тически или динамически. Статическое назначение приоритетов означает,

115

Page 117: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

-> Очередь I I

( Активный \ I процесс J

[ Диспетчер j

Р1ЛС, 8.3. Кольцевая стратегия Round Robin

что сначала будут выполняться процессы с высоким приоритетом, причем приоритет процесса не может быть изменен на протяжении всего времени выполнения процесса. При динамическом же назначении приоритет может быть изменен во время выполнения.

8.3. Управление памятью Операционная система отслеживает свободную оперативную память, pea-лизует стратегию выделения оперативной памяти и освобождения ее для последующего использования. Оперативная память делится между ядром и всеми выполняемыми процессами.

Простое распределение памяти

Стратегия простого распределения памяти была самой первой. Она состоит в том, что доступная память делится на блоки фиксированного размера, одни из которых доступны только ядру, а другие — остальным процессам. Для разделения памяти на блоки используются различные аппаратные и программные методы.

Операционная система

процесс 1

Процесс 2

адресное пространство первого процесса

адресное пространство второго процесса

нераспределенное пространство

Рис. 8.4. Фиксированное распределение памяти между процессами

116

Page 118: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Каждый блок памяти может быть занят только одним процессом и будет за­нят, даже если процесс еще не начал выполняться. Ядро обеспечивает защиту блоков памяти, занятых различными процессами. Позже эта схема была усовершенствована введением блоков переменной длины. При запуске процесс сообщает, сколько памяти ему нужно для раз­мещения. Это была уже технология переменного распределения памяти. Обе технологии распределения памяти сейчас считаются устаревшими и прак­тически не используются, потому что они не могут удовлетворить запросы современных задач. Случается, например, что размер процесса превышает весь имеющийся объем оперативной памяти. При традиционной стратегии распределения памяти операционная система просто не сможет запустить такой процесс. Вот поэтому была разработана новая стратегия распределения памяти — стратегия свопинга, которая позволяет использовать пространство жесткого диска для хранения не помещающейся в оперативную память ин­формации.

Свопинг (swapping) — организация подкачки

Пока процессы и размеры обрабатываемых данных были небольшими, всех вполне устраивало фиксированное распределение памяти. С появлением мно­гозадачных операционных систем запущенные процессы не всегда умещались в оперативную память. Поэтому было решено выгружать не используемые в данный момент данные (и процессы!) на жесткий диск. В оперативной памяти остается только структура процесса, а все остальное выгружается. Эта про­цедура называется свопингом или подкачкой. Когда системе нужны данные, находящиеся в файле (разделе) подкачки, си­стема подгружает их в оперативную память, выгрузив предварительно в файл подкачки другие данные.

Виртуальная память и страничный обмен

Закон Мерфи гласит: программа растет до тех пор, пока не заполнит всю доступную память. Решением проблемы стало «притвориться», что в распоряжении операцион­ной системы больше оперативной памяти, чем на самом деле. Этот механизм называется виртуальной памятью и чаще всего реализуется через систему страничного обмена. Каждому процессу выделено виртуальное адресное пространство, то есть диапазон адресов, ограниченный только архитектурой процессора, а не объ­емом физически установленной оперативной памяти. Виртуальное адресное пространство делится на страницы одинакового размера, обычно 4 Кб. Фи­зическая оперативная память делится на так называемые фреймы, размер каждого фрейма равен размеру страницы.

117

Page 119: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Виртуальный адрес

смещение

Рис. 8.5. Преобразование виртуального адреса в физический

Перед каждым обращением к памяти процессор запрашивает так называемый блок управления памятью (MMU, Memory Management Unit), чтобы он пре­образовал виртуальный адрес в физический. Трансляция виртуального адреса в физический производится с использова­нием таблицы страниц, каждая строка которой описывает одну виртуальную страницу.

Запись таблицы страниц

Другие флаги Флаги буфера

Физический адрес

Р Бит

присутствия

Рис. 8.6. Запись таблицы страниц

Наиболее важный бит — бит присутствия страницы. Если он установлен, то эта страница сохранена во фрейме, номер которого (адрес) определен в физическом адресе. Другие биты в таблице определяют права доступа или разрешения для страницы (read/write/execute) или используются для буфера. Таблица страниц индексирована номером виртуальной страницы. Рассмотрим пример трансляции (преобразования) виртуального адреса в физический (рис. 8.7).

118

Page 120: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Рис. 8.7. Принцип преобразования виртуального адреса в физический

По виртуальному адресу MMU вычисляется номер виртуальной страницы, на которой он находится. Разность между адресом начала страницы и требуемым адресом называется смещением. По номеру страницы ищется строка в таблице страниц. Бит присутствия в этой строке указывает, находится ли виртуальная страница в физической памяти компьютера (бит Р установлен в 1) или в файле (разделе) подкачки на диске. Если страница присутствует в физической памяти, то к физическому адресу (номеру фрейма) добавляется смещение, и нужный физический адрес готов. Если страница выгружена из оперативной памяти (бит Р установлен в 0), то процессор (или его MMU) вызывает прерывание «Страница не найдена» (Page Not Found), которое должно быть обработано операционной системой. В ответ на прерывание «Страница не найдена» операционная система должна загрузить требуемую страницу в физическую оперативную память. Если заня­ты не все фреймы, то ОС загружает страницу из файла подкачки в свободный фрейм, исправляет таблицу страниц и заканчивает обработку прерывания. Если свободного фрейма нет, то операционная система должна решить, какую страницу переместить из фрейма в область подкачки, чтобы освободить физи-

119

Page 121: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

ческую память. В освобожденный фрейм будет загружена страница, которая первоначально вызвала прерывание. После этого ОС исправит биты присут­ствия в записях об обеих страницах и закончит обработку прерывания. Для процесса процедура преобразования виртуальных адресов в физические абсолютно прозрачна («незаметна») — он «думает», что у вашего компьютера просто много оперативной памяти. В х86-совместимых компьютерах каждая программа может иметь несколько адресных пространств по 4 ГБ. В защищенном режиме значения, загруженные в сегментные регистры, используются как индексы таблицы дескрипторов, описывающей свойства отдельных сегментов, или областей, памяти. Рассмо­трение сегментов выходит далеко за пределы этой книги.

8.4. Файловые системы Файловая система — это часть операционной системы, предназначенная для управления данными, записанными на постоянных носителях. Она включает в себя механизмы записи, хранения и чтения данных. Когда компьютеры только начали развиваться, программист должен был знать точную позицию данных на носителе. Почему мы используем слово «носитель», а не «диск»? В то время дисков как таковых или еще не было, или они были слишком дороги, поэтому для хранения данных использовалась магнитная лента. С появлением дисков все еще больше усложнилось, поэтому для облегчения жизни программистам и пользователям и была разработана файловая система, которая стала частью ядра.

Структура файловой системы

Кроме программного модуля в ядре, в понятие файловой системы входит структура данных, описывающая физическое местоположение файлов на диске, права доступа к ним и, конечно же, их имена. Сам файл состоит из записей, которые могут быть фиксированной или пере­менной длины. Метод доступа к записям файла зависит от его типа. Самый простой — метод последовательного доступа, при котором для чтения нужной записи требуется сначала прочитать все предшествующие ей записи. Рхли записи проиндек­сированы, то к ним можно обращаться также по индексу. Такой тип файла называется индексно-последовательным (IBM 390, AS/400). Что же касается операционной системы, то она не устанавливает формат файла. Файл состоит из последовательности байтов, а структура самого файла должна быть понятна приложению, которое используется для его обработки. Поговорим о методах доступа к файлу в операционных системах DOS и UNIX.

120

Page 122: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Доступ к файлу

Перед началом обработки любого файла его нужно открыть. Это делает операционная система по запросу программы. В запросе нужно указать имя файла и требуемый метод доступа (чтение или запись). Ядро возвратит номер файла, который называется файловым дескриптором. В дальнейшем этот номер будет использоваться для операций с файлом. Имя файла обычно содержит путь к этому файлу, то есть перечисление всех каталогов, необходимых для того, чтобы система смогла найти файл. Имена подкаталогов в составе пути разделяются специальным символом, который зависит от операционной системы. В DOS это \, а в UNIX — /. Позиция в файле определяется числом — указателем. Его значение — это количество прочитанных или записанных байтов. С помощью специального системного вызова можно переместить указатель на заданную позицию в файле и начать процесс чтения или записи с нужного места. Максимальный размер блока данных, который может быть прочитан или записан за раз, ограничен буфером записи. Завершив работу с файлом, его нужно закрыть. В мире UNIX укоренилась идея представлять устройства ввода/вывода в виде обычных файлов: чтение символов с клавиатуры и вывод их на экран выполняются теми же самыми системными вызовами, что и чтение/запись их в файл. Дескриптор «файла» клавиатуры — О, он называется стандартным потоком ввода (stdin). У экрана два дескриптора — стандартный поток вывода (stdout) и стандартный поток ошибок (stderr). Номер первого идентификатора — 1, второго — 2. Операционная система может не только читать и записывать файлы, но также и перенаправлять потоки ввода/вывода. Например, можно перенаправить вы­вод программы в файл вместо вывода на экран, не меняя исходного текста программы и не перекомпилируя ее. Перенаправление известно как в DOS, так и в UNIX, и выполняется с помощью специального системного вызова, который обычно принимает два аргумента — файловые дескрипторы «со­единяемых» файлов. В DOS и UNIX можно перенаправить вывод программы в файл так:

I s > f i l e l

Фактически интерпретатор команд (или оболочка) открывает файл filel перед запуском Is, получает его дескриптор и, используя системный вызов, заменяет дескриптор стандартного вывода полученным дескриптором. Программа Is и понятия не имеет, куда записываются данные — она просто записывает их на стандартный вывод. Благодаря системному вызову вывод попадает в указанный файл, а не на экран.

121

Page 123: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Физическая структура диска

Файловая система обращается к диску непосредственно (напрямую), и по­этому она должна знать его физическую структуру (геометрию). Магнитный диск состоит из нескольких пластин, обслуживаемых читающими/пишущими головками (рис. 8.8). Пластины разделены на дорожки, а дорожки — на сек­тора. Дорожки, расположенные друг над другом, образуют «цилиндр». Исто­рически сложилось так, что точное место на диске определяется указанием трех «координат»: цилиндра, головки и сектора.

головка 1 Z. головка 2

головка 5 /i головка 6

Pvic. 8.8. Физическая структура диска

цилиндр

дорожка 3 дорожка 2 дорожка 1 сектор 3 сектор 2 сектор 1

Дорожки — ЭТО концентрические круги, разделенные на отдельные сектора. Подобно странице памяти, сектор диска — это наименьший блок информации, размер его обычно равен 512 байтам. Головки чтения/записи «плавают» над поверхностью диска. Значения трех вышеупомянутых «координат» позволяют головкам определить нужный сектор диска для чтения или записи данных. Для цилиндров, головок и секторов были определены диапазоны приемлемых значений. Поскольку объемы дисков росли, увеличивалось и количество ци­линдров, головок и секторов. Увеличивалось оно до тех пор, пока не вышло за пределы этого самого диапазона. Вот поэтому некоторые старые компью­теры не могут прочитать диски размером 60 Гб и более (а некоторые и того меньше). Вместо увеличения этих диапазонов были разработаны различные преобразования, которые позволяют вернуть комбинации сектора, головки и цилиндра назад в указанный диапазон. Было решено заменить адресацию геометрического типа логической (или линейной) адресацией, где отдельные секторы были просто пронумерованы от О до последнего доступного сектора. Теперь для того, чтобы обратиться к сектору, нужно просто указать его номер.

122

Page 124: Ассемблер на примерах. Базовый курс

Глава 8. Операционная система

Логические диски

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

8.5. Загрузка системы

Этапы загрузки

После нажатия кнопки питания компьютер «оживает». Сначала запускается программа, сохраненная в ROM (read-only memory). Память ROM содержит так называемую базовую систему ввода/вывода (BIOS, Basic Input Output System). В задачу BIOS входит инициализация компьютера — проверка видео­адаптера, памяти, дисков и других устройств — это так называемая процедура POST (Power On Self Test). После проверки устройств компьютера BIOS начинает загрузку операцион­ной системы. В зависимости от настроек BIOS может искать начальный за­грузчик, которому и будет передано управление, на дискете, жестком диске (сектор с номером 0), CD-ROM и т.д. Первый сектор (с номером 0) диска содержит начальный загрузчик и таблицу разделов. Этот сектор называется главной загрузочной записью — MBR (Master Boot Record). BIOS загружает код начального загрузчика из MBR в оперативную память и передает ему управление. Задача начального загрузчика — найти вторичный загрузчик операционной системы, который находится на одном из логических дисков, перечисленных в таблице разделов. Найти такой диск очень просто — в таблице разделов он единственный помечен как активный. На каждом логическом диске заре­зервировано место (первый сектор) для загрузчика операционной системы. Начальный загрузчик загружает загрузчик операционной системы и передает ему управление, а тот в свою очередь загружает ядро и передает управление ему: с этого момента операционная система запущена.

123

Page 125: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Немного о том, что такое BIOS

BIOS (Basic Input/Output System — Базовая Система Ввода/Вывода) — обя­зательная часть каждого PC. Ее задача заключается в предоставлении стан­дартного способа доступа к аппаратным ресурсам компьютера. Во времена операционной системы DOS BIOS была своеобразным мостом между опера­ционной системой и аппаратными средствами компьютера. Современные операционные системы используют BIOS только при загрузке операционной системы в память, а все функции по управлению устройствами они предоставляют самостоятельно. Сервисы BIOS доступны через программ­ные прерывания (табл. 8.1).

Программные прерывания и соответствующие им сервисы BIOS Таблица 8.1

Номер прерывания

0x10

0x13

0x14

0x15

0x16

0x17

Функция

Службы экрана (управление выводом на экран)

Дисковый ввод/вывод

Управление последовательными портами

Расширенные сервисы

Работа с клавиатурой

Работа с принтером

Видеоадаптеры (и некоторые другие устройства) оснащены своими собствен­ными BIOS, поскольку невозможно учесть особенности всех видеоадаптеров в BIOS компьютера. Мы не будем рассматривать BIOS подробнее, потому что для ввода данных и вывода на экран будем пользоваться стандартными средствами операционной системы.

Приложение

Операционная система

ВЮЗ

Аппаратные средства

Рис. 8.9, Пирамида доступа к аппаратным средствам компьютера

124

Page 126: Ассемблер на примерах. Базовый курс

Компилятор NASM

Предложения языка ассемблера

Выражения

Локальные метки

Препроцессор NASM

Директивы Ассемблера

Формат выходного файла

Page 127: Ассемблер на примерах. Базовый курс

Для компиляции наших программ мы будем использовать компилятор NASM (Netwide Assembler), который свободно распространяется (вместе со своими исходными кодами) по лицензии LGPL. Вы можете найти этот компилятор на сайте http://nasm.sourceforge.net. Синтаксис компилятора подобен синтаксису коммерческих компиляторов MASM (Microsoft Assembler) и TASM (Turbo Assembler от Borland), поэтому использовать NASM вдвойне удобно — никаких проблем с лицензией и воз­можность переноса программ, написанных для других ассемблеров.

9.1. Предложения языка ассемблера Каждая строка программы (предложение языка ассемблера) может состоять из следующих частей:

Метка Инструкция Операнды Любые комментарии

Перед меткой и после инструкции можно использовать любое количество про­бельных символов (пробелы, табуляция). Операнды отделяются друг от друга с помощью запятой. Начало комментария — точка с запятой (;), а концом комментария служит конец строки. Если предыдущая строка очень длинная, ее можно перенести на следующую, используя обратный слеш 'V (как в С). Инструкцией может быть команда или псевдокоманда (директива компиля­тора).

9.2. Выражения Под словом «выражение» мы понимаем константное выражение, значение которого известно уже во время компиляции. Синтаксис выражений NASM подобен синтаксису выражений языка С. Выражение также может быть написано как сумма адреса в символической форме и некоторого числа. Выражения такого типа служат, например, для

126

Page 128: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

доступа ко второму элементу массива в инструкции MOV еах, [array+4]. Значение выражения в квадратных скобках известно во время компиляции: это начальный адрес массива Array, увеличенный на 4. Выражения можно использовать во многих местах программы, например:

a d d d l , ' А ' ~ 1 0

Выражение 'А ' -10 будет вычислено во время компиляции, и компилятор сгенерирует команду ADD dl, 55. Нам никто не запрещает использовать сложные выражения, которые известны нам из языка С. В следующей таблице (табл. 9.1) перечислены все поддержи­ваемые операторы в порядке возрастания приоритета.

Операторы, поддерживаемые NASM, расположенные в порядке возрастания приоритета Таблица 9.1

Оператор

1 '

1 ^ « »

+ -

*/%//%% унарные + - ~

Назначение

Логическая операция OR

Логическая операция XOR

Логическая операция AND

Сдвиг влево и вправо

Плюс и минус

Умножение, целочисленное деление, деление по модулю (взятие остатка), деление со знаком, деление со знаком по модулю

Унарные операторы плюс, минус и отрицание (NOT)

Рассмотрим несколько примеров: mov е а х , { ( 5 * 6 + 2 ) + ( 0 х 4 0 < < 2 ) ) / 8 ; в ы ч и с л и т с я к а к :

;MOV е а х , 0 х 2 4

Еще один пример: mov с1,~1 ;результат: mov с1,11111110b

;ИЛИ: mov с1,OxFE

9.3. Локальные метки с метками мы познакомились при рассмотрении инструкций JMP и CALL, по­этому вы уже знаете, для чего они используются. Метка — это идентификатор, за которым следует двоеточие. Двоеточие можно и опускать, но я советую этого не делать, потому что иначе компилятор может принять имя метки за написанное с ошибкой имя команды. Каждая метка должна быть уникальной в пределах программы, поэтому при написании больших программ вам по­надобится весь ваш творческий потенциал, чтобы каждый раз придумывать

127

Page 129: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

новое имя. К счастью, нас выручит компилятор NASM, который позволяет использовать так называемые локальные метки. Локальные метки, как и локальные переменные в высокоуровневых языках программирования, могут иметь одинаковые имена в разных подпрограммах. Но в языках высокого уровня область видимости переменной (блок, функция) видна сразу, а в языке ассемблера? Локальной называется метка, действи­тельная только на протяжении фрагмента кода, находящегося между двумя глобальными метками. Различаются локальные и глобальные метки своими именами: имя локальной метки начинается с точки, а глобальной — с любого другого символа.

subprog: /глобальная метка subprog ; код

.1оса1_1оор: /локальная метка ; код

subprog2: ;глобальная метка subprog2

. loca l_ loop: ; локальная метка

9.4. Препроцессор NASM в этом параграфе мы поговорим о средствах, несколько сближающих про­граммирование на языке ассемблера с программированием на языках высокого уровня — макросах. Макрос — это символическое имя, заменяющее несколько команд языка ассемблера. Команды подставляются вместо имени макроса во время ком­пиляции. Этим занимается программа, называемая препроцессором и вы­зываемая, как правило, самим компилятором до того, как он приступит не­посредственно к трансляции символических имен команд в машинные коды. Препроцессор NASM очень мощный и гибкий, он поддерживает множество различных макросов. Директивы объявления макроса начинаются со знака процента «%». Имена макросов чувствительны к регистру символов (то есть АаА — это не ааа). Для определения не чувствительных к регистру символов макросов нужно использовать другие директивы, которые начинаются с пары символов «%i» (%idefine вместо %define и т.п.).

Однострочные макросы — %define, %undef

Эти макросы определяются подобно тому, как в языке С, но в отличие от С могут принимать аргументы. Вот пример макроса для вычисления среднего арифметического двух чисел:

128

Page 130: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

% d e f i n e a v e r a g e ( a , b ) ( ( ( а ) + ( b ) ) / 2 )

Использовать данный макрос в программе очень просто: mov a l , a v e r a g e ( 3 , 7 )

Инструкция, которая будет «подставлена» вместо макроса: mov a l , 5

Очень часто %define используется для определения констант, как в С: % d e f i n e SEC„IN_MIN 6 0 % d e f i n e SEC_JN_HOUR SEC_IN_MIN * 60

Или для условной компиляции, например: % d e f i n e USE„MMX

Так же, как и в С, мы можем проверить, определен ли макрос, с помощью %if def (определен) и %ifndef (не определен). Удалить определение макроса можно с помощью %undef.

Сложные макросы — %macro %endmacro

Директива %defme позволяет определить простые макросы, состоящие из ОДРЮЙ команды (строки кода). Для определения сложного, многострочного, макроса служат директивы %macro и %endmacro. Первая описывает начало макроса — имя и число аргументов. Затем идет тело макроса — команды, которые должны быть выполнены. Директива %endmacro «закрывает» макрос. %macro subtract 3 sub %1,%2 sub %1,%3 %endmacro

Макрос s u b t r a c t нужно вызывать с тремя аргументами: subtract еах,есх,[variablel]

Он будет преобразован в следующий код: sub е а х , е с х sub еах,[variablel]

Необязательно указывать точное число аргументов. Препроцессор NASM по­зволяет указывать переменное число в виде диапазона значений. Например, 2-3 означает, что наш макрос может принимать 2 или 3 аргумента. Опущенные при вызове макроса аргументы будут заменены значениями по умолчанию, которые следуют за диапазоном аргументов:

%macro a d d i t 2 - 3 О a d d %1,%2 a d d % 1 Д З %endmacro

129

Page 131: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Мы описали макрос addi t , который принимает два обязательных аргумента, а вместо третьего, если он не указан при вызове, будет подставлено значение по умолчанию — 0:

a d d i t еах еЬх

Будет преобразовано в: add e a x , e b x add еах,О

Объявление макроса — %assign

Директива %assign — это еще один способ объявить простой (однострочный) макрос. Объявленный макрос не должен иметь параметров, и его результатом должно быть число. Например, с помощью

%assign i i+1 мы можем увеличить числовое значение другого макроса на 1. Обычно %assign используется в сложных макросах.

Условная компиляция — %if

Как и препроцессор С, препроцессор NASM позволяет условно компилиро­вать программу. Это означает, что в зависимости от условия будет откомпи­лирован тот или иной блок кода. Рассмотрим основные инструкции условной компиляции:

%if<ycnoBHe> ;Код между i f и e l i f будет откомпилирован ; только , если условие истинно.

%elif<ycnoBHe2> ;Код между %el i f и %else будет откомпилирован ;если первое условие ложно, а второе — истинно

^e l se

^endif

;Если оба условия ложны, будет откомпилирован код ;между %else и %endif

Директивы %elif и %else необязательны и могут быть опущены. Также можно ука­зывать несколько блоков %elif, но блок %else (если он есть) должен быть один. Условие может содержать константное выражение. Набор операторов в усло­вии расширен операторами отношения: =, <, >, <=, > = , и о (равно, мень­ше, больше, меньше или равно, больше или равно, не равно). Приверженцам языка С понравится возможность писать « = =» и «! = » вместо « = » и « о » . Также при написании условия могут быть использованы логические операторы 1 1 , ^ ^ , && (OR, XOR, AND), которые нам известны из языка С.

130

Page 132: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

Определен ли макрос? Директивы %ifdef, %infndef

Директива %ifdef используется для проверки существования макроса. На­пример: %define TEST_IT %ifdef TEST_IT cmp eax,3

%endif Инструкции, заключенные в блок %ifdef (в том числе и СМР еах,3) будут откомпилированы, только если макрос TEST_IT определен с помощью %define. Кроме %ifdef можно использовать директиву %ifndef. В этом случае соот­ветствующий блок инструкций будет компилироваться, только если макрос не определен.

Вставка файла — %include

С помощью директивы %include можно вставить в текущую позицию код, находящийся в другом файле. Имя файла нужно указать в двойных кавычках. Обычно %include используется для вставки так называемых заголовочных файлов:

% i n c l u d e « m a c r o . ш а с »

Как правило, заголовочные файлы содержат определения констант или ма­кросов, а не команды программы. В отличие от С, нам не нужно добавлять в начало заголовочного файла эту традиционную конструкцию:

% i f n d e f MACROS_MAC % d e f i n e MACROS_MAC ; т е п е р ь следуют сами макрокоманды

% e n d i f

Повторное включение файла не является ошибкой. Просто все макросы будут определены заново, что не должно вызвать ошибку при компиляции.

9.5. Директивы Ассемблера Директивы Ассемблера NASM — это макрокоманды, определенные самим компилятором. Директив у NASM немного, в отличие от MASM и TASM, где их число огромно.

131

Page 133: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Директива BITS — указание режима процессора

Директива указывает компилятору, в каком режиме процессора будет работать программа. Синтаксис этой директивы позволяет указать один из двух режи­мов — 16-разрядный (BITS 16) или 32-разрядный (BITS 32). В большинстве случае нам не нужно указывать эту директиву, поскольку за нас все сделает сам NASM. Во второй главе, в которой рассматривался процессор 80386, мы говорили о двух режимах процессора — реальном и защищенном. Реальный режим ис­пользуется для обратной совместимости с предыдущими чипами. В реальном режиме используются 16-битные код и адресация. Но 80386 также поддержи­вает 32-битные инструкции. Значит ли это, что 32-битные команды нельзя использовать в 16-битном режиме? Да, нельзя. Как показано в главе 3, каждой команде соответствует свой ма­шинный код. Разработчикам Intel пришлось решать проблему — как добавить новые команды, работающие с 32-битными операндами, когда таблица кодов команд уже почти заполнена? Они нашли довольно изящное решение. 16-битная команда MOV АХ,0х1234 транслируется в машинный код 0хВ8, 0x34, 0x12. 32-битная команда MOV ЕАХ,0х00001234 транслируется в машинный код 0x66, 0хВ8, 0x34, 0x12, 0x00, 0x00. В 16-битном (реальном) режиме машинное представление всех 32-битных ко­манд начинается с префикса 0x66 (а всех обращений к памяти — с префикса 0x67). После префикса следует код 16-битной версии той же самой команды. Благодаря префиксу операнды тоже могут быть 32-битными. В защищенном режиме процессор использует 32-битную адресацию и 32-битный код. Поэтому машинная команда 0хВ8, 0x34, 0x12, 0x00, 0x00 (то есть без префикса 0x66) означает MOV ЕАХ,0х00001234. Директива BITS указывает, как будут компилироваться инструкции — с пре­фиксом или без.

Директивы SECTION и SEGMENT — задание структуры программы

Каждая программа, вне зависимости от языка программирования, состоит из трех частей: код программы, статические данные (то есть известные во время компиляции) и динамические данные (неинициализированные, то есть те, под которые во время компиляции только отводится память, а значение им не присваивается). Эти секции могут быть определены с помогцью директив SECTION и SEGMENT.

132

Page 134: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

Традиционно секция кода называется .text, секция статических данных — .data, а секция динамических данных — .bss.

Рассмотрим пример программы, содержащий все три секции (листинг 9.1)

Листинг 9,1 ;T^^^^nporpafj^^^^^^^^^^^^^^^^^^^^^

; Каждая ассемЬлерная программа, должна начинаться с описания ;своего назначения и авторства, например, ;(с)2005 Иванов И.П. <ivan(iivanov. сот> SECTION .text /Секция .text содержит нашу программу ;Это очень простая программа для сложения двух значений

ЕАХ = 2 ЕВХ = 5 ЕАХ = ЕАХ + ЕВХ ЕАХ = ЕАХ + statl сохраняем полученное в динамической

mov еах,2 mov ebx,5 add еах,ebx add еах,[statl] mov [dynl],eax переменной dynl SECTION .data

;используем псевдоинструкции DB, DW and DD ;для объявления статических данных

.data statl: dd 1 SECTION .bss В этой секции описываются динамические данные, они не инициализируются. Для объявления переменных в секции .bss используются псевдоинструкции RESB, RESW и RESD, Динамические данные не занимают места на диске, то есть для. них не отводится место в исполняемом файле, Инструкции RES* указывают только, сколько байтов переменные будут занимать в памяти после запуска программы

dynl: resd 1 ;конец программы

Иногда также определяется еще одна секция — для стека (.stack). Но посколь­ку о стеке заботится операционная система, нам беспокоиться не о чем.

Директивы SECTION и SEGMENT являются синонимами и поэтому взаи­мозаменяемы.

Примечание переводчика.

В другой литературе вы можете встретить использование термина «сегмент» вместо «секция». Соответственно, три части программы называются сегмент кода, сегмент данных и сегмент стека.

133

Page 135: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Директивы EXTERN, GLOBAL и COMMON -обмен данными с другими программными модулями

Мы будем использовать эти три директивы в главе 13 при линковке (ком­поновке) программ на языке ассемблера с программами, написанными на высокоуровневых языках программирования, поэтому сейчас мы рассмотрим их очень бегло. Директива EXTERN подобна одноименной директиве (extern) в С. Она позволяет определить идентификаторы, которые не определены в текущей программе, но определены в каком-то внешнем модуле. Она используется для «импорта» идентификаторов в текущую программу. Директива GLOBAL определяет идентификаторы для «экспорта» — они будут помечены как глобальные и могут использоваться другими модулями (программами). Директива COMMON используется вместо GLOBAL для экспорта иденти­фикаторов, объявленных в секции .bss. Если другой модуль экспортирует с помощью директивы COMMON тот же самый идентификатор, то оба символа будут размещены в памяти по одному и тому же адресу.

Директива CPU — компиляция программы для выполнения на определенном процессоре

Используя директиву CPU, мы можем заставить компилятор генерировать команды только для указанного типа процессора. Мы обсуждаем команды только процессора 80386, поэтому не нуждаемся в директиве CPU. Если эта директива опущена, то в программе можно использовать любые команды х86-совместимых процессоров. Определение набора команд с помощью директивы CPU полезно для опыт­ного программиста, который может использовать некоторые новые команды специфического типа процессора.

CPU 808 6 /Используются инструкции оригинального чипа ; I n t e l 8086 можно у к а з а т ь другой тип процессора ; т . е . 286, 386, 486, P e n t i u m . . .

Директива ORG — указание адреса загрузки

Директива ORG устанавливает начальный адрес для загрузки программы — тот адрес, от которого отсчитываются все остальные адреса в программе. Например, с помощью ORG вы можете создать СОМ-файл (подробнее о форматах выходных файлов — следующий параграф), если укажете ORG 0x100. Операционная система DOS загружает исполняемые файлы типа СОМ в сегмент памяти, начинающийся с адреса 0x100.

134

Page 136: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

Следующий материал предназначен для программистов, которые хотят «коп­нуть» NASM поглубже, начинающие программисты могут с чистой совестью пропустить этот пункт. В отличие от компиляторов MASM и TASM, NASM запрещает использовать директиву ORG несколько раз в пределах одной секции. Типичный пример многократного применения директивы ORG: вторая ORG, означающая конец загрузочного сектора. В MASM или TASM мы можем указать: ORG О

;тут загрузочный сектор

;конец загрузочного сектора ORG 510 DW 0хАА55

;В NASM вместо второго ORG мы должны использовать ;директиву TIMES:

ORG О ; тут загрузочный сектор

; конец загрузочного сектора TIMES 510-($-$$) DB О DW 0хАА55

9.6. Формат выходного файла Netwide Assembler (NASM) легко портируется на различные операционные системы в пределах х86-совместимых процессоров. Поэтому для NASM не составит труда откомпилировать программу для указанной операционной системы, даже если вы сейчас работаете под управлением другой ОС. Формат выходного файла определяется с помощью ключа командной строки ~f. Некоторые форматы расширяют синтаксис определенных инструкций, а также добавляют свои инструкции.

Создание выходного файла: компиляция и компоновка

Создание исполняемого файла состоит из двух этапов. Первый — это ком­пиляция (трансляция) исходного кода программы в некоторый объектный формат. Объектный формат содержит машинный код программы, но символы (пере­менные и другие идентификаторы) в объектном файле пока не привязаны к адресам памяти.

135

Page 137: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

На втором этапе, который называется компоновкой или линковкой (linking), из одного или нескольких объектных файлов создается исполняемый файл. Процедура компоновки состоит в том, что компоновщик связывает символы, определенные в основной программе, с символами, которые определены в ее модулях (учитываются директивы EXTERN и GLOBAL), после чего каждому символу назначается окончательный адрес памяти или обеспечивается его динамическое вычисление.

Формат bin — готовый исполняемый файл

Формат bin не является объектным форматом. Он содержит результат пере­вода команд ассемблера в машинные коды. Этот формат можно даже использовать для создания текстовых файлов. На­пример, у нас есть файл he 11 о. asm:

; если не указано иначе, адресация двоичного файла /начинается с 0x0. ;при помощи директивы DB определим строку "He l lo w o r l d ! " , ;завершенную символом конца строки s t r i n g : db "He l lo wor ld ! " ,Oxd ,Оха

Скомпилируем этот файл, вызвав компилятор с ключом -f: nasm -f b i n h e l l o . a s m

В результате получим файл he l lo , содержащий текст «Hello world». Если бы в исходном файле были еще какие-то команды, то в выходной файл hello попали бы их машинные коды, при выводе на экран они были бы интерпретированы как символы из ASCII-таблицы, и мы увидели бы на экране всякий мусор. Формат bin позволяет объявлять секции (кода, данных, неинициализированных данных). После имени секции может стоять директива ALIGN, требующая рас­полагать эту секцию по адресам, кратным указанному числу. Например, следу­ющая директива задает выравнивание секции кода по адресам, кратным 16: section .text align-16

Формат bin также можно использовать для создания файлов, исполняемых под DOS (.СОМ и .SYS) или для загрузчиков. Значение по умолчанию ди­рективы BITS равно 16. В коде, предназначенном для вывода в формате bin, может присутствовать директива ORG.

Формат OMF — объектный файл для 16-битного режима

OMF (Object Module Format) — это старый формат, разработанный корпо­рацией Intel, в котором до сих пор создает объектный код Turbo Assembler. Компиляторы MASM и NASM тоже поддерживают этот формат.

136

Page 138: Ассемблер на примерах. Базовый курс

Глава 9. Компилятор NASM

Файл в формате OMF имеет расширение .obj, и сам формат часто называется также OBJ. Файлы с расширением .obj могут быть скомпонованы в исполня­емый файл. Несмотря на то, что формат obj был изначально разработан для 16-битного режима, компилятор NASM также поддерживает его 32-битное расширение. Поэтому NASM можно использовать вместе с 32-битными компиляторами от Borland, которые тоже поддерживают этот расширенный 32-битный формат, а не другой объектный формат, поддерживаемый Microsoft. Формат OBJ расширяет синтаксис некоторых директив, в частности, SEGMENT (SECTION). Подробное описание этого формата выходит за рамки данной книги, но вы можете прочитать о нем в руководстве по компилятору NASM. Кроме того, формат OBJ добавляет новую директиву IMPORT, которую мы опишем в главе 11, посвященной программированию в Windows. Директива IMPORT позволяет импортировать указанный файл из DLL, ей нужно пере­дать два параметра — имя идентификатора и DLL. У каждого OBJ-файла должна быть своя точка входа (как минимум одна). Точка входа определяет позицию, на которую будет передано управление после загрузки программы в память. Точка входа (entry point) обозначена специальным идентификатором (или меткой) ..start:. Чтобы получить файл в формате OBJ, нужно вызвать компилятор с ключом командной строки -f obj.

Формат Win32 — объектный файл для 32-битного режима

Для компоновки программы с помощью Microsoft Visual Сн-+ используется 32-битный формат Win32. Этот формат должен быть совместим с форматом COFF (Common Object File Format), но на самом деле такой совместимости нет. Если вы будете компоновать программу компоновщиком, основанным на формате COFF, компилируйте ассемблерный код в объектный файл формата coff, описанного в следующем пункте. Чтобы получить файл в формате Win32, нужно вызвать компилятор с ключом командной строки -f Win32.

Форматы aout и aoutb — старейший формат для UNIX

Формат исполняемого файла a.out (Assembler and link editor OUTput files) преимущественно используется в Linux. Расширенная версия этого формата a.outb используется группой BSD-совместимых операционных систем (NetB-SD, FreeBSD и OpenBSD). NASM может генерировать файлы обоих форматов, если указать ключ -f aout для Linux или -f aoutb для BSD.

137

Page 139: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Формат coff — наследник а.out

Формат COFF (Common Object File Format) представляет собой существенно усовершенствованную и доработанную версию формата a.out. Он распростра­нен как в мире UNIX, так и в Windows NT, и поддерживается некоторыми свободными (не коммерческими) средами разработки, например, DJGPP. Используется для разработки приложений на С и С-Н + . Если компилятор вы­звать с параметром -f coff, мы получим на выходе объектный файл в формате COFF. Расширение файла по умолчанию для этого формата — .о

Формат elf -- основной формат в мире UNIX

Формат ELF (Executable and Linkable Format) — это формат как объектно­го, так и исполняемого файла. Он применяется во многих UNIX-подобных системах. Мы можем использовать его, чтобы откомпилировать программу для операционных систем Linux, Solaris х86, UnixWare, SCO-UNIX и UNIX System V, причем не только с языка ассемблера. Расширение объектного файла по умолчанию .о. Для получения файла в этом формате компилятор нужно запускать с ключом -f elf.

Символическая информация

Некоторые выходные форматы позволяют хранить вместе с машинными кода­ми символы исходного текста программы. Такая возможность очень полезна при отладке, она позволяет отладчику отображать адреса памяти в виде имен переменных, меток и т.п. Символьная информация значительно увеличивает размер выходного файла, поэтому после отладки программу заново компи­лируют уже без нее. Для добавления символической информации нужно запустить NASM с ключом -g. Этот ключ допустим только для форматов OBJ и ELF.

138

Page 140: Ассемблер на примерах. Базовый курс

Глава 1 Программирование в DOS

Адресация памяти в реальном режиме

Организация памяти в DOS

Типы исполняемых файлов в DOS

Основные системные вызовы

Файловые операции ввода-вывода

Управление памятью

Коды ошибок. Отладка

Резидентные программы

Page 141: Ассемблер на примерах. Базовый курс

«640 KB должно быть достаточно для любого» (Билл Гейтс 1981)

Операционная система DOS (или MS-DOS, Microsoft Disk Operating System) была первой операционной системой для компьютеров IBM PC. Ее архитек­тура и возможности были основаны на операционной системе СР/М, которая в то время была достаточно популярна среди 8- и 16-битных компьютеров. Интерфейсом DOS являлась командная строка. Что же касается многоза­дачности, то она была однозадачной операционной системой. Файловая система не предусматривала прав доступа к файлам, которые использовались в операционной системе UNIX с самого момента ее создания. Изначально DOS располагала всего 640 Кб памяти, правда, позже появились различные способы ее расширения. Система никогда не запускалась в защищенном режиме (поскольку была раз­работана для 8086), и программы могли не только управлять аппаратными средствами компьютера напрямую, но и записывать данные в любую область памяти, даже не принадлежавшую им. Процессоры 80286 и 80386 поддержи­вали защищенный режим. Приложения, работающие в защищенном режиме, должны были переключать процессор самостоятельно. Ясно, что нельзя было запустить несколько приложений в защищенном режиме одновременно.

10.1. Адресация памяти в реальном режиме Процессор 8086 имел 20-битную адресную шину и поэтому способен был адресовать только 2 ^ байтов (1 Мб) оперативной памяти. Но мы знаем, что его регистры были 16-битными: как же он мог тогда адресовать 20 битов адресной шины? Ответ находится в сегментных регистрах. Окончательный адрес, «сброшен­ный» на адресную шину, получался как сумма значений соответствующего 16-битного регистра и сегментного регистра, умноженного на 16.

140

Page 142: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

SI АН , AL 15 8 7 О

DI + + + + + + + + + + ++\

15 вЬ О

20-битный адрес Память

Рис. 10.1. Расчет физического адреса

Следующий пример поможет понять, как рассчитывается физический адрес:

mov a l , [ d s : s i ] ; з а г р у з и т ь в AL содержание памяти ;по адресу d s : s i

Предположим, что DS = 0х559Е, а SI имеет значение 0x100. Вычислить окончательный 20-битный адрес можно следуюш,им образом:

0х559Е * 0x10 (0x10 равно 16) + 0x100

Желаемое значение будет загружено с адреса памяти: 0х559Е0 4- 0x100 = 0х55АЕ0. Значение сегментного регистра называется сегментом, а другое значение (то, которое содержится в регистре общего назначения или указано как непо­средственное значение) называется смещением (offset). Заметим, что адрес 0х55АЕ0 также может определяться другими парами сег­ментов и смещений, например, сегментом 0х558Е и смещением 0x200. Адресация, использующая сегменты и смещения, имеет массу недостатков. Смещение может быть только 16-битным, и поэтому полный размер одного сегмента в памяти ограничен объемом 64 Кб. Программа, код или данные которой превышают 64 Кб, должна быть разделена на несколько сегментов. При необходимости в сегментные регистры должны быть загружены другие адреса. Решение проблемы, которая возникает при делении программы на несколько сегментов, будет рассмотрено в параграфе 10.4. Для сохранения совместимости даже новейшие процессоры поддерживают 16-битную адресацию в реальном режиме. Поэтому можно запустить самую древнюю DOS-программу, в том числе и саму DOS, на новейших х86-совме-стимых процессорах.

141

Page 143: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

10.2. Организация памяти в DOS Как мы только что объяснили, из-за реального режима процессора, опера­ционная система DOS могла адресовать только 1 Мб оперативной памяти. Таблица 10.1. — это свод отдельных адресов и блоков памяти, выделенных для операционной системы, прикладных программ и аппаратных средств.

Таблица 10.1

1 Адрес (сегмент:смещение)

1 0x0000:0x0000

1 0x0040:0x0000

1 ОхО????:ОхОООО

ОхО????:ОхОООО

ОхО????:ОхОООО

1 ОхО????:ОхОООО

ОхО????:ОхОООО

1 ОхО????:ОхОООО

1 ОхОАООО:ОхОООО

0x06000:0x0000

1 0x06800:0x0000

0x00800 :0x0000 - ОхОЕООО:ОхОООО

OxFOOO:OxOOOO

Что находится по адресу

Таблица векторов прерываний

Таблица переменных BIOS

DOS kernel (ядро системы)

Буферы, обработчики прерываний, драйвера устройств

Резидентная часть командного интерпретатора (оболочки) C0MMAND.COM

Резидентные программы (TSR, Terminate and Stay Resident)

Выполняющаяся в данный момент программа (Нортон, текстовый редактор и тп.)

Свободная память

Начало видеопамяти (в графическом режиме)

Видеопамять монохромного графического адаптера (в текстовом режиме)

Видеопамять графического адаптера в текстовых режимах

Свободные адреса для дополнительной памяти ROM (Read Only Memory)

ROM BIOS 1

В самом начале адресного пространства зарезервировано место для таблицы прерываний. Ее размер вычислить легко. Процессор поддерживает 256 разных векторов прерывания, а каждый вектор представляет собой пару сегмент-смещение, то есть занимает 4 байта. В итоге получается 1 Кб. Сразу же за таблицей прерываний, по адресу 0x400 (т.е. 0x0040:0x0000), следует таблица переменных BIOS. Она содержит список устройств, подключенных к компьютеру, и их конфигурацию. Описание отдельных устройств мы рассматри­вать в этой книге не будем, но если кого-то заинтересует этот вопрос, то всегда можно найти электронный справочник Ralf Brown Interrupt List в Интернете. В следующем блоке памяти загружена операционная система вместе с разными буферами и обработчиками прерываний. За операционной системой следует командный интерпретатор COMMAND.COM, который позволяет пользова­телю общаться с компьютером через интерфейс командной строки. Далее следуют резидентные или TSR программы. Программы такого типа запускаются, как и обычные программы, но по завершении они остаются

142

Page 144: Ассемблер на примерах. Базовый курс

Глава 10, Программирование в DOS

загруженными в память. Типичная резидентная программа — это драйвер мыши, который должен оставаться в памяти, чтобы приложения могли ис­пользовать мышь. Программный интерфейс, который обеспечивает взаимосвязь других прило­жений под управлением операционной системы DOS (между приложениями или между приложением и ОС), реализован через векторы прерываний. Мы рассмотрим этот вопрос более детально в параграфе «Основные системные вызовы». Следующий блок содержит программу, работающую в данный момент. Адрес­ное пространство этой программы простирается до предела в 640 Кб, т.е. вплоть до адреса ОхОАООО:ОхОООО. Адреса от 640 Кб до 1 Мб зарезервированы для аппаратных средств компьютера. Первые 64 Кб этой области используются видеоадаптером. Запись в эти адреса позволяет рисовать отдельные точки на экране. Адреса начиная с ОхОВ800:ОхОООО используются видеоадаптером для хране­ния отдельных символов, отображаемых на экране. Адрес 0х0В800:0х0000 представляет левый верхний угол первой отображаемой строки в текстовом режиме. Данные организованы в пары байтов. Первый байт содержит ASCII-код отображаемого символа, а второй байт обозначает цвет. Оставшееся пространство памяти зарезервировано для программ, хранящихся в памяти ROM (Read Only Memory): BIOS видеоадаптера и BIOS самого компьютера.

10.3. Расширение памяти свыше 1 MB Через несколько лет 640 Кб перестало хватать, и поэтому были разработаны способы расширения памяти, сохраняюш[ие при этом реальный режим. Про­цессор 80286 имел 24-битную адресную шину, однако в реальном режиме он все еще мог адресовать только 20 битов, т.е. 1 Мб. Последним пригодным для использования сегментом был OxOFOOO, в котором хранится BIOS. Чтобы адресовать следующий за BIOS сегмент, нужно было бы загрузить в сегментный регистр значение OxFFFF. Но после добавления к этому значению смещения адрес вышел бы за пределы 20 битов, то есть превысил бы ограничение в 1 Мб. Разработчики решили оборудовать компьютер специальным устройством для контроля над двадцать первым битом адресной шины, которое позволяло бы использовать сегмент OxOFFFF. Процессор получит доступ к памяти выше 1 Мб, если «что-то извне» установит 21-й бит адресной шины в значение 1. Этим «чем-то» стало устройство, названное А20 (21-й бит имеет номер 20, потому что нумерация начинается с нуля). А20 управляет вышеупомянутым 21-м би­том и позволяет использовать дополнительные 64 Кб оперативной памяти.

143

Page 145: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Однако таким способом можно добавить только 64 Кб памяти, что не так уж много. В диапазоне от 640 Кб до 1 Мб имеется масса неиспользованной памяти, зарезервированной для ROM-системы. Нельзя ли как-нибудь ис­пользовать эти адреса? Конечно же, можно. Консорциум из трех компаний — Lotus, Intel и Microsoft — сформулировал спецификацию памяти, которую назвали LIM EMS (Expanded Memory Specification). В области между 640 Кб и 1 Мб было создано окно памяти с размером 64 KB, которое состояло из четырех страниц, по 16 Кб каждая. Это окно называется кадром страницы (page frame), потому что EMS позволяет отображать в это окно память за пределами 1 Мб. У компьютеров типа 80286 EMS-память реализована с помощью аппаратных средств, но позже благодаря процессору 80386 и его виртуальному реальному режиму она могла реализовываться программно. Последним способом получения доступа к памяти за пределами 1 Мб в реаль­ном режиме стала спецификация XMS (Extended Memory Specification). Этот интерфейс был реализован при помощи драйвера HIMEM.SYS, который при необходимости перемещает блок памяти выше 1 Мб в память ниже 1 Мб и наоборот. При обработке запроса он переключает процессор в защищенный режим, где для адресации доступна вся память. После перемещения блока, процессор переключается обратно в реальный режим.

10.4. Типы исполняемых файлов в DOS Операционная система DOS поддерживает три типа бинарных исполняемых файлов. Первый тип — это системный драйвер устройства, который обычно имеет расширение .SYS. Файл такого типа загружается при запуске системы. Спи­сок всех загружаемых драйверов устройств содержится в конфигурационном файле CONFIG.SYS. Описание этого файла и его функций выходит за пределы этой книги. Следующие два типа исполняемых файлов — СОМ и ЕХЕ (расширения .СОМ и .ЕХЕ соответственно) — предназначены для прикладных программ. Исполняемый файл типа СОМ в отличие от других запускаемых файлов не имеет заголовка. Его максимальный размер — 64 Кб, из которых 256 байтов занимает префикс программного сегмента. Во время загрузки программы операционная система выделяет ей новый сегмент памяти (т.е. 64 KB). Про­грамма загружается с диска по адресу 0x100. В ходе запуска программы операционная система загружает адрес выделен­ного ей сегмента в сегментные регистры (CS, ES, DS и SS), устанавливает указатель вершины стека SP почти в конец сегмента — на адрес OxOFFFE, а затем передает управление на смещение 0x100, где находится первая команда исполняемого файла.

144

Page 146: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Диапазон адресов от О до 0x100 зарезервирован для операционной системы. Он называется PSP (Program Segment Prefix). Здесь операционная система сохраняет среди прочего информацию об открытых файлах и о PSP-адресе родительского процесса. Начиная со смещения 0x80 хранятся аргументы, с которыми запущена программа. Рассмотрим карту памяти СОМ-программы, загруженной в сегмент 0x1234 (см. табл.10.2): Карта памяти СОМ-программы Таблица 10.2

Адрес (сегмент:смещение)

0x1234:0x0000

1 0x1234:0x0080 0x1234:0x0100 0x1234:0x7??? 0х1234:0х???? 0х1234:0х????

0x1234:0xFFFE

Что находится по адресу

Начало области, зарезервированной для ОС. Здесь хранятся некоторые внутренние структуры | С этого адреса начинаются переданные параметры | С 0x100 операционная система загружает СОМ-файл с диска | Окончание сегмента (секции) кода | Окончание сегмента (секции) данных | Начало секции неинициализированных данных (.bss) | Память распределена до конца сегмента, а стек растет к началу сегмента | Вершина стека |

Тип исполняемого файла ЕХЕ существенно улучшен по сравнению с типом СОМ. При этом главное улучшение заключается в том, что тип ЕХЕ пригоден для размещения программ, размер которых больше 64 Кб. Файл типа ЕХЕ может содержать несколько сегментов кода и несколько сегментов данных. У стека также есть зарезервированные сегменты. Чтобы обратиться к другому сегменту в реальном режиме, нужно переключить зна­чения сегментных регистров. Так, при вызове подпрограммы, сохраненной в другом сегменте, мы должны использовать инструкцию дальнего вызова (call far), которая правильно изменяет значения обоих регистров (CS и IP). Точно так же должен быть выполнен возврат (retf). Если переменная сохранена в другом сегменте данных, мы получаем адрес ее сегмента при помощи псевдокоманды SEG и загружаем ее в сег­ментный регистр. Правильный адрес получается в результате сложения смещения, которым заведует идентификатор, с новым значением сегмента. Разработанная схема действительно хороша, если бы не одно «но». Как по­лучить адрес сегмента, если мы не знаем стартовые адреса сегментов (т.е. программы)? Скомпилированная программа сохраняется на диск так, как если бы ей пред­стояло быть загруженной с адреса 0x0000:0x0000, то есть первый сегмент на­чинается с 0x0000:0x0000 и заканчивается в OxOOOO:OxFFFF, второй сегмент начинается с 0x1000:0x0000 и так далее. Каждое место программы, в котором используются значения сегментов (т.е. 0x0000, 0x1000 и т.д.), записывается в так называемую таблицу перемещений (relocation table).

145

Page 147: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Программа загружается в память с некоторого базового адреса. По таблице перемещений определяются те места программы, содержимое которых долж­но быть «подправлено», и к хранящимся там адресам добавляется значение базового адреса» Таким образом программа целиком перемещается к базово­му адресу загрузки. Теперь все сегментные регистры содержат правильные значения, и программа может быть запущена.

10.5. Основные системные вызовы Операционная система предлагает запущенной программе множество сер­висов. Используя операционную систему, наша программа может выводить сообщения на экран, считывать ввод с клавиатуры, работать с файлами или требовать выделения дополнительной памяти. Сервисы операционной системы доступны через прерывание 0x21. Параметры системных вызовов указываются в определенных регистрах. Под DOS номер необходимой функции указывается в регистре АН. Если системный вызов завершился ошибочно, то устанавли­вается флаг переноса, а в регистр АХ помещается код ошибки.

Немедленное завершение программы

Простейшая программа — это та, которая завершается немедленно после за­пуска. Запрос на завершение программы обеспечивается системным вызовом DOS с номером 0х4С, прерывающим программу и передающим управление родительской программе.

Ввод:

Завершение программы: АН = 0х4с AL = код возврата

Вывод:

Ничего

Номер вызываемой функции передается через регистр АН, а в AL указывается так называемый код возврата, то есть значение, которое должна анализиро­вать родительская программа. Дочерняя программа может либо сообщить родительской, что завершилась нормально, либо вернуть код ошибки. Эта функция такого же назначения, как exit в языке С. Наша первая программа будет включать в себя только раздел кода (листинг 10.1).

Ли1|||||1|Я||||Простеинд^^ под D Q | | >

SECTION .text ;Наша первая программа для операционной системы DOS. ;Она ничего не делает, только завершается сразу после ;запуска org 0x100 ;генерируем СОМ файл, загружаемый с 0x10 0

146

Page 148: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

mov ah,0х4С mov al,О int 0x21 ;конец программы

;эта функция завершает программу ;код возврата О ;вызываем ядро операционной системы

Теперь сохраните программу в файл f in ish .asm. В этом и следующих при­мерах мы будем компилировать выходные файлы типа СОМ, чтобы избежать запутанной процедуры настройки сегментных регистров и сконцентрироваться на решении других проблем. Следующим шагом будет компиляция f i n i s h . asm компилятором nasm: nasm -f bin -о finish.com finish.asm

Параметр -f указывает тип выходного формата, а параметр -о указывает на создание файла с заданным именем f i n i s h . com. После компиляции мы можем запустить программу f in ish .com, написав «finish» в командной строке и нажав «Enter». Программа сразу же вернет управление командному интерпретатору.

Вывод строки. Пишем программу «Hello, World!» Другая очень полезная функция — это вывод текстовой строки на экран. Сей­час мы не будем рассматривать запись в файл, а рассмотрим разные функции вывода строки на экран. По историческим причинам выводимая строка оканчивается не нулевым байтом, а знаком доллара «$» (0x24).

Ввод:

Печать строки: АН = 0x09

1 DS:DX = указатель на строку, завершенную «$»

Вывод:

Ничего

Печать одного символа на стандартный вывод выполняет функция DOS с номером 0x02, которая считывает код ASCII необходимого символа из DL.

Ввод:

Печать символа: АН = 0x02 DL = ASCIi-код печатаемого символа

Вывод:

Ничего

Теперь у нас достаточно знаний, чтобы написать программу, которая выведет на экран всем известный текст «Hello, World!» и завершит свою работу. Сейчас мы с вами отредактируем нашу программу f i n i s h , asm. Будет до­бавлена новая секция .da ta с описанием переменной «hello», содержащая строку, состоящую из текста «Hello, World!», служебных символов CR (OxD) и LF (ОхА), которые во время вывода на экран выполняют перевод строки, и завершающего символа «$» (листинг 10.2).

147

Page 149: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Листинг 10.2. Программа Иеиб^ШбНФ под DOS

SECTION .text ;Эта программа выводит на экран текст "Hello, World!' ;конец строки {EOL - End Of Line) и завершает работу org 0x100 mov ah,0x9 mov dx,hello int 0x21 mov ah,0x4C mov al,0 int 0x21 SECTION .data hello DB "Hello, ;конец программы

;создаем файл типа СОМ /функция DOS для печати строки ;загружаем в DX указатель на строку ;вызываем DOS /функция DOS для завершения программы ;код возврата О ;вызов ядра операционной системы

World!",0xd,0xa,'$'

Нам не нужно устанавливать сегментный регистр DS: операционная система установит его сама. Эту программу назовем h e l l o . asm и скомпилируем командой

nasm -f bin -о hello.com hel lo .asm. Запустим программу с помощью команды h e l l o .

Ввод с клавиатуры

Для последовательного считывания символов используется функция DOS с номером 0x01, которая похожа на getchar в языке С и на readkey в языке Pascal.

Ввод:

Чтение символа:

1 АН = 0x01

Вывод:

AL содержит символ, считанный со стандартного ввода i (клавиатуры)

Функция 0x01 ждет нажатия клавиши. После получения символа она отражает символ на стандартном выводе (функция 0x08 только считывает символ, но не отражает его). В регистре AL окажется значение О, если была нажата клавиша с так назы­ваемым расширенным (extended) ASCII-кодом, то есть одна из клавиш Page Up, Page Down, Ноше, End, функциональные клавиши F1..F12 и т.п. Следу­ющий вызов функции 0x01 поместит в AL расширенный ASCII-код нажатой клавиши. Часто нам нужно прочитать целую строку определенной длины, завершенную нажатием клавиши «Enter». Для этого в DOS имеется функция, накапливаю-ш,ая прочитанные с клавиатуры символы в буфере.

148

Page 150: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Ввод:

Считывание строки: АН = ОхОА

1 DS.DX = указатель на буфер в специальном формате

Вывод:

В буфере находится прочитанная строка

Перед вызовом функции ОхОА указатель должен содержать адрес специаль­ной структуры, которая в результате работы функции будет заполнена про­читанной строкой. Первый байт буфера содержит его максимальный размер, то есть количество символов (1-254), по достижении которого выдается звуковой сигнал (ASCII-код 7) и требуется нажать «Enter». Следующий байт содержит фактическое количество введенных символов, не считая символа OxOD (кода клавиши «Enter»). mov ah, ОхОА mov dx,string int 0x21

/загрузить в АН ОхОА, функция чтения строки ;загрузить в DX адрес (смещение) буфера ;вызов DOS

s t r i n g db 5 , 0 , 0 , 0 , 0 , 0 , 0 mov ah , ОхОА ; з а г р у з и т ь в АН номер функции чтения строки mov d x , s t r i n g ; загрузить в DX адрес (смещение) буфера s t r i n g i n t 0x21 ;вызов DOS

s t r i n g db 5 , 0 , 0 , 0 , 0 , 0 , 0

После выполнения команды int 0x21 операционная система начнет чтение символов и будет продолжать чтение до нажатия клавиши «Enter». Под буфер отведено 7 байтов, то есть в нем можно разместить не более 4 символов плюс символ завершения — код клавиши «Enter». Если мы наберем А,В?С, а затем нажмем «Enter», то структура «string» будет выглядеть следуюш^им образом:

s t r i n g db 5,3,0x65,Охбб,0x67,OxOD,О

Первый байт остается неизменным, второй байт содержит длину считанной строки без символа окончания OxD, а с третьего байта начинается сама строка (ABC), завершающаяся символом окончания OxD. Рассмотрим несколько примеров. Пример 1: напишем программу, которая будет отображать каждую нажатую клавишу на экране дважды. Программа завершается по нажатии клавиши «Enter» (листинг 10.3). Для чтения символов мы будем использовать DOS-функцию 0x01, отобража­ющую нажатую клавишу на экране. Следующий, идентичный предыдущему, символ будем выводить DOS-функцией 0x02.

149

Page 151: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

I^MiliiHliMi№^^^^'^'^ 1|||||1Ш|111111РШЯ^ ; Двойное отображение каждой нажатой клавиши на экране SECTION .text again: mov ah,0x01 int 0x21 mov dl,al cmp dl , OxD jz endprog mov ah, 0x02 int 0x21 jmp again endprog: mov ah,0x4C int 0x21

;DOS-функция чтения символа ;вызов DOS ;копирование считанного символа в DL ;нажат "Enter"? ; если да, то переход к концу программы ;DOS-функция вывода символа ;вызов DOS ;вернуться к чтению следующего символа

;DOS-функция завершения программы ; вызов DOS ;конец программы

Эту программу назовем echo . asm. Исполняемый файл echo . com будет соз­дан командой

nasm -f bin -о echo.com echo.asm

Пример 2: напишем программу, считывающую строку символов до нажатия клавиши «Enter» и выводящую эту строку в обратном порядке (листинг 10.4).

Используя DOS-функции ОхОА, мы считываем строку и сохраняем ее во временной переменной, которая после будет выведена в обратном порядке с помощью DOS-функции 0x02.

Ш ш и и М до иажшг|||

SECTION .text ;Эта программа выводит прочитанную строку ;на экран в обратном порядке. org 0x100 mov ah, ОхОА mov dx,string int 0x21 xor ax,ax mov al, inc dx add dx,

[string+1]

mov s i,dx

создаем файл типа COM DOS-функция чтения строки загружаем в DX адрес буфера строки вызов DOS обнуляем АХ читаем количество введенных символов пропускаем первый байт буфера прибавляем длину строки к адресу начала, получаем адрес последнего символа копируем DX в SI

150

Page 152: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

s t d /устанавливаем флаг направления для /прохода обратно

p r i n t _ n e x t _ c h a r : l o d s b /читаем символ из DS:SI и уменьшаем SI на 1 сгпр s i , s t r i n g + l /конец строки? (то есть начало) j b endprog /если да , то переход к концу программы mov d l , a l /загружаем прочитанный из строки символ в DL mov ah ,0x02 /DOS-функция вывода символа i n t 0x21 /вызов DOS jmp p r i n t _ n e x t _ c h a r /возвращаемся к следующему символу endprog : mov ah,0x4C /DOS-функция завершения программы mov al,0 /КОД возврата О int 0x21 /ВЫЗОВ DOS SECTION .data string db 254,0 t i m e s 253 db ' ' /заполняем буфер строки пробелами /конец программы

Назовем эту программу r e v e r s e , asm и скомпилируем ее так же, как и предыдущую:

nasm - f b in -о r e v e r s e . c o m r e v e r s e . a s m

Эта программа немного сложнее, поэтому объясним ее функционирование шаг за шагом.

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

mov ah , ОхОА /DOS-функция чтения строки mov d x , s t r i n g /загружаем в DX адрес буфера строки i n t 0x21 /вызов DOS

После того, как пользователь нажмет «Enter», наша программа продолжит выполнение. Поскольку строка должна быть выведена в обратном порядке, то прежде всего мы должны вычислить адрес последнего символа.

хог а х , а х /обнуляем АХ mov a l , [ s t r i n g + 1 ] /читаем количество введенных символов

После возврата из системного вызова регистр DX все еще содержит адрес бу­фера «string». Теперь нам необходимо узнать адрес (смещение) конца строки, который равен DX + 2 + длина_строки — 1 (в программах типа СОМ нам не нужно рассматривать весь адрес — сегментная часть одинакова для всех секций программы, и поэтому достаточно изменения смещения).

Начало строки находится по адресу DX+2 и, добавив длину строки, мы по­лучим символ OxOD (клавиша «Enter»). Поскольку он нам не нужен, мы от­нимаем единицу.

151

Page 153: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Конечно же, вместо прибавления 2 и вычитания 1 можно просто прибавить 1, сократив программу на одну команду. Заметьте, что вначале нужно сбросить АХ и что длина строки — число восьмибитовое.

i n c dx ;пропускаем первый байт буфера add d x , a x /прибавляем длину строки к адресу начала ,

/получаем адрес последнего символа mov s i , d x /копируем DX в SI

Несмотря на то, что длина строки — число восьмибитовое, мы прибавляем к адресу начала строки не AL, а АХ, потому что если один операнд представляет собой адрес или смещение, то второй должен быть такого же размера. В результате получается адрес последнего символа в строке. Команда LODSB выводит символ, адрес которого хранится в регистре SI, поэтому скопируем вычисленный адрес в этот регистр. Для получения следующего в обратном порядке символа нужно уменьшить этот адрес на 1: значит, нужно установить флаг направления.

s t d /устанавливаем флаг направления для /прохода обратно

p r i n t _ n e x t _ c h a r : l o d s b /читаем символ из DS:SI и уменьшаем SI на 1

LODSB загружает в AL символ, считанный из [DS:SI] , и уменьшает SI на 1 после считывания. Сразу же после LODSB следует сравнение с адресом s t r i n g + 1 , который содержался бы в SI после чтения символа на последнем шаге (то есть первого символа строки). Если бы мы использовали условный переход по равенству адресов (JZ), то первый символ строки не выводился бы, поэтому мы написали команду «перейти, если меньше». Это условие станет истинным не до, а после вывода первого символа.

стр s i , s t r i n g + 1 /конец строки? j b endprog /если да , то переход к концу программы

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

mov d l , a l /загружаем прочитанный из строки символ /В DL

mov ah ,0x02 /DOS-фуккция вывода символа i n t 0x21 /вызов DOS jmp p r i n t _ n e x t _ c h a r /возвращаемся к следующему символу

Выполнение программы завершается системным вызовом 0х4С, endprog : mov ah,0x4C /DOS-функция завершения программы mov al,0 /код возврата О int 0x21 /вызов DOS

152

Page 154: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Секция данных описывает структуру буфера, SECTION . d a t a s t r i n g db 254 ,0 t i m e s 2 53 db ' ' /заполняем буфер строки пробелами

Первый байт определяет максимальную длину строки, не считая завершаю­щего символа. Второй байт содержит фактическую длину прочитанной строки (ведь строка может быть и короче, чем 254 символа), и на данный момент она составляет О байтов. Используя директиву TIMES, оставшуюся часть структуры заполняем 253 пробелами.

10.6. Файловые операции ввода-вывода в главе 8, посвященной операционным системам, мы говорили о технике ра­боты с файловой системой. Операционная система DOS работает по такому же принципу. Перед какой-либо операцией ввода-вывода файл должен быть открыт. От операционной системы необходимо получить идентификатор (дескриптор) файла, который нужно указывать при вызове других функций для работы с файлами. По окончании процесса обработки файл должен быть закрыт. Оригинальная система DOS до слияния с ОС Windows позволяла использовать только короткие имена файлов: 8 символов в имени и 3 символа в расширении файла. В Windows появились так называемые длинные имена файлов, состо­ящие из 256-и символов в имени, включая несколько расширений. Давайте начнем с системных вызовов, использующих короткие имена файлов 8-1-3.

Открытие файла

Файл открывает функция 0x3D. Указатель на имя файла передается через пару регистров DS:DX (DS — сегмент, а DX — смещение). В регистр AL нужно записать требуемый режим доступа. В большинстве случаев файл открывается для чтения и записи (AL=0). Функция возвращает в АХ дескриптор открытого файла или код ошибки. При ошибочном завершении функция устанавливает флаг переноса CF=1, при корректном завершении CF=0.

Ввод:

Открытие файла АН = 0x30 DS:DX = указатель на имя файла, завершенное нулевым байтом 0x0 AL = режим доступа: AL = 0 чтение и запись AL = 1 только запись AL = 2 только чтение

Вывод:

CF = 0 если успешно, тогда АХ = дескриптор, назначенный файлу

Или CF = 1 ошибка, тогда АХ = код ошибки: АХ - 0x0002 — файл не найден АХ = 0x0003 — путь не найден

153

Page 155: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Закрытие файла

Прежде чем переходить к другим функциям, познакомимся с функцией за­крытия файла ОхЗЕ:

Ввод:

Закрытие файла АН = ОхЗЕ ВХ = дескриптор файла

Вывод:

CF = 0 если успешно

Или CF = 1 ошибка, тогда АХ = код ошибки

Чтобы закрыть файл, поместите его дескриптор, присвоенный файлу при открытии, в регистр ВХ. Напишем простую программку для открытия и за­крытия файлов (листинг 10.5).

illli^HiiiiiiHRW Ф ^ SECTION-.text org 0x100 mov ax, 0x3D00 mov dx,file_name

int 0x21 jc error mov bx,ax

mov ah, ОхЗЕ int 0x21 mov al,0

endprog: mov ah,4Ch int 0x21 error: mov al,1 jmp short endprog

;функция DOS для открытия файла, режим ;чтение-запись ;передаем указатель на имя файла, DS уже ;установлен ;вызов DOS ;ошибка? если да, перейти к метке error ;АХ содержит дескриптор файла, ;копируем его в ВХ /функция DOS для закрытия файла ;вызов DOS ;устанавливаем код в о з в р а т а : успешное ;завершение

;функция DOS для завершения программы ;вызов DOS

;устанавливаем код в о з в р а т а : ошибка /переходим к метке окончания программы ;endprog

SECTION . d a t a f i l e_name db ' ' t e x t . t x f , 0 ;имя файла t e x t . t x t

Чтение из файла Очевидно, что приведенная в листинге 10.5 программа абсолютно бесполезна, поэтому давайте усовершенствуем ее: мы будем выводить содержимое файла на экран. Чтение данных из файла выполняется DOS-функцией 0x3F. 154

Page 156: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

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

Ввод:

Чтение из файла: АН = OxSF ВХ = дескриптор файла

DS:DX = указатель на буфер для хранения прочитанных данных СХ = размер буфера

Вывод:

в случае успеха: CF = 0 АХ = фактическое количество байтов, считанных из файла (ноль означает конец файла, EOF) В случае ошибки: CF = 1 АХ = код ошибки

Функции нужно передать дескриптор файла в регистре ВХ. Указатель на буфер, в который нужно поместить прочитанный блок данных, передается через пару DS:DX. Через регистр СХ передается максимальное количество байтов, которое должно быть считано за раз: большую часть времени оно равно длине буфера. В регистр АХ функция записывает количество факти­чески прочитанных байтов (ведь если в файле осталось меньше данных, чем мы хотим прочитать, значение в АХ будет меньше размера буфера). В случае ошибки устанавливается флаг переноса CF = 1, а в АХ будет код ошибки. Данные, прочитанные из файла, могут быть выведены на экран с помощью функции DOS 0x09. Но тогда у нас будут проблемы с любым файлом, содер-жаш;им символ $, который обозначает конец строки для функции 0x09. Чтобы обойти эту проблему, мы не будем использовать функцию 0x09, а вместо вы­вода на экран будем записывать прочитанные данные в поток стандартного вывода, а для вывода на экран используем функцию перенаправления вво­да/вывода, предоставленную нам операционной системой. Дескриптор файла стандартного вывода равен 0x0001.

Запись в файл

Для записи в файл используется функция 0x40. Она имеет те же аргументы, что и функция чтения файла:

Ввод:

Запись в файл: АН = 0x40 ВХ = дескриптор файла

DS:DX = указатель на буфер СХ = количество байтов, которые нужно записать

Вывод:

В случае успеха: CF = 0 АХ = фактическое количество байтов, записанных в файл При ошибке: CF=1 АХ = код ошибки

155

Page 157: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

В DS:DX нужно указать адрес буфера, содержащий данные, которые нам нужно записать в файл. А в СХ нужно занести количество байтов, которые нужно записать: обычно это длина буфера. Если вызов прошел успешно, то в АХ окажется количество байтов, записан­ных в файл. Если оно отличается от требуемого, то, возможно, нет свободного места на диске. Теперь у нас достаточно знаний, чтобы вывести текстовый файл t e x t . t x t на экран. Прежде всего, нам необходимо открыть файл t e x t . t x t , используя функцию 0x3D. Потом мы будем читать из него блоки одинакового размера при помо­щи функции ОхЗР, пока количество считанных байтов не станет равно нулю. Прочитанный блок мы будем выводить на стандартный вывод функцией 0x40. Перед завершением программы мы закроем файл.

Листинг 10.6. Программа вывода текстового файла на акраШ %define B_LENGTH 80 %define STDOUT 0x0001 SECTION .text org 0x100 mov bp,STDOUT

mov ax, 0x3D00

mov dx,file_name

int 0x21 jc error mov bx,ax

read_next: mov ah,0x3F mov dx,buffer mov cx,B._LENGTH int 0x21 jc error or ax,ax

jz end_reading mov ex,ax

;определяем константу — размер блока /дескриптор стандартного вывода

пока храним дескриптор файла стандартного вывода в отдельном регистре открываем файл в режиме чтения-записи указатель на имя файла. DS уже установлен вызов DOS ошибка? Если да, переход к метке error копируем дескриптор открытого файла в ВХ

функция DOS для чтения из файла буфер для хранения блока данных размер блока вызов DOS ошибка? Если да, переход к метке error прочитано О байтов? Можно использовать стр ах,О если да, переход к концу чтения на последнем шаге количество прочитанных байтов может быть меньше СХ, а записать нужно столько же байтов, сколько было считано

156

Page 158: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

mov ah,0x40 xchg bp,bx

int 0x21 xchg bp,bx jmp read_next

end_reading: mov ah, ОхЗЕ int 0x21 mov al,0 endprog: mov ah,4Ch

int 0x21

;функция DOS для записи в файл ;временно сохраняем дескриптор ;файла в ВР, а /дескриптор стандартного вывода ;из ВР кладем в ВХ ;вызов DOS ;возвращаем исходные значения ВХ и ВР ; переход к метке read__next, читаем ;следующий блок /функция DOS для закрытия файла ;вызов DOS ;код возврата: успешное завершение

;функция DOS для завершения ;программы ;вызов DOS. Это последняя команда ;программы

error: mov al,1 jmp short endprog SECTION .data file_name db ''text . txt'\ 0 SECTION .bss b u f f e r RESB B„LENGTH

устанавливаем код возврата : ошибка переходим к метке endprog

;определяем имя файла t e x t . t x t

;определяем неинициализированную ;переменную ; b u f f e r размером B_.LENGTH байтов

Назовем наш файл l i s t i t . asm и откомпилируем его с помощью команды nasm l i s t i t . a s m --о l i s t i t . c o m - f b i n

Теперь создадим текстовый файл t e x t . t x t с помощью текстового редактора или прямо из командной строки:

echo I ' l l BE BACK! > t e x t . t x t

После запуска программы текст из файла t e x t . t x t будет выведен на экран.

Мы можем модифицировать нашу программу так, чтобы вместо записи на стандартный вывод она записывала прочитанные данные в другой файл. Тогда мы получим простейшую программу копирования файла. Но мы до сих пор не знаем, как создать файл!

157

Page 159: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Открытие/создание файла

В отличие от функции 0x3D функция ОхбС не только открывает файл, но и создает новый файл, если при открытии файла обнаружится, что файла с таким именем не существует.

Ввод:

Открытие/создание файла АН = 0x60 AL = 0x00 (всегда должен быть 0) ВХ = массив битов для расширенного режима СХ = атрибуты для нового файла DX = массив битов для требуемой операции DS:DX = указатель на имя файла

Вывод:

Успех: CF = 0 АХ = идентификатор открытого файла СХ = отчет об операции (см. ниже) Ошибка: CF=1 АХ = код ошибки

Данная функция позволяет использовать несколько режимов открытия фай­лов: открытие существующего файла, создание нового файла с его последую­щим открытием и открытие файла с усечением его длины до нуля (открытие для перезаписи). Требуемый режим мы можем указать в регистре DX. Можно использовать следующие комбинации битов при системном вызове. 4 младших бита (в регистре DL):

• DL = ООООЬ — выйти с ошибкой, если файл существует (можно исполь­зовать для проверки существования файла).

• DL = 0001b — открыть файл, если он существует. • DL = 0010b — открыть файл для перезаписи (удалить все содержимое),

если он существует. 4 старших бита (в регистре DH):

• DH = ООООЬ — выйти с ошибкой, если файл не существует. • DH = 0001b — создать файл, если он не существует.

Когда мы будем вызывать функцию ОхбС в программе копирования файла, нам нужно записать в DX значение 0x0012. В этом случае файл назначения будет создан и открыт при условии, что он не существует или усечен и открыт, если файл существует. В регистр СХ (это тоже массив битов) нужно занести атрибуты вновь созда­ваемого файла, DOS — не UNIX, она не поддерживает ни имени владельца, ни прав доступа, поэтому набор атрибутов в DOS крайне ограничен:

• Бит 0: если 1, то будет установлен атрибут «только чтение».' • Бит 1: если 1, то будет установлен атрибут «скрытый». • Бит 2: если 1, то будет установлен атрибут «системный». • Бит 3: если 1, то это метка тома.

158

Page 160: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

• Бит 4: если 1, то это каталог. • Бит 5: если 1, то будет установлен атрибут «архивный». • Биты 6-15: зарезервированы для дальнейшего использования.

В большинстве случаев в регистр СХ нужно помещать значение 0x20: создание обычного архивного файла. В регистре ВХ указываются флаги расширенного режима. Для нас пока име­ют значение только два младших бита, рассмотрение остальных выходит за пределы этой книги.

• ВХ = 0: файл открывается только для чтения. • ВХ = 1: файл открывается только для записи. • ВХ = 2: файл открывается для чтения и для записи.

После успешного завершения системного вызова (CF=0) регистр АХ будет содержать дескриптор файла (как и в случае с функцией 0x3D). В СХ будет возвращен «отчет» о выполненной операции:

• СХ = 1: файл был открыт. • СХ = 2: файл был создан и открыт. • СХ = 3: файл был перезаписан и открыт.

Модифицируем нашу программу l i s t i t .asm (листинг 10.6) так, чтобы она копировала файлы. Файл назначения будем создавать (или перезаписывать) функцией ОхбС. Осталось только передать полученный из нее дескриптор файла назначения в то место программы, где происходит запись в файл. Таким образом, допишем в начало нашей программы строки для открытия второго файла:

mov ах , GCOOh

mov СХ, 0x2 0 mov dx, 0x12 mov s i , other_file_name i n t 0x21 j c e r r o r mov bp ,ax

функция DOS для COздания/открытия файла заполняем сразу оба регистра : ОхбС в АН и О в AL атрибуты нового обычного файла операция открытия адрес имени второго файла вызов DOS переход к обработке ошибки сохраняем дескриптор открытого файла в ВР

Ясно, что этот фрагмент должен занять место команды MOV bp,STDOUT. Второй файл тоже должен быть закрыт: добавим после закрытия первого файла следующие строки:

mov ah , 0x3е /функция DOS для закрытия файла mov b x , b p ;помещаем дескриптор файла в ВХ i n t 0x21

И, наконец, определим имя второго файла в секции данных: o t h e r _ f i l e _ n a m e db " t e x t l . t x t " , О ;имя второго файла

159

Page 161: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Наша программа копирует первый файл text.txt во второй файл textl.txt. Первый файл читается через свой дескриптор, хранящийся в ВХ, а второй файл записывается через свой дескриптор, который после его открытия со­хранен в ВР. Назовем нашу программу сору.asm и откомпилируем ее как обычно:

nasm -f bin -о copy.com copy.asm

Поиск позиции в файле (SEEK)

До сих пор мы читали и записывали файлы последовательно, то есть в том по­рядке, в котором они записаны на диске. В более сложных случаях нам нужно читать файлы выборочно (например, 1 блок в начале файла и 2 блока в конце файла), передвигая указатель внутренней позиции файла вперед/назад. В языке С для этого служит функция seekQ, а в DOS имеется системный вызов 0x42.

Ввод:

Изменение позиции в файле АН = 0x42 AL = начало отсчета ВХ = дескриптор файла CX;DX = смещение

Вывод:

Успех: CF = 0 DX:AX = новая позиция СХ = отчет об операции Ошибка: CF=1 АХ = код ошибки

Дескриптор файла передается через регистр ВХ. Требуемое значение сме­щения внутреннего указателя передается через пару CX:DX. Два регистра, а не один, нужны для того, чтобы можно было адресовать файлы размером до 4 Гб (размер файла в FAT16 ограничен 2 Гб). Регистр СХ содержит старгаие 16 битов, а DX — младшие 16 битов смещения. Регистр AL определяет начало отсчета, то есть от какого места в файле будет отсчитываться заданное в CX:DX смещение:

• AL = 0: смещение отсчитывается от начала файла (SEEK_SET), и новое значение позиции будет равно CX:DX.

• AL = 1: смещение отсчитывается от текущей позиции (SEEK_CUR). • AL = 2: смещение отсчитывается от конца файла (SEEK_END).

Новая позиция внутреннего указателя будет возвращена в паре DX:AX. Функция SEEK может также быть использована для определения размера фай­ла. Откройте файл и вызовите функцию 0x42 со следующими параметрами: mov ах,0x4202 mov bx,filedes xor dx,dx xor ex,ex int 0x21

функция поиека позиции от конца файла ВХ = деекриптор файла DX = О СХ - О вызываем DOS

160

Page 162: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

В результате отсчета нулевого смещения от конца файла в паре DX:AX ока­жется длина файла.

Другие функции для работы с файлами

Рассмотрим функции удаления, изменения атрибутов и переименования файла. Файл на диске мы можем удалить с помощью функции 0x41:

Ввод:

Удаление файла АН = 0x41 DS:DX = указатель на имя файла

Вывод:

Успех: CF = 0 Ошибка: CF=1 АХ = код ошибки

Если у файла установлен атрибут «только чтение», сначала нужно снять этот атрибут, иначе мы не сможем удалить его. Изменить атрибуты можно с по­мощью 0x43:

Ввод:

Получение атрибутов АН = 0x43 AL = 0x00 DS:DX = указатель на имя файла ИЛИ: Установка атрибутов СХ = требуемые атрибуты AL = 0x01 DS:DX = указатель на имя файла

Вывод:

Успех: CF = 0 СХ = атрибуты файла Ошибка: CF=1 АХ = код ошибки

Переименование файла осуществляется с помощью системного вызова 0x56. При переименовании файла мы также можем переместить его в новый каталог, но при условии, что целевой каталог находится на том же логическом диске, что и исходный файл.

Ввод:

Переименование файла АН = 0x56 DS:DX = указатель на исходное имя файла ES:DI = указатель на новое имя файла

Вывод:

Успех: CF = 0 Ошибка: CF=1 АХ = код ошибки

Рассмотрим пример: переместим файл text.txt из текущего каталога в корне­вой каталог диска. Мы будем использовать системный вызов перемещения файла. Программа очень проста и состоит всего из двух системных вызовов: переименование файла и завершение работы (листинг 10.7).

161

Page 163: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Листинг 10.7. П||огрШШ:^^^^ВУш11§<||1йл^

секция кода

/функция DOS 0x5 6 для переименования файла ;загружаем в DX адрес исходного имени файла ;а в DI — адрес нового имени файла

;завершение программы

SECTION . t e x t o rg 0x100 mov ah , 0x5 6 mov d x , s r c mov d i , d e s t i n t 0x21 mov ax ,0x4c00 i n t 0x21 SECTION . d a t a s r c db " t e x t . t x t ' \ 0 d e s t db ' 4 t e x t . t x t " , 0

Назовем программу rename.asm и откомпилируем как обычно. Ее размер будет всего несколько байтов, но ее можно сделать еще меньше. Имена файлов от­личаются только символом обратного слэша, обозначающим корневой каталог диска. Для уменьшения размера программы перепишем секцию данных так: SECTION .data dest db '' \ " src db "text.txt'\0

Теперь оба имени «делят» одну и ту же область памяти. В высокоуровневых язы­ках программирования, как правило, таких фокусов позволить себе нельзя.

Длинные имена файлов и работа с ними

Windows 95 позволяет преодолеть ограничение 8-ЬЗ на длину файлов. Для обе­спечения обратной совместимости со старыми программами длинные имена хранятся также в сокращенном, 84-3, виде. Например, каталог LinuxRulez будет сохранен как LINUXR~1. Для работы с длинными именами файлов мы должны использовать новые системные вызовы, которые доступны только под управлением Windows. Их описание вы сможете найти в документе Ralf Brown Interrupt List (http://www.ctyme.com/rbrown.htm). Все функции для работы с длинными именами файлов доступны через функцию с номером 0x70. Через регистр AL передается номер оригинальной функции (работающей с короткими именами), то есть те из рассмотренных функций, которые сами принимают аргумент через регистр AL, не поддерживаются. Например, вместо функции открытия файла 0x3D при работе с длинными именами придется использовать функцию ОхбС: в регистр АХ нужно поме­стить значение 0х706С, а остальные аргументы передавать как обычно.

162

Page 164: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

10.7. Работа с каталогами Операции над каталогами в языке ассемблера ничем не отличаются от таких же операций в языках высокого уровня. Привычные функции MKDIR, RM-DIR, CHDIR и т.п. реализованы как набор системных вызовов, которые мы рассмотрим в этом параграфе.

Создание и удаление каталога (MKDIR, RMDIR) Для создания каталога используется функция 0x39, принимающая указатель на имя нового каталога. Удалить каталог можно с помощью функции ОхЗА. Эта функция удаляет только пустой каталог, то есть предварительно нужно удалить из него все файлы и подкаталоги.

Ввод:

Создание нового каталога АН = 0x39 Удаление каталога АН = ОхЗА DS:DX = указатель на имя каталога

Вывод:

Успех: CF = 0 Ошибка: CF=1 АХ = код ошибки

Смена текущего каталога (CHDIR)

Очень полезной является функция ОхЗВ — функция смены текущего каталога (CHDIR). Именно от текущего каталога все функции DOS, работающие с файлами, отсчитывают относительное имя файла. То есть если имя файла не указано полностью, то DOS будет искать файл в текущем каталоге.

Ввод:

Смена текущего каталога АН = ОхЗВ DS:DX = указатель на имя каталога

Вывод:

Успех: CF = 0 Ошибка: CF=1 АХ = код ошибки

Получение текущего каталога (GETCWD)

Иногда нам нужна обратная операция — мы хотим знать, какой каталог является текущим. Функция 0x47 возвращает имя текущего каталога, но без буквы диска и начального слэша.

Ввод:

Получение текущего каталога АН = 0x47 DL = на каком диске: DL = 0x00 текущий диск DL = 0x01 ДИСКА:

DS:SI = указатель на буфер, в котором будет сохранено название текущего каталога

Успех: CF = 0 Ошибка: CF = 1 АХ = код ошибки

Вывод:

163

Page 165: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Если нам нужно узнать имя текущего диска, можно использовать функцию 0x19:

Ввод:

Получить имя диска АН = 0x19

Вывод:

AL = номер текущего диска: AL = 0x00 диск А: AL = 0x01 диск В:

В листинге 10.8. приведена программа pwd, выводящая на экран текущий каталог, включая букву диска

Листинг 10.8. Программа, выводящая на экран текущий каталоЩ включая букву диска

SECTION .text org 0x100 mov ah,0x19 int 0x21 add byte [buffer],al

xor dl,dl

mov ah,0x47

mov si,buffer+3 int 0x21 mov ah,0x40 mov bx,0x0001 mov cx,BUFF_LEN mov dx,buffer int 0x21 mov ax,0x4c00 int 0x21 SECTION .data buffer db "A:\" times 64 db " " пробелами db OxOD,OxOA BUFF_LEN equ $-buffer

функция DOS для получения текущего диска вызываем DOS добавляем номер к букве 'А' , которую заранее поместили в первый байт буфера будем искать текущий каталог текущем диске функция DOS для получения текущего каталога первые три байта буфера отведены под ''С:\" вызываем DOS функция DOS для вывода на экран это дескриптор стандартного вывода сколько символов нужно вывести

;вызываем DOS ;функция DOS для завершения программы ;вызываем DOS

;оставщуюся часть буфера заполняем

;и завершаем строку /оператор $ обозначает текущий адрес, ;то есть конец буфера. Отнимая от него /начальный адрес, получаем длину буфера

164

Page 166: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Для полноты описания давайте рассмотрим обратную функцию — установку текущего диска ОхОЕ:

Ввод:

Установка текущего диска АН = ОхОЕ DL = 0x00 диск А: DL = 0x01 диск В:

Вывод:

AL= количество имеющихся дисков

10.8. Управление памятью Каждая программа, работающая под управлением DOS, получает в свое рас­поряжение всю оставшуюся свободную память. Можно вернуть временно не нужную память обратно в распоряжение DOS, а потом при необходимости затребовать ее обратно. Чтобы «отдать» блок памяти операционной системе, нужно объявить его свободным при помощи системного вызова 0х4А.

Изменение размера блока памяти

Для изменения размера блока памяти используется функция 0х4А. Можно запросить как увеличение, так и уменьшение блока. Если запрос не может быть выполнен, то флаг переноса будет содержать 1, а в регистре ВХ будет максимальный доступный размер блока памяти.

Ввод:

Изменение размера блока памяти АН = 0х4А ВХ = новый размер в «параграфах» ES = сегмент блока, размер которого подлежит изменению

Вывод:

Успех: CF = 0 Ошибка CF=1 АХ = код ошибки ВХ = максимально доступный размер блока памяти

Новый размер блока указывается в «параграфах» — единицах величиной в 16 байтов. СОМ-программы обычно «заказывают» блок памяти размером в 64 Кб (то есть 0x1000 параграфов). Выделить 0x1000 параграфов можно с помощью следующего кода:

;функция ДОС для изменения размера памяти ;к-во параграфов; ES уже установлен ;вызов ДОС

mov ah, 0х4А mov bx, 0x10 00 i n t 0x21 jc e r ro r

Запросить еще несколько блоков памяти можно с помощью команды выде­ления памяти 0x48, рассмотренной ниже.

165

Page 167: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Выделение памяти

С помощью функции 0x48 мы можем попросить операционную систему вы­делить новый блок памяти в любое время. Если такой блок доступен, он будет распределен. Адрес его сегмента будет возвращен в регистре АХ.

Ввод:

Распределение блока памяти АН = 0x48 ВХ = размер в «параграфах»

Вывод:

Успех: CF = 0 АХ = адрес сегмента выделенного блока Ошибка: CF=1 АХ = код ошибки ВХ = максимально доступный размер блока памяти (в параграфах)

Посмотрим, как выделить блок размером 64 Кб, используя эту функцию. Предположим, что ненужную память мы уже вернули операционной системе, вызвав функцию 0х4А. mov ah,0x4 8 mov bx,0x1000 int 0x21 jc error

push es mov es,ax

;функция DOS для распределения памяти ;нам нужно б 4 KB ;вызываем DOS ;если CF = 1, то запрошенный блок нам не дали, ;в ВХ — максимальное число доступных /параграфов ;сохраняем оригинальный ES в стеке ;загружаем в ES адрес нового сегмента

Освобождение памяти

Выделенная (распределенная) память должна быть возвращена обратно опе­рационной системе. Сделать это можно с помощью функции 0x49:

Ввод:

Освобождение блока памяти АН = 0x49 ES = адрес сегмента освобождаемого блока

Вывод:

Успех: CF = 0 Ошибка CF=1 АХ = код ошибки

10.9. Аргументы командной строки Для получения доступа к аргументам командной строки в языке С исполь­зуются переменные argc (количество аргументов) и argv (массив самих аргу­ментов). Операционная система DOS передает все аргументы одной строкой, записывая эту строку по адресу 0x81 от начала сегмента программы. Длина этой строки с учетом завершающего символа OxOD сохраняется по смеще-

166

Page 168: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

нию 0x80 (один байт). Нам придется извлекать отдельные аргументы из этой строки вручную. Следующая программа выводит переданные ей параметры на экран и завер­шает работу. Ничего нового в ней нет: мы просто выводим строку, записанную по адресу 0x81 (листинг 10.9).

ЛистингЮ.О. Программа, Шводящая переданные ей |^^^В^ры нал^краш

SECTION .text %define STDOUT 1 org 0x100 mov ah,0x40 mov bx,STDOUT mov dx,0x81 xor ex,ex mov cl,[0x80] mov di,ex add di,dx

ine di

mov byte [di],OxA

ine ex int 0x21 mov ax,0x4e00 int 0x21

;начало кода ;етандартный вывод ;сом-файл ;функция DOS для запиеи в Ьаил

начало отроки ебраеываем длину отроки загружаем в CL длину отроки копируем в DI добавляем длину и получаем конец отроки [OxOD] устанавливаем DI поеле поеледнего еимвола отроки дописываем 0x0А, чтобы получить полноценный EOL и увеличиваем длину строки на 1 выводим строку на экран завершаем работу вызываем DOS

10.10. Коды ошибок Определить, что произошла ошибка во время системного вызова, можно по флагу переноса: если он установлен (CF=1), то вызов завершился ошибочно. В регистре АХ будет код ошибки. Подробную информацию об ошибке можно получить с помощью системного вызова АН=0x59. Также описание ошибок можно найти в Ralf Brown Interrupt List. В таблице 10.3 перечислены только коды самых распространенных ошибок.

DOS-коды самых распространенных ошибок Таблица 10.3

Код

1 0x0002

1 0x0003

1 0x0005

Описание ошибки

Файл не найден

Путь не найден

Отказано в доступе

167

Page 169: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Продолжение табл. 10.3

Код

1 0x0008

1 0x0009

OxOOOf

0x0010

0x0011

Описание ошибки

Недостаточно памяти

Неверный адрес блока памяти

Диск указан неправильно

Невозможно удалить текущий каталог

Некорректный носитель

10.11. Отладка

10.11.1. Что такое отладка программы и зачем она нужна

При разработке программы на языке ассемблера очень важным является этап отладки программы. Отладка позволяет найти и проанализировать не опечат­ки, а логические ошибки в вашей программе: именно из-за них программа работает не так, как нужно. Для отладки ваших программ вам понадобится специальная программа — отладчик (debugger). По-английски ошибка в программе называется «bug» (жук) в честь жука, который, согласно легенде, поселился в одном из первых компьютеров и нарушил его работу. Отсюда и название класса программ для «поиска и уда­ления жуков» — debugger. Эти программы позволяют шаг за шагом следить за исполнением программы, наблюдать за состоянием регистров и памяти, изменять на ходу их значения и т.п. В операционной системе DOS (и Windows) есть свой собственный отладчик — программа debug.exe. А программу Turbo Debugger, входяш ую в состав любой среды разработки от Borland, можно порекомендовать для опытных пользова­телей. Также можно использовать очень хороший отладчик IDA (Interactive Disassembler) от DataRescue. После запуска отладчика нам нужно загрузить отлаживаемую программу в память (используя команды вроде open, load). Если программа требует пара­метров командной строки, не забудьте их указать: любой отладчик позволяет сделать это. Есть два вида отладчиков: графические (более дружественные, но требующие больших ресурсов) и текстовые (ориентированные на опытных пользователей). Графические отладчики сразу открывают несколько окон, ото-бражаюгцих состояние регистров и памяти, а также окно с исходным текстом (деассемблированным машинным кодом) программы. Для отображения этой информации в текстовых отладчиках имеются команды (отдельная команда для вывода кода, отдельная — для отображения регистров и отдельная — для дампа памяти).

168

Page 170: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Наиболее важная функция отладчика — пошаговое выполнение программы (команда step). За один шаг выполняется одна команда программы. После выполнения команды выводятся новые значения регистров процессора, а также команда, которая будет выполнена следующей. Абсолютно все отладчики имеют функцию step-over, позволяющую «проска­кивать» подпрограмму, то есть все команды подпрограммы будут выполнены за один шаг. Эту функцию можно использовать, если вы уверены в правиль­ности действий подпрограммы. Для продолжения выполнения всей программы без дальнейшей отладки име­ются команды go или continue. Любой отладчик позволяет устанавливать точки прерывания (контрольные точки или breakpoints). Программа будет выполнена до этой точки, после чего при выполнении заданного условия управление будет возвращено отладчику и вы сможете просмотреть содержание регистров и памяти. Условием может быть выполнение команды, хранящейся по указанному адресу, изменение значения определенной ячейки памяти и т.п.

При отладке вместо символических имен идентификаторов (меток, перемен­ных) будут показаны их адреса. Если вас такое положение вещей не устраи­вает (конечно, удобнее, когда вместо адреса отображается имя переменной, например, result), вы должны перекомпилировать программу, «упаковав» в нее символьные имена. Размер программы существенно увеличится, но по­сле отладки вы сможете заново перекомпилировать программу, исключив символы. В этой книге мы будем использовать свободно распространяемый отладчик grdb, который вы сможете найти в Интернете.

10.11.2. Отладчик grdb.exe

Методика использования

Отладчик grdb (Get Real Debugger, то есть «настоящий отладчик) написан на языке ассемблера и распространяется бесплатно. Скачать его можно по адресу: http://www.members.tripod.com/'-'ladsoft/grdb.htm. Интерфейс про­граммы — классический, то есть текстовый. Для запуска программы введите команду grdb.

C : \ > g r d b GRDB v e r s i o n З . б C o p y r i g h t (с) LADsof t 1 9 9 7 - 2 0 0 2 H i s t o r y e n a b l e d eax:00000000 ebxiOOOOOOOO ecx:00000000 edx:00000000 es i :00000000 ed i :00000000 ebp:00000000 espiOOOOFFEE e ip :00000100 e f l ags :00000202 NV UP EI PL NZ NA PO NC

169

Page 171: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

d s : 10FB e s : 1 0 F B f s : 1 0 F B g s i l O F B s s : 1 0 F B c s i l O F B 1 0 F B : 0 1 0 0 74 IE j z 0120 ->

Появление на экране приглашения (->) означает, что отладчик готов к вы­полнению наших команд. Загрузим в отладчик нашу программу pwd.com (она выводит текущий каталог на стандартный вывод). Для этого введем команду 1 pwd.com (1 — это load, то есть загрузить). После имени исполняемого файла можно указать через пробел аргументы программы. ->1 pwd.com Size: 00000069

Теперь начнем пошаговое выполнение программы. Для выполнения одной команды программы служит команда отладчика t. Введите t и нажмите «Enter»:

-> t еах:00001900 ebx:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 espiOOOOFFEE eip:00000102 eflags:00000202 NV UP EI PL NZ NA PO NC ds: lOFB es:10FB fs:10FB gsrlOFB ssilOFB cs:10FB 10FB:0102 CD 21 int 21 ->

Команда t выполнила первую команду нашей программы, и в регистре АН появилось значение 0x19. Следующая команда — INT 21, то есть вызов опе­рационной системы. Слева от нее вы видите ее адрес (пара регистров CSiIP) и машинный код. После нажатия t и кливиши «Enter» будет выполнен вызов DOS, и в AL окажется номер текуш^его диска.

-> t еах:00001902 еЬх:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:OOOOFFEE eip:00000104 eflags:00000202 NV UP EI PL NZ NA PO NC ds: lOFB es:10FB fs:10FB gs:10FB ss:10FB cs:10FB 10FB:0104 00 06 24 01 add [0124],al ds:[0124]-41 ->

Теперь AL содержит значение 02, означаюш^ее диск С:. Следующая инструкция — это ADD [buffer],al, добавляющая содержимое AL к символу 'А'. Как видите, вместо символьного имени buffer указан адрес этой переменной. Мы можем этим воспользоваться: чтобы увидеть значение переменной buffef, нужно сделать дамп памяти по адресу (смещению) 0124. Для этого введем команду d 124. Для команды d самый распространенный аргумент —- это начальный адрес памяти, содержимое которой нужно вывести, хотя можно указывать и имя регистра.

170

Page 172: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

- > d 124 1 0 F B : 0 1 2 0 - 4 1 ЗА 5С 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 А : \ 1 0 F B : 0 1 3 0 20 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 1 0 F B : 0 1 4 0 20 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 1 0 F B : 0 1 5 0 20 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 1 0 F B : 0 1 6 0 20 20 20 2 0 - 2 0 20 20 OD-OA C7 06 8C-CD 01 00 EB

10FB:0170 60 26 8A 47-01 32 E4 40-Dl EO 03 D8-26 8A OF 32 '&.G.2.@. . . .&. .2 10FB:0180 ED OB C9 74-OF 43 53 26-8B IF E8 C6-00 5B 73 41 . . .t.CS& [sA 10FB:0190 43 43 E2 F2-2E C7 06 8C-CD 03 00 EB-34 26 8A 47 CC 4&.G lOFBiOlAO 01 32 E4 40-Dl EO 03 D8-26 8A 07 32-E4 Dl EO 40 .2 Л &. . 2 . . .@ ->

После выполнения ADD введите еще одну команду t. Строка А:\ будет замене­на строкой С:\ (наш текущий диск). Проверьте это с помощью команды d:

- > d 124 10FB:0120 -43 ЗА 5С 20-20 20 20 20-20 20 20 20 С:\

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

- > и 100 lOFB-.OlOO В4 19 mov ah,0019 10FB:0102 CD 21 int 21 10FB:0104 00 OG 24 01 add [0124],al 10FB:0108 30 D2 xor dl, dl lOFBrOlOA B4 47 mov ah,0047 lOFBrOlOC BE 27 01 mov si,0127 lOFBrOlOF CD 21 int 21 lOFBiOlll B4 40 mov ah,0040 10FB:0113 BB 01 00 mov bx,0001 10FB:0116 B9 45 00 mov ex,0045 10FB:0119 BA 24 01 mov dx,0124

171

Page 173: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

lOFBiOllC CD 21 i n t lOFBiOllE В8 00 4C mov ->

21 ax,4C00

Завершить работу отладчика можно командой q.

Основные команды отладчика grdb

Основные команды отладчика grdb приведены в табл. 10.4.

Основные команды отладчика grdb Таблица 10.4

Команда

1 <filename> [р]

t

Р

U

g

а <addr>

?

b<num>,addr

1 ^ q

Назначение

Загружает программу <filename> с аргументами (необязательными) [р]

Выполняет одну команду, возвращая управление отладчику после ее выполнения

Подобна предыдущей команде, но проходит подпрограмму за один шаг

Преобразует машинный код команд обратно в символический код ассемблера. Обычно используется для просмотра листинга программы

Продолжает выполнение программы, передавая управление отладчику после завершения программы

Записывает новую команду по адресу <addr>

Выводит список всех команд и краткое их описание

Устанавливает точку прерывания номер пит по адресу addr. Номер точки должен быть в диапазоне от 0 до F, то есть всего можно установить 16 различных точек прерывания

Выводит список всех установленных точек прерывания

Выход

Пример разработки и отладки программы

Напишем небольшую программу, читающую два или более десятичных числа из командной строки и выводящую на экран их сумму. Для преобразования строк в числа и наоборот будем использовать разрабо­танную нами подпрограмму из главы 7. Как выводить строку на экран, мы тоже знаем, поэтому в данной главе сосредоточим наше внимание именно на отладке нашей программы. Давайте вспомним, как нужно вызывать нашу подпрограмму ASCIIToNum:

ASCIIToNum

Gsi = указатель на конвертируем/ю cipoi^, заканчивающуюся символом 0x0 есх = основание системы счисления Результат : еах - число

172

Page 174: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

Поскольку мы работаем в операционной системе DOS, мы будем использо­вать только 16-битную адресацию. Указатель (смещение) будет передан в SI, а не в ESI. Наша программа должна уметь работать с аргументами командной строки. Все аргументы передаются операционной системой программе как одна строка, внутри которой аргументы разделены пробелами. Для извлечения отдельных аргументов из этой строки мы напишем несколько подпрограмм. Первая из них — SkipSpace, пропускающая любое количество пробелов в начале строки. Подпрограмме нужно передать указатель на строку, а на выходе мы получим адрес первого символа строки, отличного от пробела.

SkipSpace

SI = у к а з а т е л ь на строку

Возвращает: s i = первый символ, отличный от пробела

Sk ipSpace : . a g a i n : l o d s b ;загружаем в AL байт по адресу DS:SI ,

/увеличиваем S1 стр a l , ' ' ;сравниваем символ с пробелом j z a g a i n ;если равно, продолжаем r e t

Итак, мы удалили из начала строки аргументов все пробелы, первый пробел в строке будет после первого аргумента. Напомним, что строка аргументов начинается с адреса 0x81, а заканчивается символом OxD. Теперь преобразуем первый аргумент в число. У нас есть подпрограмма, которая умеет считывать число из строки, завершенной нулевым байтом. Чтобы воспользоваться ею, нужно поместить нулевой байт на место символа пробела после первого аргумента. Напишем еще одну подпрограмму, которая пропускает в строке аргументов последовательность подряд идущих цифр и возвращает указатель на позицию в строке после числа.

SkipNum Ввод: SI = указатель на строку Вывод: DI = указатель на позицию в строке после числа

SkipNum: mov di,si ;DI используется как указатель .find_end_of_number: inc di ;DI=DI+1 cmp [di],'0' ;сравнить символ с ASCIT-кодом

;цифры нуль

173

Page 175: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

jb .end_found cmp [di],'9'

ja .end_found jmp .find_end_of„number .end_found: ret

A вот тело основной программы:

;если меньше, конец числа найден ;сравнить символ с ASCII-кодом ;цифры 9 ;если больше, конец числа найден ; переходим к следуюш;ему символу

mov si, 0x81

call SkipNum mov [di],0 mov ecx,10 call ASCIIToNum

mov edx,eax mov si,di

inc si call Skipspace call SkipNum mov [di],0

call ASCIIToNum

add e a x , e d x

/загружаем в SI указатель на начало ;строки аргументов /переходим в конец первого числа ;пишем нулевой байт ;читаем число в десятичной записи ;в ЕАХ будет р е з у л ь т а т , SI о с т а е т с я ;неизменным ;сохраняем число в EDX /загружаем в SI указатель на позицию ;в строке сразу после первого числа

;пропускаем любые пробелы ;и пропускаем число ;записываем нулевой символ после ;второго аргумента ;в ЕАХ — р е з у л ь т а т , SI о с т а е т с я /неизменным /добавляем предыдущее число, ;сохраненное в EDX

Первым делом мы пропускаем число и записываем нулевой байт после него. Подпрограмма преобразования строки в число будет «думать», что строка закончилась. После этого мы преобразуем фрагмент строки параметров от ее начала до первого нулевого байта в число. Регистр DI теперь указывает на нулевой байт, значит, нужно увеличить указатель на единицу. Наша под­программа преобразования строки в число ожидает указателя в регистре SI, и мы копируем его из DL Теперь наша строка будет начинаться со второго аргумента, перед обработкой которого мы снова пропускаем все пробелы в начале строки. После преобразования второго числа мы складываем его с первым. После этого в ЕАХ будет сумма двух чисел, которую нужно преобразовать в строку и вывести на экран. Для преобразования числа в строку будем использовать подпрограмму NumToASCII:

174

Page 176: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

NumToASCII

еах = 3 2-битное число еЬх = основание системы счисления edi = указатель на буфер для сохранения строки Результат: заполненный буфер

mov ebx,1О счисления mov di,buffer call NumToASCII

;заносим в EBX основание системы

;в DI — адрес буфера ;перед вызовом подпрограммы в /находится вычисленная сумма

ЕАХ

Теперь выведем полученную строку на экран, используя функцию записи в файл стандартного вывода. Но мы не знаем длину строки, которая получилась после преобразования. Пока сделаем так: зарезервированное для строки место заполним пробелами и выведем строку вместе с лишними пробелами. Потом мы можем модифицировать программу, удалив из строки лишние пробелы.

mov ah ,0x40 ;функция DOS для записи в файл mov d x , d i ; эта функция ожидает у к а з а т е л я на

;строку в DS:DX ;количество выводимых символов

;завершаем программу

mov с х , 2 5 i n t 0x21 mov ах , 0х4с00 i n t 0x21

В секции данных заполним наш буфер 25 пробелами: b u f f e r t i m e s 2 5 db ' '

Теперь нам осталось написать только секцию кода для нашей программы. SECTION .text org 0x100 mov si, 0x81

call SkipNum mov [di],0 mov есхДО call ASCIIToNum

mov edx,eax mov si,di

inc si call Skipspace call SkipNum

загружаем в SI указатель на начало строки аргументов переходим в конец первого числа пишем нулевой байт читаем число в десятичной записи в ЕАХ будет результат, SI остается неизменным сохраняем число в EDX загружаем в SI указатель на позицию в строке сразу после первого числа ;пропускаем любые пробелы ;и пропускаем число

175

Page 177: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

mov [ d i ] , 0

call ASCIIToNum

add eax,edx

mov еЬхДО

mov d i , bu f f e r

call NumToASCII mov ah,0x40 mov dx,di

mov ex,2 5 int 0x21 mov ax, 0x4c00 int 0x21

записываем нулевой символ после второго аргумента в ЕАХ — результат, S1 остается неизменным добавляем предыдущее число, сохраненное в EDX основание системы счисления для преобразования числа загружаем в DI адрес буфера выводимой строки преобразуем ЕАХ в строку функция ДОС для записи в файл указатель на строку ожидается в DX, у нас он в DI количество выводимых символов вызываем DOS завершаем программу

SkipSpace - пропуск пробелов

S1 указатель на строку

Возвращает: si = первый символ,

SkipSpace: .again: lodsb cmp a1,' ' jz again ret

отличный от пробела

/загружаем в AL байт по адресу DS:SI, ;увеличиваем SI ;сравниваем символ с пробелом ;если равно, продолжаем

SkipNum ~ пропуск числа Ввод: S1 = указатель на строку Вывод: DI = указатель на позицию в строке после числа

SkipNum: mov di,si . find_end_, of „number inc di cmp [di],'0' jb .end_found cmp [di],'9' ja .end_found j mp .f i nd„end_o f_numbe r .end_found: ret

;DI используется как указатель

;DI=D1+1 /сравнить символ с ASCII-кодом цифры нуль ;если меньше, конец числа найден ;сравнить символ с ASCII-кодом цифры 9 ;если больше, конец числа найден

;переходим к следующему символу

176

Page 178: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

nasm test test test test test test test

-f bin asm asm asm asm asm asm asm:

6: 13 15 41 53 55 141

-o tes error: error error error error error : erroi

; ***** Далее следуют все необходимые подпрограммы ***** SECTION .data buffer times 2 5 db ' ' ;буфер для сохранения результата

Теперь сохраним нашу программу и попытаемся ее откомпилировать. В ре­зультате компиляции увидим это:

:.com tes t . a sm opera t ion s ize not spec i f i ed

: symbol 'Skipspace ' undefined : operat ion s ize not spec i f i ed : symbol ' aga in ' undefined : opera t ion s ize not spec i f i ed ; opera t ion s ize not spec i f i ed

e r r o r : phase e r ror detected at end of assembly. Как видите, в нашей программе много ошибок. Давайте их исправлять. Первая ошибка — в строке 6:

mov [d i ] ,0 ;пишем нулевой байт Эта команда требует указания размера операнда, выполним ее требование:

mov byte [d i ] , 0 ;пишем нулевой байт Следующая наша ошибка заключается в том, что мы неправильно написали имя подпрограммы. Ассемблер чувствителен к регистру символов, поэтому Skipspace — это не SkipSpace. Исправим это. После этого исправим ошибку в строке 13 — это та же самая ошибка, что и в строке 6. В строке 41 мы забыли указать точку перед 'again':

jz .again Команды в строках 53 и 55 тоже требуют указания размера операнда. Просто добавьте ключевое слово 'byte' между командой стр и квадратной скобкой. Исправив все ошибки, откомпилируем программу снова. Она выполняется, но ничего не выводит на экран. Проверим код для вывода строки на экран:

mov ah,0x40 ;функция ДОС для записи в файл mov dx,di ;указатель ожидается в DX, у нас он в DI mov сх,2 5 /количество печатаемых символов i n t 0x21 /вызываем DOS

Все ясно, мы ведь забыли поместить дескриптор потока стандартного вывода 0x01 в ВХ — в этом регистре все еще содержится значение 10. Исправим эту ошибку и снова откомпилируем программу. Теперь можно запустить про­грамму с параметрами 45 и 50. Мы получим следуюш;ее значение:

С : \ t e s t 45 50 945

177

Page 179: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Мы должны получить 95, но никак на 945. Что произошло? Ответить на этот вопрос нам поможет отладчик. Запустим grdb:

с:>grdb GRDB version З.б Copyright (с) LADsoft 1997-2002 ->

Введем команду'1 test .com 45 5 О' для загрузки программы с параметрами: ->1 test.com 45 50 Size: 000000Е1 ->

Предположим, что наши подпрограммы работают правильно, поэтому будем использовать команду р для прохождения подпрограмм за один шаг. Посмот­рим, что возвращает подпрограмма ASCIIToNum в регистр ЕАХ. Первый вызов ASCIIToNum следует сразу после инструкции MOV есхДО. Рассмотрим весь сеанс отладки до первого вызова ASCIIToNum.

- > 1 t e s t . c o m 45 50 Size: 000000Е1 ->р еах:00000000 еЬх:00000000 есх:000000Е1 edx:00000000 esi:00000081 edi:00000000 ebp:00000000 esp:OOOOFFEE eip:00000103 eflags:00000202 NV UP EI PL NZ NA PO NC ds: lOFB es:10FB fs:lOFB gs:lOFB ss:10FB cs:lOFB 10FB:0103 E8 44 00 call 014A ->p eax:00000000 ebx:00000000 ecx:OOOOOOEl edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000106 eflags:00000287 NV UP EI MI NZ NA PE CY ds: lOFB es:10FB fs:10FB gs:10FB ss:10FB CS:10FB 10FB:0106 C6 05 00 mov byte [di],0000 ds:[0084]-20 ->p eax:00000000 ebx:00000000 ecx:OOOOOOEl edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000109 eflags:00000287 NV UP EI MI NZ NA PE CY ds: lOFB es:10FB fs:10FB gs:10FB ss:10FB CS:10FB 10FB:0109 66 B9 OA 00 00 00 mov ecx,OOOOOOOA ->P eax:00000000 ebx:00000000 ecx:OOOOOOOA edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:0000010F eflags:00000287 NV UP EI MI NZ NA PE CY ds: lOFB es:10FB fs:10FB gs:10FB ss:10FB CS:10FB 10FB:010F E8 6D 00 call 017F ->p eax:000003Bl ebx:00000000 ecx:0000000A edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000112 eflags:00000297

178

Page 180: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

NV UP EI MI NZ AC PE CY ds: lOFB es:10FB fs:10FB gs:lOFB ss:10FB cs:10FB 10FB:0112 66 89 C2 mov edx,eax ->

После первого преобразования получим результат 0хЗВ1, который никак не соответствует десятичному 45. 0хЗВ1 — это 945. Почему мы получили имен­но этот результат? Если мы предположили, что подпрограмма корректна, то тогда нужно проверить параметры, которые мы передали подпрограмме. Посмотрим, что находится в памяти по адресу, который содержится в SI:

->d s i 10KB:0080 20 34 35-00 35 30 OD-01 01 01 01-01 01 01 01 45.50 10FB:0090 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00A0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFBrOOBO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00C0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFBrOODO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFBiOOEO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFBiOOFO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:0100 BE 81 00 E8-44 00 C6 05-00 66 B9 OA-00 00 00 E8 ....D....f ->

Наша функция преобразования не проверяет входящие символы, поэтому у нас и вышла ошибочка. Обратите внимание: перед первым символом у нас образовался пробел (код 20). Лечится это очень просто: перед вызовом подпрограммы преобразования нужно вызвать SkipSpace, чтобы удалить все символы в начале строки. Отредактируем нашу программу и снова откомпи­лируем ее. Запускаем с теми же самими параметрами:

C : \ t e s t 3 45 50 5

Опять мы получили не то, что нужно. Снова запускаем отладчик и переходим к первому вызову ASCIIToNum:

->Р еах:00000034 еЬх:00000000 есх:0000000А edx:00000000 esi:00000083 edi:00000084 ebp:00000000 espiOOOOFFEE eip:00000112 eflags:00000287 NV UP EI MI NZ NA PE CY ds: lOFB esrlOFB fs:10FB gsilOFB ssilOFB csilOFB 10FB:0112 E8 6D 00 call 0182 ->d si 10FB:0080 35-00 35 30 0D~01 01 01 01-01 01 01 01 5.50 10FB:0090 01 01 01 01-01 01 01 01-01 01 01 01~01 01 01 01 10FB:00A0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFEiOOBO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 lOFBiOOCO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00D0 01 01 01 01-01 01 01 01-01 01 01 01~01 01 01 01

179

Page 181: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

lOFB-.OOEO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00F0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 ЮРБ.-ОЮО BE 81 00 Е8-41 00 Е8 44-00 Сб 05 00-66 В9 ОА 00 . . . .А. .D. . . .f. . . ->

Получается, что виновата подпрограмма SkipSpace, которая пропускает один лишний символ, поэтому в качестве аргументов мы получаем 5 и О — отсюда и результат 5 + 0 = 5. Команда LODSB увеличивает указатель SI в любом случае, даже тогда, когда встреченный символ — не пробел. Значит, перед выходом из подпрограммы этот указатель нужно уменьшить и проблема ис­чезнет. Перепишем подпрограмму так:

SkipSpace : . a g a i n : l o d s b /загружаем в AL байт по адресу DS:SI ,

/увеличиваем SI стр a l , ' ' /сравниваем символ с пробелом j z a g a i n /если равно , продолжаем dec s i r e t

Все, «баг» исправлен, сохраним полученную программу, откомпилируем и запустим ее:

С : \ t e s t 4 45 50 95

Полученное значение правильно. Поздравляю! Процесс разработки про­граммы завершен!

10.12. Резидентные программы Обычно после завершения очередной программы DOS освобождает память, которую эта программа занимала, и загружает на ее место новую. Но неко­торые программы, завершившись, продолжают оставаться в памяти. Такие программы называются резидентными. В большинстве случаев резидентные программы используются для обработки прерываний процессора. Типичный пример резидентной программы — драйвер мыши, который опрашивает по­следовательный порт компьютера, читая коды, передаваемые мышью. Этот драйвер использует программное прерывание 0x33, через которое он передает свои события (перемещение мышки, нажатие той или иной клавиши мышки) прикладным программам. На резидентные программы накладывается ряд ограничений. Так, им нельзя самим вызывать программные прерывания. Дело в том, что DOS — принципи­ально однозадачная система, поэтому функции прерываний DOS не обладают свойством реентерабельности (повторной входимости). Если программа, об-

180

Page 182: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

рабатывающая системный вызов DOS, сама выполнит системный вызов, то ОС рухнет. Существуют, правда, способы обойти это ограничение, но они слишком сложны для того, чтобы рассматривать их в этой книге. Напишем небольшую резидентную программу, которая активизируется при нажатии клавиши Scroll Lock. Программа изменяет цвет всех символов, ото­браженных на экране, на красный. До сих пор мы старались избегать прямого взаимодействия с периферийными устройствами и использовали функции операционной системы. В случае с резидентной программой использовать DOS мы больше не можем, поэтому ничего другого нам не остается — мы должны взаимодействовать с устрой­ствами напрямую. Примите пока как данность, что из порта 0x60 можно получить так называе­мый скан-код нажатой клавиши, то есть не ASCII-код посылаемого ею символа, а просто число, указываюгцее ее положение на клавиатуре. Скан-код клавиши Scroll Lock равен 0x46. Каждое нажатие клавиши генерирует аппаратное прерывание IRQ1, которое принимает скан-код от клавиатуры, преобразовывает его в ASCII-код и ста­вит в очередь непрочитанных клавиш. Эта очередь доступна операционной системе и прикладным программам через вызовы BIOS. Чтобы активизировать программу, мы должны перехватить прерывание IRQ 1 (int 0x9), то есть сделать нашу программу его обработчиком. Затем мы будем в цикле читать скан-код из порта 0x60. Если нажатая клавиша — не Scroll Lock, то мы вернем ее скан-код первоначальному обработчику прерывания. В противном случае мы выполним намеченное действие (сменим цвет символов) и вернем управление первоначальному обработчику прерывания. Выход из прерывания осуш;ествляется по команде iret. Во время обработки прерывания мы должны сохранить контекст выполняемой программы, то есть сохранить все регистры «внутри» нашей подпрограммы. Если этого не сделать, последствия предугадать будет сложно, но в большин­стве случаев крах выполняемой программы будет обеспечен. Первым делом напишем подпрограмму color, изменяющую цвет всех симво­лов на красный. Организация видеопамяти была рассмотрена (хоть и очень кратко) в начале этой главы. Символы, выведенные на экран, хранятся начиная с адреса сегмента 0хВ800 в формате символщвет. Наша задача — модифици­ровать значения байтов, находящихся по нечетным адресам: 0хВ800:0х0001, 0хВ800:0х0003, 0хВ800:0х0005 и т.д. — это и есть цвет символов. Не вдаваясь в подробности, скажем, что красному цвету соответствует значение 0x04. Подпрограмма заполняет 80x25 байтов памяти, что соответствует стандарт­ному разрешению текстового режима.

181

Page 183: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

;сохраняем в стеке значения /регистров, которые ;будем использовать

/сбрасываем SI ;загружаем адрес сегмента в АХ ;и в сегментный регистр /количество повторений

/увеличиваем SI на 1 /записываем цвет 0x04 — красный /увеличиваем на 1 /уменьшаем СХ на 1 /переходим к .repeat, пока СХ > О /восстанавливаем все регистры

color: push ах

push СХ push si push es xor si,si mov ax, OxBSOO mov es,ax mov ex,80*25 .repeat: inc si mov byte [es:si],0x4 inc si dec ex jnz .repeat pop es pop si pop ex pop ax ret

Мы можем протестировать нашу подпрограмму. Допишем к ней заголовок секции кода и завершим обычную, не резидентную, программу системным вызовом 0х4С:

SECTION . t e x t c a l l c o l o r mov ax , 0x4c00 i n t 0x21 c o l o r :

Теперь напишем обработчик прерывания IRQ 1: new_handle r : push ax i n a l , 0x60 cmp a l , 0x4 6 jnz pass_on c a l l c o l o r p a s s _ o n : pop ax jmp f a r [ c s : o l d _ v e c t o r ]

/сохраняем значение AX /читаем скан-код клавиши /сравниваем с 0x4 6 ( S c r o l l - L o c k ) /если нет , переходим к pass_on /вызываем подпрограмму

восстанавливаем значение АХ передаем управление первоначальному обработчику

Переменная old_vector должна содержать адрес первоначального обработ­чика прерывания (старое значение вектора прерывания вектора). В качестве

182

Page 184: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

сегментного регистра мы используем CS, потому что другие сегментные реги­стры в ходе обработки прерывания могут получить произвольные значения. Сохраним значение старого вектора прерывания в переменную old_vector , а указатель на наш обработчик поместим вместо него в таблицу прерываний. Назовем эту процедуру setup. setup: cli xor ax, moV e s ,

ax ax

mov ax,new_handler

xchg ax,[es:0x9*4]

mov [ds:old_vector],ax

mov ax,cs xchg ax,[es:0x9*4+2]

mov [ds:old_vector+2],ax

sti ret

;отключаем прерывания ;сбрасываем AX ;таблица прерываний находится ;в сегменте О ;с метки new_handler начинается ;новый обработчик прерывания ;вычисляем адрес старого вектора ;и меняем местами со значением АХ. ;Теперь в таблице смещение нового /обработчика, в АХ - старого ;сохраняем старый адрес в /переменной old_vector /адрес текущего сегмента — CS /пишем в таблицу сегмент нового /обработчика, /в АХ загружаем сегмент старого /сохраняем сегмент на 2 байта /дальше old_vector /включаем прерывания /выходим из подпрограммы

Теперь мы попросим операционную систему не разрушать программу после ее завершения, а хранить ее в памяти. Для этого используется системный вызов 0x31.

Ввод:

Сохраняем программу резидентной АН = 0x31 AL = выходной код DX = число параграфов памяти, необходимых для хранения резидентной программы

Вывод:

Ничего

Код всей нашей резидентной программы r e s i d e n t . asm приведен в листинге 10.10.

Листинг 10.10. Пример резидентной программы

SECTION .text org 0x100 jmp initialize

183

Page 185: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

new„handler: push ах in al, 0x60 cmp al, 0x4 6 jnz pass_on call color pass_on: pop ax jmp far [cs:old_vector]

color: push ax

push ex push si push es xor si,si mov ax, OxBSOO mov es,ax mov ex,80*25 .repeat: inc si mov byte [es:si],0x4 inc si dec ex jnz .repeat pop es pop si pop ex pop ax ret old_vector dd 0 initialize: call setup

mov ax,0x3100

mov dx,initialize

shr dx,4 inc dx int 0x21

setup: cli

;сохраняем значение AX ;читаем скан-код клавиши /сравниваем с 0x4 6 (Scroll--Lock) ;если нет, переходим к pass_on ;вызываем подпрограмму

;восстанавливаем значение АХ /передаем управление /первоначальному обработчику

;сохраняем в стеке значения /регистров, которые /будем использовать

/сбрасываем SI /загружаем адрес сегмента в АХ /И в сегментный регистр /количество повторений

/увеличиваем SI на 1 /записываем цвет 0x04 — красный /увеличиваем на 1 /уменьшаем СХ на 1 /переходим к .repeat, пока СХ > О /восстанавливаем все регистры

вызываем регистрирацию своего обработчика функция DOS: делаем программу резидентной вычисляем число параграфов: в памяти нужно разместить весь код вплоть до метки initialize делим на 16 добавляем 1 завершаем программу и остаемся резидентом ;отключаем прерывания

184

Page 186: Ассемблер на примерах. Базовый курс

Глава 10. Программирование в DOS

хог а х , а х mov e s , a x

mov ax,new_handler

xchg ax,[es:0x9*4]

mov [ds:old_vector],ax

mov a x , c s xchg ax Л е з :0x9*4+2]

mov [ds:old_vector+2],ax

sti ret

сбрасываем AX таблица прерываний находится в сегменте О с метки new_handler начинается новый обработчик прерывания вычисляем адрес старого вектора и меняем местами со значением АХ. Теперь в таблице смещение нового обработчика, в АХ — старого сохраняем старый адрес в переменной old_vector адрес текущего сегмента — CS пишем в таблицу сегмент нового обработчика, в АХ загружаем сегмент старого сохраняем сегмент на 2 байта дальше old_vector включаем прерывания выходим из подпрограммы

Теперь откомпилируем программу: nasm -f bin -о resident.com resident.asm.

После ее запуска мы ничего не увидим, но после нажатия клавиши Scroll Lock весь текст на экране «покраснеет». Избавиться от этого резидента (выгрузить программу из памяти) можно только перезагрузкой компьютера или закрыти­ем окна эмуляции DOS, если программа запущена из-под Windows.

10.13. Свободные источники информации Дополнительную информацию можно найти на сайтах:

• www.ctyme.com/rbrown.htiTi — HTML-версия списка прерываний Ральфа Брауна (Ralf Brown's Interrupt List);

• http://programmistu.narod.ru/asm/lib__l/index.htm — «Ассемблер и про­граммирование для IBM PC» Питер Абель.

185

Page 187: Ассемблер на примерах. Базовый курс

11 Программирование в Windows

«Родные» Windows-приложения

Программная совместимость

Запуск DOS-приложений од Windows

Свободные источники информации

" " 1 " ^ ' -

Page 188: Ассемблер на примерах. Базовый курс

11.1. Введение Когда-то Microsoft Windows была всего лишь графической оболочкой для операционной системы DOS. Но со временем она доросла до самостоятельной полнофункциональной операционной системы, которая использует защищен­ный режим процессора. В отличие от UNIX-подобных операционных систем (Linux, BSD и др.), в Windows функции графического интерфейса пользова­теля (GUI) встроены непосредственно в ядро операционной системы.

11.2. «Родные» Windows-приложения «Родные» Windows-приложения взаимодействуют с ядром операционной системы посредством так называемых API-вызовов. Через API (Application Programming Interface) операционная система предоставляет все услуги, в том числе управление графическим интерфейсом пользователя. Графические элементы GUI имеют объектную структуру, и функции API часто требуют аргументов в виде довольно сложных структур со множеством параметров. Поэтому исходный текст простейшей ассемблерной программы, управляющей окном и парой кнопок, займет несколько страниц. В этой главе мы напишем простенькую программу, которая отображает окошко со знакомым текстом «Hello, World!» и кнопкой ОК. После нажатия ОК окно будет закрыто.

11.2.1. Системные вызовы API в операционной системы DOS мы вызывали ядро с помощью прерывания 0x21. В Windows вместо этого нам нужно использовать одну из функций API. Функции API находятся в различных динамических библиотеках (DLL). Мы должны знать не только имя функции и ее параметры, но также и имя библи­отеки, которая содержит эту функцию: user32.dll, kernel32.dll и т.д. Описание API можно найти, например, в справочной системе Borland Delphi (файл win32.hlp). Если у вас нет Delphi, вы можете скачать только файл win32.zip (это архив, содержащий файл win32.hlp): ftp://ftp.borland.com/pub/delphi/techpubs/delphi2/win32.zip

187

Page 189: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

11.2.2. Программа «Hello, World!» с кнопкой под Windows

Наша программа должна отобразить диалоговое окно и завершить работу. Для отображения диалогового окна используется API-функция MessageBoxA, а для завершения программы можно использовать функцию ExitProcess. В документации по Windows API MessageBoxA написано: int MessageBox( HWND hWnd, // дескриптор окна владельца LPCTSTR IpText, // адрес текста окна сообщения LPCTSTR IpCaption, // адрес текста заголовка окна сообщения UINT иТуре // стиль окна );

Первый аргумент — это дескриптор окна владельца, то есть родительского окна. Поскольку у нас нет никакого родительского окна, мы укажем значение 0. Второй аргумент — это указатель на текст, который должен быть отображен в диалоговом окне. Обычно это строка, заканчивающаяся нулевым байтом. Третий аргумент аналогичен второму, только он определяет не текст окна, а текст заголовка. Последний аргумент — это тип (стиль) диалогового окна, мы будем использовать символическую константу МВ_ОК. Второй вызов API — это ExitProcess, ему нужно передать всего один аргумент (как в DOS), который определяет код завершения программы. Чтобы упростить разработку Windows-программы на языке ассемблера, под­ключим файл Win32.inc, который определяет все типы аргументов API-функ­ций (например, HWND и LPCTSTR соответствуют простому типу dword) и значения констант. Подключим этот файл:

%include « w i n 3 2 n . i n c » ;

Мы будем использовать АРТфункции, которые находятся в динамических библиотеках, поэтому нам нужно воспользоваться директивами EXTERN и IMPORT:

EXTERN MessageBoxA ;MessageBoxA определен вне программы IMPORT MessageBoxA u s e r 3 2 . d l l ;а именно — в u s e r 3 2 . d l l EXTERN E x i t P r o c e s s / E x i t P r o c e s s определен вне программы IMPORT E x i t P r o c e s s k e r n e l 3 2 . d l l ;а именно - в k e r n e l 3 2 . d l l

Точно так же, как в DOS, нужно создать две секции: кода и данных. SECTION CODE USE32 CLASS^CODE ;наш к о д SECTION DATA USE32 CLASS=DATA ;наши с т а т и ч е с к и е данные

Осталось понять, как можно вызвать функции API. Подробнее мы обсудим это в главе 13, посвященной компоновке программ, написанных частью на ассемблере, а частью на языках высокого уровня, а сейчас рассмотрим только правила передачи аргументов функциям API. 188

Page 190: Ассемблер на примерах. Базовый курс

Глава 11. Программирование в Windows

Соглашение о передаче аргументов называется STDCALL. Аргументы пере­даются через стек в порядке справа налево (так же, как в языке С), а очистка стека входит в обязанности вызванной функции (как в Паскале). Мы помещаем аргументы в стек по команде PUSH, а затем вызываем нужную функцию по команде CALL. Не нужно беспокоиться об очистке стека. Код нашей программы приведен в листинге ILL

Листинг 11.1 . Програц с кнопкб y^j^jilig^iaiiMiiBiii

%include «win32n. о год^кттщча^^ заголовочный (^ai J EXTERN MessageBoxA ;MessageBoxA определен вне программы IMPORT MessageBoxA u s e r 3 2 . d l l ;a именно — в u s e r 3 2 . d l l EXTERN E x i t P r o c e s s ; E x i t P r o c e s s определен вне программы IMPORT E x i t P r o c e s s k e r n e l 3 2 . d l l ;a именно - в k e r n e l 3 2 . d l l SECTION CODE USE32 CLASS-CODE /начало кода . . s t a r t : ;метка для компоновщика,

/указывающая точку входа push UINT МВ_ОК /помещаем в стек последний аргумент.

;тип окна: с единственной кнопкой ОК push LPCTSTR t i t l e / теперь помещаем в стек адрес

/нуль-завершенной строки заголовка push LPCTSTR b a n n e r / адрес нуль-завершенной строки,

/которая будет отображена в окне push HWND NULL / у к а з а т е л ь на родительское окно -

/нулевой: нет такого окна c a l l [MessageBoxA] /вызываем функцию API. Она выведет

/окно сообщения и вернет управление /после нажатия ОК

push UINT NULL /аргумент E x i t P r o c e s s — код возврата c a l l [ E x i t P r o c e s s ] /завершаем процесс SECTION DATA USE32 CLASS^DATA banner db ' H e l l o wor ld! ' ,OxD,OxA,0 /строка сообщения

/с символом EOL t i t l e db ' H e l l o ' , 0 / строка заголовка

Для того, чтобы откомпилировать эту программу, нам нужна версия NASM для Windows, которую можно скачать по адресу: http://nasm.sourceforge.net. NASM создаст объектный файл, который нужно будет скомпоновать в исполняемый файл. Для компоновки мы используем свободно распространяемый компонов­щик alink, который можно скачать по адресу: http://alink.sourceforge,net. Назовем наш файл msgbox.asm. Теперь запустим nasmw с параметром -fobj:

C:\WIN32>NASMW - f o b j msgbox.asm

189

Page 191: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

В результате получим файл msgbox.obj, который нужно передать компонов­щику alink:

С:\WIN32>ALINK -оРЕ m s g b o x

Параметры -о определяет тип исполняемого файла. Родным для Windows является тип РЕ. После компоновки появится файл msgbox.exe, который можно будет запустить.

11.3. Программная совместимость Для процессора, работающего в защищенном режиме, доступен другой специ­альный режим — VM86, обеспечивающий эмуляцию реального режима. Все привилегированные команды (то есть сИ, popf и др.), а также все обращения к периферийным устройствам (команды in и out) перехватываются и эмулиру­ются ядром операционной системы так, что прикладной программе «кажется», что она действительно управляет компьютером. На самом же деле все вызовы функций DOS и BIOS обрабатываются ядром операционной системы.

11.4. Запуск DOS-приложений под Windows Для запуска DOS~пpилoжeния в среде Windows мы используем так называ­емый «Сеанс MS DOS». Для запуска DOS-режима выполните команду cmd (Пуск -^ Выполнить -> cmd). Открывшееся окно работает в режиме VM86 и полностью эмулирует функции DOS. Если файлы компилятора NASM на­ходятся в каталоге C:\NASM, мы можем использовать команду DOS, чтобы перейти в этот каталог:

cd С:\NASM

Помните, что в DOS существует ограничение на длину файлов — все имена файлов должны быть в формате 8+3 (8 символов — имя, 3 — расширение). Для редактирования исходных файлов используйте редактор, который по­казывает номера строк — так будет прош;е найти ошибку. Избегайте также «слишком дружественных» приложений, которые добавляют расширение «.txt» после расширения «.asm».

11.5. Свободные источники информации Мы рекомендуем следующие источники, посвященные программированию на языке ассемблера в Windows:

http://win32asm.cjb.net http://rsl .szif.hu/~tomcat/win32 http://asm.shadrinsk.net/toolbar.html

190

Page 192: Ассемблер на примерах. Базовый курс

Глава 1 Программирование в Linux

Структура памяти процесса

Передача параметров командной строки и переменных окружения

Вызов операционной системы

Коды ошибок

Облегчим себе работу: утилиты Asmutils. Макросы Asmutils

Отладка. Отладчик ALD

Ассемблер GAS

Ключи командной строки компилятора

Page 193: Ассемблер на примерах. Базовый курс

12.1. Введение Linux — современная многозадачная операционная система. Большая часть ядра Linux написана на С, но небольшая его часть (аппаратно-зависимая) на­писана на языке ассемблера. Благодаря портируемости языка С Linux быстро распространилась за пределы х86-процессоров. Ведь для переноса ядра на другую аппаратную платформу разработчикам пришлось переписать только ту самую маленькую часть, которая написана на ассемблере. Как любая другая современная многозадачная система, Linux строго разде­ляет индивидуальные процессы. Это означает, что ни один процесс не может изменить ни другой процесс, ни тем более ядро, вследствие чего сбой одного приложения не отразится ни на других приложениях, ни на операционной системе. В х86-совместимых компьютерах процессы в памяти защищены так называ­емым защищенным режимом процессора. Этот режим позволяет контроли­ровать действия программы: доступ программы к памяти и периферийным устройствам ограничен правами доступа. Механизмы защиты разделены между ядром операционной системы (которому разрешается делать абсолютно все) и процессами (им можно выполнять только непривилегированные команды и записывать данные только в свою область памяти). Защищенный режим также поддерживает виртуальную память. Ядро опе­рационной системы предоставляет все операции для работы с виртуальной памятью. Кроме, конечно, трансляции логических адресов в физические — эта функция выполняется «железом» (см. главу 8). Благодаря виртуальной памяти (и, конечно же, 32-битным регистрам), любой процесс в Linux может адресовать 4 Гб адресного пространства. Именно 4 Гб и выделены каждому процессу. Что имеется в виду? Если сделать дамп памяти от О до самого конца (4 Гб), вы не обнаружите ни кода другого процесса, ни данных другого процесса, ни кода ядра — 4 Гб полностью предоставлены в ваше распоряжение. Ясно, что реальный объем виртуальной памяти будет зависеть от физической памяти и от объема раздела подкачки, но суть в том, что процессы скрыты друг от друга и друг другу не мешают.

192

Page 194: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

12.2. Структура памяти процесса Мы уже знаем, что нам доступны все 4 Гб и ни один процесс не может втор­гнуться в наше адресное пространство. Как же распределена память нашего процесса? Как было сказано в предыдущих главах, программа состоит из че­тырех секций: секция кода, секция статических данных, секция динамических данных (куча) и стек. Порядок, в котором загружаются эти секции в память, определяется форматом исполняемого файла. Linux поддерживает несколь­ко форматов, но самым популярным является формат ELF (Executable and Linkable Format). Рассмотрим структуру адресного пространства ELF-файла. Предположим для простоты, что программа не использует динамических библиотек. Адрес: 0x08048000

.text

.data

.bss

.stack

Исполняемый код

Статические данные (известны во время компиляции)

Динамические данные (так называемая куча)

Свободная память

Стек

OxBFFFFFFF (3 Гб)

Обычно программа загружается с адреса 0x08048000 (примерно 128 Мб). При загрузке программы загружается только одна ее страница. Остальные страни­цы загружаются по мере необходимости (неиспользуемые части программы никогда физически не хранятся в памяти). На диске программа хранится без секций .bss и .stack — эти секции появля­ются только тогда, когда программа загружается в память. Если программа подключает какие-нибудь динамические библиотеки, их модули загружаются в ее адресное пространство, но начинаются с другого адреса (обычно с 1 Гб и выше). Секции этих модулей аналогичны секциям обычной программы (то есть .text, .data, .bss). А что хранится в трехгигабайтном «зазоре» между .bss и .stack, то есть в сво­бодной памяти? Эта память принадлежит процессу, но она не распределена по страницам. Запись в эту область вызовет сбой страницы (page fault) — после этого ядро уничтожит вашу программу. Для создания динамических переменных вам нужно попросить ядро распре­делить соответствующую память, но об это мы поговорим чуть позже.

193

Page 195: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

12.3. Передача параметров командной строки и переменных окружения

Если процессы полностью изолированы друг от друга, то как нам получить параметры командной строки и переменные окружения? Ведь они — собствен­ность другого процесса — оболочки, запустившей нашу программу. Решили эту проблему очень просто: при запуске программы параметры командной строки и переменные окружения помещаются в стек программы. А поскольку стек нашей программы — это наша собственность, мы можем без проблем получить к нему доступ. Рассмотрим структуру стека:

ESP после запуска программы

1 argc argv[0]

argv[1]

argv[argc-1]

NULL

env[0] env[1]

env[n]

[ NULL

Свободная память

Количество параметров (dword) Указатель на имя программы

Указатели на аргументы программы

Конец аргументов командной строки |

Указатели на переменные окружения

Конец переменных окружения

Нужный аргумент легко вытолкнуть из стека по команде POP, а потом запи­сать в какую-нибудь переменную. Первое значение, которое мы получим из стека — это количество аргументов командной строки (argc), второй — ука­затель на имя нашей программы. Если argc > 1, значит, программа была запуш^ена с аргументами и дальше в стеке находятся они. Примеры обработки аргументов командной строки будут рассмотрены после того, как вы научитесь выводить данные на экран.

12.4. Вызов операционной системы Вызвать функции операционной системы DOS можно было с помощью пре­рывания 0x21. В Linux используется похожий метод: прерывание с номером 0x80. Но как мы можем с помощью прерывания добраться до ядра, если оно находится за пределами нашего адресного пространства? Благодаря защищен­ному режиму, при вызове прерывания 0x80 изменяется контекст программы (значение сегментного регистра) и процессор начинает выполнять код ядра. После завершения обработки прерывания будет продолжено выполнение нашей программы.

194

Page 196: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

Подобно DOS, аргументы отдельных системных вызовов (syscalls) передаются через регистры процессора, что обеспечивает небольшой выигрыш в скорости. Номер системного вызова помеш;ается в регистр ЕАХ. Если системный вызов принимает аргументы, то он ожидает их в регистрах ЕВХ, ЕСХ и т.д. По со­глашению для передачи аргументов служит набор регистров в фиксированном порядке: ЕВХ, ЕСХ, EDX, ESI и EDI. Ядро версии 2.4.x и выше допускает также передачу аргумента через регистр ЕВР.

12.5. К о д ы о ш и б о к Возвращаемое системным вызовом значение заносится в ЕАХ. Это код ошибки, определяющий состояние завершения системного вызова, то есть уточняющий причину ошибки. Код ошибки всегда отрицательный. В про­грамме на языке ассемблера, как правило, достаточно отличать успешное завершение от ошибочного, но в ходе отладки значение кода ошибки может помочь выяснить ее причину.

Справочная система Linux содержит man-страницы, описывающие каждый системный вызов вместе с кодами, значениями и причинами всех ошибок, которые он может вернуть.

12.6. Мап-страницы в отличие от DOS и Windows ОС Linux полностью документирована. В ее спра­вочной системе (которая называется Manual Pages — Страницы Руководства) вы найдете исчерпывающие сведения не только обо всех установленных про­граммах, но и обо всех системных вызовах Linux. Конечно, для эффективного использования man-страниц (как и остальной Linux-документации) вам при-дetcя выучить английский, поскольку далеко не все man-страницы переведены на русский язык. Достаточно уметь читать руководства хотя бы со словарем. Предположим, что мы хотим написать простейшую программку, которая завершает свою работу сразу после запуска. В DOS нам нужно было ис­пользовать системный вызов АН=0х4С. Помните? Теперь давайте напишем подобную программку под Linux. Для этого найдите файл unistd.h, который обычно находится в каталоге /usr/src/linux/include/asm:

# i f n d e f _ASM_I3 8 6_UNISTD_H_ # d e f i n e _ASM_I3 8 6_UNISTD_H_ /* * Th i s f i l e c o n t a i n s t h e sys tem c a l l numbers . V # d e f i n e ,NR_exit 1 # d e f i n e NR_fork 2

195

Page 197: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

#define NR_read 3 #define NR_write 4 #define NR_open 5 #define NR_close б

#define _syscalll(type,name,typel,argl) \ type name(typel argl) \ { \ long res; \ asm volatile («int $0x80» \

: «=a» ( res) \ : «0» { NR_##name), «b» ((long)(argl))); \ syscall_return(type, res); \

}

В этом файле вы найдете номера системных вызовов Linux. Нас интересует вызов NR_exit:

#define NR_exit 1

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

man 2 ex i t

Вы увидите содержимое man-страницы: _ЕХ1Т(2) Linux Programmer's Manual _EXIT(2) NAME _exit, _Exit — terminate the current process SYNOPSIS #include <unistd.h> void _exit(int status); #include <stdlib.h> void _Exit(int status); DESCRIPTION The function _exit terminates the calling process «immedi­ately». Any open file descriptors belonging to the process are closed; any children of the process are inherited by process 1, init, and the process's parent is sent a SIGCHLD signal. The value status is returned to the parent process as the process's exit status, and can be col­lected using one of the wait family of calls. The function _Exit is equivalent to _exit. RETURN VALUE These functions do not return.

196

Page 198: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

Системному вызову 'exit' нужно передать всего один параметр (как в DOS) — код возврата (ошибки) программы. Код О соответствует нормальному завер­шению программы. Код нашего «терминатора» будет выглядеть так:

mov е а х Д ; номер системного вызова — e x i t mov e b x , 0 ;код возврата О i n t 0x80 /ВЫЗОВ ядра и завершение текущей программы

12-7. Программа «Hello, World!» под Linux Давайте немного усложним нашу программу — пусть она выведет заветную фразу и завершит работу. Вспомните, что в главе 8 говорилось о файловых дескрипторах стандартных потоков — ввода (STDIN, обычно клавиатура), вывода (STDOUT, обычно экран) и ошибок (STDERR, обычно экран). Наша программа «Hello, World!» должна вывести текст на устройство стандартного вывода STDOUT и завершиться. С точки зрения операционной системы STDOUT — это файл, следовательно, мы должны записать нашу строку в файл. Снова заглянув в unistd.h, находим системный вызов write(). Смотрим его описание:

man 2 w r i t e WRITE(2) Linux Programmer's Manual WRITE(2) NAME write — write to a file descriptor SYNOPSIS #include <unistd.h> s s i ze_ t w r i t e ( i n t fd, const void *buf, s i ze_ t count) ;

Если вы не знаете С, то данная шап-страница, скорее всего, для вас будет не­понятна. Как передать аргументы системному вызову? В языке ассемблера вы можете загрузить в регистр либо непосредственное значение, либо значение какого-то адреса памяти. Для простоты будем считать, что для аргументов, не отмеченных звездочкой, нужно указывать непосредственные значения, а аргументы со звездочкой требуют указателя. Функции write нужно передать три аргумента: дескриптор файла, указатель на строку, данные из которой будут записаны в файл (это buf), и количество байтов этой строки, которые нужно записать. Функция возвратит количество успешно записанных байтов или отрицательное число, которое будет кодом ошибки. Для компилирования нашей программы в объектный файл будем использовать nasm, а для компоновки — компоновщик Id, который есть в любой версии

197

Page 199: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Linux. Для получения объектного файла в формате ELF нужно использовать опцию компилятора -f elf.

Компоновщик Id обычно вызывается с ключами, перечисленными в табл. 12. L

Наиболее часто используемые ключи компоновщика Id Таблица 12.1

Ключ

-o<name>

-S

Назначение

Установить имя выходного (исполняемого) файла <name>

Удалить символьную информацию из файла

Чтобы компоновщик Id определил точку входа программы, мы должны ис­пользовать глобальную метку _start.

Код нашей программы «Hello, World!» приведен в листинге 12.1.

Листинг 12.1. Программа ЖеПо^ World!» под У ПУХ SECTION .text global _start

_start: mov eax,4

mov ebx,1 mov ecx,hello mov edx,len

int 0x80 mov eax,1 mov ebx,0 int 0x80 SECTION .data hello db «Hello,

;указываем компоновщику точку входа. ;это сделать необходимо

;первый аргумент — номер системного ;вызова - write ;константа STDOUT определена как 1 ;адрес выводимой строки ;длина «Hello, World!» вместе с символом ;конца строки ;вызываем ядро для вывода строки ;системный вызов номер 1 — exit ;код завершения программы О ;вызываем ядро для завершения программы

world!»,Оха /наша строка плюс символ ;конца строки ;вычисляем длину строки len equ $ - hello

Теперь откомпилируем программу: nasm -f elf hello.asm

A теперь запустим компоновщик: Id -s -о hello hello.о

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

198

Page 200: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

Теперь осталось запустить программу: ./hello Hello, World!

12.8. Облегчим себе работу; утилиты Asmutils

Asmutils — это коллекция системных программ, написанных на языке ас­семблера. Эти программы работают непосредственно с ядром операционной системы и не требуют библиотеки LIBC. Asmutils — идеальное решение для небольших встроенных систем или для «спасательного» диска. Программы написаны на ассемблере NASM и совместимы с х86-процессорами. Благодаря набору макросов, представляющих системные вызовы, Asmutils легко портируется на другие операционные системы (нужно модифицировать только конфигурационный файл). Поддерживаются следующие операционные системы: BSD (FreeBSD, OpenBSD, NetBSD), UnixWare, Solaris и AtheOS. Для каждой операционной системы Asmutils определяет свой набор симво­лических констант и макросов, побочным эффектом которых оказывается улучшение читабельности ассемблерных программ. В листинге 12.2 можно увидеть, как будет выглядеть наша программа hello с использованием Asmutils.

. |у.рдщцдд~д^^^^ шттшттшшшттттжттшшшшттттщщт

u xxv ^ de «s_ stem, nc» CODESEG ;начало секции кода START: ;начало программы для

;компоновщика sys_write STDOUT,helloДen

;этот макрос записывает в регистры ;аргументы для системного вызова write ;и вызывает write

sys_exit О ;то же самое для системного вызова exit DATASEG ;начало секции данных hello db «Hello, World!»,Оха len equ $-hello END

199

Page 201: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Теперь код нашей программы выглядит намного проще. После подстановки макросов этот код превратится в текст из предыдущего примера и работать будет точно так же. Если вы хотите запустить эту же программу на FreeBSD, модифицируйте только файл MCONFIG. Домашняя страница Asmutils — http://asm.sourceforge.net/asmutils.html, там же вы сможете скачать последнюю версию Asmutils. Пакет Asmutils распростра­няется по Стандартной Общественной лицензии GNU, то есть бесплатно. Рекомендуем вам скачать Asmutils прямо сейчас, поскольку в последующих программах мы будем использовать именно этот пакет. Теперь давайте от­компилируем нашу программу. Убедитесь, что у вас установлен NASM (он входит в состав разных дистри­бутивов — как RedHat-совместимых, так и Debian-совместимых). Распакуйте файл asmutils-0.17.tar.gz. После распаковки получим обычную структуру ка­талогов — /doc, /src и /inc. Целевая платформа задается в файле MCONFIG. Он самодокументирован, так что если вам придется его немного подправить, то вы легко разберетесь, как именно. В каталоге /src есть файл Makefile. В первых строках этого файла указываются имена программ, подлежащих сборке (компиляции и компоновке). Пропиши­те в нем имя выходного файла — hello (без расширения .asm). После этого выполните команду make. В результате получите готовый исполняемый файл (компоновщик запускать уже не нужно).

12.9. Макросы Asmutils Аргументы системного вызова передаются как часть макроса. В зависимости от операционной системы генерируются соответствующие команды ассембле­ра, а аргументы заносятся в соответствующие регистры. Макросы для системных вызовов начинаются с префикса sys_, после этого следует имя системного вызова так, как оно пишется в man-странице. В Linux параметры передаются в том же порядке, в котором они описаны в man-стра-ницах. Системный вызов возвращает значение в регистре ЕАХ. Макрос sys_exit О после подстановки превратится в следующие команды:

mov еахД ;номер системного вызова 1 — ex i t mov ebx,0 ;код возврата О in t 0x80 /вызываем ядро и завершаем программу

Аргумент макроса не обязательно должен быть непосредственным значением. Если вы храните код возврата в переменной rtn, то можете написать макрос sys^exit [rtn], и после подстановки он превратится в:

mov еахД ;номер системного вызова 1 — ex i t mov ebx , [ r tn] ;код возврата — содержится в переменной r tn i n t 0x80 ;вызываем ядро и завершаем программу

200

Page 202: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

Если регистр уже содержит требуемое значение, вы можете пропустить установку аргумента, используя ключевое слово EMPTY — тогда значение данного регистра останется как есть. Пример использования «аргумента» EMPTY будет приведен в программе для копирования файлов.

12.10. Операции файлового ввода/вывода (I/O)

При работе с файлами в Linux помните, что Linux — это UNIX-подобная операционная система, и просто так доступ к файлам вы не получите: у каждого файла есть права доступа, а у вашего процесса должны быть соот­ветствующие полномочия.

Открытие файла

С помощью системного вызова ореп() мы можем не только открыть, но и создать файл, если это необходимо. Во второй секции справочной системы Linux вы найдете полное описание этого вызова (man 2 open). Мы же рас­смотрим его в общих чертах:

i n t open(const char ^pathname, i n t f l a g s ) ; i n t open(const char ^pathname, i n t f l a g s , mode__t mode) ;

Первый аргумент системного вызова содержит указатель на имя файла (нуль-завершенную строку), второй аргумент — это массив битов, определяющий режим открытия файла (чтение/запись и др.). Третий аргумент необязатель­ный — он определяет права доступа для вновь создаваемого файла. Вызов возвращает дескриптор файла или отрицательное значение в случае ошибки. Символические названия битовых флагов представлены в таблице 12.2 (ин­формация из man-страницы).

Символические названия битовых флагов Таблица 12.2

Флаг

0_RDONLY

0_WRONLY

0_RDWR

0_CREAT

0_TRUNC

0_APPEND

1 0_LARGEFILE

Режим открытия

Открывает файл только для чтения

Открывает файл только для записи

Открывает файл для чтения и для записи

Создает файл, если его не существует

Обрезает файл до нулевой длины

Открывает файл для дозаписи, то есть новые данные будут записаны в конец файла (опасно на NFS)

Используется для обработки файлов размером более 4 Гб

201

Page 203: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Если второй аргумент содержит флаг 0_CREAT, то третий аргумент обязателен. Он указывает, какие права доступа будут присвоены новому файлу. Для указания прав доступа можно комбинировать символические имена, из которых чаще всего используются следующие:

Флаг

S_IRWXU SJRGRP S_IROTH

Описание

Владелец может читать, записывать и запускать файл | Члены группы владельца могут читать файл | Все остальные могут читать файл

Откроем файл, имя которого хранится в переменной name, для чтения и за­писи:

sys_open name, 0_RDWR, EMPTY t e s t e a x , e a x ;проверяем EAX на отрицательное значение j s . e r r o r _ o p e n ;более подробно этот пример описывается

;в главе 7

Имя файла можно определить в секции данных с помощью псевдоко­манды DB:

паше DB «iny_f i l e_which_has_a_very_ long_name . t x t » , О

Отдельные флаги могут комбинироваться с помощью оператора | (побитовое OR). Откроем файл для чтения и записи, а если файла не существует, то соз­дадим его, присвоив ему права доступа 700 (чтение/запись/выполнение для владельца, ничего для остальных):

sys_open name, 0_RDWR I 0_CREAT, S_IRWXU t e s t e a x , e a x j s . e r r o r _ o p e n /переходим к e r ro r_open , если произошла ошибка . . . ;в случае успеха в ЕАХ будет дескриптор файла

Закрытие файла

Как и в DOS, файл нужно закрыть с помощью системного вызова. Этот вызов называется cIoseQ, а соответствующий макрос — sys__cIose. Этот макрос тре­бует одного аргумента — дескриптора закрываемого файла. Если дескриптор файла находится в ЕАХ, то для закрытия файла воспользуемся макросом:

sys_close еах

Чтение из файла

Данные из файла читаются блоками различной длины. Если явно не указано иное, то чтение данных начинается с той позиции, на которой закончилась предыдущая операция чтения или записи, то есть последовательно. Для чтения данных из файла используется системный вызов read:

202

Page 204: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

s s i ze_ t r e a d ( i n t fd, void *buf, s ize_ t count ) ; Первый аргумент — дескриптор файла, второй содержит указатель на область памяти, в которую будут записаны прочитанные данные, а третий — макси­мальное количество байтов, которые нужно прочитать. Вызов возвращает либо количество прочитанных байтов, либо отрицательное число — код ошибки. Макрос sys_read можно также использовать для чтения данных с клавиатуры, если в качестве файлового дескриптора передать STDIN — дескриптор по­тока стандартного ввода.

Запись в файл

Функция записи требует таких же аргументов, как и функция чтения: дескрип­тора файла, буфера с данными, которые нужно записать в файл, и количество байтов, которые нужно записать. Вызов возвращает либо количество запи­санных байтов, либо отрицательное число — код ошибки:

s s i ze_ t w r i t e ( i n t fd, const void *buf, s i ze_ t count ) ; Давайте напишем простую программу, читающую последовательность символов с клавиатуры до нажатия клавиши «Enter» и преобразующую их в верхний регистр. Мы ограничимся только латинским алфавитом, то есть первой половиной таблицы ASCII. Работать программа будет в бесконечном цикле — прочитали, преобразовали, вывели, опять прочитали — а прервать ее работу можно будет нажатием комбинации Ctrl -i- С. По нажатии клавиши «Enter» системный вызов read завершит работу и вернет управление нашей программе, заполнив буфер прочитанными символами. Каждый символ из диапазона 'а' — 'z' будет преобразован в 'А' — 'Z'. После преобразования мы выведем получившийся результат на STDOUT. При использовании Asmutils секция кода должна начинаться идентифика­тором CODESEG, секция статических данных — DATASEG, а секция ди­намических данных — UDATASEG. Мы обязательно должны подключить заголовочный файл system.inc (листинг 12.3).

%include «system.inc» %define MAXIDATA 10 CODESEG START: again: ;читаем следующую строку sys__read STDIN, read_data, MAX_DATA test eax^eax ;ошибка? (отрицательное значение EAX)

203

Page 205: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

j S endprog

add е с х , е а х

compare_next : dec ecx cmp byte [ecx],'a'

jb no_conversion cmp byte [ecx], ' ъ' ja no_conversion sub byte [ecx],0x20

;да ? выходим ;нет? EAX содержит количество ;прочитанных символов ;в ЕСХ был указатель на первый символ ;в буфере, ;теперь это позиция последнего ;элемента + 1

;уменьшаем указатель строки ;если символ < 'а', то он вне нашего /диапазона, ;поэтому его преобразовывать не нужно ;если > 'Z' ; с н о в а н е п р е о б р а з у е м ; п р е о б р а з у е м в в е р х н и й р е г и с т р ; в ы ч и т а н и е м 0x2 О

n o _ c o n v e r s i o n : cmp e c x , r e a d _ d a t a j z p r i n t i t

jmp s h o r t compare_next p r i n t i t :

у к а з а т е л ь у к а з ы в а е т на н а ч а л о б у ф е р а ? д а ? З а в е р ш а е м п р е о б р а з о в а н и е , выводим с т р о к у и н а ч е п е р е х о д и м к следуюш;ему с и м в о л у к о л и ч е с т в о п р о ч и т а н н ы х б а й т о в с е й ч а с в ЕАХ. С о х р а н я т ь э т о з н а ч е н и е в промежуточном р е г и с т р е н е нужно , потому ч т о макросы п о д с т а в л я ю т с я с п р а в а н а л е в о , и з н а ч е н и е ЕАХ б у д е т с н а ч а л а з а н е с е н о в EDX, а потом з а м е н е н о н а к о д в ы з о в а w r i t e

s y s _ w r i t e S T D O U T , r e a d _ d a t a , e a x jmp s l i o r t a g a i n e n d p r o g : s y s _ e x i t 2 55 UDATASEG

; п о с л е вывода ч и т а е м с л е д . с т р о к у

;выходим с кодом 2 5 5 / с е к ц и я н е и н и ц и а л и з и р о в а н н ы х п е р е м е н н ы х

r e a d _ d a t a r e s b MAX_DATA END ; к о н е ц программы

Программа ведет себя правильно, даже если ввести строку длиннее, чем пред­усматривает константа MAXIDATA. Ядро операционной системы сохранит лишние символы и передает их программе при следующем вызове sys_read.

Рассмотрим еще один пример. На этот раз мы будем копировать файл А в файл В. Имена файлов мы будем передавать программе как аргументы ко­мандной строки.

Сначала нашей программе нужно прочитать число аргументов из стека: если оно меньше 3, программа должна завершить работу с ошибкой (первый аргу-

204

Page 206: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

мент — это имя нашей программы, остальные два — имена файлов). Затем по команде POP программа прочитает имена файлов. Файл А будет открыт на чтение, а файл В будет создан или «обрезан» (перезаписан). В случае ошибки, программа завершит работу.

Если ошибки нет, программа будет читать блоками файл А, пока не «упрется» в конец файла, и записывать прочитанные блоки в файл В. По окончании работы программа закроет оба файла. Код программы приведен в листинге 12.4.

Листинг 12.4. Программа копирования файла А в файл Ш|||

%include «system.inc» %define BUFF„LEN 409 6 CODESEG START: pop eax ;

cmp eax,3 jae enough_params ; mov eax,255 endprog: sys_exit eax enough_params: pop ebx

;извлекаем в EAX количество аргументов ;командной строки ;их должно быть 3 ;если не меньше, пытаемся открыть файлы ; если меньше, выходим с кодом ошибки 255

;системный вызов для завершения работы

/извлекаем из стека первый аргумент. ;он нам не нужен, поэтому сразу ;извлекаем

pop ebx ; второй — указатель на имя файла А. sy s_open EMPTY, 0__RDONLY I 0_LARGEFILE

;открываем только для чтения test eax,eax ;ошибка? Выходим... j s endprog mov ebp,eax ;дескриптор файла записываем в EBP pop ebx ;извлекаем в EBX имя файла В sys_open EMPTY,0_WRONLYI0_LARGEFILEI0_CREATI0_TRUNC,S_IRWXU

;открываем и перезаписываем, ;или создаем заново с правами 700

test eax,eax js endprog /ошибка? выходим mov ebx,eax ;дескриптор файла В записываем в EBX copy_next: xchg ebp,ebx ;меняем местами EBX и EBP,

;в EBX — дескриптор файла A sys_read EMPTY,buff,BUFF_LEN

/читаем 1й блок данных из файла А test eax,eax /проверка на ошибку

205

Page 207: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

j s e n d _ c l o s e ;ошибка? закрываем файлы и выходим j z e n d _ c l o s e ;конец файла? закрываем файлы и выходим xchg ebp^ebx ;снова меняем местами ЕВР и ЕВХ,

;в ЕВХ - дескриптор файла В s y s _ w r i t e EMPTY,EMPTY,еах

/выводим столько байтов , сколько /Прочитано из файла А

t e s t е а х , е а х j s endprog /ошибка? jmp s h o r t copy_next /копируем новый блок e n d _ c l o s e : s y s _ c l o s e EMPTY /перед завершением закрываем оба файла xchg e b p , e b x /теперь закрываем второй файл s y s _ c l o s e EMPTY jmp s h o r t endprog / все UDATASEG buff r e s b BUFF_LEN /зарезервировано 4 Кб для буфера END

Поиск позиции в файле

Иногда нужно изменить позицию чтения/записи файла. Например, если нам нужно прочитать файл дважды, то после первого прохода намного быстрее будет «перемотать» его, установив указатель позиции на начало файла, чем закрыть файл и открыть его заново.

Изменить позицию следующей операции чтения/записи можно с помощью системного вызова Iseek:

off_t l s e e k ( i n t f i l d e s , off_t o f f s e t , i n t whence)/ Первый аргумент — это, как обычно, дескриптор файла, второй —• смещение — задает новую позицию в байтах, а третий определяет место в файле, откуда будет отсчитано смещение:

• SEEK_SET — смещение будет отсчитываться с начала файла; • SEEK_CUR — с текущей позиции; • SEEK_END — с конца файла.

Системный вызов Iseek возвращает новую позицию — расстояние от начала файла в байтах — или код ошибки.

Небольшой пример: используя Iseek, можно легко вычислить длину файла: sys_lseek [fd]. О, SEEK_END

Вызов «перематывает» файл в конец, поэтому возвращаемое значение — это номер последнего байта, то есть размер файла.

206

Page 208: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

А что случится, если создать файл, переместить указатель за пределы его конца, записать данные и закрыть файл? В DOS место от начала файла до по­зиции записи окажется заполнено случайными байтами. А в UNIX-подобных ОС вместо этого получится «разреженный» файл, логический размер которого больше его физического размера: физическое место под незаписанные данные отведено не будет.

Другие функции для работы с файлами

Файловая система UNIX-подобных операционных систем позволяет создавать файлы типа ссылки — жесткой или символической. Жесткая ссылка — это не что иное, как альтернативное имя файла: одно и то же содержимое, с одним и тем же владельцем и правами доступа, может быть доступно под разными именами. Все эти имена (жесткие ссылки) равноправны, и вы не можете удалить файл до тех пор, пока существует хотя бы одна жесткая ссылка на него. Механизм жестких ссылок не позволяет организовать ссылку на файл, находящийся в другой файловой системе (на другом логическом диске). Для создания жесткой ссылки используется системный вызов link:

in t l i nk (cons t char ^oldpath, const char *newpath); Первый аргумент — оригинальное имя файла, второй — новое имя файла (имя жесткой ссылки). Более гибким решением являются символические ссылки (symlinks). Такая ссылка указывает не непосредственно на данные файла, а только на его имя. Поэтому она может вести на файл на любой файловой системе и даже на несуществующий файл. Когда операционная система обращается к симво­лической ссылке, та возвращает имя оригинального файла. Исключением из этого правила являются операции удаления и переименования файла — эти операции работают со ссылкой, а не с оригинальным файлом. Для создания символической ссылки используется вызов symlink:

i n t symlink(const char ^oldpath, const char ^newpath); Аргументы этого системного вызова такие же, как у системного вызова link. Теперь давайте рассмотрим, как можно удалить и переместить файл. Для удаления файла используется системный вызов unlink, который удаляет файл или одно из его имен. Напомним, что файл удаляется только после удаления последней жесткой ссылки на него. int unlink(const char ^pathname);

Системному вызову нужно передать только имя файла. В случае успеха вызов возвращает О, а если произошла ошибка — отрицательное значение. Для переименования файла используется вызов rename:

in t rename(const char *oldpath, const char ^newpath);

207

Page 209: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Оба аргумента полностью идентичны аргументам вызова link: это исходное и новое имя (точнее, путь) файла. Если новый путь отличается от старого только родительским каталогом, то результатом переименования окажется перемещение файла в другой каталог. Напишем небольшую программу symhard. asm, которая создает одну жесткую и одну символическую ссылку на указанный файл (листинг 12.5). Созданные ссылки будут называться 1 и 2 соответственно. Аргументы программы будут прочитаны из стека. Как обычно, первый элемент стека — количество аргументов, второй — имя программы, остальные — переданные аргументы.

Листинг 12.5. Пример щрщрммшШШ^ШШШШШШШ:-

%include «system.inc» CODESEG START: pop ebx cmp ebx,2

jz ok endprog: sys_exit 0 ok: pop ebx

pop ebx sys_link EMPTY,one sys_symlink EMPTY,two

jmp short endprog DATASEG one DB «1»,0 two DB «2»,0 END

;начало кода

;читаем количество аргументов ;первый — имя программы, второй ~ имя ;файла, для которого будут ;созданы ссылки ;да, переданы все параметры

;выходим из программы

;это имя нашей программы ;нужно ;загружаем в ЕВХ адрес имени ;создаем жесткую ссылку ;и символическую ссылку ;коды ошибок не проверяем ;заканчиваем обработку ;секция данных

;конец

нам оно не

Ьаила

«Пропишите» имя программы в Makefile и запустите make. В качестве ар­гумента вы можете передать имя файла или каталога. По окончании работы программы будут созданы два файла «1» и «2». Файл «1» — это жесткая ссыл­ка, «2» — символическая. Если какой-то из этих файлов не создан, значит, произошла ошибка — например, ваша файловая система не поддерживает этого типа ссылок.

208

Page 210: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

По команде ./symhard ./symhard вы можете создать символическую и жесткую ссылки на сам исполняемый файл программы, а потом с помощью команд Is -1, chown, chmod и rm изучить на них свойства и поведение ссылок разного типа.

12.11. Работа с каталогами Как и в DOS, в Linux есть набор системных вызовов, предназначенных для работы с каталогами. Благодаря Asmutils использовать эти вызовы почти так же просто, как в высокоуровневых языках программирования.

Создание и удаление каталога (MKDIR, RMDIR)

Создать каталог позволяет системный вызов mkdir: i n t mkdir(const char ^pathname, mode_t mode);

Первый аргумент — это указатель на имя файла, а второй — права доступа, которые будут установлены для нового каталога. Права доступа указываются точно так же, как для системного вызова open. Программка из листинга 12.6 создаст новый каталог my_directory в ката­логе /tmp.

[Листинг 12,6: П

%include «system.inc» CODESEG START: sys_mkdir name, S_IRWXU sys_exi t 0 DATASEG name DB «/tmp/my„directory END

/начало секции кода /начало программы /создаем каталог, права 07 00 /завершаем программу

О

Права доступа могут быть указаны также в восьмеричной форме. В отличие от языка С (и команды chmod) они записываются немного по-другому: без предваряющего нуля и с символом q в конце. Например, права доступа 0700 в восьмеричной системе будут выглядеть как 700q. Для удаления каталога используется системный вызов RMDIR, которому нужно передать только имя каталога:

in t rmdir(const char ^pathname)/

209

Page 211: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Смена текущего каталога (CHDIR)

Для смены текущего каталога используется системный вызов chdir: int. chdir (const char *path) ;

Чтобы в предыдущем примере перейти в только что созданный каталог, до­пишите перед вызовом макроса sys__exit вызов sys_chdir name

Определение текущего каталога (GETCWD)

Системный вызов getcwd, возвращающий рабочий каталог, появился в ядре Linux версии 2.0 (он есть и в современных версиях 2.4-2.6). Этот системный вызов принимает два аргумента: первый — это буфер, в который будет записан путь, а второй — количество байтов, которые будут записаны:

long sys_getcwd(char *buf, unsigned long s ize) Вот фрагмент программы, выводящей текущий каталог: sys_getcwd path,PATHSIZE

mov esi,ebx xor edx,edx .next: inc edx lodsb or al,al jnz .next mov byte [esi-1] , n

sub esi,edx sys_write STDOUT,esi,EMPTY

sys_exit_true

;записываем в переменную path имя ;текущего каталога ;сохраняем указатель в ESI ; сбрасываем EDX

в EDX считаем длину строки path читаем символ в AL, увеличиваем ESI конец строки? нет? Читаем следующий символ заменяем нулевой байт на символ конца строки заново получаем адрес строки выводим текущий каталог на STDOUT длина строки уже находится в EDX макрос для нормального завершения

12.12. Ввод С клавиатуры. Изменение поведения потока стандартного ввода.

Системный вызов lOCTL в этой главе мы уже рассматривали один способ ввода с клавиатуры — с помощью системного вызова read. Этот системный вызов ожидает нажатия клавиши «Enter» и только после этого возвращает прочитанную строку. Ино­гда полезно читать символы по одному или не дублировать их на устройство стандартного вывода (например, при вводе пароля). Для изменения поведения потока стандартного ввода служит системный вызов IOCTL. 210

Page 212: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

Драйверы устройств не добавляют новых системных вызовов для управления устройствами, а вместо этого регистрируют наборы функций управления, вы­зываемых через системный вызов IOCTL. IOCTL — это сокращение от Input/Output Control — управление вводом/ выводом. Чтобы описать все виды IOCTL, нужно написать другую книгу или даже несколько книг, поэтому сосредоточимся только на терминальном вводе/выводе. В Linux мы можем управлять поведением терминала (то есть клавиатуры и экрана), используя две IOCTL-функции — TCGETS и TCSETS. Первая, TCGETS, получает текущие настройки терминала, а вторая — устанавливает их. Подробное описание обеих функций может быть найдено в man-странице termios. Структура данных, описывающая поведение терминала, доступна из Asmutils. Если нам нужно читать с клавиатуры символ за символом, не отображая при этом символы на экране, нам нужно изменить два атрибута терминала — ICANON и ECHO. Значение обоих атрибутов нужно установить в 0. Мы прочитаем эти атрибуты с помощью функции TCGETS, изменим значение битовой маски и применим новые атрибуты с помощью функции TCSETS. Для работы с терминалом нам понадобится структура B_STRUC, которая описана в файле system.inc.

mov edx , t e rma t t r s ;заносим адрес структуры в EDX sys_ ioc t l STDIN,TCGETS /загружаем структуру mov еах, [ t e rma t t r s . c__lf lag] /читаем флаги терминала push еах ;и помещаем их в стек and еах,~(ICANONI ECHO) ;переключаем флаги ECHO и ICANON mov [ t e r m a t t r s . c _ l f l a g ] , е а х /заносим флаги в струтуру sys_ ioc t l STDIN, TCSETS /устанавливаем опции терминала pop dword [ te rmat t r s .c_ l f lag] /загружаем старые опции

Сама структура должна быть объявлена в сегменте UDATASEG: t e rma t t r s B_STRUC t e r m i o s , . c _ l f l a g

К сожалению, более простого способа изменить настройки терминала нет.

12.13. Распределение памяти При запуске новой программы ядро распределяет для нее память, как указано во время компиляции. Если нам нужна дополнительная память, мы должны попросить ядро нам ее выделить. В отличие от DOS, вместо того, чтобы за­прашивать новый блок памяти определенного размера, мы просим увеличить секцию .bss (это последняя секция программы, содержащая неинициализиро­ванные переменные). Ответственность за всю свободную и распределенную в секции .bss память несет сам программист, а не ядро операционной системы.

211

Page 213: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Если мы не хотим использовать С-библиотеку для распределения памяти, мы должны сами программировать управление памятью (проще всего использо­вать heap.asm из Asmutils). Системный вызов, предназначенный для увеличения или уменьшения секции .bss, называется Ьгк: void * brk(void *end__data_seginent) ;

Ему нужно передать всего один параметр — новый конечный адрес секции .bss. Функция возвращает последний доступный адрес в секции данных .bss. Чтобы просто получить этот адрес, нужно вызвать эту функцию с нулевым указателем. Обычно Ьгк используется следующим образом:

sys_brk О ;получаем последний адрес add еах,сколько_еще_байтов_мне_нужно

;увеличиваем это значение sys_brk еах ;устанавливаем новый адрес

После этого секция .bss будет увеличена на указанное количество байтов.

12.14. Отладка. Отладчик ALD в UNIX-подобных операционных системах стандартным инструментом от­ладки является gdb, но из-за своей громоздкости он больше подходит для отладки программ на языке С, а не на языке ассемблера. В нашем случае лучше использовать более компактный отладчик, разработанный специаль­но для программ на языке ассемблера — отладчик ALD (Assembly Language Debugger). Это компактный, быстрый и бесплатный отладчик, распространяющийся по Стандартной Общественной лицензии GNU. Скачать его можно с сайта http://aId.sourceforge.net. Пока он поддерживает только х86-совместимые процессоры и только ELF в качестве формата исполняемых файлов, но вам этого должно хватить. Давайте рассмотрим работу с ALD на примере нашей программы для преоб­разования символов из нижнего регистра в верхний. Запустите отладчик с помощью команды aid:

aid Assembly Language Debugger 0.1.3 Copyright (C) 2000-2002 Patrick Aiken ald>

Теперь загрузим нашу программу convert: ald> load convert echo: ELF I n t e l 80386 (32 b i t ) , LSB, Executable, Version 1

212

Page 214: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

(current) Loading debugging symbols...(no symbols found) ald>

Как и в любом другом отладчике, важнейшей из всех команд для нас явля­ется команда пошагового прохождения программы. В ALD она назыавется S (step). Она выполняет одну команду программы и возвращает управление отладчику:

a ld> S еах = 0x00000000 еЬх = 0x00000000 есх = 0x00000000 edx = 0x00000000 esp - OxBFFFFSCC ebp ---- 0x00000000 esi - 0x00000000 edi = 0x00000000 ds - 0x0000002B es - 0x0000002B fs = 0x00000000 gs = 0x00000000 ss - 0x0000002B cs - 0x00000023 eip = 0x08048082 eflags =0x000000346 Flags: PF ZF TF IF 0 8 0 4 8 0 8 2 5A p o p e d x

Следующая команда, которая будет выполнена, — pop edx. Она расположена в памяти по адресу 0x8048082. В регистре признаков установлен флаг нуля — ZF (остальные флаги нам не нужны). Чтобы снова выполнить последнюю введенную команду (в нашем случае s), просто нажмите «Enter». Выполняйте пошаговое прохождение, пока не дойдете до команды int 0x80, которая про­читает строку со стандартного ввода (ЕАХ = 0x00000003):

a ld> еах = 0x00000003 еЬх = 0x00000000 есх - 0х080490С8 edx = ОхОООООООА esp - 0XBFFFF8D0 ebp - 0x00000000 esi = 0x00000000 edi - 0x00000000 ds = 0x00000023 es - 0x0000002В fs - 0x00000000 gs = 0x00000000 ss = 0X0000002B' cs = 0x00000023 eip = 0x0804808D eflags =0x00000346 Flags: PF ZF TF IF 0804808D CD80 i n t 0x80

Значение регистра EDX (ОхОООООООА = lOd) ограничивает максимальную длину строки 10 символами. В ЕСХ находится указатель на область памяти, в которую будет записана прочитанная строка. Мы можем просмотреть со­держимое памяти с помощью команды е (examine): е есх:

a ld> е есх Dumping 64 bytes of memory starting at 0x080490C8 in hex 080490C8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

080490D8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

080490E8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

080490F8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

213

Page 215: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Теперь снова введем команду отладчика s — этим мы выполним команду про­граммы int 0x80. Программа будет ждать, пока вы введете строку и нажмете «Enter». Сейчас снова введите е есх — посмотрим, что за строку мы ввели:

a ld> е есх Dumping 64 bytes of memory starting at 0x080490C8 in hex 080490C8: 61 73 6D 20 72 75 6C 65 7A OA 00 00 00 00 00 00 asm rulez 0 8 0 4 9 0 D 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0 8 0 4 9 0 E 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0 8 0 4 9 0 F 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Используя команду с, мы можем выполнить оставшуюся часть программы без остановок. Она преобразует и выведет строку и снова будет ожидать ввода. Напоминаю, что мы написали программу так, что прервать ее можно только комбинацией клавиш Ctrl+С. Команда help выводит список всех доступных команд отладчика, а help имя_команды — справку по указанной команде. В табл. 12.3 сведены наиболее полезные команды отладчика ALD.

Наиболее полезные команды отладчика ALD Таблица 12.3

Команда

load <filename>

set args <args>

step [n]

next [n]

disassemble

continue

exannine

register

help

break <addr>

Ibreak

quit

Назначение

Загружает файл (программу) в отладчик

Устанавливает аргументы программы

Выполняет одну или несколько (п) команд программы. Вместо step можно использовать сокращение s

Подобна step, но каждая подпрограмма выполняется за один шаг

Преобразовывает машинньт код команд обратно в символические имена языка ассемблера. Можно использовать сокращение d. Аргументом команды служит начальны!^ адрес в программе, например d 0x08048061

Продолжает выполнение программы (можно использовать сокращение с)

Дамп памяти — выводит содержимое памяти начиная с указанного адреса в указанном формате и системе счисления. Можно использовать сокращение е. В качестве аргумента можно указать непосредственное значение адреса или регистр, содержащий адрес, например, е edx или е 0x08048000

Выводит значения всех регистров

Выводит список команд. Если нужно получить справку по той или иной команде, укажите ее имя как аргумент, например, help examine

Устанавливает точку прерывания (breakpoint) по адресу addr

Выводит список установленных точек прерывания

Выход. Можно использовать сокращение q

Отладчик ALD поддерживает установку точек прерывания (они же — кон­трольные точки, breakpoints). Когда программа во время выполнения «дохо-

214

Page 216: Ассемблер на примерах. Базовый курс

Глава 12. Программирование в Linux

дит» до точки прерывания, ее выполнение приостанавливается и управление передается отладчику. Таким способом можно проверить значения регистров или памяти в ключевых точках программы без пошагового выполнения. Последняя на момент перевода книги версия отладчика 0.1.7 уже поддержи­вает работу с символьной информацией (именами меток и переменных), что очень помогает при отладке больших программ. Напомним, что символьная информация может быть «упакована» в испол­няемый файл с помош ью ключа -g компилятора nasm. А при использовании Asmutils в файле MCONFIG нужно указать опцию DEBUG=у.

12.15. Ассемблер GAS в этом параграфе мы вкратце рассмотрим «родной» ассемблер мира UNIX — GAS. Он используется вместе с компилятором gcc, когда в коде С-программы есть ассемблерные вставки. Будучи «заточен» под сотрудничество с gcc, этот ассемблер не имеет развитых средств обработки макросов. Еще один его недостаток: при описании ошибок он очень лаконичен. Синтаксис GAS заметно отличается от синтаксиса NASM: NASM-подобные (то есть MASM и TASM) компиляторы используют синтаксис Intel, а GAS использует синтаксис AT&T, который ориентирован на отличные от Intel чипы. Давайте рассмотрим программу «Hello, World!», написанную в синтаксисе AT&T (листинг 12.7).

Листинг |1дИ|0граймё^ ад написанная на ассемблере GAS .data # секция данных msg: .ascii «Hello, world!\n»# наша строка len = . - msg # ее длина .text # начало секции кода

# метка „start — точка входа, # то есть

.global „start # начало программы для компоновщика

„start:

movl $len,%edx movl $msg,%ecx movl $l,%ebx

# выводим строку на s t d o u t : # третий аргумент - длина строки # второй - указатель на строку # первый ~ дескриптор файла STDOUT - 1

215

Page 217: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

movl $4,%еах i n t $0x80

movl $0,%ebx movl $ 1 Д е а х i n t $0x80

# номер системного вызова 'write' # вызываем ядро # завершаем работу: # помещаем код возврата в ЕВХ # номер системного вызова 'exit' в ЕАХ # и снова вызываем ядро

12.16. Свободные источники информации Если вы заинтересовались программированием на языке ассемблера под Linux, рекомендуем сайт http://!inuxassembly.org. Здесь вы найдете не только ссылки на различные программы (компиляторы, редакторы), но и различную документацию по использованию системных вызовов. На этом же сайте вы найдете самый маленький в мире Web-сервер, занимающий всего 514 байтов (ищите httpd.asm).

12.17. Ключи командной строки компилятора Чаще всего компилятор NASM вызывается со следующими ключами:

Ключ

-V

-f <f mt>

-fh

-о <name>

1 -1 <path>

Назначение

Вывести номер версии компилятора

«Упаковать» символьную информацию

Установить формат выходного файла <fmt> (см. главу 9)

Вывести список всех поддерживаемых форматов

Задает имя выходного файла <name>

Добавляет каталог для поиска «упакованных» файлов

216

Page 218: Ассемблер на примерах. Базовый курс

Компоновка — стыковка ассемблерных программ с программами, написанными на языках высокого уровня

Передача аргументов

Что такое стек-фрейм?

Компоновка с С-программой

Компоновка с Pascal-программой

Page 219: Ассемблер на примерах. Базовый курс

До сих пор при написании своих программ мы использовали только функции операционной системы, не обращаясь ни к каким библиотекам. В этой главе мы поговорим о «стыковке» нашей программы с программами, написанными на языках высокого уровня, а также с различными библиотеками. Поскольку различных языков высокого уровня очень много, мы ограничимся только языками С и Pascal.

13.1. Передача аргументов Языки программирования высокого уровня поддерживают два способа пере­дачи аргументов подпрограммам (функциям, процедурам): по значению и по ссылке. В первом случае значение подпрограммы не может изменить значения переданной переменной, поэтому способ «по значению» служит для передачи данных только в одном направлении: от программы к подпрограмме. Во втором случае подпрограмма может изменить значение переменной, то есть передать значение в основную программу. Аргументы подпрограмм в языках высокого уровня всегда передаются через стек и никогда — через регистры процессора. Подпрограммы работают с переданными аргументами так, как если бы они были расположены в памяти, то есть как с обычными перемен­ными — программисту не нужно ни извлекать самостоятельно аргумент из стека, ни помещать его в стек. С точки зрения программиста на языке ассемблера, подпрограммы вызываются с помощью команды CALL, а возврат из подпрограммы осуществляется с по­мощью команды RET. Вызову команды CALL обычно предшествуют одна или несколько команд PUSH, которые помешают аргументы в стек. После входа в подпрограмму (то есть после выполнения CALL) в стеке выделяется опреде­ленное место для временного хранения локальных переменных. Определить позицию аргумента в стеке очень просто, поскольку во время компиляции известен порядок следования аргументов. Пространство для локальных переменных в стеке выделяется простым умень­шением указателя вершины стека (E)SP на требуемый размер. Поскольку стек нисходящий, то, уменьшая указатель на вершину стека, мы получаем дополни­тельное пространство для хранения локальных переменных. Нужно помнить,

218

Page 220: Ассемблер на примерах. Базовый курс

Глава 13. Компоновка — стыковка ассемблерных программ

что значения локальных переменных при их помещении в стек не определены, поэтому в подпрограмме нужно первым делом их инициализировать. Перед выходом из подпрограммы нужно освободить стек, увеличив указатель на вершину стека, а затем уже выполнить инструкцию RET. Различные языки высокого уровня по-разному работают со стеком, поэтому в этой главе мы рассмотрим только С и Паскаль.

13.2. Что такое стек-фрейм? Доступ к локальным переменным непосредственно через указатель вершины стека (Е) SP не очень удобен, поскольку аргументы могут быть различной длины, что усложняет нашу задачу. Проще сразу после входа в подпрограмму сохранить адрес (смещение) начала стека в некотором регистре и адресовать все параметры и локальные переменные, используя этот регистр. Идеально для этого подходит регистр (Е)ВР. Исходное значение (Е)ВР сохраняется в стеке после входа в подпрограмму, а в (Е)ВР загружается указатель вершины стека (E)SP. После этого в стеке отводится место для локальных переменных, а переданные аргументы до­ступны через (Е)ВР.

(E)SP вершина стека

(Е)ВР

врем.хранение

лок. переменные

исходный (Е) ВР

возвр.значение

аргументы

врем, хранение

лок. переменные

исходный (Е) ВР

возвр.значение

аргументы

• • •

<-

у

> вложенные функции

Рис. 13.1. Распределение стека (stackframe)

219

Page 221: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

«Окно» стека, содержащее аргументы, возвращаемое значение, локальные переменные и исходное значение (Е)ВР, называется стек-фреймом. Чем больше глубина вложенности подпрограммы (то есть количество под­программ, вызывающих одна другую), тем больше окон в стеке. Исходное значение (Е)ВР, сохраненное в стеке, указывает на предыдущий стек-фрейм. Получается связный список окон, который при отладке служит для отслежи­вания вызовов функций.

13.2.1. Стек-фрейм в языке С (32-битная версия) Рассмотрим передачу аргументов функции в языке С. Следующая программа откомпилирована в 32-битном окружении, а именно в операционной системе Linux:

i n t a d d i t ( i n t а , i n t b) { i n t d = a + b ; r e t u r n d; } i n t ma in (vo id ) { i n t e ; e = a d d i t ( 0 x 5 5 , 0 x A A ) ; }

Основная программа передает два значения 0x55 и ОхАА подпрограмме addit, которая их складывает и возвращает сумму. Вот листинг основной программы (функции main) на языке ассемблера:

push ebp mov dword ebp , esp sub dword e s p , 0x18 add dword e s p , Oxf f f f f f fS push Oxaa push 0x55 c a l l n e a r +0xf f f f f f c7 add dword e s p , 0x10 mov dword eax , eax mov dword [ebp+Oxfc] , eax m.ov dword e s p , ebp pop ebp r e t n

Функция main первым делом сохраняет исходное значение EBP (указатель на предыдущий фрейм) в стеке. Вторая команда копирует в ЕВР текущее значе­ние ESP, начиная этим новый фрейм. Две следующие команды, SUB и ADD, отнимают значения 0x18 и 0x8 от указателя вершины стека, выделяя место для локальных переменных. Обратите внимание, что вычитание 8 записано как прибавление отрицательного числа в дополнительном коде.

220

080483F4 080483F5 080483F7 080483FA 080483FD 08048402 08048404 08048409 0804840С 0804840Е 08048411 08048413 08048414

55 89Е5 83ЕС18 83C4F8 68АА000000 6А55 E8C7FFFFFF 83С410 89С0 8945FC 89ЕС 5D СЗ

Page 222: Ассемблер на примерах. Базовый курс

Глава 13. Компоновка — стыковка ассемблерных программ

Аргументы передаются подпрограмме справа налево, то есть обратно тому порядку, в котором они записаны в исходном коде на С. Это сделано для того, чтобы разрешить использование в языке С функций с переменным ко­личеством аргументов. Пара команд PUSH помещает наши значения (0x55 и ОхАА) в стек. Следующая команда, CALL, вызывает подпрограмму addit. Рассмотрим окно стека после входа в addit. Сделаем это в табличной форме (см. табл. 13.1).

Окно стека после входа в addit Таблица. 13.1

Адрес стека

1 0XBFFFF860 = ESP

1 0XBFFFF864

0XBFFFF868

0XBFFFF86C -0xBFFFF84

0XBFFFF888

0xBFFFF88C = EBP

0XBFFFF890

Значение по адресу

0x08048409 (адрес возврата в основную функцию)

0x00000055

ОхООООООАА

временная память функции main

не определено: локальная переменная е

0XBFFFF8C8 — исходный ЕВР какой-то функции библиотеки LIBC

0х40039СА2 — адрес возврата в библиотечную функцию

Вершина стека содержит 32~битный адрес возврата, то есть адрес той команды в теле вызывающей функции, которая следует сразу после команды CALL. За ним следуют два 32-битных значения — это аргументы, переданные функции addit. Для локальной переменной е зарезервировано место в окне стека, но ее значение пока не определено. За ней следует значение регистра ЕВР, ко­торое он имел сразу перед вызовом функции main, а за ним — адрес возврата в библиотеку ИЬс, откуда была вызвана функция main. Подробности вызова самой main для нас сейчас не важны. Вот листинг функции addit:

080483D0 080483D1 080483D3 080483D6 080483D9 080483DC 080483DF 080483Е2 080483Е5 080483Е7 080483Е9 080483F0 080483F2 080483F3

55 89Е5 83ЕС18 8В4508 8В550С 8D0C02 894DFC 8B55FC 89D0 ЕВ07 8DB42600000000 89ЕС 5D СЗ

p u s h e b p mov dword e b p , e s p s u b d w o r d e s p , 0x18 mov e a x , d w o r d [ e b p + 0 x 8 ] mov e d x , d w o r d [ebp+Oxc] l e a ecx, [ e a x + e d x ] mov d w o r d [ e b p + O x f c ] , e c x mov e d x , d w o r d [ e b p + O x f c ] mov d w o r d e a x , e d x jmp s h o r t +0x7 l e a e s i , [ e s i+OxO]

mov d w o r d e s p , e b p p o p e b p r e t n

221

Page 223: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Начало кода addit выглядит точно так же. Сначала она сохраняет в стеке исходное значение ЕВР, указывающее на стек-фрейм функции main. Затем создается новое окно стека для функции addit: в ЕВР загружается значение ESP. Команда SUB резервирует в стеке место для локальной переменной d, которая будет использоваться для хранения суммы a-f-b. Рассмотрим состояние стека до выполнения первой собственной команды подпрограммы addit, то есть после выполнения команды SUB (табл. 13.2).

Состояние стека до выполнения первой собственной команды подпрограммы addit Таблица 13.2

1 Адрес стека

|0xBFFFF844=ESP

1 0XBFFFF848 - 0xBFFFF854

I 0xBFFFF858 = EBP — 4 (+OxFFFFFFFC)

0xBFFFF85C = EBP

|0xBFFFF860 = EBP + 4

|0xBFFFF864=EBP + 8

|0xBFFFF868 = EBP + 0xC

1 0xBFFFF86C - 0xBFFFF84

10xBFFFF888

0xBFFFF88C

0XBFFFF890

Значение no адресу

не определено: выравнивание

не определено: выравнивание

не определено: локальная переменная d

0xBFFFF88C ~ исходный ЕВР функции main (указатель на ее стек-фрейм)

0x08048409 (адрес возврата в функцию main)

0x00000055 1

ОхООООООАА 1

временное хранилище функции main

не определено: локальная переменная е из функции main

0xBFFFF8C8 — исходный ЕВР какой-то функции библиотеки LIBC

0х40039СА2 — адрес возврата в библиотечную функцию

Как видите, в стеке есть несколько двойных слов (dword) — они используются исключительно для выравнивания памяти по адресам, кратным степени двой­ки. За ними следует двойное слово, отведенное для переменной d, значение которой пока не определено. После этого следует указатель на окно стека функции main. Далее следуют аргументы подпрограммы addit, место для ло­кальной переменной функции main и данные для возврата из самой main. Чтение аргументов функции addit в регистры ЕАХ и EDX выполняют сле­дующие команды:

080483D6 8В4508 mov еах , dword [ebp+0x8] 080483D9 8В550С mov edx, dword [ebp+Oxc]

Почему программа ищет первый аргумент именно по смещению 0x8? По­тому что описание стек-фрейма требует, чтобы аргументы подпрограммы начинались на «расстоянии» 8 байтов (то есть два элемента стек-фрейма) от значения ЕВР. Следующий аргумент находится на расстоянии ОхС байтов, то есть 8 + 4 = 12 байтов.

В табл. 13.3 перечислены адреса в стеке ЕВР (смещения относительно ЕВР), важные с точки зрения программиста.

222

Page 224: Ассемблер на примерах. Базовый курс

Глава 13. Компоновка — стыковка ассемблерных программ

Важные адреса в стеке ЕВР Таблица 13,3

Адрес

1 [еЬр - 4]

[еЬр + 0]

[еЬр + 4]

1 [еЬр + 8]

[еЬр + ОхС]

Назначение j

Место для локальной переменной d

Оригинальное значение ЕВР

Адрес возврата

Первый аргумент

Второй аргумент

Первый аргумент функции addit равен 0x55. Помещенный в стек последнрш, теперь он находится ближе к вершине стека ЕВР, то есть извлекается из стека первым. Оставшаяся часть функции addit вычисляет сумму аргументов, загруженных в ЕАХ и EDX, с помощью команды LEA. Результат подпрограммы ожидается в ЕАХ. После завершения addit ее стек-фрейм будет разрушен следующими командами: 080483F0 080483F2

89ЕС 5D

mov dword e s p , ebp pop ebp

Первая команда «теряет» локальные переменные и выравнивание, а вторая восстанавливает исходный стек-фрейм функции jnain. Удаление переданных в стек аргументов входит в обязанности вызывающей функции (main), потому что только ей известно их количество. Смещение переданного аргумента относительно ЕВР зависит от его типа. Значение, тип которого имеет размер меньший двойного слова (char, short), выравнивается по адресу, кратному 4 байтам.

13.2.2. Стек-фрейм в языке С (16-битная версия) Все, что было сказано о стек-фрейме, остается в силе и для 16-битных версий языка С, но со следующими очевидными отличиями:

• минимальный размер передаваемого через стек значения — не двойное слово (4 байта), а слово (2 байта);

• вместо 32-битных регистров используются их 16-битные версии (т.е. ВР вместо ЕВР, SP вместо ESP и т.д.);

• возвращаемое функцией значение ожидается не в ЕАХ, а в паре DX:AX. Заметьте, что в своей ассемблерной программе вы можете использовать лю­бые 32-битные команды, если они доступны (то есть если вы не собираетесь выполнять программу на древнем 80286 процессоре). В 16-битном мире у нас будут небольшие проблемы из-за различных способов вызова функций. В С используются так называемые модели памяти, которые задают максимальные размеры секций кода и данных программы. Компиляция

223

Page 225: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

С-программы с моделью памяти medium, large или huge сгенерирует для вызова функций команды CALL FAR, а с остальными моделями — CALL NEAR. «Ближний» вызов сохраняет в стеке только смещение следующей команды, поскольку предполагается, что сегментная часть адреса сохранится и так. От­сюда следует, что «расстояние» от вершины стека ВР до первого аргумента будет на 2 байта меньше, чем при использовании вызова FAR. Для возврата из FAR-ВЫЗОВОВ служит команда RETF, а не RETN. Компилятор NASM содержит набор макросов, позволяющих решить про­блему разных моделей памяти, что значительно облегчает компоновку с С- и Паскаль-программами.

13.3. Компоновка с С-программой Большинство компиляторов с снабжает глобальные символы из пространства имен С префиксом «_». Например, функция printf будет доступна програм­мисту на ассемблере как _printf. Исключением является только формат ELF (использующийся в Linux), не требующий символа подчеркивания. Давайте напишем небольшую С-программу, вызывающую функцию printit, которая вычисляет сумму глобальной переменной plus и единственного аргу­мента и выводит ее на экран. Функцию printit напишем на языке ассемблера, а из нее вызовем библиотечную функцию printf. Писать и запускать программу будем в операционной системе Linux. С-часть нашей программы очень проста:

const in t plus == б; void p r i n t i t ( i n t ) ; i n t main(void) { p r i n t i t ( 5 ) ; }

Мы определили глобальную константу plus и присвоили ей значение 6. Затем идет объявление прототипа функции printit. После этого — функция main, вызывающая функцию printit с аргументом 5. В ассемблерной программе нам нужно объявить используемые нами глобаль­ные символы plus и printf: extern plus extern printf

Поскольку мы собираемся использовать компилятор gee и формат исполняе­мого файла ELF, нам не нужно возиться с символами подчеркивания. Благодаря следующей директиве include нам доступны макросы ргос, arg и endproc, которые облегчают написание нашей функции:

%include «misc/c32.mac» 224

Page 226: Ассемблер на примерах. Базовый курс

Глава 13. Компоновка — стыковка ассемблерных программ

Макрос ргос определяет начало функции printit. Подстановка превращает его в две команды: push ebp и mov ebp^esp. Следующий макрос arg определяет наш единственный аргумент. Если нужно передать несколько аргументов, то первый arg будет соответствовать крайнему слева аргументу в коде С, Размер аргумента по умолчанию — 4 байта (определен в с32.тас).

ргос printit %$what arg

Оставшаяся часть программы понятна: mov еах,[ebp + add еах,[plus]

push еах

push strl

call printf endproc

%$whatj читаем первый аргумент из стека прибавляем глобальную переменную plus сохраняем в стеке последний аргумент функции printf первый аргумент -~ строка формата вывода вызываем p r i n t f макрос endproc восстанавливает ЕВР, уничтожает все локальные переменные (у нас PIX нет) и выхо^-гг из подпрограммы

Вся программа представлена в листинге 13.1.

Листинг 13 .1 . Пример к6М|1<>1Йр^щШ %include «misc/c32.mac» section .text extern plus extern printf global printit proc printit %$what arg mov eax,[ebp + %$what] add eax,[plus] push eax

push strl call printf endproc

section .data strl db «SUM - %d.»,OxOA

подключаем макросы начало секции кода объявляем глоб. переменьгую plus объявляем глоб. функцию printf экспортируем функцию printit начало функции printit, пришшающей один аргумент по имени what читаем из стека аргумент прибавляем глобальщ^ю переменную plus сохраняем в стеке последний аргумент функции printf первый аргумент — строка формата вывода вызываем printf макрос endproc восстанавливает ЕВР, уничтожает все локальные переменные {у нас PDC нет) и вькодит из подпрограммы секция данных 0x0 ;строка формата, завершенная

;символом перевода ; строки и н левым байтом

225

Page 227: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Сохраним нашу программу в файле printit.asm и откомпилируем его: nasm - f e l f p r i n t i t . a s m

Исходный код С-программы сохраним в файле main.с и откомпилируем его с помощью gcc, скомпоновав с модулем printit.o:

gcc -о p r i n t i t main.с p r i n t i t . o Запустив полученную программу printit, мы увидим, что она выводит «SUM = 11». Чтобы собрать 16-битную версию программы, нужно подключить другой за­головочный файл — с16.тас. Макросы ргос и endproc сами решают проблему с NEAR и FAR-вызовами. При использовании FAR-модели памяти нужно указать:

% d e f i n e FARCODE

Обо всем остальном позаботятся макросы. Если вы используете компилятор, требующий символов подчеркивания, вам поможет небольшой макрос: %macro cglobal 1 global _%1 %define %1 _%1 %endmacro %macro cextern 1 extern _%1 %define %1 _%1 %endmacro

После этого вы можете использовать директивы cglobal и cextern вместо обыч­ных global и extern. Новые версии директив автоматически будут добавлять символы подчеркивания.

13.4. Компоновка с Pascal-программой в общих чертах механизм передачи аргументов в Паскале не отличается от только что рассмотренного механизма в языке С. Аргументы передаются через стек, оба типа подпрограмм (процедуры и функции) используют стек-фрейм (или его 16-битную версию). Переменные и аргументы доступны через регистр ВР. Паскаль изначально разрабатывался как язык для обучения программированию, поэтому он не поддерживает некоторых конструкций языка С — например, функций с переменным количеством аргументов. Поэтому компилятор Паскаля помещает аргументы в стек слева направо, а не в обратном порядке, как в С. Вызов внешних подпрограмм на Паскале из ассемблерной программы упро­щен, потому что в Паскале используются только FAR-вызовы. «Генеральная уборка» стека — забота не вызывающей программы, а вызываемой функции,

226

Page 228: Ассемблер на примерах. Базовый курс

Глава 13. Компоновка — стыковка ассемблерных программ

которая должна выполнить команду retf с аргументом, указывающим, сколько байтов выбросить из стека. Рассмотрим таблицу размещения стека подпро­граммы относительно ВР (табл. 13.4).

Таблица размещения стека подпрограммы относительно ВР Таблица 13.4

Адрес

[ В Р - . . . ]

[ВР + 0]

[ВР + 2]

[ВР + 4]

[ВР + 6]

[ВР + ...]

Назначение

Локальные переменные

Исходное значение ВР (2 байта)

Адрес возврата — регистр IP (2 байта)

Сегмент адреса возврата — регистр CS (2 байта)

Первый аргумент

Следующие аргументы

Давайте напишем на Паскале программу, подобную только что рассмотренной. Новая версия нашей программы для вывода значения суммы будет вызывать функцию writeln из основной программы, то есть подпрограмма addit должна только подсчитать сумму и вернуть ее в основную программу.

{$L a d d i t . o b j } u s e s c r t ; v a r p l u s : in tegers-f u n c t i o n a d d i t (x: i n t e g e r ) : l o n g i n t ; f a r ; ex terna ls -b e g i n p l u s := 6; w r i t e l n ( ' S U M = ' , a d d i t ( 5 ) ) ; end.

Функцию addit, как и в предыдущем примере, напишем на языке ассемблера. Программе на Паскале она становится известна посредством ключевого слова external. В той же строке декларации мы объявляем, что функции addit нужно передать один целочисленный параметр, она возвраш^ает значение типа longint (4 байта) и вызывается как FAR. Не забудьте также написать директиву $L, указывающую имя объектного файла с откомпилированной функцией addit, который нужно подключить при компоновке. Теперь напишем функцию addit, которую мы сохраним в файле addit.asm. Borland Turbo Pascal использует не стандартный формат объектного файла obj, а свой собственный, что накладывает строгие ограничения на именование секций программы. Секция кода должна называться CODE, CSEG или име­нем, заканчивающимся на «_ТЕХТ», секция данных — CONST или именем, заканчивающимся на «_DATA», а секция неинициализированных данных — DATA, или DSEG, или именем, заканчивающимся на «_BSS». Если вы уже привыкли называть секции программы именами .text, .data и .bss, то в программах, предназначенных для компоновки с Паскаль-кодом, просто добавьте знак подчеркивания после точки, и компилятор будет доволен.

227

Page 229: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Функция addit будет вызываться в 16-битной среде, где размер типа данных integer равен 2 байтам. Возвращает функция значение типа longint (4 байта) и передает его через пару регистров DX:AX. Все эти вопросы, в том числе вопросы компоновки с Паскаль-кодом, решает макрос с16.тас. Код нашей подпрограммы addit представлен в листинге 13.2.

Листинг 13.2/Пример подпрограммы для паскалевской програмы

SECTION ._ТЕХТ %define PASCAL % include «mi sс/с1б.mac» extern plus

global addit proc addit

%$what arg

xor dx,dx mov ax,[bp+%$whatj add ax,[plus] adc dx,0 endproc

начало секции кода будем использовать FAR-вызовы подключаем макросы получаем доступ к внешртей переменной plus экспортируем функцию addit начало функции addit макрос включает создание стек-фрейма объявляем единственный аргумент по имени what сбрасываем DX АХ = what АХ = АХ -ь p l u s не забудьте флаг переноса удаляем полученные аргументы и выходим из подпрограммы

Макрос arg принимает в качестве необязательного параметра размер пере­даваемого аргумента. Размер по умолчанию в 16-битной среде равен 2 баритам. Если бы мы передавали аргумент типа longint или указатель, нам пришлось бы явно задать его размер, то есть 4 байта. Теперь посмотрим, во что превратятся макросы после подстановки:

макрос ргос макрос ргос DX-0 получаем 1-й аргумент сложение аргумента и переменной p l u s сложение с переносом макрос endproc макрос endproc макрос endproc все

После компиляции и запуска программы она выведет требуемую сумму на экран.

CS CS CS CS

CS

CS CS CS CS

:007б :0077 :0079 :007В

.007Е

0082 •0086 0088 0089

55 89Е5 31D2 8В4606

03065200

81D20000 89ЕС 5D СА02 00

push mov xor mov

add

adc mov pop retf

bp bp, sp dx,dx ax,[bp+0 6

ax, [0052]

dx,0000 sp, bp bp 0002

228

Page 230: Ассемблер на примерах. Базовый курс

Гл; Заключение

Несмотря на то, что вы дочитали эту книгу до конца, ваше изучение языка ассемблера на этом не заканчивается, а, наоборот, только начинается. Вы заложили только фундамент дальнейшего из­учения, познакомившись с основными понятиями и конструкциями языка ассемблера, которых долж­но хватить для написания простеньких программ. Если вы заинтересовались и хотите разрабатывать сложные ассемблерные программы, рекомендуем ознакомиться с расширенными наборами команд процессоров (ММХ, SSE, 3DNow), описания ко­торых вы найдете в Интернете. А также прочитать книгу, посвяш;енную программированию на языке ассемблера под конкретную операционную систе­му ~- DOS, Windows или Linux. Все программы и фрагменты программ, представ­ленные в этой книге, распространяются свободно без лицензионных ограничений. Все торговые марки и наименования программных продуктов являются зарегистрированными торго­выми марками их владельцев.

&i

Page 231: Ассемблер на примерах. Базовый курс

Гл; Часто используемые команды

^v

' - I % :с

Page 232: Ассемблер на примерах. Базовый курс

15. Часто используемые команды

1 Группа команд

Команды перемещения значений

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

1 Логические команды

Сравнение

Сдвиг и ротация

Команды перехода

Условные переходы

Подпрограммы

Операции над строками i

Назначение

MOV — скопировать

XCHG — поменять местами значения

PUSH — сохранить в стеке

POP — извлечь из стека

ADD— сложение

SUB — вычитание

MUL —умножение

DIV —деление

INC — инкремент (увеличение на 1)

DEC — декремент (уменьшение на 1)

AND — логическое И (логическое умножение)

OR — логическое ИЛИ (логическая сумма)

XOR — исключительное ИЛИ

NOT — отрицание

СМР —сравнение

TEST — поразрядное сравнение

SHR — логический (поразрядный) сдвиг вправо

SHL — логический (поразрядный) сдвиг влево

RCR — ротация через флаг переноса вправо

RCL — ротация через флаг переноса влево

JMP ~ безусловный переход

LOOP — переход, пока (Е)СХ не сравняется с 0

JZ — если флаг нуля (ZF) установлен

JC — если флаг переноса (CF) установлен

JNZ — если флаг нуля (ZF) не установлен

JNC — если флаг переноса (CF) не установлен

CALL — вызов подпрограммы

RET — возврат из подпрограммы

INT — вызов прерывания

REP — повторять следующую команду, пока (Е)СХ не 0

MOVSx — копирование строк

CMPSx — сравнение строк

SCASx — поиск в строке

231

Page 233: Ассемблер на примерах. Базовый курс

Ассемблер на примерах. Базовый курс

Гаво 0&i. 002 003 004 005 006 007 008 009 т и Oil 012 013 014 015 016 017 ШН 019 020 021 022 023 024 025 026 027 028

1 HZV 030 031 032 033 034 035 036 037 038 039 И4И 041 042

ее 01 82 во 04 85 06 07 86 09 Ий ев ОС 811 81-OF 19 11 1>! 13 14 15; 16 17 18 1^ Ifi IB 1С 1» IE IF 28 21 22 23 24 25 26 27 y.}i 25 2fi

Q Q

^ • ^ • » • з-•? ^ * K -i t 11

41 5 ^ t r i t-1-

•f*

i. T » " ^ ^ И ^ 1 f

С > * _J

1 643 044 845 846 047 848 049 050 851 052 иь:-1 854 ©55 856 857 058 859 060 И61 862 063 Plh4 865 066 867 866 069 878 ©71 nvv 873 074 875 876 077 878 879 OSO 881 082 ИНН 884 085

2B 2C 2D 2E 2F 36 31 32 33 34 Л\ 36 37 38 39 ЗЙ 3B 3C M) 3E 3F 4П 41 42 43 44 45 46 4? 4» 4? 4Й 4B 4C 4D 4E 4P 58 51 52 s:< 54 55

f

J"

— , / 0 1 2 3 4 h 6 7 8 9 f f < = > ? P Й В С D E F G H I J К L M N 1 0 p ! Q 1 в к Т JJ J

1 086 087 08g 089

1 090 091

1 092 j 093 ! 094 ! 095 И9Ь 097 098 099 100 101 i02 103 1И4 105 106 107 108 109 110 111 112 113 114 11b 116 117 118 119 120 I2i 122 123 124 125 i;!fi 127 128

56 57 53 59 5Й SB 5C 5D 5E 5F WA 61 Б2 63 G4 &5 66 Ь7 hK 5? &» ISR 6C &D 6E Gr 70 71 72 /Л 74 75 76 77 78 7Э 7ft 7B 7Z 7D 7h 7F ВЭ

и и м J

z [ \ ]

• *

• ^

d. ь с d e f 9' h i J 1< 1 PI П G P Cr r я I u. i V %•.' X j v 1 z < 1 > " ' с 1

г 12? 13Э 131 132 133 134

1 135 i 13& 137 138 W'i 140 141 142 143 144 145 14b 147 143 149 IFirt 151 152 153 154 155 156 157 IfiK 159 1&9 161 1G2 163 154 1&5 1&& 167 1&9 IhV 179 171

81 82 83 04 85: 86 87 88 89 8П KH 8C 8D 8E or 90 91 92 v: 94 9Б 9fi 97 98 99 9ft 9B 9C 9D 9b 9F ЙО fil Й2 ft3 A4 ftG Й6 A7 П8 flV ftft ЙБ

u e a a a a 9 i ё с 1 1 i. Й 8 Ё ffi ^ n и о и u у и и ^ £ V Р.. X к 1 о U. ГГ " а ~ С г т Ji

1 172 173 174 175 176 177 178 179 180 181 1КУ. 183 184 185 106 187 188 189 19И 191 192 193 194 195 196 197 198 199 200 >?И1 202 203 204 205; 206 207 2О0 209 210 211 7.1 7. 213 214

ЙС ЙФ Й£ ЙГ ВО В1 В2 ВЗ В4 В5 Кб В7 В8 В9 Вй БВ ВС ВВ К К BF СО Г.1 С2 СЗ С4 С5 С6 С7 се CV Cfi св ее св CF CF ВО В1 В2 ВЗ D4 В5 В6

^ \ с< » ^ i ш \ \ \ \\ Т1 1 1 II п il и J 1 JL

\

\ 1= \\ Li ^ II

1Й1 \\

X. II тг ^ L1 Ь Y гт 1

1 215 216 217 216 219 228 221 222 223 224 •/!У!Ь 226 227 228 229 230 231 232 У'Л'Л 234 235 2Я6 237 238 239 248 241 242 243 V.44 245 246 247 24G 249 258 251 252 253 254 'J\^}л

D7 \\ D3 i D? -* DI) 1 1(3 I DC • DD 1 DE 1 DF • £9 ос I K1 и E2 Г ЕЗ И Е4 Е Е5 ст £6 J.I Е7 X ЕЗ 5 КУ Н ЕА П ЕВ б F.r. та ED Jtf ЕЕ € EF II F9 = F1 + F2 > F3 L К4 f F5 J F6 -f F7 ~ F9 " F9 -FA -FD NT FC « FD = FE 1 KK

Рис. 15.1. Расширенная таблица ASCII (без символов национальных алфавитов)

232