2

2.1. Подготовка и отладка программы

Процесс подготовки и отладки программы на языке ассемблера включает этапы подготовки исходного текста, трансляции, компоновки и отладки.
Подготовка исходного текста программы выполняется с помощью любого текстового редактора, хотя бы редактора, встроенного в программу Norton Commander, FAR, или другого по вашему выбору. При использовании одного из более совершенных текстовых процессоров, вроде Microsoft Word, следует иметь в виду, что эти программы добавляют в выходной файл служебную информацию о формате (размер страниц, тип шрифта и др.), которая будет непонятна транслятору. Однако практически все текстовые редакторы и процессоры позволяют вывести в выходной файл "чистый текст", без каких-либо служебных символов. Именно таким режимом и надлежит воспользоваться в нашем случае.
В принципе для подготовки исходного текста можно воспользоваться любым редактором системы Windows, например, программой WordPad или Блокнотом. Однако в этом случае возникнут неприятности с русским шрифтом. Как известно, корпорация Microsoft приняла для своих русифицированных продуктов собственную кодировку русских символов, расходящуюся со стандартной, используемой в приложениях DOS. Если программу, использующую русский текст в качестве комментариев, или выводящую его на экран, подготовить в одном из редакторов Windows, то при ее просмотре и запуске в среде DOS вместо русского текста вы увидите бессмысленный набор символов. Поэтому программы, предназначенные для выполнения под управлением MS-DOS, лучше и подготавливать в среде DOS. Файл с исходным текстом должен иметь расширение .ASM.
Следующая операция состоит в трансляции исходного текста программы, т.е. в преобразовании строк исходного языка в коды машинных команд. Эта операция выполняется с помощью транслятора с языка ассемблера (т.е. с помощью программы ассемблера). Известные разработчики программного обеспечения - корпорации IBM, Borland, Microsoft и др. предлагают свои варианты трансляторов, несколько различающиеся своими возможностями и системой обозначений. Однако входной язык любого транслятора, включающий в себя мнемонику машинных команд и других операторов и правила написания предложений ассемблера, для всех ассемблеров одинаков, поэтому при подготовке и отладке примеров данной книги можно с равным успехом воспользоваться любой из указанных программ. Мы, как уже отмечалось, использовали программы пакета TASM 5.0 (фирменные названия этих программ - Turbo Assembler,
Turbo Link и Turbo Debugger, а имена соответствующих им файлов - TASM.EXE, TLINK.EXE и TD.EXE).
После трансляции образуются два файла - листинг трансляции и объектный файл с расширением OBJ. Листинг представляет собой текстовый файл, предназначенный для чтения в каком-либо редакторе, и содержит исходный текст оттранслированной программы вместе с машинными кодами команд. В случае обнаружения транслятором каких-либо ошибок, в листинг также включаются сообщения об этих ошибках.
Рассмотрим элементы листинга трансляции примера 1-1

;Пример 1-1. Простая программа с тремя сегментами
;Укажем соответствие сегментных регистров сегментам
assume CS:code,DS:data
;Опишем сегмент команд
code segment ;Откроем сегмент команд
begin: mov AX,data ;Настроим DS
mov DS,AX ;на сегмент данных;
Выведем на экран строку текста
mov АН,09h ;Функция DOS вывода на экран
mov DX,offset msg ;Адрес выводимой строки
int 21h ;Вызов DOS
;Завершим программу
mov AX,4C00h ;Функция DOS завершения программы
int 21h ;Вызов DOS
code ends ;Закроем сегмент команд
;Опишем сегмент данных
data segment ;Откроем сегмент данных
msg db "Программа работает!$' ;Выводимая строка
data ends ;Закроем сегмент данных
;Опишем сегмент стека
stk segment stack ;Откроем сегмент стека
db 256 dup (?) ;Отводим под стек 256 байт
stk ends ;Закроем сегмент стека
end begin ;Конец текста с точкой входа

На рис. 2.1 приведен несколько сокращенный текст этого листинга, из которого удалены комментарии к отдельным предложениям.

Рис. 2.1. Листинг трансляции программы 1-1.

