Файл: 2005 Рудольф Марек. Ассемблер на примерах. Базовый курс. .pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 26.10.2023
Просмотров: 535
Скачиваний: 28
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
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 загружает код начального загрузчика изв оперативную память и передает ему управление. Задача начального загрузчика — найти вторичный загрузчик операционной системы, который находится на одном из логических дисков, перечисленных в таблице разделов. Найти такой диск очень просто — в таблице разделов он единственный помечен как активный. На каждом логическом диске зарезервировано место (первый сектор) для загрузчика операционной системы. Начальный загрузчик загружает загрузчик операционной системы и передает ему управление, а тот в свою очередь загружает ядро и передает управление ему с этого момента операционная система запущена.
123
Ассемблер на примерах. Базовый курс Немного о том, что такое 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
Глава 9 Компилятор NASM Предложения языка ассемблера Выражения Локальные метки Препроцессор NASM Директивы Ассемблера Формат выходного файла Ассемблер на примерах. Базовый курс
Для компиляции наших программ мы будем использовать компилятор NASM
(Netwide Assembler), который свободно распространяется (вместе со своими исходными кодами) по лицензии LGPL. Вы можете найти этот компилятор на сайте http://nasm.sourceforge.net. Синтаксис компилятора подобен синтаксису коммерческих компиляторов
MASM (Microsoft Assembler) и TASM (Turbo Assembler от Borland), поэтому использовать NASM вдвойне удобно — никаких проблем с лицензией и возможность переноса программ, написанных для других ассемблеров.
9.1. Предложения языка ассемблера Каждая строка программы (предложение языка ассемблера) может состоять из следующих частей Метка Инструкция Операнды Любые комментарии Перед меткой и после инструкции можно использовать любое количество пробельных символов (пробелы, табуляция. Операнды отделяются друг от друга с помощью запятой. Начало комментария — точка с запятой (;), а концом комментария служит конец строки. Если предыдущая строка очень длинная, ее можно перенести наследующую, используя обратный слеш 'V (как в С. Инструкцией может быть команда или псевдокоманда (директива компилятора.
9.2. Выражения Под словом выражение мы понимаем константное выражение, значение которого известно уже вовремя компиляции. Синтаксис выражений NASM подобен синтаксису выражений языка С. Выражение также может быть написано как сумма адреса в символической форме и некоторого числа. Выражения такого типа служат, например, для
126
Глава 9. Компилятор NASM доступа ко второму элементу массива в инструкции MOV eax, [аггау+4]. Значение выражения в квадратных скобках известно вовремя компиляции это начальный адрес массива Array, увеличенный на 4. Выражения можно использовать во многих местах программы, например a d d d l , ' А ' - Ю Выражение ' А' -10 будет вычислено вовремя компиляции, и компилятор сгенерирует команду ADD dl, 55. Нам никто не запрещает использовать сложные выражения, которые известны нам из языка СВ следующей таблице (табл. 9.1) перечислены все поддерживаемые операторы в порядке возрастания приоритета. Операторы, поддерживаемые NASM, расположенные в порядке возрастания приоритета Таблица 9.1 Оператор
I
-
&
«>>
+ -
* / % // %% унарные + - Назначение Логическая операция OR Логическая операция XOR Логическая операция AND Сдвиг влево и вправо Плюс и минус Умножение, целочисленное деление, деление по модулю (взятие остатка, деление со знаком, деление со знаком по модулю Унарные операторы плюс, минус и отрицание (NOT) Рассмотрим несколько примеров mov х ;вычислится как
;MOV e a x , 0 x 2 4 Еще один пример mov c l , l результат mov cl,11111110b ИЛИ mov c l , 0 x F E
9.3. Локальные метки С метками мы познакомились при рассмотрении инструкций JMP и CALL, поэтому вы уже знаете, для чего они используются Метка — это идентификатор, за которым следует двоеточие Двоеточие можно и опускать, ноя советую этого не делать, потому что иначе компилятор может принять имя метки за написанное с ошибкой имя команды. Каждая метка должна быть уникальной в пределах программы, поэтому при написании больших программ вам понадобится весь ваш творческий потенциал, чтобы каждый раз придумывать
127
Ассемблер на примерах. Базовый курс новое имя. К счастью, нас выручит компилятор NASM, который позволяет использовать так называемые локальные метки. Локальные метки, как и локальные переменные в высокоуровневых языках программирования, могут иметь одинаковые имена в разных подпрограммах. Нов языках высокого уровня область видимости переменной (блок, функция) видна сразу, а в языке ассемблера Локальной называется метка, действительная только на протяжении фрагмента кода, находящегося между двумя глобальными метками. Различаются локальные и глобальные метки своими именами имя локальной метки начинается сточки, а глобальной — с любого другого символа.
subprog: глобальная метка subprog
; код
.1оса1_1оор: локальная метка
; код subprog2: глобальная метка subprog2
. l o c a l _ l o o p : ; локальная метка
9.4. Препроцессор NASM В этом параграфе мы поговорим о средствах, несколько сближающих программирование на языке ассемблера с программированием на языках высокого уровня — макросах. Макрос — это символическое имя, заменяющее несколько команд языка ассемблера. Команды подставляются вместо имени макроса вовремя компиляции Этим занимается программа, называемая препроцессором ивы зываемая, как правило, самим компилятором до того, как он приступит непосредственно к трансляции символических имен команд в машинные коды. Препроцессор NASM очень мощный и гибкий, он поддерживает множество различных макросов. Директивы объявления макроса начинаются со знака процента «%». Имена макросов чувствительны к регистру символов (то есть АаА — это не ааа). Для определения нечувствительных к регистру символов макросов нужно использовать другие директивы, которые начинаются с пары символов «%i»
(%idefine вместо %define и т.п.).
Однострочные макросы — %define, %undef Эти макросы определяются подобно тому, как в языке Снов отличие от Смогут принимать аргументы. Вот пример макроса для вычисления среднего арифметического двух чисел
128
Глава 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 60
% d e f i n e SEC_IN_HOUR SEC_IN_MIN * 60 Или для условной компиляции, например
% d e f i n e USE_MMX Также, как ив С, мы можем проверить, определен ли макрос, с помощью
%ifdef (определен) и %ifndef (не определен. Удалить определение макроса можно с помощью %undef. Сложные макросы — %macro %endmacro Директива %define позволяет определить простые макросы, состоящие из одной команды (строки кода. Для определения сложного, многострочного, макроса служат директивы %macro и %endmacro. Первая описывает начало макроса — имя и число аргументов. Затем идет тело макроса — команды, которые должны быть выполнены. Директива %endmacro закрывает макрос.
%macro subtract 3 sub %1,%2 sub %1,%3
%endmacro Макрос s u b t r a c t нужно вызывать стремя аргументами s u b t r a c t e a x , e c x , [ v a r i a b l e l ] Он будет преобразован в следующий код sub eax,ecx sub eax,[variablel] Необязательно указывать точное число аргументов. Препроцессор NASM позволяет указывать переменное число в виде диапазона значений. Например,
2-3 означает, что наш макрос может принимать 2 или 3 аргумента. Опущенные при вызове макроса аргументы будут заменены значениями по умолчанию, которые следуют за диапазоном аргументов
%macro addit 2-3 0 a d d %1,%2 a d d %1,%3
%endmacro
5 Зак. 293 129
Ассемблер на примерах. Базовый курс Мы описали макрос a d d i t , который принимает два обязательных аргумента, а вместо третьего, если он не указан при вызове, будет подставлено значение по умолчанию — 0: a d d i t eax ebx Будет преобразовано в add eax,ebx add eax,0 Объявление макроса — %assign Директива %assign — это еще один способ объявить простой (однострочный) макрос. Объявленный макрос не должен иметь параметров, и его результатом должно быть число. Например, с помощью
%assign i i+1 мы можем увеличить числовое значение другого макроса на 1. Обычно %assign используется в сложных макросах. Условная компиляция — %if Как и препроцессор С, препроцессор NASM позволяет условно компилировать программу. Это означает, что в зависимости от условия будет откомпилирован тот или иной блок кода. Рассмотрим основные инструкции условной компиляции
%И:<условие> Код между if и e l i f будет откомпилирован только, если условие истинно.
%еШ:<условие2> Код между %elif и %else будет откомпилирован если первое условие ложно, а второе — истинно
%else Если оба условия ложны, будет откомпилирован код между %else и %endif
%endif Директивы %elif и %else необязательны и могут быть опущены. Также можно указывать несколько блоков %elif, но блок %else (если он есть) должен быть один. Условие может содержать константное выражение. Набор операторов в условии расширен операторами отношения = , < , > , < = , > = , и < > (равно, меньше, больше, меньше или равно, больше или равно, неравно. Приверженцам языка С понравится возможность писать « ==» и «!=» вместо « = » и « о » . Также при написании условия могут быть использованы логические операторы
|,
А л, && (OR, XOR, AND), которые нам известны из языка С.
130
Глава 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 используется для вставки так называемых заголовочных файлов
%include «macro.mac» Как правило, заголовочные файлы содержат определения констант или макросов, а не команды программы. В отличие от Снам ненужно добавлять в начало заголовочного файла эту традиционную конструкцию
%ifndef MACROS_MAC
%define MACROS_MAC теперь следуют сами макрокоманды
%endif Повторное включение файла не является ошибкой. Просто все макросы будут определены заново, что не должно вызвать ошибку при компиляции.
9.5. Директивы Ассемблера Директивы Ассемблера NASM — это макрокоманды, определенные самим компилятором Директиву немного, в отличие от MASM и TASM, где их число огромно.
131
Ассемблер на примерах. Базовый курс Директива BITS — указание режима процессора Директива указывает компилятору, в каком режиме процессора будет работать программа. Синтаксис этой директивы позволяет указать один из двух режимов — разрядный (BITS 16) или разрядный (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
Глава 9. Компилятор NASM Традиционно секция кода называется .text, секция статических данных —
.data, а секция динамических данных — .bss. Рассмотрим пример программы, содержащий все трисекции (листинг 9.1) Листинг 9.1. Пример программы, содержащий все трисекции Каждая ассемблерная программа должна начинаться с описания своего назначения и авторства, например, с Иванов И.П.
SECTION .text Секция .text содержит нашу программу Это очень простая программа для сложения двух значений mov eax,2 mov ebx,5 add eax,ebx add eax,[statl] mov [dynl],eax переменной dynl
SECTION .data
EAX = 2
EBX = 5
EAX = EAX + EBX
EAX = EAX + statl сохраняем полученное в динамической используем псевдоинструкции DB, DW and DD для объявления статических данных
.data statl: dd 1
SECTION .bss
;B этой секции описываются динамические данные, они не
/инициализируются. Для объявления переменных в секции .bss используются псевдоинструкции RESB, RESW и RESD. Динамические данные не занимают места на диске, то есть для. них не отводится место в исполняемом файле. Инструкции
;RES* указывают только, сколько байтов переменные будут занимать в памяти после запуска программы dynl: resd l конец программы Иногда также определяется еще одна секция — для стека (.stack). Но поскольку о стеке заботится операционная система, нам беспокоиться не о чем. Директивы SECTION и S E G M E N T являются синонимами и поэтому взаимозаменяемы. Примечание переводчика. В другой литературе вы можете встретить использование термина сегмент вместо секция. Соответственно, три части программы называются сегмент кода, сегмент данных и сегмент стека.
133
Ассемблер на примерах. Базовый курс Директивы 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 можно указать другой тип процессора
; те. Директива ORG — указание адреса загрузки Директива ORG устанавливает начальный адрес для загрузки программы — тот адрес, от которого отсчитываются все остальные адреса в программе. Например, с помощью ORG вы можете создать СОМ-файл (подробнее о форматах выходных файлов — следующий параграф, если укажете ORG
0x100. Операционная система DOS загружает исполняемые файлы типа СОМ в сегмент памяти, начинающийся с адреса 0x100.
134
Глава 9. Компилятор NASM Следующий материал предназначен для программистов, которые хотят копнуть NASM поглубже, начинающие программисты могут с чистой совестью пропустить этот пункт. В отличие от компиляторов MASM и TASM, NASM запрещает использовать директиву ORG несколько разв пределах одной секции. Типичный пример многократного применения директивы ORG: вторая ORG, означающая конец загрузочного сектора. Вили мы можем указать
ORG О тут загрузочный сектор конец загрузочного сектора
ORG 510
DW 0хАА55 В NASM вместо второго ORG мы должны использовать директиву TIMES:
ORG 0
; тут загрузочный сектор
; конец загрузочного сектора
TIMES 510-($-$$) DB 0
DW 0XAA5 5
1 ... 7 8 9 10 11 12 13 14 ... 20
System). В задачу BIOS входит инициализация компьютера — проверка видеоадаптера, памяти, дисков и других устройств — это так называемая процедура
POST (Power On Self Test). После проверки устройств компьютера BIOS начинает загрузку операционной системы. В зависимости от настроек BIOS может искать начальный загрузчик, которому и будет передано управление, на дискете, жестком диске сектор с номером 0), CD-ROM и т.д. Первый сектор (с номером 0) диска содержит начальный загрузчики таблицу разделов. Этот сектор называется главной загрузочной записью — MBR (Master Boot Record). BIOS загружает код начального загрузчика изв оперативную память и передает ему управление. Задача начального загрузчика — найти вторичный загрузчик операционной системы, который находится на одном из логических дисков, перечисленных в таблице разделов. Найти такой диск очень просто — в таблице разделов он единственный помечен как активный. На каждом логическом диске зарезервировано место (первый сектор) для загрузчика операционной системы. Начальный загрузчик загружает загрузчик операционной системы и передает ему управление, а тот в свою очередь загружает ядро и передает управление ему с этого момента операционная система запущена.
123
Ассемблер на примерах. Базовый курс Немного о том, что такое 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
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
Глава 9 Компилятор NASM Предложения языка ассемблера Выражения Локальные метки Препроцессор NASM Директивы Ассемблера Формат выходного файла Ассемблер на примерах. Базовый курс
Для компиляции наших программ мы будем использовать компилятор NASM
(Netwide Assembler), который свободно распространяется (вместе со своими исходными кодами) по лицензии LGPL. Вы можете найти этот компилятор на сайте http://nasm.sourceforge.net. Синтаксис компилятора подобен синтаксису коммерческих компиляторов
MASM (Microsoft Assembler) и TASM (Turbo Assembler от Borland), поэтому использовать NASM вдвойне удобно — никаких проблем с лицензией и возможность переноса программ, написанных для других ассемблеров.
9.1. Предложения языка ассемблера Каждая строка программы (предложение языка ассемблера) может состоять из следующих частей Метка Инструкция Операнды Любые комментарии Перед меткой и после инструкции можно использовать любое количество пробельных символов (пробелы, табуляция. Операнды отделяются друг от друга с помощью запятой. Начало комментария — точка с запятой (;), а концом комментария служит конец строки. Если предыдущая строка очень длинная, ее можно перенести наследующую, используя обратный слеш 'V (как в С. Инструкцией может быть команда или псевдокоманда (директива компилятора.
9.2. Выражения Под словом выражение мы понимаем константное выражение, значение которого известно уже вовремя компиляции. Синтаксис выражений NASM подобен синтаксису выражений языка С. Выражение также может быть написано как сумма адреса в символической форме и некоторого числа. Выражения такого типа служат, например, для
126
(Netwide Assembler), который свободно распространяется (вместе со своими исходными кодами) по лицензии LGPL. Вы можете найти этот компилятор на сайте http://nasm.sourceforge.net. Синтаксис компилятора подобен синтаксису коммерческих компиляторов
MASM (Microsoft Assembler) и TASM (Turbo Assembler от Borland), поэтому использовать NASM вдвойне удобно — никаких проблем с лицензией и возможность переноса программ, написанных для других ассемблеров.
9.1. Предложения языка ассемблера Каждая строка программы (предложение языка ассемблера) может состоять из следующих частей Метка Инструкция Операнды Любые комментарии Перед меткой и после инструкции можно использовать любое количество пробельных символов (пробелы, табуляция. Операнды отделяются друг от друга с помощью запятой. Начало комментария — точка с запятой (;), а концом комментария служит конец строки. Если предыдущая строка очень длинная, ее можно перенести наследующую, используя обратный слеш 'V (как в С. Инструкцией может быть команда или псевдокоманда (директива компилятора.
9.2. Выражения Под словом выражение мы понимаем константное выражение, значение которого известно уже вовремя компиляции. Синтаксис выражений NASM подобен синтаксису выражений языка С. Выражение также может быть написано как сумма адреса в символической форме и некоторого числа. Выражения такого типа служат, например, для
126
Глава 9. Компилятор NASM доступа ко второму элементу массива в инструкции MOV eax, [аггау+4]. Значение выражения в квадратных скобках известно вовремя компиляции это начальный адрес массива Array, увеличенный на 4. Выражения можно использовать во многих местах программы, например a d d d l , ' А ' - Ю Выражение ' А' -10 будет вычислено вовремя компиляции, и компилятор сгенерирует команду ADD dl, 55. Нам никто не запрещает использовать сложные выражения, которые известны нам из языка СВ следующей таблице (табл. 9.1) перечислены все поддерживаемые операторы в порядке возрастания приоритета. Операторы, поддерживаемые NASM, расположенные в порядке возрастания приоритета Таблица 9.1 Оператор
I
-
&
«>>
+ -
* / % // %% унарные + - Назначение Логическая операция OR Логическая операция XOR Логическая операция AND Сдвиг влево и вправо Плюс и минус Умножение, целочисленное деление, деление по модулю (взятие остатка, деление со знаком, деление со знаком по модулю Унарные операторы плюс, минус и отрицание (NOT) Рассмотрим несколько примеров mov х ;вычислится как
;MOV e a x , 0 x 2 4 Еще один пример mov c l , l результат mov cl,11111110b ИЛИ mov c l , 0 x F E
9.3. Локальные метки С метками мы познакомились при рассмотрении инструкций JMP и CALL, поэтому вы уже знаете, для чего они используются Метка — это идентификатор, за которым следует двоеточие Двоеточие можно и опускать, ноя советую этого не делать, потому что иначе компилятор может принять имя метки за написанное с ошибкой имя команды. Каждая метка должна быть уникальной в пределах программы, поэтому при написании больших программ вам понадобится весь ваш творческий потенциал, чтобы каждый раз придумывать
127
I
-
&
«>>
+ -
* / % // %% унарные + - Назначение Логическая операция OR Логическая операция XOR Логическая операция AND Сдвиг влево и вправо Плюс и минус Умножение, целочисленное деление, деление по модулю (взятие остатка, деление со знаком, деление со знаком по модулю Унарные операторы плюс, минус и отрицание (NOT) Рассмотрим несколько примеров mov х ;вычислится как
;MOV e a x , 0 x 2 4 Еще один пример mov c l , l результат mov cl,11111110b ИЛИ mov c l , 0 x F E
9.3. Локальные метки С метками мы познакомились при рассмотрении инструкций JMP и CALL, поэтому вы уже знаете, для чего они используются Метка — это идентификатор, за которым следует двоеточие Двоеточие можно и опускать, ноя советую этого не делать, потому что иначе компилятор может принять имя метки за написанное с ошибкой имя команды. Каждая метка должна быть уникальной в пределах программы, поэтому при написании больших программ вам понадобится весь ваш творческий потенциал, чтобы каждый раз придумывать
127
Ассемблер на примерах. Базовый курс новое имя. К счастью, нас выручит компилятор NASM, который позволяет использовать так называемые локальные метки. Локальные метки, как и локальные переменные в высокоуровневых языках программирования, могут иметь одинаковые имена в разных подпрограммах. Нов языках высокого уровня область видимости переменной (блок, функция) видна сразу, а в языке ассемблера Локальной называется метка, действительная только на протяжении фрагмента кода, находящегося между двумя глобальными метками. Различаются локальные и глобальные метки своими именами имя локальной метки начинается сточки, а глобальной — с любого другого символа.
subprog: глобальная метка subprog
; код
.1оса1_1оор: локальная метка
; код subprog2: глобальная метка subprog2
. l o c a l _ l o o p : ; локальная метка
9.4. Препроцессор NASM В этом параграфе мы поговорим о средствах, несколько сближающих программирование на языке ассемблера с программированием на языках высокого уровня — макросах. Макрос — это символическое имя, заменяющее несколько команд языка ассемблера. Команды подставляются вместо имени макроса вовремя компиляции Этим занимается программа, называемая препроцессором ивы зываемая, как правило, самим компилятором до того, как он приступит непосредственно к трансляции символических имен команд в машинные коды. Препроцессор NASM очень мощный и гибкий, он поддерживает множество различных макросов. Директивы объявления макроса начинаются со знака процента «%». Имена макросов чувствительны к регистру символов (то есть АаА — это не ааа). Для определения нечувствительных к регистру символов макросов нужно использовать другие директивы, которые начинаются с пары символов «%i»
(%idefine вместо %define и т.п.).
Однострочные макросы — %define, %undef Эти макросы определяются подобно тому, как в языке Снов отличие от Смогут принимать аргументы. Вот пример макроса для вычисления среднего арифметического двух чисел
128
subprog: глобальная метка subprog
; код
.1оса1_1оор: локальная метка
; код subprog2: глобальная метка subprog2
. l o c a l _ l o o p : ; локальная метка
9.4. Препроцессор NASM В этом параграфе мы поговорим о средствах, несколько сближающих программирование на языке ассемблера с программированием на языках высокого уровня — макросах. Макрос — это символическое имя, заменяющее несколько команд языка ассемблера. Команды подставляются вместо имени макроса вовремя компиляции Этим занимается программа, называемая препроцессором ивы зываемая, как правило, самим компилятором до того, как он приступит непосредственно к трансляции символических имен команд в машинные коды. Препроцессор NASM очень мощный и гибкий, он поддерживает множество различных макросов. Директивы объявления макроса начинаются со знака процента «%». Имена макросов чувствительны к регистру символов (то есть АаА — это не ааа). Для определения нечувствительных к регистру символов макросов нужно использовать другие директивы, которые начинаются с пары символов «%i»
(%idefine вместо %define и т.п.).
Однострочные макросы — %define, %undef Эти макросы определяются подобно тому, как в языке Снов отличие от Смогут принимать аргументы. Вот пример макроса для вычисления среднего арифметического двух чисел
128
Глава 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 60
% d e f i n e SEC_IN_HOUR SEC_IN_MIN * 60 Или для условной компиляции, например
% d e f i n e USE_MMX Также, как ив С, мы можем проверить, определен ли макрос, с помощью
%ifdef (определен) и %ifndef (не определен. Удалить определение макроса можно с помощью %undef. Сложные макросы — %macro %endmacro Директива %define позволяет определить простые макросы, состоящие из одной команды (строки кода. Для определения сложного, многострочного, макроса служат директивы %macro и %endmacro. Первая описывает начало макроса — имя и число аргументов. Затем идет тело макроса — команды, которые должны быть выполнены. Директива %endmacro закрывает макрос.
%macro subtract 3 sub %1,%2 sub %1,%3
%endmacro Макрос s u b t r a c t нужно вызывать стремя аргументами s u b t r a c t e a x , e c x , [ v a r i a b l e l ] Он будет преобразован в следующий код sub eax,ecx sub eax,[variablel] Необязательно указывать точное число аргументов. Препроцессор NASM позволяет указывать переменное число в виде диапазона значений. Например,
2-3 означает, что наш макрос может принимать 2 или 3 аргумента. Опущенные при вызове макроса аргументы будут заменены значениями по умолчанию, которые следуют за диапазоном аргументов
%macro addit 2-3 0 a d d %1,%2 a d d %1,%3
%endmacro
5 Зак. 293 129
% 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 60
% d e f i n e SEC_IN_HOUR SEC_IN_MIN * 60 Или для условной компиляции, например
% d e f i n e USE_MMX Также, как ив С, мы можем проверить, определен ли макрос, с помощью
%ifdef (определен) и %ifndef (не определен. Удалить определение макроса можно с помощью %undef. Сложные макросы — %macro %endmacro Директива %define позволяет определить простые макросы, состоящие из одной команды (строки кода. Для определения сложного, многострочного, макроса служат директивы %macro и %endmacro. Первая описывает начало макроса — имя и число аргументов. Затем идет тело макроса — команды, которые должны быть выполнены. Директива %endmacro закрывает макрос.
%macro subtract 3 sub %1,%2 sub %1,%3
%endmacro Макрос s u b t r a c t нужно вызывать стремя аргументами s u b t r a c t e a x , e c x , [ v a r i a b l e l ] Он будет преобразован в следующий код sub eax,ecx sub eax,[variablel] Необязательно указывать точное число аргументов. Препроцессор NASM позволяет указывать переменное число в виде диапазона значений. Например,
2-3 означает, что наш макрос может принимать 2 или 3 аргумента. Опущенные при вызове макроса аргументы будут заменены значениями по умолчанию, которые следуют за диапазоном аргументов
%macro addit 2-3 0 a d d %1,%2 a d d %1,%3
%endmacro
5 Зак. 293 129
Ассемблер на примерах. Базовый курс Мы описали макрос a d d i t , который принимает два обязательных аргумента, а вместо третьего, если он не указан при вызове, будет подставлено значение по умолчанию — 0: a d d i t eax ebx Будет преобразовано в add eax,ebx add eax,0 Объявление макроса — %assign Директива %assign — это еще один способ объявить простой (однострочный) макрос. Объявленный макрос не должен иметь параметров, и его результатом должно быть число. Например, с помощью
%assign i i+1 мы можем увеличить числовое значение другого макроса на 1. Обычно %assign используется в сложных макросах. Условная компиляция — %if Как и препроцессор С, препроцессор NASM позволяет условно компилировать программу. Это означает, что в зависимости от условия будет откомпилирован тот или иной блок кода. Рассмотрим основные инструкции условной компиляции
%И:<условие> Код между if и e l i f будет откомпилирован только, если условие истинно.
%еШ:<условие2> Код между %elif и %else будет откомпилирован если первое условие ложно, а второе — истинно
%else Если оба условия ложны, будет откомпилирован код между %else и %endif
%endif Директивы %elif и %else необязательны и могут быть опущены. Также можно указывать несколько блоков %elif, но блок %else (если он есть) должен быть один. Условие может содержать константное выражение. Набор операторов в условии расширен операторами отношения = , < , > , < = , > = , и < > (равно, меньше, больше, меньше или равно, больше или равно, неравно. Приверженцам языка С понравится возможность писать « ==» и «!=» вместо « = » и « о » . Также при написании условия могут быть использованы логические операторы
|,
А л, && (OR, XOR, AND), которые нам известны из языка С.
130
%assign i i+1 мы можем увеличить числовое значение другого макроса на 1. Обычно %assign используется в сложных макросах. Условная компиляция — %if Как и препроцессор С, препроцессор NASM позволяет условно компилировать программу. Это означает, что в зависимости от условия будет откомпилирован тот или иной блок кода. Рассмотрим основные инструкции условной компиляции
%И:<условие> Код между if и e l i f будет откомпилирован только, если условие истинно.
%еШ:<условие2> Код между %elif и %else будет откомпилирован если первое условие ложно, а второе — истинно
%else Если оба условия ложны, будет откомпилирован код между %else и %endif
%endif Директивы %elif и %else необязательны и могут быть опущены. Также можно указывать несколько блоков %elif, но блок %else (если он есть) должен быть один. Условие может содержать константное выражение. Набор операторов в условии расширен операторами отношения = , < , > , < = , > = , и < > (равно, меньше, больше, меньше или равно, больше или равно, неравно. Приверженцам языка С понравится возможность писать « ==» и «!=» вместо « = » и « о » . Также при написании условия могут быть использованы логические операторы
|,
А л, && (OR, XOR, AND), которые нам известны из языка С.
130
Глава 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 используется для вставки так называемых заголовочных файлов
%include «macro.mac» Как правило, заголовочные файлы содержат определения констант или макросов, а не команды программы. В отличие от Снам ненужно добавлять в начало заголовочного файла эту традиционную конструкцию
%ifndef MACROS_MAC
%define MACROS_MAC теперь следуют сами макрокоманды
%endif Повторное включение файла не является ошибкой. Просто все макросы будут определены заново, что не должно вызвать ошибку при компиляции.
9.5. Директивы Ассемблера Директивы Ассемблера NASM — это макрокоманды, определенные самим компилятором Директиву немного, в отличие от MASM и TASM, где их число огромно.
131
%define TEST_IT
%ifdef TEST_IT cmp eax,3
%endif Инструкции, заключенные в блок %ifdef (в том числе и СМР еах,3) будут откомпилированы, только если макрос TEST_IT определен с помощью
% define. Кроме %ifdef можно использовать директиву %ifndef. В этом случае соответствующий блок инструкций будет компилироваться, только если макрос не определен. Вставка файла — %include С помощью директивы %include можно вставить в текущую позицию код, находящийся в другом файле. Имя файла нужно указать в двойных кавычках. Обычно %include используется для вставки так называемых заголовочных файлов
%include «macro.mac» Как правило, заголовочные файлы содержат определения констант или макросов, а не команды программы. В отличие от Снам ненужно добавлять в начало заголовочного файла эту традиционную конструкцию
%ifndef MACROS_MAC
%define MACROS_MAC теперь следуют сами макрокоманды
%endif Повторное включение файла не является ошибкой. Просто все макросы будут определены заново, что не должно вызвать ошибку при компиляции.
9.5. Директивы Ассемблера Директивы Ассемблера NASM — это макрокоманды, определенные самим компилятором Директиву немного, в отличие от MASM и TASM, где их число огромно.
131
Ассемблер на примерах. Базовый курс Директива BITS — указание режима процессора Директива указывает компилятору, в каком режиме процессора будет работать программа. Синтаксис этой директивы позволяет указать один из двух режимов — разрядный (BITS 16) или разрядный (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
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
Глава 9. Компилятор NASM Традиционно секция кода называется .text, секция статических данных —
.data, а секция динамических данных — .bss. Рассмотрим пример программы, содержащий все трисекции (листинг 9.1) Листинг 9.1. Пример программы, содержащий все трисекции Каждая ассемблерная программа должна начинаться с описания своего назначения и авторства, например, с Иванов И.П.
SECTION .text Секция .text содержит нашу программу Это очень простая программа для сложения двух значений mov eax,2 mov ebx,5 add eax,ebx add eax,[statl] mov [dynl],eax переменной dynl
SECTION .data
EAX = 2
EBX = 5
EAX = EAX + EBX
EAX = EAX + statl сохраняем полученное в динамической используем псевдоинструкции DB, DW and DD для объявления статических данных
.data statl: dd 1
SECTION .bss
;B этой секции описываются динамические данные, они не
/инициализируются. Для объявления переменных в секции .bss используются псевдоинструкции RESB, RESW и RESD. Динамические данные не занимают места на диске, то есть для. них не отводится место в исполняемом файле. Инструкции
;RES* указывают только, сколько байтов переменные будут занимать в памяти после запуска программы dynl: resd l конец программы Иногда также определяется еще одна секция — для стека (.stack). Но поскольку о стеке заботится операционная система, нам беспокоиться не о чем. Директивы SECTION и S E G M E N T являются синонимами и поэтому взаимозаменяемы. Примечание переводчика. В другой литературе вы можете встретить использование термина сегмент вместо секция. Соответственно, три части программы называются сегмент кода, сегмент данных и сегмент стека.
133
.data, а секция динамических данных — .bss. Рассмотрим пример программы, содержащий все трисекции (листинг 9.1) Листинг 9.1. Пример программы, содержащий все трисекции Каждая ассемблерная программа должна начинаться с описания своего назначения и авторства, например, с Иванов И.П.
SECTION .text Секция .text содержит нашу программу Это очень простая программа для сложения двух значений mov eax,2 mov ebx,5 add eax,ebx add eax,[statl] mov [dynl],eax переменной dynl
SECTION .data
EAX = 2
EBX = 5
EAX = EAX + EBX
EAX = EAX + statl сохраняем полученное в динамической используем псевдоинструкции DB, DW and DD для объявления статических данных
.data statl: dd 1
SECTION .bss
;B этой секции описываются динамические данные, они не
/инициализируются. Для объявления переменных в секции .bss используются псевдоинструкции RESB, RESW и RESD. Динамические данные не занимают места на диске, то есть для. них не отводится место в исполняемом файле. Инструкции
;RES* указывают только, сколько байтов переменные будут занимать в памяти после запуска программы dynl: resd l конец программы Иногда также определяется еще одна секция — для стека (.stack). Но поскольку о стеке заботится операционная система, нам беспокоиться не о чем. Директивы SECTION и S E G M E N T являются синонимами и поэтому взаимозаменяемы. Примечание переводчика. В другой литературе вы можете встретить использование термина сегмент вместо секция. Соответственно, три части программы называются сегмент кода, сегмент данных и сегмент стека.
133
Ассемблер на примерах. Базовый курс Директивы 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 можно указать другой тип процессора
; те. Директива ORG — указание адреса загрузки Директива ORG устанавливает начальный адрес для загрузки программы — тот адрес, от которого отсчитываются все остальные адреса в программе. Например, с помощью ORG вы можете создать СОМ-файл (подробнее о форматах выходных файлов — следующий параграф, если укажете ORG
0x100. Операционная система DOS загружает исполняемые файлы типа СОМ в сегмент памяти, начинающийся с адреса 0x100.
134
CPU 808 6 Используются инструкции оригинального чипа
; I n t e l 8086 можно указать другой тип процессора
; те. Директива ORG — указание адреса загрузки Директива ORG устанавливает начальный адрес для загрузки программы — тот адрес, от которого отсчитываются все остальные адреса в программе. Например, с помощью ORG вы можете создать СОМ-файл (подробнее о форматах выходных файлов — следующий параграф, если укажете ORG
0x100. Операционная система DOS загружает исполняемые файлы типа СОМ в сегмент памяти, начинающийся с адреса 0x100.
134
Глава 9. Компилятор NASM Следующий материал предназначен для программистов, которые хотят копнуть NASM поглубже, начинающие программисты могут с чистой совестью пропустить этот пункт. В отличие от компиляторов MASM и TASM, NASM запрещает использовать директиву ORG несколько разв пределах одной секции. Типичный пример многократного применения директивы ORG: вторая ORG, означающая конец загрузочного сектора. Вили мы можем указать
ORG О тут загрузочный сектор конец загрузочного сектора
ORG 510
DW 0хАА55 В NASM вместо второго ORG мы должны использовать директиву TIMES:
ORG 0
; тут загрузочный сектор
; конец загрузочного сектора
TIMES 510-($-$$) DB 0
DW 0XAA5 5
ORG О тут загрузочный сектор конец загрузочного сектора
ORG 510
DW 0хАА55 В NASM вместо второго ORG мы должны использовать директиву TIMES:
ORG 0
; тут загрузочный сектор
; конец загрузочного сектора
TIMES 510-($-$$) DB 0
DW 0XAA5 5
1 ... 7 8 9 10 11 12 13 14 ... 20
9.6. Формат выходного файла
Netwide Assembler (NASM) легко портируется на различные операционные системы в пределах х86-совместимых процессоров. Поэтому для NASM не составит труда откомпилировать программу для указанной операционной системы, даже если вы сейчас работаете под управлением другой ОС. Формат выходного файла определяется с помощью ключа командной строки
-f. Некоторые форматы расширяют синтаксис определенных инструкций, а также добавляют свои инструкции. Создание выходного файла компиляция и компоновка Создание исполняемого файла состоит из двух этапов. Первый — это компиляция (трансляция) исходного кода программы в некоторый объектный формат. Объектный формат содержит машинный код программы, но символы (переменные и другие идентификаторы) в объектном файле пока не привязаны к адресам памяти.
135
Ассемблер на примерах. Базовый курс На втором этапе, который называется компоновкой или линковкой (linking), из одного или нескольких объектных файлов создается исполняемый файл. Процедура компоновки состоит в том, что компоновщик связывает символы, определенные в основной программе, с символами, которые определены в ее модулях (учитываются директивы EXTERN и GLOBAL), после чего каждому символу назначается окончательный адрес памяти или обеспечивается его динамическое вычисление. Формат bin — готовый исполняемый файл Формат bin не является объектным форматом. Он содержит результат перевода команд ассемблера в машинные коды. Этот формат можно даже использовать для создания текстовых файлов. Например, у насесть файл hel lo . asm: если не указано иначе, адресация двоичного файла начинается с 0x0. при помощи директивы DB определим строку " H e l l o w o r l d ! " , завершенную символом конца строки s t r i n g : d b " H e l l o w o r l d ! " , O x d , O x a Скомпилируем этот файл, вызвав компилятор с ключом -f: nasm
-t bin hello.asm В результате получим файл hello, содержащий текст «Hello world». Если бы в исходном файле были еще какие-то команды, тов выходной файл hello попали бы их машинные коды, при выводе на экран они были бы интерпретированы как символы из таблицы, и мы увидели бы на экране всякий мусор. Формат bin позволяет объявлять секции (кода, данных, неинициализированных данных. После имени секции может стоять директива ALIGN, требующая располагать эту секцию по адресам, кратным указанному числу. Например, следующая директива задает выравнивание секции кода по адресам, кратным 16: s e c t i o n . t e x t align=16 Формат bin также можно использовать для создания файлов, исполняемых поди) или для загрузчиков. Значение по умолчанию директивы BITS равно 16. В коде, предназначенном для вывода в формате bin, может присутствовать директива ORG. Формат OMF — объектный файл для 16-битного режима
OMF (Object Module Format) — это старый формат, разработанный корпорацией Intel, в котором до сих пор создает объектный код Turbo Assembler. Компиляторы MASM и NASM тоже поддерживают этот формат.
136
-t bin hello.asm В результате получим файл hello, содержащий текст «Hello world». Если бы в исходном файле были еще какие-то команды, тов выходной файл hello попали бы их машинные коды, при выводе на экран они были бы интерпретированы как символы из таблицы, и мы увидели бы на экране всякий мусор. Формат bin позволяет объявлять секции (кода, данных, неинициализированных данных. После имени секции может стоять директива ALIGN, требующая располагать эту секцию по адресам, кратным указанному числу. Например, следующая директива задает выравнивание секции кода по адресам, кратным 16: s e c t i o n . t e x t align=16 Формат bin также можно использовать для создания файлов, исполняемых поди) или для загрузчиков. Значение по умолчанию директивы BITS равно 16. В коде, предназначенном для вывода в формате bin, может присутствовать директива ORG. Формат OMF — объектный файл для 16-битного режима
OMF (Object Module Format) — это старый формат, разработанный корпорацией Intel, в котором до сих пор создает объектный код Turbo Assembler. Компиляторы MASM и NASM тоже поддерживают этот формат.
136
Глава 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. У каждого файла должна быть своя точка входа (как минимум одна. Точка входа определяет позицию, на которую будет передано управление после загрузки программы в память. Точка входа (entry point) обозначена специальным идентификатором (или меткой) ..start:. Чтобы получить файл в формате OBJ, нужно вызвать компилятор с ключом командной строки -1 obj. Формат Win32 — объектный файл для 32-битного режима Для компоновки программы с помощью Microsoft Visual C+ + используется
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 используется группой совместимых операционных систем (NetB-
SD, FreeBSD и OpenBSD). NASM может генерировать файлы обоих форматов, если указать ключ -f aout для Linux или -f aoutb для BSD.
137
Borland, которые тоже поддерживают этот расширенный 32-битный формата не другой объектный формат, поддерживаемый Microsoft. Формат OBJ расширяет синтаксис некоторых директив, в частности,
SEGMENT (SECTION). Подробное описание этого формата выходит за рамки данной книги, новы можете прочитать о нем в руководстве по компилятору
NASM. Кроме того, формат OBJ добавляет новую директиву IMPORT, которую мы опишем в главе 11, посвященной программированию в Windows. Директива
IMPORT позволяет импортировать указанный файл из DLL, ей нужно передать два параметра — имя идентификатора и DLL. У каждого файла должна быть своя точка входа (как минимум одна. Точка входа определяет позицию, на которую будет передано управление после загрузки программы в память. Точка входа (entry point) обозначена специальным идентификатором (или меткой) ..start:. Чтобы получить файл в формате OBJ, нужно вызвать компилятор с ключом командной строки -1 obj. Формат Win32 — объектный файл для 32-битного режима Для компоновки программы с помощью Microsoft Visual C+ + используется
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 используется группой совместимых операционных систем (NetB-
SD, FreeBSD и OpenBSD). NASM может генерировать файлы обоих форматов, если указать ключ -f aout для Linux или -f aoutb для BSD.
137
Ассемблер на примерах. Базовый курс Формат coff — наследник a.out Формат COFF (Common Object File Format) представляет собой существенно усовершенствованную и доработанную версию формата a.out. Он распространен как в мире UNIX, таки в Windows NT, и поддерживается некоторыми свободными (некоммерческими) средами разработки, например, DJGPP. Используется для разработки приложений на Си. Если компилятор вызвать с параметром -f coff, мы получим на выходе объектный файл в формате
COFF. Расширение файла по умолчанию для этого формата — о Формат elf — основной формат в мире UNIX Формат ELF (Executable and Linkable Format) — это формат как объектного, таки исполняемого файла. Он применяется во многих подобных системах. Мы можем использовать его, чтобы откомпилировать программу для операционных систем Linux, Solaris x86, UnixWare, SCO-UNIX и UNIX
System V, причем не только с языка ассемблера. Расширение объектного файла по умолчанию о. Для получения файла в этом формате компилятор нужно запускать с ключом -f elf. Символическая информация Некоторые выходные форматы позволяют хранить вместе с машинными кодами символы исходного текста программы. Такая возможность очень полезна при отладке, она позволяет отладчику отображать адреса памяти в виде имен переменных, меток и т.п. Символьная информация значительно увеличивает размер выходного файла, поэтому после отладки программу заново компилируют уже без нее. Для добавления символической информации нужно запустить NASM с ключом
-g. Этот ключ допустим только для форматов OBJ и ELF.
COFF. Расширение файла по умолчанию для этого формата — о Формат elf — основной формат в мире UNIX Формат ELF (Executable and Linkable Format) — это формат как объектного, таки исполняемого файла. Он применяется во многих подобных системах. Мы можем использовать его, чтобы откомпилировать программу для операционных систем Linux, Solaris x86, UnixWare, SCO-UNIX и UNIX
System V, причем не только с языка ассемблера. Расширение объектного файла по умолчанию о. Для получения файла в этом формате компилятор нужно запускать с ключом -f elf. Символическая информация Некоторые выходные форматы позволяют хранить вместе с машинными кодами символы исходного текста программы. Такая возможность очень полезна при отладке, она позволяет отладчику отображать адреса памяти в виде имен переменных, меток и т.п. Символьная информация значительно увеличивает размер выходного файла, поэтому после отладки программу заново компилируют уже без нее. Для добавления символической информации нужно запустить NASM с ключом
-g. Этот ключ допустим только для форматов OBJ и ELF.
Глава 10 Программирование в DOS Адресация памяти в реальном режиме Организация памяти в DOS Типы исполняемых файлов в DOS Основные системные вызовы Файловые операции ввода-вывода Управление памятью Коды ошибок. Отладка Резидентные программы Ассемблер на примерах. Базовый курс
«640 KB должно быть достаточно для любого Билл Гейтс 1981) Операционная система DOS (или MS-DOS, Microsoft Disk Operating System) была первой операционной системой для компьютеров IBM PC. Ее архитектура и возможности были основаны на операционной системе СР/М, которая в то время была достаточно популярна среди 8- и 16-битных компьютеров. Интерфейсом DOS являлась командная строка. Что же касается многозадачности, то она была однозадачной операционной системой. Файловая система не предусматривала прав доступа к файлам, которые использовались в операционной системе UNIX с самого момента ее создания. Изначально
DOS располагала всего 640 Кб памяти, правда, позже появились различные способы ее расширения. Система никогда не запускалась в защищенном режиме (поскольку была разработана для 8086), и программы могли не только управлять аппаратными средствами компьютера напрямую, но и записывать данные в любую область памяти, даже не принадлежавшую им. Процессоры 80286 и 80386 поддерживали защищенный режим. Приложения, работающие в защищенном режиме, должны были переключать процессор самостоятельно. Ясно, что нельзя было запустить несколько приложений в защищенном режиме одновременно. Ю. Адресация памяти в реальном режиме Процессор 8086 имел 20-битную адресную шину и поэтому способен был адресовать только 2 20
байтов (1 Мб) оперативной памяти. Номы знаем, что его регистры были 16-битными: как же он мог тогда адресовать 20 битов адресной шины Ответ находится в сегментных регистрах. Окончательный адрес, сброшенный на адресную шину, получался как сумма значений соответствующего
16-битного регистра и сегментного регистра, умноженного на 16.
140
Глава 10. Программирование в DOS Рис. 10.1. Расчет физического адреса Следующий пример поможет понять, как рассчитывается физический адрес mov a l , [ d s : s i ] ; загрузить в AL содержание памяти по адресу d s : s i Предположим, что DS = 0х559Е, a SI имеет значение 0x100. Вычислить окончательный 20-битный адрес можно следующим образом
0х559Е * 0x10 (0x10 равно 16) + 0x100 Желаемое значение будет загружено с адреса памяти 0х559Е0 + 0x100 =
0х55АЕ0. Значение сегментного регистра называется сегментом, а другое значение (то, которое содержится в регистре общего назначения или указано как непосредственное значение) называется смещением (offset). Заметим, что адрес 0х55АЕ0 также может определяться другими парами сегментов и смещений, например, сегментом 0х558Е и смещением 0x200. Адресация, использующая сегменты и смещения, имеет массу недостатков. Смещение может быть только 16-битным, и поэтому полный размер одного сегмента в памяти ограничен объемом 64 Кб. Программа, код или данные которой превышают 64 Кб, должна быть разделена на несколько сегментов. При необходимости в сегментные регистры должны быть загружены другие адреса. Решение проблемы, которая возникает при делении программы на несколько сегментов, будет рассмотрено в параграфе 10.4. Для сохранения совместимости даже новейшие процессоры поддерживают
16-битную адресацию в реальном режиме. Поэтому можно запустить самую древнюю программу, в том числе и саму DOS, на новейших х86-совме- стимых процессорах.
141
0х559Е * 0x10 (0x10 равно 16) + 0x100 Желаемое значение будет загружено с адреса памяти 0х559Е0 + 0x100 =
0х55АЕ0. Значение сегментного регистра называется сегментом, а другое значение (то, которое содержится в регистре общего назначения или указано как непосредственное значение) называется смещением (offset). Заметим, что адрес 0х55АЕ0 также может определяться другими парами сегментов и смещений, например, сегментом 0х558Е и смещением 0x200. Адресация, использующая сегменты и смещения, имеет массу недостатков. Смещение может быть только 16-битным, и поэтому полный размер одного сегмента в памяти ограничен объемом 64 Кб. Программа, код или данные которой превышают 64 Кб, должна быть разделена на несколько сегментов. При необходимости в сегментные регистры должны быть загружены другие адреса. Решение проблемы, которая возникает при делении программы на несколько сегментов, будет рассмотрено в параграфе 10.4. Для сохранения совместимости даже новейшие процессоры поддерживают
16-битную адресацию в реальном режиме. Поэтому можно запустить самую древнюю программу, в том числе и саму DOS, на новейших х86-совме- стимых процессорах.
141
Ассемблер на примерах. Базовый курс
10.2. Организация памяти в DOS Как мы только что объяснили, из-за реального режима процессора, операционная система DOS могла адресовать только 1 Мб оперативной памяти. Таблица 10.1. — это свод отдельных адресов и блоков памяти, выделенных для операционной системы, прикладных программ и аппаратных средств. Таблица 10.1 Адрес (сегмент:смещение)
0x0000:0x0000 0x0040:0x0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000
Ох0АООО:Ох000О
ОхОВООО:ОхОО0О
Ох0В8ОО:0х000О
ОхОС80О:ОхОО0О - 0х0Е00О:Ох000О
0xF000:OxO000 Что находится по адресу Таблица векторов прерываний Таблица переменных BIOS
DOS kernel (ядро системы) Буферы, обработчики прерываний, драйвера устройств Резидентная часть командного интерпретатора (оболочки)
COMMAND.COM Резидентные программы (TSR, Terminate and Stay Resident) Выполняющаяся в данный момент программа (Нортон, текстовый редактор и т.п.) Свободная память Начало видеопамяти (в графическом режиме) Видеопамять монохромного графического адаптера в текстовом режиме) Видеопамять графического адаптера в текстовых режимах Свободные адреса для дополнительной памяти ROM
(Read Only Memory)
ROM BIOS В самом начале адресного пространства зарезервировано место для таблицы прерываний. Ее размер вычислить легко. Процессор поддерживает 256 разных векторов прерывания, а каждый вектор представляет собой пару сегмент- смещение, то есть занимает 4 байта. В итоге получается 1 Кб. Сразу же за таблицей прерываний, по адресу 0x400 (те. 0x0040:0x0000), следует таблица переменных BIOS. Она содержит список устройств, подключенных к компьютеру, и их конфигурацию. Описание отдельных устройств мы рассматривать в этой книге не будем, но если кого-то заинтересует этот вопрос, то всегда можно найти электронный справочник Ralf Brown Interrupt List в Интернете. В следующем блоке памяти загружена операционная система вместе с разными буферами и обработчиками прерываний. За операционной системой следует командный интерпретатор COMMAND.COM, который позволяет пользователю общаться с компьютером через интерфейс командной строки. Далее следуют резидентные или TSR программы. Программы такого типа запускаются, как и обычные программы, но по завершении они остаются
142
10.2. Организация памяти в DOS Как мы только что объяснили, из-за реального режима процессора, операционная система DOS могла адресовать только 1 Мб оперативной памяти. Таблица 10.1. — это свод отдельных адресов и блоков памяти, выделенных для операционной системы, прикладных программ и аппаратных средств. Таблица 10.1 Адрес (сегмент:смещение)
0x0000:0x0000 0x0040:0x0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000 0х0????:0х0000
Ох0АООО:Ох000О
ОхОВООО:ОхОО0О
Ох0В8ОО:0х000О
ОхОС80О:ОхОО0О - 0х0Е00О:Ох000О
0xF000:OxO000 Что находится по адресу Таблица векторов прерываний Таблица переменных BIOS
DOS kernel (ядро системы) Буферы, обработчики прерываний, драйвера устройств Резидентная часть командного интерпретатора (оболочки)
COMMAND.COM Резидентные программы (TSR, Terminate and Stay Resident) Выполняющаяся в данный момент программа (Нортон, текстовый редактор и т.п.) Свободная память Начало видеопамяти (в графическом режиме) Видеопамять монохромного графического адаптера в текстовом режиме) Видеопамять графического адаптера в текстовых режимах Свободные адреса для дополнительной памяти ROM
(Read Only Memory)
ROM BIOS В самом начале адресного пространства зарезервировано место для таблицы прерываний. Ее размер вычислить легко. Процессор поддерживает 256 разных векторов прерывания, а каждый вектор представляет собой пару сегмент- смещение, то есть занимает 4 байта. В итоге получается 1 Кб. Сразу же за таблицей прерываний, по адресу 0x400 (те. 0x0040:0x0000), следует таблица переменных BIOS. Она содержит список устройств, подключенных к компьютеру, и их конфигурацию. Описание отдельных устройств мы рассматривать в этой книге не будем, но если кого-то заинтересует этот вопрос, то всегда можно найти электронный справочник Ralf Brown Interrupt List в Интернете. В следующем блоке памяти загружена операционная система вместе с разными буферами и обработчиками прерываний. За операционной системой следует командный интерпретатор COMMAND.COM, который позволяет пользователю общаться с компьютером через интерфейс командной строки. Далее следуют резидентные или TSR программы. Программы такого типа запускаются, как и обычные программы, но по завершении они остаются
142
Глава 10. Программирование в DOS загруженными в память. Типичная резидентная программа — это драйвер мыши, который должен оставаться в памяти, чтобы приложения могли использовать мышь. Программный интерфейс, который обеспечивает взаимосвязь других приложений под управлением операционной системы DOS (между приложениями или между приложением и ОС, реализован через векторы прерываний. Мы рассмотрим этот вопрос более детально в параграфе Основные системные вызовы. Следующий блок содержит программу, работающую в данный момент. Адресное пространство этой программы простирается до предела в 640 Кб, те. вплоть до адреса ОхОА000:0хОО0О. Адреса от 640 Кб до 1 Мб зарезервированы для аппаратных средств компьютера. Первые 64 Кб этой области используются видеоадаптером. Запись в эти адреса позволяет рисовать отдельные точки на экране. Адреса начиная с 0х0В800:0х0000 используются видеоадаптером для хранения отдельных символов, отображаемых на экране. Адрес 0х0В800:0х0000 представляет левый верхний угол первой отображаемой строки в текстовом режиме. Данные организованы в пары байтов. Первый байт содержит ASCII- код отображаемого символа, а второй байт обозначает цвет. Оставшееся пространство памяти зарезервировано для программ, хранящихся в памяти ROM (Read Only Memory): BIOS видеоадаптера и BIOS самого компьютера.
10.3. Расширение памяти свыше 1 MB Через несколько лет 640 Кб перестало хватать, и поэтому были разработаны способы расширения памяти, сохраняющие при этом реальный режим. Процессор 80286 имел 24-битную адресную шину, однако в реальном режиме он все еще мог адресовать только 20 битов, те. 1 Мб. Последним пригодным для использования сегментом был 0x0F000, в котором хранится BIOS. Чтобы адресовать следующий за BIOS сегмент, нужно было бы загрузить в сегментный регистр значение OxFFFF. Но после добавления к этому значению смещения адрес вышел бы за пределы 20 битов, то есть превысил бы ограничение в 1 Мб. Разработчики решили оборудовать компьютер специальным устройством для контроля над двадцать первым битом адресной шины, которое позволяло бы использовать сегмент OxOFFFF. Процессор получит доступ к памяти выше 1
Мб, если «что-то извне установит й бит адресной шины в значение 1. Этим
«чем-то» стало устройство, названное Ай бит имеет номер 20, потому что нумерация начинается с нуля. А управляет вышеупомянутым м битом и позволяет использовать дополнительные 64 Кб оперативной памяти.
143
10.3. Расширение памяти свыше 1 MB Через несколько лет 640 Кб перестало хватать, и поэтому были разработаны способы расширения памяти, сохраняющие при этом реальный режим. Процессор 80286 имел 24-битную адресную шину, однако в реальном режиме он все еще мог адресовать только 20 битов, те. 1 Мб. Последним пригодным для использования сегментом был 0x0F000, в котором хранится BIOS. Чтобы адресовать следующий за BIOS сегмент, нужно было бы загрузить в сегментный регистр значение OxFFFF. Но после добавления к этому значению смещения адрес вышел бы за пределы 20 битов, то есть превысил бы ограничение в 1 Мб. Разработчики решили оборудовать компьютер специальным устройством для контроля над двадцать первым битом адресной шины, которое позволяло бы использовать сегмент OxOFFFF. Процессор получит доступ к памяти выше 1
Мб, если «что-то извне установит й бит адресной шины в значение 1. Этим
«чем-то» стало устройство, названное Ай бит имеет номер 20, потому что нумерация начинается с нуля. А управляет вышеупомянутым м битом и позволяет использовать дополнительные 64 Кб оперативной памяти.
143
Ассемблер на примерах. Базовый курс Однако таким способом можно добавить только 64 Кб памяти, что не так уж много. В диапазоне от 640 Кб до 1 Мб имеется масса неиспользованной памяти, зарезервированной для системы. Нельзя ли как-нибудь использовать эти адреса Конечно же, можно. Консорциум из трех компаний — Lotus, Intel и Microsoft — сформулировал спецификацию памяти, которую назвали LIM EMS (Expanded
Memory Specification). В области между 640 Кб и 1 Мб было создано окно памяти с размером 64 KB, которое состояло из четырех страниц, по 16 Кб каждая. Это окно называется кадром страницы (page frame), потому что EMS позволяет отображать в это окно память за пределами 1 Мб. У компьютеров типа 80286 память реализована с помощью аппаратных средств, но позже благодаря процессору 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, a затем передает управление на смещение 0x100, где находится первая команда исполняемого файла.
144
Memory Specification). В области между 640 Кб и 1 Мб было создано окно памяти с размером 64 KB, которое состояло из четырех страниц, по 16 Кб каждая. Это окно называется кадром страницы (page frame), потому что EMS позволяет отображать в это окно память за пределами 1 Мб. У компьютеров типа 80286 память реализована с помощью аппаратных средств, но позже благодаря процессору 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, a затем передает управление на смещение 0x100, где находится первая команда исполняемого файла.
144
Глава 10. Программирование в DOS Диапазон адресов от 0 до 0x100 зарезервирован для операционной системы. Он называется PSP (Program Segment Prefix). Здесь операционная система сохраняет среди прочего информацию об открытых файлах и о адресе родительского процесса. Начиная со смещения 0x80 хранятся аргументы, с которыми запущена программа. Рассмотрим карту памяти СОМ-программы, загруженной в сегмент 0x1234 см. табл. 10.2): Карта памяти СОМ-программы Таблица 10.2 Адрес (сегмент:смещение)
0x1234:0x0000 0x1234:0x0080 0x1234:0x0100 0х1234:0х????
0х1234:0х????
0х1234:0х????
0x1234:0xFFFE Что находится по адресу Начало области, зарезервированной для ОС. Здесь хранятся некоторые внутренние структуры С этого адреса начинаются переданные параметры С 0x100 операционная система загружает файл с диска Окончание сегмента (секции) кода Окончание сегмента (секции) данных Начало секции неинициализированных данных (.bss) Память распределена до конца сегмента, а стек растет к началу сегмента Вершина стека Тип исполняемого файла ЕХЕ существенно улучшен по сравнению с типом СОМ. При этом главное улучшение заключается в том, что тип ЕХЕ пригоден для размещения программ, размер которых больше 64 Кб. Файл типа ЕХЕ может содержать несколько сегментов кода и несколько сегментов данных. У стека также есть зарезервированные сегменты. Чтобы обратиться к другому сегменту в реальном режиме, нужно переключить значения сегментных регистров. Так, при вызове подпрограммы, сохраненной в другом сегменте, мы должны использовать инструкцию дальнего вызова (call far), которая правильно изменяет значения обоих регистров (CS и IP). Точно также должен бьпь выполнен возврат (retf). Если переменная сохранена в другом сегменте данных, мы получаем адрес ее сегмента при помощи псевдокоманды SEG и загружаем ее в сегментный регистр. Правильный адрес получается в результате сложения смещения, которым заведует идентификатор, с новым значением сегмента. Разработанная схема действительно хороша, если бы не одно но. Как получить адрес сегмента, если мы не знаем стартовые адреса сегментов (те. программы Скомпилированная программа сохраняется на диск так, как если бы ей предстояло быть загруженной с адреса 0x0000:0x0000, то есть первый сегмент начинается си заканчивается в 0x0000:0xFFFF, второй сегмент начинается си так далее. Каждое место программы, в котором используются значения сегментов (те.
0x0000, 0x1000 и т.д.), записывается в так называемую таблицу перемещений
(relocation table).
145
0x1234:0x0000 0x1234:0x0080 0x1234:0x0100 0х1234:0х????
0х1234:0х????
0х1234:0х????
0x1234:0xFFFE Что находится по адресу Начало области, зарезервированной для ОС. Здесь хранятся некоторые внутренние структуры С этого адреса начинаются переданные параметры С 0x100 операционная система загружает файл с диска Окончание сегмента (секции) кода Окончание сегмента (секции) данных Начало секции неинициализированных данных (.bss) Память распределена до конца сегмента, а стек растет к началу сегмента Вершина стека Тип исполняемого файла ЕХЕ существенно улучшен по сравнению с типом СОМ. При этом главное улучшение заключается в том, что тип ЕХЕ пригоден для размещения программ, размер которых больше 64 Кб. Файл типа ЕХЕ может содержать несколько сегментов кода и несколько сегментов данных. У стека также есть зарезервированные сегменты. Чтобы обратиться к другому сегменту в реальном режиме, нужно переключить значения сегментных регистров. Так, при вызове подпрограммы, сохраненной в другом сегменте, мы должны использовать инструкцию дальнего вызова (call far), которая правильно изменяет значения обоих регистров (CS и IP). Точно также должен бьпь выполнен возврат (retf). Если переменная сохранена в другом сегменте данных, мы получаем адрес ее сегмента при помощи псевдокоманды SEG и загружаем ее в сегментный регистр. Правильный адрес получается в результате сложения смещения, которым заведует идентификатор, с новым значением сегмента. Разработанная схема действительно хороша, если бы не одно но. Как получить адрес сегмента, если мы не знаем стартовые адреса сегментов (те. программы Скомпилированная программа сохраняется на диск так, как если бы ей предстояло быть загруженной с адреса 0x0000:0x0000, то есть первый сегмент начинается си заканчивается в 0x0000:0xFFFF, второй сегмент начинается си так далее. Каждое место программы, в котором используются значения сегментов (те.
0x0000, 0x1000 и т.д.), записывается в так называемую таблицу перемещений
(relocation table).
145
Ассемблер на примерах. Базовый курс Программа загружается в память с некоторого базового адреса. По таблице перемещений определяются те места программы, содержимое которых должно быть подправлено, и к хранящимся там адресам добавляется значение базового адреса. Таким образом программа целиком перемещается к базовому адресу загрузки. Теперь все сегментные регистры содержат правильные значения, и программа может быть запущена.
1 ... 8 9 10 11 12 13 14 15 ... 20
10.5. Основные системные вызовы Операционная система предлагает запущенной программе множество сер
висов. Используя операционную систему, наша программа может выводить сообщения на экран, считывать ввод с клавиатуры, работать с файлами или требовать выделения дополнительной памяти. Сервисы операционной системы доступны через прерывание 0x21. Параметры системных вызовов указываются в определенных регистрах. Под DOS номер необходимой функции указывается в регистре АН. Если системный вызов завершился ошибочно, то устанавливается флаг переноса, а в регистр АХ помещается код ошибки. Немедленное завершение программы Простейшая программа — это та, которая завершается немедленно после запуска Запрос на завершение программы обеспечивается системным вызовом
DOS с номером 0х4С, прерывающим программу и передающим управление родительской программе. Ввод Завершение программы АН • с
AL = код возврата Вывод Ничего Номер вызываемой функции передается через регистр АН, а в AL указывается так называемый код возврата, то есть значение, которое должна анализировать родительская программа. Дочерняя программа может либо сообщить родительской, что завершилась нормально, либо вернуть код ошибки. Эта функция такого же назначения, как exit в языке С. Наша первая программа будет включать в себя только раздел кода (листинг 10.1). Листинг 10.1. Простейшая программа под DOS
SECTION .text Наша первая программа для операционной системы DOS. Она ничего не делает, только завершается сразу после запуска org 0x100 генерируем СОМ файл, загружаемый с 0x100 146
Глава 10. Программирование в DOS mov ah,0x4C эта функция завершает программу mov al,0 код возврата 0 int 0x21 вызываем ядро операционной системы конец программы Теперь сохраните программу в файл finish.asm. В этом и следующих примерах мы будем компилировать выходные файлы типа СОМ, чтобы избежать запутанной процедуры настройки сегментных регистров и сконцентрироваться на решении других проблем. Следующим шагом будет компиляция f i n i s h . asm компилятором nasm: nasm -f bin -о finish.com finish.asm Параметр -f указывает тип выходного формата, а параметр -о указывает на создание файла с заданным именем f i n i s h , com. После компиляции мы можем запустить программу finish.com, написав
«finish» в командной строке и нажав «Enter». Программа сразу же вернет управление командному интерпретатору. Вывод строки. Пишем программу «Hello, World!» Другая очень полезная функция — это вывод текстовой строки на экран. Сейчас мы не будем рассматривать запись в файла рассмотрим разные функции вывода строки на экран. По историческим причинам выводимая строка оканчивается ненулевым байтом, а знаком доллара «$» (0x24). Ввод Печать строки АН = 0x09
DS:DX = указатель на строку, завершенную «$» Вывод Ничего Печать одного символа на стандартный вывод выполняет функция DOS с номером 0x02, которая считывает код ASCII необходимого символа из DL. Ввод Печать символа АН = 0x02
DL = код печатаемого символа Вывод Ничего Теперь у нас достаточно знаний, чтобы написать программу, которая выведет на экран всем известный текст «Hello, World!» и завершит свою работу. Сейчас мыс вами отредактируем нашу программу f i n i s h , asm. Будет добавлена новая секция . d a t a с описанием переменной «hello», содержащая строку, состоящую из текста «Hello, World!», служебных символов CR (OxD) и LF (ОхА), которые вовремя вывода на экран выполняют перевод строки, и завершающего символа «$» (листинг 10.2).
147
«finish» в командной строке и нажав «Enter». Программа сразу же вернет управление командному интерпретатору. Вывод строки. Пишем программу «Hello, World!» Другая очень полезная функция — это вывод текстовой строки на экран. Сейчас мы не будем рассматривать запись в файла рассмотрим разные функции вывода строки на экран. По историческим причинам выводимая строка оканчивается ненулевым байтом, а знаком доллара «$» (0x24). Ввод Печать строки АН = 0x09
DS:DX = указатель на строку, завершенную «$» Вывод Ничего Печать одного символа на стандартный вывод выполняет функция DOS с номером 0x02, которая считывает код ASCII необходимого символа из DL. Ввод Печать символа АН = 0x02
DL = код печатаемого символа Вывод Ничего Теперь у нас достаточно знаний, чтобы написать программу, которая выведет на экран всем известный текст «Hello, World!» и завершит свою работу. Сейчас мыс вами отредактируем нашу программу f i n i s h , asm. Будет добавлена новая секция . d a t a с описанием переменной «hello», содержащая строку, состоящую из текста «Hello, World!», служебных символов CR (OxD) и LF (ОхА), которые вовремя вывода на экран выполняют перевод строки, и завершающего символа «$» (листинг 10.2).
147
Ассемблер на примерах. Базовый курс Листинг 10.2. Программа Hello, World! под DOS
SECTION .text Эта программа выводит на экран текст "Hello, World!' конец строки (E0L - 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 hello.asm. Запустим программу с помощью команды h e l l o . Ввод с клавиатуры Для последовательного считывания символов используется функция DOS с номером 0x01, которая похожа на getchar в языке Си на readkey в языке
Pascal. Ввод Чтение символа АН = 0x01 Вывод
AL содержит символ, считанный со стандартного ввода клавиатуры) Функция 0x01 ждет нажатия клавиши После получения символа она отражает символ на стандартном выводе (функция 0x08 только считывает символ, ноне отражает его. В регистре AL окажется значение 0, если была нажата клавиша с так называемым расширенным (extended) кодом, то есть одна из клавиш Page
Up, Page Down, Home, End, функциональные клавиши F1..F12 и т.п. Следующий вызов функции 0x01 поместит в AL расширенный код нажатой клавиши. Часто нам нужно прочитать целую строку определенной длины, завершенную нажатием клавиши «Enter». Для этого в DOS имеется функция, накапливающая прочитанные с клавиатуры символы в буфере.
148
SECTION .text Эта программа выводит на экран текст "Hello, World!' конец строки (E0L - 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 hello.asm. Запустим программу с помощью команды h e l l o . Ввод с клавиатуры Для последовательного считывания символов используется функция DOS с номером 0x01, которая похожа на getchar в языке Си на readkey в языке
Pascal. Ввод Чтение символа АН = 0x01 Вывод
AL содержит символ, считанный со стандартного ввода клавиатуры) Функция 0x01 ждет нажатия клавиши После получения символа она отражает символ на стандартном выводе (функция 0x08 только считывает символ, ноне отражает его. В регистре AL окажется значение 0, если была нажата клавиша с так называемым расширенным (extended) кодом, то есть одна из клавиш Page
Up, Page Down, Home, End, функциональные клавиши F1..F12 и т.п. Следующий вызов функции 0x01 поместит в AL расширенный код нажатой клавиши. Часто нам нужно прочитать целую строку определенной длины, завершенную нажатием клавиши «Enter». Для этого в DOS имеется функция, накапливающая прочитанные с клавиатуры символы в буфере.
148
Глава 10. Программирование в DOS Ввод Считывание строки АН = ОхОА
DS:DX = указатель на буфер в специальном формате Вывод В буфере находится прочитанная строка Перед вызовом функции ОхОА указатель должен содержать адрес специальной структуры, которая в результате работы функции будет заполнена прочитанной строкой. Первый байт буфера содержит его максимальный размер, то есть количество символов (1-254), по достижении которого выдается звуковой сигнал (ASCII- код 7) и требуется нажать «Enter». Следующий байт содержит фактическое количество введенных символов, не считая символа OxOD (кода клавиши
«Enter»). mov a h , ОхОА ; загрузить в АН ОхОА, функция чтения строки mov dx, s t r i n g загрузить в DX адрес (смещение) буфера i n t 0x21 вызов DOS s t r i n g d b 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 d b 5 , 0 , 0 , 0 , 0 , 0 , 0 После выполнения команды int 0x21 операционная система начнет чтение символов и будет продолжать чтение до нажатия клавиши «Enter». Под буфер отведено 7 байтов, то есть в нем можно разместить не более 4 символов плюс символ завершения — код клавиши «Enter». Если мы наберем А,В,С, а затем нажмем «Enter», то структура «string» будет выглядеть следующим образом s t r i n g d b 5,3,0x65,0x66,0x67,OxOD,0 Первый байт остается неизменным, второй байт содержит длину считанной строки без символа окончания OxD, ас третьего байта начинается сама строка
(ABC), завершающаяся символом окончания OxD. Рассмотрим несколько примеров. Пример 1: напишем программу, которая будет отображать каждую нажатую клавишу на экране дважды. Программа завершается по нажатии клавиши
«Enter» (листинг 10.3). Для чтения символов мы будем использовать функцию 0x01, отображающую нажатую клавишу на экране. Следующий, идентичный предыдущему, символ будем выводить функцией 0x02.
149
DS:DX = указатель на буфер в специальном формате Вывод В буфере находится прочитанная строка Перед вызовом функции ОхОА указатель должен содержать адрес специальной структуры, которая в результате работы функции будет заполнена прочитанной строкой. Первый байт буфера содержит его максимальный размер, то есть количество символов (1-254), по достижении которого выдается звуковой сигнал (ASCII- код 7) и требуется нажать «Enter». Следующий байт содержит фактическое количество введенных символов, не считая символа OxOD (кода клавиши
«Enter»). mov a h , ОхОА ; загрузить в АН ОхОА, функция чтения строки mov dx, s t r i n g загрузить в DX адрес (смещение) буфера i n t 0x21 вызов DOS s t r i n g d b 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 d b 5 , 0 , 0 , 0 , 0 , 0 , 0 После выполнения команды int 0x21 операционная система начнет чтение символов и будет продолжать чтение до нажатия клавиши «Enter». Под буфер отведено 7 байтов, то есть в нем можно разместить не более 4 символов плюс символ завершения — код клавиши «Enter». Если мы наберем А,В,С, а затем нажмем «Enter», то структура «string» будет выглядеть следующим образом s t r i n g d b 5,3,0x65,0x66,0x67,OxOD,0 Первый байт остается неизменным, второй байт содержит длину считанной строки без символа окончания OxD, ас третьего байта начинается сама строка
(ABC), завершающаяся символом окончания OxD. Рассмотрим несколько примеров. Пример 1: напишем программу, которая будет отображать каждую нажатую клавишу на экране дважды. Программа завершается по нажатии клавиши
«Enter» (листинг 10.3). Для чтения символов мы будем использовать функцию 0x01, отображающую нажатую клавишу на экране. Следующий, идентичный предыдущему, символ будем выводить функцией 0x02.
149
Ассемблер на примерах. Базовый курс Листинг 10.3. Программа, отображающая дважды на экране каждую нажатую клавишу
; Двойное отображение каждой нажатой клавиши на экране
SECTION .text again: mov ah,0x01 int 0x21 mov dl,al cmp dl, OxD j z endprog mov ah, 0x02 int 0x21 jmp again endprog: mov ah,0x4C int 0x21 функция чтения символа вызов DOS копирование считанного символа в DL нажат "Enter"? если да, то переход к концу программы функция вывода символа вызов DOS вернуться к чтению следующего символа функция завершения программы
; вызов DOS конец программы Эту программу назовем echo . asm. Исполняемый файл echo . com будет создан командой nasm -f bin -о echo.com echo.asm Пример 2: напишем программу, считывающую строку символов до нажатия клавиши «Enter» и выводящую эту строку в обратном порядке (листинг 10.4). Используя функции ОхОА, мы считываем строку и сохраняем ее во временной переменной, которая после будет выведена в обратном порядке с помощью функции 0x02. Листинг 10.4. Программа, считывающая строку символов до нажатия клавиши «Enter» и выводящая эту строку в обратном порядке
SECTION .text Эта программа выводит прочитанную строку на экран в обратном порядке. org 0x100 mov ah, OxOA mov dx,string int 0x21 xor ax,ax mov al,[string+1] inc dx add dx, ax mov s i,dx создаем файл типа COM функция чтения строки загружаем в DX адрес буфера строки вызов DOS обнуляем АХ читаем количество введенных символов пропускаем первый байт буфера прибавляем длину строки к адресу начала, получаем адрес последнего символа копируем DX в SI
150
; Двойное отображение каждой нажатой клавиши на экране
SECTION .text again: mov ah,0x01 int 0x21 mov dl,al cmp dl, OxD j z endprog mov ah, 0x02 int 0x21 jmp again endprog: mov ah,0x4C int 0x21 функция чтения символа вызов DOS копирование считанного символа в DL нажат "Enter"? если да, то переход к концу программы функция вывода символа вызов DOS вернуться к чтению следующего символа функция завершения программы
; вызов DOS конец программы Эту программу назовем echo . asm. Исполняемый файл echo . com будет создан командой nasm -f bin -о echo.com echo.asm Пример 2: напишем программу, считывающую строку символов до нажатия клавиши «Enter» и выводящую эту строку в обратном порядке (листинг 10.4). Используя функции ОхОА, мы считываем строку и сохраняем ее во временной переменной, которая после будет выведена в обратном порядке с помощью функции 0x02. Листинг 10.4. Программа, считывающая строку символов до нажатия клавиши «Enter» и выводящая эту строку в обратном порядке
SECTION .text Эта программа выводит прочитанную строку на экран в обратном порядке. org 0x100 mov ah, OxOA mov dx,string int 0x21 xor ax,ax mov al,[string+1] inc dx add dx, ax mov s i,dx создаем файл типа COM функция чтения строки загружаем в DX адрес буфера строки вызов DOS обнуляем АХ читаем количество введенных символов пропускаем первый байт буфера прибавляем длину строки к адресу начала, получаем адрес последнего символа копируем DX в SI
150
Глава 10. Программирование в DOS s t d устанавливаем флаг направления для прохода обратно p r i n t _ n e x t _ c h a r : lodsb читаем символ из DS:SI и уменьшаем SI наконец строки (то есть начало) jb endprog ; если да, то переход к концу программы mov d l , a l загружаем прочитанный из строки символ в DL mov ah,0x02 функция вывода символа i n t 0x21 вызов DOS jmp print_next_char возвращаемся к следующему символу endprog: mov ah,0x4C ;РОЭ-функция завершения программы mov a l , 0 код возврата 0 i n t 0x21 вызов DOS
SECTION .data s t r i n g db 254,0 times 253 db ' ' заполняем буфер строки пробелами конец программы Назовем эту программу r e v e r s e . asm и скомпилируем ее также, как и предыдущую nasm -f bin -о reverse.com reverse.asm Эта программа немного сложнее, поэтому объясним ее функционирование шаг за шагом. Прежде всего, мы должны прочитать строку. Для этого мы вызываем функцию
ОхОА для чтения строки, передав ей подготовленный в специальном формате буфер для размещения считанной строки. mov ah, OxOA функция чтения строки 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
SECTION .data s t r i n g db 254,0 times 253 db ' ' заполняем буфер строки пробелами конец программы Назовем эту программу r e v e r s e . asm и скомпилируем ее также, как и предыдущую nasm -f bin -о reverse.com reverse.asm Эта программа немного сложнее, поэтому объясним ее функционирование шаг за шагом. Прежде всего, мы должны прочитать строку. Для этого мы вызываем функцию
ОхОА для чтения строки, передав ей подготовленный в специальном формате буфер для размещения считанной строки. mov ah, OxOA функция чтения строки 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
Ассемблер на примерах. Базовый курс Конечно же, вместо прибавления 2 и вычитания 1 можно просто прибавить 1, сократив программу на одну команду. Заметьте, что вначале нужно сбросить АХ и что длина строки — число восьмибитовое. inc dx пропускаем первый байт буфера add dx,ax прибавляем длину строки к адресу начала, получаем адрес последнего символа mov s i , d x копируем DX в SI Несмотря на то, что длина строки — число восьмибитовое, мы прибавляем к адресу начала строки не AL, а АХ, потому что если один операнд представляет собой адрес или смещение, то второй должен быть такого же размера. В результате получается адрес последнего символа в строке. Команда LODSB выводит символ, адрес которого хранится в регистре SI, поэтому скопируем вычисленный адрес в этот регистр. Для получения следующего в обратном порядке символа нужно уменьшить этот адрес назначит, нужно установить флаг направления. 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), то первый символ строки не выводился бы, поэтому мы написали команду перейти, если меньше. Это условие станет истинным не до, а после вывода первого символа. cmp s i , s t r i n g + 1 конец строки jb endprog если да, то переход к концу программы Оставшаяся часть программы предельно проста. Символ, сохраненный в AL, копируется в DL, как того требует функция DOS для вывода символа. После вывода мы должны вернуться к LODSB, чтобы извлечь из строки следующий символ. mov d l , a l загружаем прочитанный из строки символ в DL mov ah,0x02 функция вывода символа i n t 0x21 вызов DOS jmp p r i n t _ n e x t _ c h a r возвращаемся к следующему символу Выполнение программы завершается системным вызовом 0х4С. e n d p r o g : mov ah,0x4C функция завершения программы mov al,0 код возврата 0 int 0x21 вызов DOS
152
LODSB загружает в AL символ, считанный из [DS:SI] , и уменьшает SI на
1 после считывания. Сразу же после LODSB следует сравнение с адресом s t r i n g + 1 , который содержался бы в SI после чтения символа на последнем шаге (то есть первого символа строки. Если бы мы использовали условный переход по равенству адресов (JZ), то первый символ строки не выводился бы, поэтому мы написали команду перейти, если меньше. Это условие станет истинным не до, а после вывода первого символа. cmp s i , s t r i n g + 1 конец строки jb endprog если да, то переход к концу программы Оставшаяся часть программы предельно проста. Символ, сохраненный в AL, копируется в DL, как того требует функция DOS для вывода символа. После вывода мы должны вернуться к LODSB, чтобы извлечь из строки следующий символ. mov d l , a l загружаем прочитанный из строки символ в DL mov ah,0x02 функция вывода символа i n t 0x21 вызов DOS jmp p r i n t _ n e x t _ c h a r возвращаемся к следующему символу Выполнение программы завершается системным вызовом 0х4С. e n d p r o g : mov ah,0x4C функция завершения программы mov al,0 код возврата 0 int 0x21 вызов DOS
152
Глава 10. Программирование в DOS Секция данных описывает структуру буфера.
SECTION .data s t r i n g db 254,0 times 253 db ' ' заполняем буфер строки пробелами Первый байт определяет максимальную длину строки, не считая завершающего символа. Второй байт содержит фактическую длину прочитанной строки ведь строка может быть и короче, чем 254 символа, и на данный момент она составляет 0 байтов. Используя директиву TIMES, оставшуюся часть структуры заполняем 253 пробелами.
10.6. Файловые операции ввода-вывода В главе 8, посвященной операционным системам, мы говорили о технике работы с файловой системой. Операционная система DOS работает по такому же принципу. Перед какой-либо операцией ввода-вывода файл должен быть открыт. От операционной системы необходимо получить идентификатор дескриптор) файла, который нужно указывать при вызове других функций для работы с файлами. По окончании процесса обработки файл должен быть закрыт. Оригинальная система DOS до слияния с ОС Windows позволяла использовать только короткие имена файлов 8 символов в имении символа в расширении файла. В Windows появились так называемые длинные имена файлов, состоящие из и символов в имени, включая несколько расширений. Давайте начнем с системных вызовов, использующих короткие имена файлов 8+3. Открытие файла Файл открывает функция 0x3D. Указатель на имя файла передается через пару регистров DS:DX (DS — сегмент, a DX — смещение. В регистр AL нужно записать требуемый режим доступа. В большинстве случаев файл открывается для чтения и записи (AL=0). Функция возвращает в АХ дескриптор открытого файла или код ошибки. При ошибочном завершении функция устанавливает флаг переноса CF=1, при корректном завершении CF=0. Ввод. Открытие файла АН = 0x3D
DS:DX = указатель на имя файла, завершенное нулевым байтом 0x0
AL. = режим доступа
AL = 0 чтение и запись
AL = 1 только запись
AL = 2 только чтение Вывод
CF = 0 если успешно, тогда АХ = дескриптор, назначенный файлу Или
CF= 1 ошибка, тогда АХ = код ошибки АХ = 0x0002 — файл не найден АХ = 0x0003 — путь не найден
153
SECTION .data s t r i n g db 254,0 times 253 db ' ' заполняем буфер строки пробелами Первый байт определяет максимальную длину строки, не считая завершающего символа. Второй байт содержит фактическую длину прочитанной строки ведь строка может быть и короче, чем 254 символа, и на данный момент она составляет 0 байтов. Используя директиву TIMES, оставшуюся часть структуры заполняем 253 пробелами.
10.6. Файловые операции ввода-вывода В главе 8, посвященной операционным системам, мы говорили о технике работы с файловой системой. Операционная система DOS работает по такому же принципу. Перед какой-либо операцией ввода-вывода файл должен быть открыт. От операционной системы необходимо получить идентификатор дескриптор) файла, который нужно указывать при вызове других функций для работы с файлами. По окончании процесса обработки файл должен быть закрыт. Оригинальная система DOS до слияния с ОС Windows позволяла использовать только короткие имена файлов 8 символов в имении символа в расширении файла. В Windows появились так называемые длинные имена файлов, состоящие из и символов в имени, включая несколько расширений. Давайте начнем с системных вызовов, использующих короткие имена файлов 8+3. Открытие файла Файл открывает функция 0x3D. Указатель на имя файла передается через пару регистров DS:DX (DS — сегмент, a DX — смещение. В регистр AL нужно записать требуемый режим доступа. В большинстве случаев файл открывается для чтения и записи (AL=0). Функция возвращает в АХ дескриптор открытого файла или код ошибки. При ошибочном завершении функция устанавливает флаг переноса CF=1, при корректном завершении CF=0. Ввод. Открытие файла АН = 0x3D
DS:DX = указатель на имя файла, завершенное нулевым байтом 0x0
AL. = режим доступа
AL = 0 чтение и запись
AL = 1 только запись
AL = 2 только чтение Вывод
CF = 0 если успешно, тогда АХ = дескриптор, назначенный файлу Или
CF= 1 ошибка, тогда АХ = код ошибки АХ = 0x0002 — файл не найден АХ = 0x0003 — путь не найден
153
Ассемблер на примерах. Базовый курс Закрытие файла Прежде чем переходить к другим функциям познакомимся с функцией закрытия файла ОхЗЕ: Ввод Закрытие файла АН = ОхЗЕ
ВХ = дескриптор файла Вывод
CF = 0 если успешно Или
CF = 1 ошибка, тогда АХ = код ошибки Чтобы закрыть файл, поместите его дескриптор, присвоенный файлу при открытии, в регистр ВХ. Напишем простую программку для открытия и закрытия файлов (листинг 10.5). Листинг 10.5. Простая программка-пример для открытия и закрытия файлов
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 _ n a m e d b " t e x t . t x t " , 0 имя файла t e x t . t x t Чтение из файла Очевидно, что приведенная в листинге 10.5 программа абсолютно бесполезна, поэтому давайте усовершенствуем ее мы будем выводить содержимое файла на экран Чтение данных из файла выполняется функцией 0x3F.
154
ВХ = дескриптор файла Вывод
CF = 0 если успешно Или
CF = 1 ошибка, тогда АХ = код ошибки Чтобы закрыть файл, поместите его дескриптор, присвоенный файлу при открытии, в регистр ВХ. Напишем простую программку для открытия и закрытия файлов (листинг 10.5). Листинг 10.5. Простая программка-пример для открытия и закрытия файлов
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 _ n a m e d b " t e x t . t x t " , 0 имя файла t e x t . t x t Чтение из файла Очевидно, что приведенная в листинге 10.5 программа абсолютно бесполезна, поэтому давайте усовершенствуем ее мы будем выводить содержимое файла на экран Чтение данных из файла выполняется функцией 0x3F.
154
Глава 10. Программирование в DOS Данные могут читаться из файла блоками произвольного размера. Следующий системный вызов считывает следующий блок (внутренний указатель позиции файла увеличивается после операции чтения, то есть для чтения всего файла нам нужен цикл. Ввод Чтение из файла АН = 0X3F
ВХ = дескриптор файла
DS:DX = указатель на буфер для хранения прочитанных данных
СХ = размер буфера Вывод В случае успеха
CF = 0 АХ фактическое количество байтов, считанных из файла (ноль означает конец файла, EOF) В случае ошибки
CF=1 АХ код ошибки Функции нужно передать дескриптор файла в регистре ВХ. Указатель на буфер, в который нужно поместить прочитанный блок данных, передается через пару DS:DX. Через регистр СХ передается максимальное количество байтов, которое должно быть считано зараз большую часть времени оно равно длине буфера. В регистр АХ функция записывает количество фактически прочитанных байтов (ведь если в файле осталось меньше данных, чем мы хотим прочитать, значение в АХ будет меньше размера буфера. В случае ошибки устанавливается флаг переноса CF=1, а в АХ будет код ошибки. Данные, прочитанные из файла, могут быть выведены на экран с помощью функции DOS 0x09. Но тогда у нас будут проблемы с любым файлом, содержащим символ $, который обозначает конец строки для функции 0x09. Чтобы обойти эту проблему, мы не будем использовать функцию 0x09, а вместо вывода на экран будем записывать прочитанные данные в поток стандартного вывода, а для вывода на экран используем функцию перенаправления вво
да/вывода, предоставленную нам операционной системой. Дескриптор файла стандартного вывода равен 0x0001. Запись в файл Для записи в файл используется функция 0x40. Она имеет те же аргументы, что и функция чтения файла Ввод Запись в файл АН = 0x40
ВХ = дескриптор файла
DS:DX = указатель на буфер
СХ = количество байтов, которые нужно записать Вывод В случае успеха
CF = 0 АХ = фактическое количество байтов, записанных в файл При ошибке
CF = 1 АХ код ошибки
ВХ = дескриптор файла
DS:DX = указатель на буфер для хранения прочитанных данных
СХ = размер буфера Вывод В случае успеха
CF = 0 АХ фактическое количество байтов, считанных из файла (ноль означает конец файла, EOF) В случае ошибки
CF=1 АХ код ошибки Функции нужно передать дескриптор файла в регистре ВХ. Указатель на буфер, в который нужно поместить прочитанный блок данных, передается через пару DS:DX. Через регистр СХ передается максимальное количество байтов, которое должно быть считано зараз большую часть времени оно равно длине буфера. В регистр АХ функция записывает количество фактически прочитанных байтов (ведь если в файле осталось меньше данных, чем мы хотим прочитать, значение в АХ будет меньше размера буфера. В случае ошибки устанавливается флаг переноса CF=1, а в АХ будет код ошибки. Данные, прочитанные из файла, могут быть выведены на экран с помощью функции DOS 0x09. Но тогда у нас будут проблемы с любым файлом, содержащим символ $, который обозначает конец строки для функции 0x09. Чтобы обойти эту проблему, мы не будем использовать функцию 0x09, а вместо вывода на экран будем записывать прочитанные данные в поток стандартного вывода, а для вывода на экран используем функцию перенаправления вво
да/вывода, предоставленную нам операционной системой. Дескриптор файла стандартного вывода равен 0x0001. Запись в файл Для записи в файл используется функция 0x40. Она имеет те же аргументы, что и функция чтения файла Ввод Запись в файл АН = 0x40
ВХ = дескриптор файла
DS:DX = указатель на буфер
СХ = количество байтов, которые нужно записать Вывод В случае успеха
CF = 0 АХ = фактическое количество байтов, записанных в файл При ошибке
CF = 1 АХ код ошибки
1 ... 9 10 11 12 13 14 15 16 ... 20
155
Ассемблер на примерах. Базовый курс В DS:DX нужно указать адрес буфера, содержащий данные, которые нам нужно записать в файл. А в СХ нужно занести количество байтов, которые нужно записать обычно это длина буфера. Если вызов прошел успешно, тов АХ окажется количество байтов, записанных в файл. Если оно отличается от требуемого, то, возможно, нет свободного места на диске. Теперь у нас достаточно знаний, чтобы вывести текстовый файл t e x t . t x t на экран. Прежде всего, нам необходимо открыть файл t e x t . t x t , используя функцию
0x3D. Потом мы будем читать из него блоки одинакового размера при помощи функции 0x3F, пока количество считанных байтов не станет равно нулю. Прочитанный блок мы будем выводить на стандартный вывод функцией 0x40. Перед завершением программы мы закроем файл. Листинг 10.6. Программа вывода текстового файла на экран
%define B_LENGTH 8 0
%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 прочитано 0 байтов Можно использовать стр ах,О если да, переход к концу чтения на последнем шаге количество прочитанных байтов может быть меньше СХ, а записать нужно столько же байтов, сколько было считано
156
Глава 10. Программирование в DOS mov ah,0x40 xchg bp,bx int 0x21 xchg bp,bx jmp read_next end_reading: mov ah, 0x3E int 0x21 mov al,0 endprog: mov ah,4Ch int 0x21 error: mov al,1 jmp short endprog
SECTION .data file_name db "text.txt'
SECTION .bss buffer RESB B_LENGTH функция DOS для записи в файл временно сохраняем дескриптор файла в ВР, а дескриптор стандартного вывода из ВР кладем в ВХ вызов DOS возвращаем исходные значения ВХ и ВР переход к метке read_next, читаем следующий блок функция DOS для закрытия файла вызов DOS код возврата успешное завершение функция DOS для завершения программы вызов DOS. Это последняя команда программы устанавливаем код возврата ошибка переходим к метке endprog
0 определяем имя файла text.txt определяем неинициализированную переменную
;buffer размером B_LENGTH байтов Назовем наш файл list it. asm и откомпилируем его с помощью команды nasm listit.asm -о listit.com -f bin Теперь создадим текстовый файл t e x t . t x t с помощью текстового редактора или прямо из командной строки echo I'll BE BACK! > text.txt После запуска программы текст из файла t e x t . t x t будет выведен на экран. Мы можем модифицировать нашу программу так, чтобы вместо записи на стандартный вывод она записывала прочитанные данные в другой файл. Тогда мы получим простейшую программу копирования файла. Номы до сих пор не знаем, как создать файл
157
Ассемблер на примерах. Базовый курс
Открытие/создание файла В отличие от функции 0x3D функция Ох6С не только открывает файл, но и создает новый файл, если при открытии файла обнаружится, что файла с таким именем не существует. Ввод
Открытие/создание файла АН = ОхбС
AL = 0x00 (всегда должен быть 0)
ВХ = массив битов для расширенного режима
CX = атрибуты для нового файла
DX = массив битов для требуемой операции
DS;DX = указатель на имя файла Вывод Успех
CF = 0 АХ = идентификатор открытого файла
CX = отчет об операции (см. ниже) Ошибка
CF = 1 АХ • код ошибки Данная функция позволяет использовать несколько режимов открытия файлов открытие существующего файла, создание нового файла сего последующим открытием и открытие файла с усечением его длины до нуля (открытие для перезаписи. Требуемый режим мы можем указать в регистре DX. Можно использовать следующие комбинации битов при системном вызове.
4 младших бита (в регистре DL):
• DL = 0000b — выйти с ошибкой, если файл существует (можно использовать для проверки существования файла.
• DL = 0001b — открыть файл, если он существует.
• DL = 0010b — открыть файл для перезаписи (удалить все содержимое, если он существует.
4 старших бита (в регистре DH):
• DH = 0000b — выйти с ошибкой, если файл не существует.
• DH = 0001b — создать файл, если он не существует. Когда мы будем вызывать функцию ОхбС в программе копирования файла, нам нужно записать в DX значение 0x0012. В этом случае файл назначения будет создан и открыт при условии, что он не существует или усечен и открыт, если файл существует. В регистр СХ (это тоже массив битов) нужно занести атрибуты вновь создаваемого файла. DOS — неона не поддерживает ни имени владельца, ни прав доступа, поэтому набор атрибутов в DOS крайне ограничен
• Бит 0: если 1, то будет установлен атрибут только чтение.
• Бит 1: если 1, то будет установлен атрибут скрытый.
• Бит 2: если 1, то будет установлен атрибут системный.
• Бит 3: если 1, то это метка тома.
158
Глава 10. Программирование в DOS
• Бит 4: если 1, то это каталог.
• Бит 5: если 1, то будет установлен атрибут архивный.
• Биты 6-15: зарезервированы для дальнейшего использования. В большинстве случаев в регистр СХ нужно помещать значение 0x20: создание обычного архивного файла. В регистре ВХ указываются флаги расширенного режима. Для нас пока имеют значение только два младших бита, рассмотрение остальных выходит за пределы этой книги.
• ВХ = 0: файл открывается только для чтения.
• ВХ = 1: файл открывается только для записи.
• ВХ = 2: файл открывается для чтения и для записи. После успешного завершения системного вызова (CF=0) регистр АХ будет содержать дескриптор файла (как ив случае с функцией 0x3D). В СХ будет возвращен отчет о выполненной операции
• СХ = 1: файл был открыт.
• СХ = 2: файл был создан и открыт.
• СХ = 3: файл был перезаписан и открыт. Модифицируем нашу программу l i s t i t . a s m (листинг 10.6) так, чтобы она копировала файлы. Файл назначения будем создавать (или перезаписывать) функцией ОхбС. Осталось только передать полученный из нее дескриптор файла назначения в то место программы, где происходит запись в файл. Таким образом, допишем в начало нашей программы строки для открытия второго файла mov ax, 6C00h функция DOS для создания/открытия файла заполняем сразу оба регистра
;0х6С ВАН ив атрибуты нового обычного файла mov dx, 0x12 операция открытия mov si, other_file_name ; адрес имени второго файла i n t 0x21 вызов DOS jc e r r o r переход к обработке ошибки mov ах сохраняем дескриптор открытого файла в ВР Ясно, что этот фрагмент должен занять место команды MOV bp,STDOUT. Второй файл тоже должен быть закрыт добавим после закрытия первого файла следующие строки mov е функция DOS для закрытия файла mov bx,bp помещаем дескриптор файла в ВХ i n t 0x21 И, наконец, определим имя второго файла в секции данных other_file_name db " t e x t l . t x t " , 0 имя второго файла
159
Ассемблер на примерах. Базовый курс Наша программа копирует первый файл text.txt во второй файл textl.txt. Первый файл читается через свой дескриптор, хранящийся в ВХ, а второй файл записывается через свой дескриптор, который после его открытия сохранен в ВР. Назовем нашу программу copy.asm и откомпилируем ее как обычно nasm -f bin -о copy.com copy.asm Поиск позиции в файле) До сих пор мы читали и записывали файлы последовательно, то есть в том порядке, в котором они записаны на диске. В более сложных случаях нам нужно читать файлы выборочно (например, 1 блок вначале файла и 2 блока в конце файла, передвигая указатель внутренней позиции файла вперед/назад. В языке С для этого служит функция seek(), а в DOS имеется системный вызов 0x42. Ввод Изменение позиции в файле АН = 0x42
AL = начало отсчета
ВХ = дескриптор файла
CX:DX = смещение Вывод Успех
CF = 0
DX:AX = новая позиция
CX = отчет об операции Ошибка
CF = 1 АХ = код ошибки Дескриптор файла передается через регистр ВХ. Требуемое значение смещения внутреннего указателя передается через пару CX:DX. Два регистра, а не один, нужны для того, чтобы можно было адресовать файлы размером до
4 Гб (размер файла в FAT16 ограничен 2 Гб). Регистр СХ содержит старшие
16 битов, a DX — младшие 16 битов смещения. Регистр AL определяет начало отсчета, то есть от какого места в файле будет отсчитываться заданное в CX:DX смещение
• AL = 0: смещение отсчитывается от начала файла (SEEK_SET), и новое значение позиции будет равно CX:DX.
• AL = 1: смещение отсчитывается от текущей позиции (SEEK_CUR).
• AL = 2: смещение отсчитывается от конца файла (SEEK_END). Новая позиция внутреннего указателя будет возвращена в паре DX:AX. Функция SEEK может также быть использована для определения размера файла. Откройте файл и вызовите функцию 0x42 со следующими параметрами mov mov x o r x o r i n t ax,0x4202 b x , f i l e d e s d x , d x c x , c x
0x21 функция поиска позиции от конца файла
ВХ = дескриптор файла
DX = 0
СХ = 0 вызываем DOS
160
Глава 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).
6 Зак. 293
161
Ассемблер на примерах. Базовый курс Листинг 10.7. Программа перемещения файла из текущего каталога в корневой секция кода функция DOS 0x5 6 для переименования файла
; загружаем в DX адрес исходного имени файла а в DI — адрес нового имени файла завершение программы
SECTION .text org 0x100 mov ah, 0x5 6 mov dx,src mov di,dest int 0x21 mov ax,0x4c00 int 0x21
SECTION .data src db "text.txt",0 dest db "\text.txt",0 Назовем программу rename.asm и откомпилируем как обычно. Ее размер будет всего несколько байтов, но ее можно сделать еще меньше. Имена файлов отличаются только символом обратного слэша, обозначающим корневой каталог диска. Для уменьшения размера программы перепишем секцию данных так
SECTION .data dest db "\" src db "text.txt",0 Теперь оба имени делят одну и туже область памяти. В высокоуровневых языках программирования, как правило, таких фокусов позволить себе нельзя. Длинные имена файлов и работа сними позволяет преодолеть ограничение 8+3 на длину файлов. Для обеспечения обратной совместимости со старыми программами длинные имена хранятся также в сокращенном, 8+3, виде. Например, каталог LinuxRulez будет сохранен как LINUXR1. Для работы с длинными именами файлов мы должны использовать новые системные вызовы, которые доступны только под управлением Windows. Их описание вы сможете найти в документе Ralf
Brown Interrupt List (http://www.ctyme.com/rbrown.htm). Все функции для работы с длинными именами файлов доступны через функцию с номером 0x70. Через регистр AL передается номер оригинальной функции работающей с короткими именами, то есть те из рассмотренных функций, которые сами принимают аргумент через регистр AL, не поддерживаются. Например, вместо функции открытия файла 0x3D при работе с длинными именами придется использовать функцию ОхбС: в регистр АХ нужно поместить значение 0х706С, а остальные аргументы передавать как обычно.
162
Глава 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
Ассемблер на примерах. Базовый курс Если нам нужно узнать имя текущего диска, можно использовать функцию
0x19: Ввод Получить имя диска АН = 0x19 Вывод
AL = номер текущего диска
AL • 0x00 диск А
AL = 0x01 диск В В листинге 10.8. приведена программа pwd, выводящая на экран текущий каталог, включая букву диска Листинг 10.8. Программа, выводящая на экран текущий каталог, включая букву диска
SECTION .text org 0x100 mov ah,0x19 функция DOS для получения текущего диска i n t 0x21 вызываем DOS add byte [buffer] ,al ; добавляем номер к букве 'А, которую заранее поместили в первый байт буфера будем искать текущий каталог текущем диске функция DOS для получения текущего каталога первые три байта буфера отведены под "С" вызываем DOS функция DOS для вывода на экран это дескриптор стандартного вывода сколько символов нужно вывести 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 0x0D,0x0A
BUFF_LEN equ $-buffer вызываем DOS функция DOS для завершения программы вызываем DOS оставшуюся часть буфера заполняем и завершаем строку оператор $ обозначает текущий адрес, то есть конец буфера. Отнимая от него начальный адрес, получаем длину буфера
164
Глава 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 параграфов можно с помощью следующего кода mov ah, 0x4A функция ДОС для изменения размера памяти mov bx, 0x1000 кво параграфов ES уже установлен i n t 0x21 вызов ДОС j c e r r o r Запросить еще несколько блоков памяти можно с помощью команды выделения памяти 0x48, рассмотренной ниже.
165
Ассемблер на примерах. Базовый курс Выделение памяти С помощью функции 0x48 мы можем попросить операционную систему выделить новый блок памяти в любое время Если такой блок доступен, он будет распределен. Адрес его сегмента будет возвращен в регистре АХ. Ввод Распределение блока памяти АН = 0x48
ВХ = размер в параграфах Вывод Успех
CF = 0 АХ = адрес сегмента выделенного блока Ошибка
CF=1 АХ = код ошибки
ВХ = максимально доступный размер блока памяти (в параграфах) Посмотрим, как выделить блок размером 64 Кб, используя эту функцию. Предположим, что ненужную память мы уже вернули операционной системе, вызвав функцию 0х4А. mov ah,0x48 функция DOS для распределения памяти mov bx,0x1000 нам нужно 64 KB i n t 0x21 вызываем DOS jc e r r o r если CF = 1, то запрошенный блок нам не дали, в ВХ — максимальное число доступных параграфов push es сохраняем оригинальный ES в стеке mov e s , a x загружаем в ES адрес нового сегмента Освобождение памяти Выделенная (распределенная) память должна быть возвращена обратно операционной системе. Сделать это можно с помощью функции 0x49: Ввод Освобождение блока памяти АН = 0x49
ES = адрес сегмента освобождаемого блока Вывод Успех CF=0 Ошибка
CF=1 АХ = код ошибки
1 ... 10 11 12 13 14 15 16 17 ... 20
0x3D. Потом мы будем читать из него блоки одинакового размера при помощи функции 0x3F, пока количество считанных байтов не станет равно нулю. Прочитанный блок мы будем выводить на стандартный вывод функцией 0x40. Перед завершением программы мы закроем файл. Листинг 10.6. Программа вывода текстового файла на экран
%define B_LENGTH 8 0
%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 прочитано 0 байтов Можно использовать стр ах,О если да, переход к концу чтения на последнем шаге количество прочитанных байтов может быть меньше СХ, а записать нужно столько же байтов, сколько было считано
156
SECTION .data file_name db "text.txt'
SECTION .bss buffer RESB B_LENGTH функция DOS для записи в файл временно сохраняем дескриптор файла в ВР, а дескриптор стандартного вывода из ВР кладем в ВХ вызов DOS возвращаем исходные значения ВХ и ВР переход к метке read_next, читаем следующий блок функция DOS для закрытия файла вызов DOS код возврата успешное завершение функция DOS для завершения программы вызов DOS. Это последняя команда программы устанавливаем код возврата ошибка переходим к метке endprog
0 определяем имя файла text.txt определяем неинициализированную переменную
;buffer размером B_LENGTH байтов Назовем наш файл list it. asm и откомпилируем его с помощью команды nasm listit.asm -о listit.com -f bin Теперь создадим текстовый файл t e x t . t x t с помощью текстового редактора или прямо из командной строки echo I'll BE BACK! > text.txt После запуска программы текст из файла t e x t . t x t будет выведен на экран. Мы можем модифицировать нашу программу так, чтобы вместо записи на стандартный вывод она записывала прочитанные данные в другой файл. Тогда мы получим простейшую программу копирования файла. Номы до сих пор не знаем, как создать файл
157
Открытие/создание файла В отличие от функции 0x3D функция Ох6С не только открывает файл, но и создает новый файл, если при открытии файла обнаружится, что файла с таким именем не существует. Ввод
Открытие/создание файла АН = ОхбС
AL = 0x00 (всегда должен быть 0)
ВХ = массив битов для расширенного режима
CX = атрибуты для нового файла
DX = массив битов для требуемой операции
DS;DX = указатель на имя файла Вывод Успех
CF = 0 АХ = идентификатор открытого файла
CX = отчет об операции (см. ниже) Ошибка
CF = 1 АХ • код ошибки Данная функция позволяет использовать несколько режимов открытия файлов открытие существующего файла, создание нового файла сего последующим открытием и открытие файла с усечением его длины до нуля (открытие для перезаписи. Требуемый режим мы можем указать в регистре DX. Можно использовать следующие комбинации битов при системном вызове.
4 младших бита (в регистре DL):
• DL = 0000b — выйти с ошибкой, если файл существует (можно использовать для проверки существования файла.
• DL = 0001b — открыть файл, если он существует.
• DL = 0010b — открыть файл для перезаписи (удалить все содержимое, если он существует.
4 старших бита (в регистре DH):
• DH = 0000b — выйти с ошибкой, если файл не существует.
• DH = 0001b — создать файл, если он не существует. Когда мы будем вызывать функцию ОхбС в программе копирования файла, нам нужно записать в DX значение 0x0012. В этом случае файл назначения будет создан и открыт при условии, что он не существует или усечен и открыт, если файл существует. В регистр СХ (это тоже массив битов) нужно занести атрибуты вновь создаваемого файла. DOS — неона не поддерживает ни имени владельца, ни прав доступа, поэтому набор атрибутов в DOS крайне ограничен
• Бит 0: если 1, то будет установлен атрибут только чтение.
• Бит 1: если 1, то будет установлен атрибут скрытый.
• Бит 2: если 1, то будет установлен атрибут системный.
• Бит 3: если 1, то это метка тома.
158
• Бит 4: если 1, то это каталог.
• Бит 5: если 1, то будет установлен атрибут архивный.
• Биты 6-15: зарезервированы для дальнейшего использования. В большинстве случаев в регистр СХ нужно помещать значение 0x20: создание обычного архивного файла. В регистре ВХ указываются флаги расширенного режима. Для нас пока имеют значение только два младших бита, рассмотрение остальных выходит за пределы этой книги.
• ВХ = 0: файл открывается только для чтения.
• ВХ = 1: файл открывается только для записи.
• ВХ = 2: файл открывается для чтения и для записи. После успешного завершения системного вызова (CF=0) регистр АХ будет содержать дескриптор файла (как ив случае с функцией 0x3D). В СХ будет возвращен отчет о выполненной операции
• СХ = 1: файл был открыт.
• СХ = 2: файл был создан и открыт.
• СХ = 3: файл был перезаписан и открыт. Модифицируем нашу программу l i s t i t . a s m (листинг 10.6) так, чтобы она копировала файлы. Файл назначения будем создавать (или перезаписывать) функцией ОхбС. Осталось только передать полученный из нее дескриптор файла назначения в то место программы, где происходит запись в файл. Таким образом, допишем в начало нашей программы строки для открытия второго файла mov ax, 6C00h функция DOS для создания/открытия файла заполняем сразу оба регистра
;0х6С ВАН ив атрибуты нового обычного файла mov dx, 0x12 операция открытия mov si, other_file_name ; адрес имени второго файла i n t 0x21 вызов DOS jc e r r o r переход к обработке ошибки mov ах сохраняем дескриптор открытого файла в ВР Ясно, что этот фрагмент должен занять место команды MOV bp,STDOUT. Второй файл тоже должен быть закрыт добавим после закрытия первого файла следующие строки mov е функция DOS для закрытия файла mov bx,bp помещаем дескриптор файла в ВХ i n t 0x21 И, наконец, определим имя второго файла в секции данных other_file_name db " t e x t l . t x t " , 0 имя второго файла
159
AL = начало отсчета
ВХ = дескриптор файла
CX:DX = смещение Вывод Успех
CF = 0
DX:AX = новая позиция
CX = отчет об операции Ошибка
CF = 1 АХ = код ошибки Дескриптор файла передается через регистр ВХ. Требуемое значение смещения внутреннего указателя передается через пару CX:DX. Два регистра, а не один, нужны для того, чтобы можно было адресовать файлы размером до
4 Гб (размер файла в FAT16 ограничен 2 Гб). Регистр СХ содержит старшие
16 битов, a DX — младшие 16 битов смещения. Регистр AL определяет начало отсчета, то есть от какого места в файле будет отсчитываться заданное в CX:DX смещение
• AL = 0: смещение отсчитывается от начала файла (SEEK_SET), и новое значение позиции будет равно CX:DX.
• AL = 1: смещение отсчитывается от текущей позиции (SEEK_CUR).
• AL = 2: смещение отсчитывается от конца файла (SEEK_END). Новая позиция внутреннего указателя будет возвращена в паре DX:AX. Функция SEEK может также быть использована для определения размера файла. Откройте файл и вызовите функцию 0x42 со следующими параметрами mov mov x o r x o r i n t ax,0x4202 b x , f i l e d e s d x , d x c x , c x
0x21 функция поиска позиции от конца файла
ВХ = дескриптор файла
DX = 0
СХ = 0 вызываем DOS
160
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).
6 Зак. 293
161
; загружаем в DX адрес исходного имени файла а в DI — адрес нового имени файла завершение программы
SECTION .text org 0x100 mov ah, 0x5 6 mov dx,src mov di,dest int 0x21 mov ax,0x4c00 int 0x21
SECTION .data src db "text.txt",0 dest db "\text.txt",0 Назовем программу rename.asm и откомпилируем как обычно. Ее размер будет всего несколько байтов, но ее можно сделать еще меньше. Имена файлов отличаются только символом обратного слэша, обозначающим корневой каталог диска. Для уменьшения размера программы перепишем секцию данных так
SECTION .data dest db "\" src db "text.txt",0 Теперь оба имени делят одну и туже область памяти. В высокоуровневых языках программирования, как правило, таких фокусов позволить себе нельзя. Длинные имена файлов и работа сними позволяет преодолеть ограничение 8+3 на длину файлов. Для обеспечения обратной совместимости со старыми программами длинные имена хранятся также в сокращенном, 8+3, виде. Например, каталог LinuxRulez будет сохранен как LINUXR1. Для работы с длинными именами файлов мы должны использовать новые системные вызовы, которые доступны только под управлением Windows. Их описание вы сможете найти в документе Ralf
Brown Interrupt List (http://www.ctyme.com/rbrown.htm). Все функции для работы с длинными именами файлов доступны через функцию с номером 0x70. Через регистр AL передается номер оригинальной функции работающей с короткими именами, то есть те из рассмотренных функций, которые сами принимают аргумент через регистр AL, не поддерживаются. Например, вместо функции открытия файла 0x3D при работе с длинными именами придется использовать функцию ОхбС: в регистр АХ нужно поместить значение 0х706С, а остальные аргументы передавать как обычно.
162
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
0x19: Ввод Получить имя диска АН = 0x19 Вывод
AL = номер текущего диска
AL • 0x00 диск А
AL = 0x01 диск В В листинге 10.8. приведена программа pwd, выводящая на экран текущий каталог, включая букву диска Листинг 10.8. Программа, выводящая на экран текущий каталог, включая букву диска
SECTION .text org 0x100 mov ah,0x19 функция DOS для получения текущего диска i n t 0x21 вызываем DOS add byte [buffer] ,al ; добавляем номер к букве 'А, которую заранее поместили в первый байт буфера будем искать текущий каталог текущем диске функция DOS для получения текущего каталога первые три байта буфера отведены под "С" вызываем DOS функция DOS для вывода на экран это дескриптор стандартного вывода сколько символов нужно вывести 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 0x0D,0x0A
BUFF_LEN equ $-buffer вызываем DOS функция DOS для завершения программы вызываем DOS оставшуюся часть буфера заполняем и завершаем строку оператор $ обозначает текущий адрес, то есть конец буфера. Отнимая от него начальный адрес, получаем длину буфера
164
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 параграфов можно с помощью следующего кода mov ah, 0x4A функция ДОС для изменения размера памяти mov bx, 0x1000 кво параграфов ES уже установлен i n t 0x21 вызов ДОС j c e r r o r Запросить еще несколько блоков памяти можно с помощью команды выделения памяти 0x48, рассмотренной ниже.
165
ВХ = размер в параграфах Вывод Успех
CF = 0 АХ = адрес сегмента выделенного блока Ошибка
CF=1 АХ = код ошибки
ВХ = максимально доступный размер блока памяти (в параграфах) Посмотрим, как выделить блок размером 64 Кб, используя эту функцию. Предположим, что ненужную память мы уже вернули операционной системе, вызвав функцию 0х4А. mov ah,0x48 функция DOS для распределения памяти mov bx,0x1000 нам нужно 64 KB i n t 0x21 вызываем DOS jc e r r o r если CF = 1, то запрошенный блок нам не дали, в ВХ — максимальное число доступных параграфов push es сохраняем оригинальный ES в стеке mov e s , a x загружаем в ES адрес нового сегмента Освобождение памяти Выделенная (распределенная) память должна быть возвращена обратно операционной системе. Сделать это можно с помощью функции 0x49: Ввод Освобождение блока памяти АН = 0x49
ES = адрес сегмента освобождаемого блока Вывод Успех CF=0 Ошибка
CF=1 АХ = код ошибки
1 ... 10 11 12 13 14 15 16 17 ... 20
10.9. Аргументы командной строки Для получения доступа к аргументам командной строки в языке С используются переменные argc (количество аргументов) и argv (массив самих аргументов. Операционная система DOS передает все аргументы одной строкой, записывая эту строку по адресу 0x81 от начала сегмента программы. Длина этой строки с учетом завершающего символа OxOD сохраняется по смеще-
166
Глава 10. Программирование в DOS нию 0x80 (один байт. Нам придется извлекать отдельные аргументы из этой строки вручную. Следующая программа выводит переданные ей параметры на экран и завершает работу. Ничего нового в ней нет мы просто выводим строку, записанную по адресу 0x81 (листинг 10.9). Листинг 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 inc di mov byte [di],OxA inc ex int 0x21 mov ax,0x4c00 int 0x21 начало кода стандартный вывод
;СОМ-файл функция DOS для записи в файл начало строки сбрасываем длину строки загружаем в CL длину строки копируем в DI добавляем длину и получаем конец строки [OxOD] устанавливаем DI после последнего символа строки дописываем ОхОА, чтобы получить полноценный EOL и увеличиваем длину строки на 1 выводим строку на экран завершаем работу вызываем DOS
10.10. Коды ошибок Определить, что произошла ошибка вовремя системного вызова, можно по флагу переноса если он установлен (CF=1), то вызов завершился ошибочно. В регистре АХ будет код ошибки Подробную информацию об ошибке можно получить с помощью системного вызова АН Также описание ошибок можно найти в Ralf Brown Interrupt List. В таблице 10.3 перечислены только коды самых распространенных ошибок. коды самых распространенных ошибок Таблица 10.3 Код
0x0002 0x0003 0x0005 Описание ошибки Файл не найден Путь не найден Отказано в доступе
167
Ассемблер на примерах. Базовый курс Продолжение табл. 10.3 Код
0x0008 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
Глава 10. Программирование в DOS Наиболее важная функция отладчика — пошаговое выполнение программы команда step). За один шаг выполняется одна команда программы. После выполнения команды выводятся новые значения регистров процессора, а также команда, которая будет выполнена следующей. Абсолютно все отладчики имеют функцию step-over, позволяющую проскакивать подпрограмму, то есть все команды подпрограммы будут выполнены за один шаг. Эту функцию можно использовать, если вы уверены в правильности действий подпрограммы. Для продолжения выполнения всей программы без дальнейшей отладки имеются команды go или continue. Любой отладчик позволяет устанавливать точки прерывания (контрольные точки или breakpoints). Программа будет выполнена до этой точки, после чего при выполнении заданного условия управление будет возвращено отладчику ивы сможете просмотреть содержание регистров и памяти. Условием может быть выполнение команды, хранящейся по указанному адресу, изменение значения определенной ячейки памяти и т.п. При отладке вместо символических имен идентификаторов (меток, переменных) будут показаны их адреса. Если вас такое положение вещей не устраивает (конечно, удобнее, когда вместо адреса отображается имя переменной, например, result), вы должны перекомпилировать программу, упаковав в нее символьные имена. Размер программы существенно увеличится, но после отладки вы сможете заново перекомпилировать программу, исключив символы. В этой книге мы будем использовать свободно распространяемый отладчик
grdb, который вы сможете найти в Интернете. ю. Отладчик grdb.exe Методика использования Отладчик grdb (Get Real Debugger, то есть настоящий отладчик) написан на языке ассемблера и распространяется бесплатно. Скачать его можно по адресу http://www.members.tripod.com/ladsoft/grdb.htm. Интерфейс программы — классический, то есть текстовый. Для запуска программы введите команду grdb. С
GRDB v e r s i o n 3.6 C o p y r i g h t (c) LADsoft 1997-2002
H i s t o r y e n a b l e d eax:00000000 ebx:00000000 ecx:00000000 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000100 eflags:00000202
NV UP EI PL NZ NA PO NC
169
Ассемблер на примерах. Базовый курс ds: 10FB es:10FB fs:10FB gs:10FB SS:10FB CS:10FB
10FB:0100 74 IE jz 0120
-> Появление на экране приглашения (->) означает, что отладчик готов к выполнению наших команд. Загрузим в отладчик нашу программу pwd.com (она выводит текущий каталог на стандартный вывод. Для этого введем команду 1 pwd.com (1 — это load, то есть загрузить. После имени исполняемого файла можно указать через пробел аргументы программы.
->1 pwd.com
S i z e : 0 0 0 0 0 0 6 9
-> Теперь начнем пошаговое выполнение программы. Для выполнения одной команды программы служит команда отладчика t. Введите t и нажмите
«Enter»:
- > t еах:00001900 ebx:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000102 eflags:00000202
NV UP EI PL NZ NA PO NC ds: 10FB es:10FB fs:10FB gs:10FB SS:10FB cs:10FB
10FB:0102 CD 21 int 21
-> Команда t выполнила первую команду нашей программы, ив регистре АН появилось значение 0x19. Следующая команда — INT 21, то есть вызов операционной системы. Слева от нее вы видите ее адрес (пара регистров CS:IP) и машинный код. После нажатия t и кливиши «Enter» будет выполнен вызови в AL окажется номер текущего диска.
->t еах:00001902 ebx:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000104 eflags:00000202
NV UP EI PL NZ NA PO NC ds: 10FB 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 указан адрес этой переменной. Мы можем этим воспользоваться чтобы увидеть значение переменной buffer, нужно сделать дамп памяти по адресу (смещению) 0124. Для этого введем команду d 124. Для команды d самый распространенный аргумент — это начальный адрес памяти, содержимое которой нужно вывести, хотя можно указывать и имя регистра.
170
Глава 10. Программирование в DOS
- > d 124
" 1 0 F B : 0 1 2 0 - 4 1 ЗА С 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 0D-0A C7 06 8C-CD 01 00 EB
10FB:0170 60 26 8A 47-01 32 E4 40-D1 E0 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-D1 EO 03 D8-26 8A 07 32-E4 Dl EO 40 . 2 . 0 . . . . & . . 2 . . . 0
-> После выполнения ADD введите еще одну команду t. Строка А будет заменена строкой С (наш текущий диск. Проверьте это с помощью команды d:
- > d 124 1 0 F B : 0 1 2 0 - 4 3 ЗА С 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 С : \ Остальное содержимое памяти не изменилось, поэтому часть вывода я отрезал. Для выполнения оставшейся части программы вместо команды t введите команду g (go), которая возвращает управление отладчику только после завершения программы. Команда а позволяет модифицировать команды и данные программы. Ее параметры — это адрес (смещение) для сохранения новой инструкции. Начинающим рекомендуется не использовать эту команду, а исправлять ошибки в исходном коде, после чего перекомпилировать свою программу. Если вам нужно выполнить подпрограмму за один шаг (функция step over), используйте команду р. Просмотреть код программы можно с помощью команды и, которой в качестве параметра нужно передать стартовый адрес и 100 10FB:0100 10FB:0102 10FB:0104 10FB:0108 10FB:010A
10FB:010C
10FB:010F
10FB:0111 10FB:0113 10FB:0116 10FB:0119
B4
CD
00 30
B4
BE
CD
B4
BB
B9
BA
19 21 06
D2 47 27 21 40 01 45 24 24 01 00 00 01 mov int
01 add xor mov mov int mov mov mov mov ah,0019 21
[0124],al dl,dl ah,0047 si,0127 21 ah,0040 bx,0001 ex,0045 dx,0124 171
Ассемблер на примерах. Базовый курс
10FB:011C CD 21 i n t
10FB:011E B8 00 4C mov
->
21 ax,4C00 Завершить работу отладчика можно командой q. Основные команды отладчика grdb Основные команды отладчика grdb приведены в табл. Основные команды отладчика grdb Таблица 10.4 Команда
I [p] t Р и g a
? b,addr b q Назначение Загружает программу с аргументами (необязательными) р Выполняет одну команду, возвращая управление отладчику после ее выполнения Подобна предыдущей команде, но проходит подпрограмму за один шаг Преобразует машинный код команд обратно в символический код ассемблера. Обычно используется для просмотра листинга программы Продолжает выполнение программы, передавая управление отладчику после завершения программы Записывает новую команду по адресу Выводит список всех команд и краткое их описание Устанавливает точку прерывания номер num по адресу addr. Номер точки должен быть в диапазоне от 0 до F, то есть всего можно установить 16 различных точек прерывания Выводит список всех установленных точек прерывания Выход Пример разработки и отладки программы Напишем небольшую программу, читающую два или более десятичных числа из командной строки и выводящую на экран их сумму. Для преобразования строк в числа и наоборот будем использовать разработанную нами подпрограмму из главы 7. Как выводить строку на экран, мы тоже знаем, поэтому в данной главе сосредоточим наше внимание именно на отладке нашей программы. Давайте вспомним, как нужно вызывать нашу подпрограмму ASCIIToNum:
; ASCIIToNum
; esi = указатель на конвертируемую строку, заканчивающуюся символом 0x0
; есх = основание системы счисления
; Результат еах = число
172
Глава 10. Программирование в DOS Поскольку мы работаем в операционной системе DOS, мы будем использовать только 16-битную адресацию. Указатель (смещение) будет передан в SI, а не в ESI. Наша программа должна уметь работать с аргументами командной строки. Все аргументы передаются операционной системой программе как одна строка, внутри которой аргументы разделены пробелами. Для извлечения отдельных аргументов из этой строки мы напишем несколько подпрограмм. Первая из них — SkipSpace, пропускающая любое количество пробелов вначале строки. Подпрограмме нужно передать указатель на строку, а на выходе мы получим адрес первого символа строки, отличного от пробела.
; SkipSpace
; si = указатель на строку
; Возвращает
; si = первый символ, отличный от пробела
SkipSpace:
.again: lodsb загружаем в AL байт по адресу DS:SI, увеличиваем SI cmp a l , ' ' сравниваем символ с пробелом jz again если равно, продолжаем r e t Итак, мы удалили изначала строки аргументов все пробелы, первый пробел в строке будет после первого аргумента. Напомним, что строка аргументов начинается с адреса 0x81, а заканчивается символом OxD. Теперь преобразуем первый аргумент в число. У насесть подпрограмма, которая умеет считывать число из строки, завершенной нулевым байтом. Чтобы воспользоваться ею, нужно поместить нулевой байт на место символа пробела после первого аргумента. Напишем еще одну подпрограмму, которая пропускает в строке аргументов последовательность подряд идущих цифр и возвращает указатель на позицию в строке после числа.
; SkipNum
; Ввод SI = указатель на строку
; Вывод DI = указатель на позицию в строке после числа
SkipNum: mov di,si ;DI используется как указатель
.find_end_of_numberi inc di ;DI=DI+1 cmp [di],'0' сравнить символ с кодом цифры нуль
173
Ассемблер на примерах. Базовый курс j b .end_found cmp [ d i ] , ' 9 ' ja .end_found jmp .find_end_of_number
.end_found: ret А вот тело основной программы если меньше, конец числа найден сравнить символ с кодом шифры 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 указатель на начало строки аргументов переходим вконец первого числа пишем нулевой байт читаем число в десятичной записи в ЕАХ будет результат остается неизменным сохраняем число в EDX загружаем в SI указатель на позицию в строке сразу после первого числа пропускаем любые пробелы и пропускаем число
; записываем нулевой символ после второго аргумента в ЕАХ — результат остается неизменным добавляем предыдущее число, сохраненное в EDX Первым делом мы пропускаем число и записываем нулевой байт после него. Подпрограмма преобразования строки в число будет думать, что строка закончилась. После этого мы преобразуем фрагмент строки параметров от ее начала до первого нулевого байта в число. Регистр DI теперь указывает на нулевой байт, значит, нужно увеличить указатель на единицу. Наша подпрограмма преобразования строки в число ожидает указателя в регистре SI, и мы копируем его из DI. Теперь наша строка будет начинаться со второго аргумента, перед обработкой которого мы снова пропускаем все пробелы вначале строки. После преобразования второго числа мы складываем его с первым. После этого в ЕАХ будет сумма двух чисел, которую нужно преобразовать в строку и вывести на экран. Для преобразования числа в строку будем использовать подпрограмму NumToASCII:
174
Глава 10. Программирование в DOS
; NumToASCII
; eax = 32-битное число
; ebx = основание системы счисления
; edi = указатель на буфер для сохранения строки
; Результат
; заполненный буфер mov ebx,10 заносим в ЕВХ основание системы счисления mov di,buffer в DI — адрес буфера call NumToASCII перед вызовом подпрограммы в ЕАХ находится вычисленная сумма Теперь выведем полученную строку на экран, используя функцию записи в файл стандартного вывода. Номы не знаем длину строки, которая получилась после преобразования. Пока сделаем так зарезервированное для строки место заполним пробелами и выведем строку вместе с лишними пробелами. Потом мы можем модифицировать программу, удалив из строки лишние пробелы. mov ah,0x40 функция DOS для записи в файл mov dx,di эта функция ожидает указателя на строку в DS:DX mov cx,25 количество выводимых символов i n t 0x21 mov ax, 0x4c00 завершаем программу i n t 0x21 В секции данных заполним наш буфер 25 пробелами buffer times 2 5 db ' ' Теперь нам осталось написать только секцию кода для нашей программы.
SECTION . t e x t org 0x100 mov si, 0x81 загружаем в SI указатель на начало строки аргументов call SkipNum переходим вконец первого числа mov [di],0 пишем нулевой байт mov есхДО читаем число в десятичной записи call ASCIIToNum в ЕАХ будет результат, SI остается неизменным mov edx,eax сохраняем число в EDX mov si,di загружаем в SI указатель на позицию в строке сразу после первого числа inc si call Skipspace пропускаем любые пробелы call SkipNum и пропускаем число
175
Ассемблер на примерах. Базовый курс mov [ d i ] , 0 call ASCIIToNum add eax,edx mov ebx,10 mov di,buffer call NumToASCII mov ah,0x40 mov dx,di mov ex,2 5 int 0x21 mov ax, 0x4c00 int 0x21 записываем нулевой символ после второго аргумента в ЕАХ — результат, SI остается неизменным добавляем предыдущее число, сохраненное в EDX основание системы счисления для преобразования числа загружаем в DI адрес буфера выводимой строки преобразуем ЕАХ в строку функция ДОС для записи в файл указатель на строку ожидается в DX, у нас он в DI количество выводимых символов вызываем DOS завершаем программу
; SkipSpace - пропуск пробелов
; si = указатель на строку
; Возвращает
; si = первый символ,
SkipSpace:
.again: lodsb cmp al,' ' ; jz again ret отличный от пробела загружаем в AL байт по адресу DS:SI, увеличиваем SI сравниваем символ с пробелом если равно, продолжаем
; SkipNum — пропуск числа
; Ввод SI = указатель на строку
; Вывод DI = указатель на позицию в строке после числа
SkipNum: mov di,si
.find_end_of_number: inc di emp [di],'0' jb .end_found emp [di],'9' ja .end_found jmp .find_end_of_number
.end_found: ret
;DI используется как указатель
;DI=DI+1 сравнить символ с кодом цифры нуль если меньше, конец числа найден
; сравнить символ с кодом цифры 9 если больше, конец числа найден переходим к следующему символу
176
Глава 10. Программирование в DOS
. ***** далее следуют все необходимые подпрограммы *****
SECTION .data buffer times 2 5 db ' ' буфер для сохранения результата Теперь сохраним нашу программу и попытаемся ее откомпилировать. В результате компиляции увидим это nasm -f bin -о t e s t . c o m t e s t . a s m t e s t . a s m : 6 : e r r o r : operation s i z e not s p e c i f i e d t e s t . a s m : 1 3 : e r r o r : symbol 'Skipspace' undefined t e s t . a s m : 1 5 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 4 1 : e r r o r : symbol ' a g a i n ' undefined t e s t . a s m : 5 3 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 5 5 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 1 4 1 : e r r o r : phase e r r o r 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,0x4 0 функция ДОС для записи в файл mov dx,di указатель ожидается в DX, у нас он в DI mov cx,25 количество печатаемых символов i n t 0x21 вызываем DOS Все ясно, мы ведь забыли поместить дескриптор потока стандартного вывода
0x01 в ВХ — в этом регистре все еще содержится значение 10. Исправим эту ошибку и снова откомпилируем программу. Теперь можно запустить программу с параметрами 45 и 50. Мы получим следующее значение
C : \ t e s t 45 50 945 7 Зак. 293
1 ... 12 13 14 15 16 17 18 19 20
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 inc di mov byte [di],OxA inc ex int 0x21 mov ax,0x4c00 int 0x21 начало кода стандартный вывод
;СОМ-файл функция DOS для записи в файл начало строки сбрасываем длину строки загружаем в CL длину строки копируем в DI добавляем длину и получаем конец строки [OxOD] устанавливаем DI после последнего символа строки дописываем ОхОА, чтобы получить полноценный EOL и увеличиваем длину строки на 1 выводим строку на экран завершаем работу вызываем DOS
10.10. Коды ошибок Определить, что произошла ошибка вовремя системного вызова, можно по флагу переноса если он установлен (CF=1), то вызов завершился ошибочно. В регистре АХ будет код ошибки Подробную информацию об ошибке можно получить с помощью системного вызова АН Также описание ошибок можно найти в Ralf Brown Interrupt List. В таблице 10.3 перечислены только коды самых распространенных ошибок. коды самых распространенных ошибок Таблица 10.3 Код
0x0002 0x0003 0x0005 Описание ошибки Файл не найден Путь не найден Отказано в доступе
167
0x0008 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
grdb, который вы сможете найти в Интернете. ю. Отладчик grdb.exe Методика использования Отладчик grdb (Get Real Debugger, то есть настоящий отладчик) написан на языке ассемблера и распространяется бесплатно. Скачать его можно по адресу http://www.members.tripod.com/ladsoft/grdb.htm. Интерфейс программы — классический, то есть текстовый. Для запуска программы введите команду grdb. С
GRDB v e r s i o n 3.6 C o p y r i g h t (c) LADsoft 1997-2002
H i s t o r y e n a b l e d eax:00000000 ebx:00000000 ecx:00000000 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000100 eflags:00000202
NV UP EI PL NZ NA PO NC
169
10FB:0100 74 IE jz 0120
-> Появление на экране приглашения (->) означает, что отладчик готов к выполнению наших команд. Загрузим в отладчик нашу программу pwd.com (она выводит текущий каталог на стандартный вывод. Для этого введем команду 1 pwd.com (1 — это load, то есть загрузить. После имени исполняемого файла можно указать через пробел аргументы программы.
->1 pwd.com
S i z e : 0 0 0 0 0 0 6 9
-> Теперь начнем пошаговое выполнение программы. Для выполнения одной команды программы служит команда отладчика t. Введите t и нажмите
«Enter»:
- > t еах:00001900 ebx:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000102 eflags:00000202
NV UP EI PL NZ NA PO NC ds: 10FB es:10FB fs:10FB gs:10FB SS:10FB cs:10FB
10FB:0102 CD 21 int 21
-> Команда t выполнила первую команду нашей программы, ив регистре АН появилось значение 0x19. Следующая команда — INT 21, то есть вызов операционной системы. Слева от нее вы видите ее адрес (пара регистров CS:IP) и машинный код. После нажатия t и кливиши «Enter» будет выполнен вызови в AL окажется номер текущего диска.
->t еах:00001902 ebx:00000000 есх:00000069 edx:00000000 esi:00000000 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000104 eflags:00000202
NV UP EI PL NZ NA PO NC ds: 10FB 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 указан адрес этой переменной. Мы можем этим воспользоваться чтобы увидеть значение переменной buffer, нужно сделать дамп памяти по адресу (смещению) 0124. Для этого введем команду d 124. Для команды d самый распространенный аргумент — это начальный адрес памяти, содержимое которой нужно вывести, хотя можно указывать и имя регистра.
170
- > d 124
" 1 0 F B : 0 1 2 0 - 4 1 ЗА С 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 0D-0A C7 06 8C-CD 01 00 EB
10FB:0170 60 26 8A 47-01 32 E4 40-D1 E0 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-D1 EO 03 D8-26 8A 07 32-E4 Dl EO 40 . 2 . 0 . . . . & . . 2 . . . 0
-> После выполнения ADD введите еще одну команду t. Строка А будет заменена строкой С (наш текущий диск. Проверьте это с помощью команды d:
- > d 124 1 0 F B : 0 1 2 0 - 4 3 ЗА С 2 0 - 2 0 20 20 2 0 - 2 0 20 20 20 С : \ Остальное содержимое памяти не изменилось, поэтому часть вывода я отрезал. Для выполнения оставшейся части программы вместо команды t введите команду g (go), которая возвращает управление отладчику только после завершения программы. Команда а позволяет модифицировать команды и данные программы. Ее параметры — это адрес (смещение) для сохранения новой инструкции. Начинающим рекомендуется не использовать эту команду, а исправлять ошибки в исходном коде, после чего перекомпилировать свою программу. Если вам нужно выполнить подпрограмму за один шаг (функция step over), используйте команду р. Просмотреть код программы можно с помощью команды и, которой в качестве параметра нужно передать стартовый адрес и 100 10FB:0100 10FB:0102 10FB:0104 10FB:0108 10FB:010A
10FB:010C
10FB:010F
10FB:0111 10FB:0113 10FB:0116 10FB:0119
B4
CD
00 30
B4
BE
CD
B4
BB
B9
BA
19 21 06
D2 47 27 21 40 01 45 24 24 01 00 00 01 mov int
01 add xor mov mov int mov mov mov mov ah,0019 21
[0124],al dl,dl ah,0047 si,0127 21 ah,0040 bx,0001 ex,0045 dx,0124 171
10FB:011C CD 21 i n t
10FB:011E B8 00 4C mov
->
21 ax,4C00 Завершить работу отладчика можно командой q. Основные команды отладчика grdb Основные команды отладчика grdb приведены в табл. Основные команды отладчика grdb Таблица 10.4 Команда
I
? b
; ASCIIToNum
; esi = указатель на конвертируемую строку, заканчивающуюся символом 0x0
; есх = основание системы счисления
; Результат еах = число
172
; SkipSpace
; si = указатель на строку
; Возвращает
; si = первый символ, отличный от пробела
SkipSpace:
.again: lodsb загружаем в AL байт по адресу DS:SI, увеличиваем SI cmp a l , ' ' сравниваем символ с пробелом jz again если равно, продолжаем r e t Итак, мы удалили изначала строки аргументов все пробелы, первый пробел в строке будет после первого аргумента. Напомним, что строка аргументов начинается с адреса 0x81, а заканчивается символом OxD. Теперь преобразуем первый аргумент в число. У насесть подпрограмма, которая умеет считывать число из строки, завершенной нулевым байтом. Чтобы воспользоваться ею, нужно поместить нулевой байт на место символа пробела после первого аргумента. Напишем еще одну подпрограмму, которая пропускает в строке аргументов последовательность подряд идущих цифр и возвращает указатель на позицию в строке после числа.
; SkipNum
; Ввод SI = указатель на строку
; Вывод DI = указатель на позицию в строке после числа
SkipNum: mov di,si ;DI используется как указатель
.find_end_of_numberi inc di ;DI=DI+1 cmp [di],'0' сравнить символ с кодом цифры нуль
173
.end_found: ret А вот тело основной программы если меньше, конец числа найден сравнить символ с кодом шифры 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 указатель на начало строки аргументов переходим вконец первого числа пишем нулевой байт читаем число в десятичной записи в ЕАХ будет результат остается неизменным сохраняем число в EDX загружаем в SI указатель на позицию в строке сразу после первого числа пропускаем любые пробелы и пропускаем число
; записываем нулевой символ после второго аргумента в ЕАХ — результат остается неизменным добавляем предыдущее число, сохраненное в EDX Первым делом мы пропускаем число и записываем нулевой байт после него. Подпрограмма преобразования строки в число будет думать, что строка закончилась. После этого мы преобразуем фрагмент строки параметров от ее начала до первого нулевого байта в число. Регистр DI теперь указывает на нулевой байт, значит, нужно увеличить указатель на единицу. Наша подпрограмма преобразования строки в число ожидает указателя в регистре SI, и мы копируем его из DI. Теперь наша строка будет начинаться со второго аргумента, перед обработкой которого мы снова пропускаем все пробелы вначале строки. После преобразования второго числа мы складываем его с первым. После этого в ЕАХ будет сумма двух чисел, которую нужно преобразовать в строку и вывести на экран. Для преобразования числа в строку будем использовать подпрограмму NumToASCII:
174
; NumToASCII
; eax = 32-битное число
; ebx = основание системы счисления
; edi = указатель на буфер для сохранения строки
; Результат
; заполненный буфер mov ebx,10 заносим в ЕВХ основание системы счисления mov di,buffer в DI — адрес буфера call NumToASCII перед вызовом подпрограммы в ЕАХ находится вычисленная сумма Теперь выведем полученную строку на экран, используя функцию записи в файл стандартного вывода. Номы не знаем длину строки, которая получилась после преобразования. Пока сделаем так зарезервированное для строки место заполним пробелами и выведем строку вместе с лишними пробелами. Потом мы можем модифицировать программу, удалив из строки лишние пробелы. mov ah,0x40 функция DOS для записи в файл mov dx,di эта функция ожидает указателя на строку в DS:DX mov cx,25 количество выводимых символов i n t 0x21 mov ax, 0x4c00 завершаем программу i n t 0x21 В секции данных заполним наш буфер 25 пробелами buffer times 2 5 db ' ' Теперь нам осталось написать только секцию кода для нашей программы.
SECTION . t e x t org 0x100 mov si, 0x81 загружаем в SI указатель на начало строки аргументов call SkipNum переходим вконец первого числа mov [di],0 пишем нулевой байт mov есхДО читаем число в десятичной записи call ASCIIToNum в ЕАХ будет результат, SI остается неизменным mov edx,eax сохраняем число в EDX mov si,di загружаем в SI указатель на позицию в строке сразу после первого числа inc si call Skipspace пропускаем любые пробелы call SkipNum и пропускаем число
175
; SkipSpace - пропуск пробелов
; si = указатель на строку
; Возвращает
; si = первый символ,
SkipSpace:
.again: lodsb cmp al,' ' ; jz again ret отличный от пробела загружаем в AL байт по адресу DS:SI, увеличиваем SI сравниваем символ с пробелом если равно, продолжаем
; SkipNum — пропуск числа
; Ввод SI = указатель на строку
; Вывод DI = указатель на позицию в строке после числа
SkipNum: mov di,si
.find_end_of_number: inc di emp [di],'0' jb .end_found emp [di],'9' ja .end_found jmp .find_end_of_number
.end_found: ret
;DI используется как указатель
;DI=DI+1 сравнить символ с кодом цифры нуль если меньше, конец числа найден
; сравнить символ с кодом цифры 9 если больше, конец числа найден переходим к следующему символу
176
. ***** далее следуют все необходимые подпрограммы *****
SECTION .data buffer times 2 5 db ' ' буфер для сохранения результата Теперь сохраним нашу программу и попытаемся ее откомпилировать. В результате компиляции увидим это nasm -f bin -о t e s t . c o m t e s t . a s m t e s t . a s m : 6 : e r r o r : operation s i z e not s p e c i f i e d t e s t . a s m : 1 3 : e r r o r : symbol 'Skipspace' undefined t e s t . a s m : 1 5 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 4 1 : e r r o r : symbol ' a g a i n ' undefined t e s t . a s m : 5 3 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 5 5 : e r r o r : o p e r a t i o n s i z e not s p e c i f i e d t e s t . a s m : 1 4 1 : e r r o r : phase e r r o r 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,0x4 0 функция ДОС для записи в файл mov dx,di указатель ожидается в DX, у нас он в DI mov cx,25 количество печатаемых символов i n t 0x21 вызываем DOS Все ясно, мы ведь забыли поместить дескриптор потока стандартного вывода
0x01 в ВХ — в этом регистре все еще содержится значение 10. Исправим эту ошибку и снова откомпилируем программу. Теперь можно запустить программу с параметрами 45 и 50. Мы получим следующее значение
C : \ t e s t 45 50 945 7 Зак. 293
1 ... 12 13 14 15 16 17 18 19 20