Рассматривая листинг, можно отметить ряд полезных моментов общего характера. Предложения программы с операторами assume, segment, ends, end, как уже отмечалось ранее, не транслируются в какие-либо машинные коды и не находят отражения в памяти. Они нужны лишь для передачи транслятору служебной информации о способе трансляции команд (assume), границах сегментов (segment и end) и строке, на которой следует завершить обработку исходного текста (end).
Каждому транслируемому предложению программы соответствует определенное смещение, причем задание смещений выполняется в каждом сегменте в отдельности. Первая команда mov AX,data имеет смещение от начала сегмента команд, равное нулю. Она занимает 3 байта, поэтому следующая команда начинается с байта 3 и имеет соответствующее смещение.
Транслятор не смог полностью определить код команды mov AX,data. В этой команде в регистр АХ засылается сегментный адрес сегмента data. Однако этот адрес станет известен лишь в процессе загрузки выполнимого файла программы в память. Поэтому в листинге на месте этого адреса стоят нули, помеченные буквой s, напоминающей о том, что здесь должен быть пока неизвестный сегментный адрес.
Еще одна помеченная команда с кодом ВА 0000 располагается в строке 8 листинга. В этой команде в регистр DX заносится смещение поля с именем msg, расположенное в сегменте данных (ключевое слово offset, указанное перед именем поля, говорит о том, что речь идет не о содержимом ячейки msg, а об ее смещении). Поле msg расположено в самом начале сегмента данных, и его смещение от начала сегмента равно 0, что и указано в коде команды. Почему же эта команда помечена буквой т, являющейся сокращением слова relocatable, переместимый?
Чтобы ответить на этот вопрос, нам придется рассмотреть, как сегменты программы размещаются в памяти. Как уже говорилось, любой сегмент может располагаться в памяти только с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Конкретный адрес программы в памяти зависит от конфигурации компьютера, - какой размер занимает DOS, сколько загружено резидентных программ и драйверов, а также в каком режиме запускается программа - в отладчике или без него. Предположим, что сегментный адрес сегмента команд оказался равным 1306п (рис. 2.2, а). В нашей программе сегмент команд имеет размер 11h байт (что указано в строке 13 листинга), т.е. занимает целый параграф плюс один байт. Сегмент данных имеет размер 14h байт (строка 19 листинга) и тоже требует для своего размещения немного больше одного парафафа. Из-за того, что сегмент данных должен начаться на границе параграфа, ему будет назначен сегментный адрес 1308h и между сегментами образуется пустой промежуток размером 15 байт.
Потеря 15 байт из многомегабайтовой памяти, разумеется, не имеет никакого значения, однако в некоторых случаях, например, при компоновке единой профаммы из большого количества модулей с небольшими по размеру подпрофаммами, суммарная потеря памяти может оказаться значительной.

Рис. 2.2. Расположение сегментов программы в памяти при выравнивании по умолчанию (а) и на байт (б).

Для того, чтобы устранить потери памяти, можно сегмент данных объявить с выравниванием на байт:

data segment byte

Такое объявление даст возможность системе загрузить сегмент данных так, как показано на рис. 2.2, б. Сегмент данных частично перекрывает сегмент команд, начинаясь на границе его последнего параграфа (в нашем случае по адресу 1307h). Для того, чтобы данные не наложились на последние команды сегмента команд, они смещаются вниз так, что начинаются сразу же за сегментом команд. В нашем примере, где сегмент команд "выступает" за сегментный адрес 1307h всего на 1 байт, данные и надо сместить на этот 1 байт. В результате поле msg, с которого начинается сегмент данных, и которое в листинге имело смещение 0, получит смещение 1. Все остальные адреса в сегменте данных также сместятся на один байт вперед. В результате данные будут располагаться в физической памяти вплотную за командами, без всяких промежутков, однако все обращения в сегменте команд к данным должны быть скорректированы на величину перекрытия сегментов, в нашем случае - на 1 байт. Эта коррекция выполняется системой после загрузки программы в память, но еще до ее запуска. Адреса, которые могут потребовать описанной коррекции, и помечаются в листинге трансляции буквой "г".Из сказанного следует очень важный и несколько неожиданный вывод: коды команд программы в памяти могут не совпадать с кодами, показанными в листинге трансляции. Это обстоятельство необходимо учитывать при отладке программ с помощью интерактивного отладчика, который, естественно, показывает в точности то, что находится в памяти, и что не всегда соответствует листингу трансляции.
Вернемся к рассмотрению листинга трансляции. Данные, введенные нами в программу, также оттранслировались: вместо символов текста в загрузочный файл попадут коды ASCII этих символов. Так, буква "П" преобразовалась в код 8Fh, буква "р" в код ЕО и т. д. При выводе этих кодов на экран видеосистема компьютера преобразует их назад в изображения символов, записанных в исходном тексте программы.
Из листинга трансляции легко определить размер отдельных составляющих программы. В нашем случае длина сегмента команд составляет 11h = 17 байт, длина сегмента данных - 14h = 20 байт, а под стек отведено ровно столько, сколько мы запросили в программе - 100h = 256 байт. Размер же всей программы окажется больше суммы длин сегментов, во-первых, из-за пустых промежутков между сегментами (у нас на них уйдет 15 + 12 = 27 байт), и, во-вторых, за счет подсоединения к программе обязательного префикса программы, имеющего всегда размер 256 байт.
Как мы уже отмечали, в результате трансляции программы образуется два файла - с листингом и с объектным модулем программы.
Объектный файл является основным результатом работы транслятора и представляет собой текст программы, преобразованный в машинные коды. Хотя в этом файле уже присутствуют коды команд, он не может быть выполнен. Для того чтобы получить выполнимую программу, объектный файл необходимо скомпоновать.
Компоновка объектного файла выполняется с помощью программы компоновщика (редактора связей). Эта программа получила такое название потому, что ее основное назначение - подсоединение к файлу с основной программой файлов с подпрограммами и настройка связей между ними. Однако компоновать необходимо даже простейшие программы, не содержащие подпрограмм. Дело в том, что у компоновщика имеется и вторая функция - изменение формата объектного файла и преобразование его в выполнимый файл, который может быть загружен в оперативную память и выполнен. Файл с программой компоновщика обычно имеет имя LINK.EXE, хотя это может быть и не так. Например, компоновщик пакета TASM назван TLINK.EXE. В результате компоновки образуется загрузочный, или выполнимый файл с расширением .ЕХЕ.
Отладку и изучение работы готовой программы удобнее всего осуществлять с помощью интерактивного отладчика, который позволяет выполнять отлаживаемую программу по шагам или с точками останова, выводить на экран содержимое регистров и областей памяти, модифицировать (в известных пределах) загруженную в память программу, принудительно изменять содержимое регистров и выполнять другие действия, позволяющие в наглядной и удобной форме контролировать выполнение программы.
Рассмотрим вкратце основные приемы работы с "турбоотладчиком" TD.EXE из пакета TASM. Приступая к работе с отладчиком, следует убедиться, что в рабочем каталоге имеются и загрузочный (Р.ЕХЕ), и исходный (P.ASM) файлы, так как отладчик в своей работе использует оба эти файла. Для запуска отладчика следует ввести команду

td р

На экране появится кадр отладчика, в котором видны два окна - окно Module с исходным текстом отлаживаемой программы и окно Watches для наблюдения за ходом изменения заданных переменных в процессе выполнения программы (рис. 2.3). Окно Watches нам не понадобится, и его можно убрать, щелкнув мышью по маленькому квадратику в левом верхнем углу окна, или введя команду <Alt>+<F3>, предварительно сделав это окно активным. Переключение (по кругу) между окнами осуществляется клавишей <F6>.

Рис. 2.З. Начальный кадр отладчика с текстом отлаживаемой программы.

В процессе отладки программы на экран приходится выводить много дополнительных окон; они перекрываются и часто скрывают друг друга. Чтобы увидеть их все одновременно, размер окон приходится уменьшать, а сами окна перемещать по экрану. Режим изменения размеров и положения окна включается командой <Ctrl>+<F5>, после чего клавиши со стрелками перемещают окно по экрану, а те же клавиши при нажатой клавише <Shift> позволяют изменять его размер. Выход из режима настройки окна осуществляется нажатием клавиши <Enter>.
Начальное окно отладчика дает слишком мало информации для отладки программы. В нем можно выполнять программу по частям до местоположения курсора (клавиша <F4>) и команда за командой (клавиша <F8>); можно также с помощью окна Watches наблюдать изменения заданных полей данных. Однако для отладки программы на уровне языка ассемблера необходимо контролировать все регистры процессора, включая регистр флагов, а также, во многих случаях, поля данных вне программы (например, векторы прерываний или системные таблицы). Гораздо более информативным является "окно процессора", которое вызывается с помощью пункта Vicw>CPU верхнего меню или командой <Alt>+<V>+<C> (рис. 2.4).

Рис. 2.4. Окно процессора с внутренними окнами.

Окно процессора состоит, в свою очередь, из 5 внутренних окон для наблюдения текста программы на языке ассемблера и в машинных кодах, регистров процессора, флагов, стека и содержимого памяти. С помощью этих окон можно полностью контролировать ход выполнения отлаживаемой программы. Для того чтобы можно было работать с конкретным окном, например, прокручивать его содержимое, надо сделать его активным, щелкнув по нему мышью. Перейти из окна в окно можно также с помощью клавиатуры, нажимая клавишу Tab. Посмотрим, какие сведения можно извлечь из содержимого окна процессора.
Содержимое сегментных регистров DS и ES одинаково и составляет HF5h. Эта значит, что программа загружена в память, начиная с физического адреса 11F50, т.е. приблизительно с 70-го килобайта. Чем заняты первые 70 Кбайт памяти? Обычно компьютер конфигурируется так, что в обычной памяти размещается только малая часть DOS (около 16 Кбайт), драйверы обслуживания расширенной памяти и резидентная часть COMMAND.COM. Основная часть DOS, остальные драйверы и необходимые резидентные программы (например, русификатор) переносятся в расширенную память. В этом случае системные области в начале памяти занимают всего 20 - 25 Кбайт. Тем не менее наша программа начинается не с 25-го, а с 70-го килобайта. Произошло это из-за того, что программа запущена под управлением отладчика, который сначала загружается в память сам, и лишь затем загружает отлаживаемую программу. Но отсюда следует, что если бы мы запустили программу без отладчика, она попала бы на другое место в памяти, гораздо ближе к ее началу. В большинстве случаев это обстоятельство не имеет особого значения, так как любая программа должна одинаково успешно выполняться в любом месте памяти, однако необходимо отдавать себе отчет, что отладчик изменяет операционную среду программы (в частности, переносит ее на другое место в памяти). Строго говоря, программа под управлением отладчика выполняется не совсем так, как она выполнялась бы непосредственно в DOS.
Еще один пример "самодеятельности" отладчика можно увидеть в том же окне регистров процессора. Содержимое всех регистров общего назначения (АХ, ВХ, СХ, DX, SI, DI и ВР) равно 0. Отсюда можно сделать вывод, что DOS, загружая программу в память, очищает регистры процессора. Однако на самом деле это совсем не так! Регистры очищает не DOS, а отладчик. При обычном запуске программы исходное содержимое регистров практически непредсказуемо, и ни в коем случае нельзя рассчитывать, что в них будут нули. Иногда можно столкнуться и с более тонким влиянием отладчика на ход выполнения программы, вплоть до того, что некоторые виды программ, например, управляющие подключенной к компьютеру аппаратурой, в отладчике будут выполняться просто неверно.
Итак, после загрузки программы в память содержимое регистров DS и ES оказалось одинаковым. Это вполне естественно, если вспомнить, что перед выполнением оба регистра указывают на префикс программы (см, рис. 1.9). Вслед за префиксом располагается сегмент команд и поскольку префикс всегда занимает точно lOOh байт (т.е. 10h параграфов по 16 байт), то содержимое CS в нашем случае должно быть равно HF5h + 10h = 1205h. Так оно и есть (см. рис. 2.4).
В нашем примере программа должна начать выполняться с метки begin, поскольку именно эту метку мы указали в качестве операнда завершающей директивы end. Эта метка относится к самой первой команде сегмента команд и ее значение (или, что то же самое, смещение первой команды программы) должно быть равно 0. Поэтому исходное значение указателя команд, как это видно из рис. 2.4, тоже равно 0. В дальнейшем, по мере выполнения команд, значение IP будет возрастать. Выполним две первые команды программы, дважды нажав клавишу <F8>. Состояние программы после этой операции показано на рис. 2.5.

Рис. 2.5. Состояние программы после выполнения двух первых команд.

Видно, что указатель команд получил значение 5 и показывает на очередную (еще не выполнявшуюся) команду mov AH,09h, относительный адрес которой равен 5. Сегментный регистр DS получил значение 1207h, что должно соответствовать сегментному адресу сегмента данных. Вспомним, что сегмент команд у нас занимает 11h байт и требует в памяти 2 параграфа. Сегмент команд имеет сегментный адрес 1205h, следовательно, сегментный адрес сегмента данных должен быть равен 1207h, что мы и получили.
Обратим внимание на самую правую колонку в окне процессора, в которой индицируются состояния флагов процессора. Как уже говорилось, состояния флагов заново устанавливаются процессором после выполнения каждой команды, и по ним можно в определенной степени судить о результате команды. С самого начала у нас был установлен только флаг IF (i в окне отладчика), что свидетельствует о включенном механизме аппаратных прерываний; остальные флаги сброшены. После выполнения двух первых команд состояние регистра флагов не изменилось. Произошло это потому, что команда пересылки mov не изменяет состояния флагов. Поскольку в нашей программе нет никаких команд, кроме mov и hit, а команда hit тоже состояния флагов обычно не изменяет, то наблюдать с помощью нашего примера функционирование регистра флагов не удастся.
Рассмотрим теперь стек. Сегмент данных имеет у нас размер 14h байт, и под него в памяти надлежит выделить 2 параграфа. Это объясняет содержимое сегментного регистра стека SS - 1209п. Под стек отведено 256 байт, поэтому исходное положение SP (под дном стека) соответствует смещению l00h.
Наконец, стоит еще обратить внимание на нижнюю половину окна команд, заполненную странными командами add [bx+si],al. Таких команд, да еще в таком количестве, в нашей программе нет, их "придумал" отладчик, пытаясь деассемблировать промежуток между сегментом команд и сегментом данных, заполненный нулями. Код 0000h соответствует команде add [bx+si],al, которую и изобразил отладчик.
Таким образом, рассмотрев информацию, предоставленную отладчиком, мы подтвердили все предыдущие рассуждения о расположении в памяти сегментов программы и об инициализации регистров процессора при загрузке программы в память.
Обратимся теперь к окну дампа. При запуске отладчика в окно дампа выводится содержимое памяти, начиная с адреса DS:0000h, т.е. начало префикса программы (см. рис. 2.4 и 2.5). Для того, чтобы вывести на экран что-либо иное, надо воспользоваться командой <Alt>+<F10>, которая для каждого внутреннего окна процессора открывает дополнительное меню. Вид этого меню зависит от того, какое окне было активным в момент ввода команды. На рис. 2.6 показано дополнительное меню окна дампа.

Рис. 2.6. Дополнительное меню окна дампа памяти.

Чаще всего приходится пользоваться первым пунктом этого меню Goto, с помощью которого можно задать любой адрес (входящий или не входящий в сегменты программы), и получить дамп этого участка.. На рис. 2.7. изображено содержимое окна дампа после ввода начального адреса в виде DS:0 (тот же результат даст начальный адрес DS:msg, а так же и просто msg, так как по умолчанию сегментный адрес берется из DS). Как и следовало ожидать, по этому адресу расположено наше единственное данное - строка текста, выводимая программой на экран. Кстати, в окне дампа видно начало промежутка между сегментами (данных и стека), заполненного нулями.

Рис. 2.7. Дамп сегмента данных.