
- •Аппаратно-ориентированное программирование
- •Ббк 32.973.73
- •Удк 681.3 ббк 32.973.73ф 73
- •1. Основы программирования на ассемблере
- •1.1. Принципы построения ассемблерных программ
- •1.2. Понятие архитектуры компьютера
- •1.3. Регистры программиста в ia32
- •1.4. Описание сегментной структуры программы
- •2. Простейшие средства ассемблера
- •2.1. Средства описания данных
- •2.2. Обращения к функциям ос посредством прерываний
- •2.3. Средства преобразования в исполняемый файл
- •2.4. Управление строками при выводе и ввод данных
- •2.5. Простейшие способы адресации
- •3. Архитектурные элементы для построения программ
- •3.1. Организация условных переходов
- •3.2. Средства организации циклов
- •3.3. Особенности команд умножения и деления
- •3.4. Организация процедур
- •3.5. Неарифметические операции над кодами
- •4. Использование неэлементарных способов адресации
- •4.1. Косвенно-регистровая адресация
- •4.2. Использование индексной адресации данных
- •4.3. Базовая и индексно базовая адресации
- •4.4. Адресация с масштабированием
- •5. Взаимосвязи программных единиц
- •5.1. Многомодульная разработка программ
- •5.2. Использование библиотек объектных модулей
- •5.3. Организация стекового кадра подпрограммы
- •5.4. Программный доступ к системным функциям Win32
- •5.5. Особенности использования объектных файлов формата coff
- •5.6. Стандартный доступ к системным функциям Unix
- •6. Вспомогательные средства базовой архитектуры
- •6.1. Использование строковых команд пересылки
- •6.2. Применение строковых команд сравнения
- •7. Использование ассемблерных отладчиков
- •7.1. Особенности отладчика gdb для программ в Linux
- •7.2. Отладчики текстового режима для Windows
- •Библиографический список
- •Оглавление
7. Использование ассемблерных отладчиков
7.1. Особенности отладчика gdb для программ в Linux
Разработка достаточно сложных программ практически невозможна без использования отладчиков. К сожалению для начинающих программистов, современные широко распространенные отладчики ориентированы на отладку программ, написанных на языках высокого уровня, и, в первую очередь, на языке Си. Такая ситуация обусловлена тем, что использование ассемблера остается к настоящему времени уделом профессионалов и даже стандартный интерфейс доступа к программным функциям современной операционной системы (API ОС) задается теперь исключительно на языке Си.
Тем не менее, имеющиеся отладчики могут использоваться и для отладки ассемблерных программ, хотя множество наиболее мощных средств этих отладчиков нацелены на более высокоуровневые языки.
В ОС Linux стандартным отладчиком, являющимся как и вся ОС Linux, свободно распространяемым продуктом, является отладчик с программным именем gdb (GNU debugger). Согласно документации, это так называемый символический отладчик. Его возможности очень многообразны, но в данном изложении будут рассмотрены лишь простейшие из них и те, без которых трудно обойтись даже в простейшей отладке.
Запустить программу для отладки с помощью gdb чрезвычайно просто: следует в командной строке консоли набрать вызов отладчика в виде
gdb имя_исполняемой_программы
Если в разработке не указывалась опция, задающая порождение отладочной информации, то непосредственно после вызова отладчика для программы появится сообщение
(no debugging symbol found) . . .
и будет выведено приглашение (prompt) для команд отладчика в виде
(gdb)
Заметим, что текущие версии NASM не позволяют формировать отладочную информацию в процессе трансляции, а стандартный ассемблер Unix'а с именем as не удобен для начального изучения предмета. Поэтому мы ограничимся изучением использования отладчика gdb при отсутствии отладочной информации от транслятора. В то же время оказывается, что минимальная и достаточная информация для отладчика поступает от соответствующего исходного файла. В частности, оказываются доступными обычные (необязательно глобальные) метки и именованные области данных, а поэтому, фактически и имена подпрограмм. (А больше никакой символьной информации программисту и не надо.) Заметим, что отсутствие отладочной информации от транслятора делает очень неудобной отладку программ, написанных на языке высоко уровня, так как в этом случае отладчик может предоставить отладочную информацию на уровне ассемблера, а нам больше "ничего и не надо". Поэтому в данном случае замечание об отсутствии отладочной информации не может быть предметом неудовольствия.
Отладчику gdb присуще одно специфическое свойство, кажущееся неудобным для тех, кто привык к отладке простейших программ под MS-DOS. Именно, оказывается практически невозможным остановиться перед первой исполняемой командой программы, а можно только на любой другой. Такое поведение - косвенный результат строгой ориентации отладчика на структуры программ, построенных компиляторами с языков высокого уровня. Такая структура обязательно строится как структура, систематически использующая кадры (фреймы) подпрограмм, где все подпрограммы, начиная с программной функции main, создаются по этим соглашениям.
При работе отладчика gdb и его использовании широко применяются понятия текущего кадра, вложенных кадров вызова и переключения между кадрами для доступа к локальным данным процедуры любого более высокого уровня вызова. Расширенные возможности, вытекающие из этого подхода, влекут требование для разработчика по входу для отладки хотя бы в какой-то кадр и, поэтому начального выполнения хотя бы одной команды. Заметим, что при отладке любых программ, отличных от ассемблерных, указанное ограничение никак не заметно программисту, так как все программы с языков высокого уровня используют процедуру начального вызова главной подпрограммы разрабатываемой программы. Так для программы с языка Си используется (часто незаметно для малоквалифицированного программиста) вспомогательная программа запуска главной подпрограммы.
Поэтому придется удовлетвориться тем, что фактически отладчик gdb можно будет использовать для ассемблерных программ только с их второй команды. Отсюда следуют два практических варианта: либо на веру принять правильность предполагаемых действия первой команды (тем более, что ее результат с помощью отладчика нетрудно увидеть), либо (для наиболее сомневающихся) вставить в качестве первой команды какую-то вспомогательную, от которой не зависят дальнейшие результаты (например, команду CLD). Можно для успокоения просто считать, что приостановиться можно только после первой команды, действия которой мы можем наблюдать.
Собственно отладка начинается с выдачи отладчику команды run (выполнить). Но читателю не следует торопиться сразу же ее использовать. Если задать команду run сразу после вызова отладчика, то все выполнения будут происходить в автоматическом режиме (без отображения и приостановок отладчиком) и результат окажется во многих случаях таким же, как и при запуске той же программы без отладчика.
Мощнейшие средства, которые предоставляет отладчик - это задаваемые приостановки в автоматическом выполнении и пошаговое выполнение. Обычно разработчик использует и то, и другое. Заметим дополнительно, что нельзя задавать пошаговое выполнение, пока не выдана команда run.
Команда отладчика «установка точки приостанова» задается именем breakpoint, которое можно сокращать до двух начальных символов. Эта команда может использоваться в одной из двух следующих основных форм:
br имя_метки_в_программе
или
br *адрес
Проще всего для наших целей воспользоваться этой командой для начальной приостановки автоматического выполнения, если в исходной программе на ассемблере поставить метку у второй ее команды и эту метку использовать в команде приостановки, выдаваемой программистом сразу после команды запуска отладчика с отлаживаемой программой.
Для удобства демонстрировать конкретные средства отладки возьмем в качестве примера программу, исходный текст которой приведен в листинге 7.1.1.
GLOBAL _start
SEGMENT .text
_start: mov eax, 1000h
beg: mov esi,10 ; base of position digit system
mov ecx, 0 ; reset digit counter
pov: mov edx, 0 ; null into left part of devident
div esi ; divide for next digit = rest
add dl, '0'
push edx
inc ecx ; step into counter
cmp eax, 0
jne pov
mov [cnt], ecx
mov ebx, numtxt
izv: pop edx
mov byte [ebx],dl ; digit into array for text value
inc ebx
loop izv ; izv,ecx
call outa
mov eax, 1 ; N function = exit
int 80h ;
outa:
mov eax,4 ; N function=write
mov ebx,1 ; N handle=1 (stdout)
mov ecx, numtxt ; address of text
mov edx,[cnt] ; number of byte
int 80h
ret
SEGMENT .data
numtxt times 10 db 0
cnt dd 0
Листинг 7.1.1. Пример программы для отладки
Пусть исполняемый файл формата ELF, который получается из программы в листинге 7.1.1, называется primer.exe. Тогда начало использования отладчика дает следующие строки, вводимые с консоли и выводимые на нее.
gdb primer.exe
(no debugging symbol found) . . .
(gdb) br beg
Breakpoint 1 at 0x8048085
(gdb) run
Starting program /home/student/primer.exe
Breakpoint 1, 0x8048085 in beg ()
(gdb)
Здесь отладчик выводит всю изображенную информацию, кроме находящейся в строках, которые начинаются с приглашения (gdb). Текст в строках после этого приглашения всегда вводится пользователем (и завершается нажатием клавиши Enter). В данном примере предполагается, что программа primer.exe была помещена в базовом каталоге пользователя student. В общем случае после слов "Starting program" идет полное имя исполняемого файла, запущенного "под отладчиком".
Далее следует решить, какой вариант отображения команд желает использовать пользователь. Дело в том, что по умолчанию отображаются команды в форме универсального ассемблера ATT, которая для пользователей, привыкших к соглашениям Intel и Microsoft, может показаться непривычной. В этом ассемблере все операнды получатели записываются справа, а операнды исходной информации - слева (обратно принятому в Intel). Кроме того, обозначения всех регистров предваряются служебным символом %, а константы - служебным символом $.
Если пользователю кажется неудобным такая форма обозначения команд, то перейти к более привычной он может с помощью вспомогательной команды
set disassembly-flavor intel
Обратный переход осуществляется командой
set disassembly-flavor intel
Заметим, что даже в режиме отображения Intel обозначениям регистров при выводе обязательно предшествуют служебные символы %.
Теперь (как и в любой последующий момент) мы можем воспользоваться командой disassemble, которую проще сокращать до пяти первых букв. Эта команда без аргументов выдает ассемблерную запись команд текущего кадра.
Для ассемблерных программ при отсутствии более детальной информации отладчик считает кадром последовательность команд от одной метки до другой. Поэтому задание команды disas отладчику в нашем примере (после приостановки на первой точке приостановки) даст следующий вывод:
0x8048085 <beg>: mov %esi,0xa
0x804808a <beg+5>: mov %ecx,0x0
(gdb)
Для ассемблерных программ эту команду удобней всего использовать с двумя аргументами, задающими начало и конец отображаемой области команд. Причем эти аргументы можно задавать именами меток в программе. Например, задав отладчику команду в виде
disas _start izv
мы получим в качестве отображаемого на экране следующий текст рассматриваемого примера
Dump of assembler code from 0x8048080 to 0x80480ac:
0x8048080 <_start>: mov %eax,0x1000
0x8048085 <beg>: mov %esi,0xa
0x804808a <beg+5>: mov %ecx,0x0
0x804808f <pov>: mov %edx,0x0
0x8048094 <pov+5>: div %eax,%esi
0x8048096 <pov+7>: add %dl,0x30
0x8048099 <pov+10>: push %edx
0x804809a <pov+11>: inc %ecx
0x804809b <pov+12>: cmp %eax,0x0
0x80480a0 <pov+17>: jne 0x804808f <pov>
0x80480a2 <pov+19>: mov %ds: 0x80490e2, %ecx
0x80480a8 <pov+25>: mov %ebx,0x80490d8
End of assembler dump.
(gdb)
Этот вариант появится, если было произведено переключение на ассемблер типа intel. В противоположном случае появится текст
Dump of assembler code from 0x8048080 to 0x80480ac:
0x8048080 <_start>: mov $0x1000, %eax
0x8048085 <beg>: mov $0xa, %esi
0x804808a <beg+5>: mov $0x0, %ecx
0x804808f <pov>: mov $0x0, %edx
0x8048094 <pov+5>: div %esi, %eax
0x8048096 <pov+7>: add $0x30, %dl
0x8048099 <pov+10>: push %edx
0x804809a <pov+11>: inc %ecx
0x804809b <pov+12>: cmp $0x0, %eax
0x80480a0 <pov+17>: jne 0x804808f <pov>
0x80480a2 <pov+19>: mov %ecx, 0x80490e2
0x80480a8 <pov+25>: mov $0x80490d8, %ebx
End of assembler dump.
(gdb)
По умолчанию стандартный режим отладчика gdb не отображает информацию о текущей команде в пошаговом режиме этого отладчика. (Это вполне целесообразно, так как он обычно используется для отладки программ, написанных на языке высокого уровня.) Для установки режима автоматического отображения следующей команды следует передать отладчику приказ в виде директивы
display/i $eip
после чего в нашем примере автоматически выдается (с учетом предыдущей остановки на первой точки приостановки)
1: x/i $eip 0x8048085 <beg> mov %esi, 0xa
(если текущим режимом отображения команд будет режим intel).
Само приказание на выполнение одной следующей команды задается в виде команды отладчика
stepi
В частности, в нашем примере после полученной выше остановки на первой точке приостановки, выдача команды stepi приводит к следующей реакции отладчика в виде
(gdb) stepi
0x804808a in beg()
1: x/i $eip 0x804808a <beg+5> mov %ecx, 0x0
Дальнейшие выдачи команды stepi (в частности, после извлечения ее в командную строку с помощью клавиши "стрелка вверх") будут давать следующие выводы
(gdb) stepi
0x804808f in pov()
1: x/i $eip 0x804808f <pov>: mov %edx,0x0
(gdb) stepi
0x804808a in pov()
1: x/i $eip 0x8048094 <pov+5>: div %esi, %eax
(gdb) stepi
0x804808a in pov()
1: x/i $eip 0x8048096 <pov+7>: add $0x30, %dl
Для отображения информации служит команда с именем, восходящим к слову print, но задаваемая обычно единственным начальным символом p. В частности, выдача содержимого регистра в виде десятичного числа требует задания команды в виде p $имя_регистра, для вывода в виде шестнадцатеричного числа - команда задается в виде p/x $имя_регистра. Причем имя регистра в этой команде допускается только для 32-битных регистров (регистры меньшего размера все равно являются частью некоторого 32-битного регистра).
При отладке рассматриваемой программы-примера в качестве первой цифры должен появиться символ '6', и, действительно, в ситуации остановки перед строкой <pov+7>, т.е. после вывода последней из изображенных выше строк информации от отладчика, запрос
p $edx
приводит к выдаче информации в виде
$1 = 0x6
а запрос в виде
p/x $eax
к выводу результата
$2 = 0x199
Аналогичный запрос в этом же месте отлаживаемой программы в виде
p/x $eax
дает результат
$3 = 409
В самом конце прохождения программы с помощью отладчика, когда будет достигнут ее конец, на экран выдается сообщение
Program exited with code число
где параметр число есть значение, переданное в регистре ebx при вызове первой функции (функции exit) Linux..
Следует иметь в виду, что программисту предоставляется возможность установки любого нужного ему множества контрольных точек. Для этого проще всего предварительно позаботиться о расстановке (в исходном тексте программы на ассемблере) меток в соответствующих местах программы и при работе отладчика задавать команды в виде
br имя_метки
Ненужные уже метки можно удалить командой clear в виде
clear имя_метки
Можно временно выключать (выводить из использования) точки приостановки с помощью команд
disable номер_точки_приостанова
а повторно (или в очередной раз) включить ее в использование с помощью команды
enable номер_точки_приостанова
Команда print (в сокращенной форме задаваемая одной буквой p) может использовать кроме формата, задаваемого символом x (вывод в шестнадцатеричной системе), еще и другие форматы типа. Они задаются символами d, u, o, t и a. Формат d выводит значение как целое со знаком в десятичной системе, формат u задает целое как беззнаковое десятичное, формат o выводит целое в восьмеричной системе счисления, а формат t - в двоичной системе. Для указания вывода в виде отдельного символа служит формат c.
Собственно аргументом команды print могут быть не только регистры, но и имена областей данных, а также адреса, задаваемые в виде шестнадцатеричных констант.
Кроме того, имеется еще более мощная возможность отображения информации, но применимая только к памяти (но не к регистрам). Это команда examine, которая обычно задается единственным начальным символом сокращенного имени - символом x.
Общая форма этой команды имеет вид
x/NFU ADDR
где параметр ADDR обозначает адрес места в памяти, параметр N задает число отображаемых элементов, параметр F заказывает формат вывода, а параметр U дает размер информационной единицы. Любой из параметров N, F, U может отсутствовать, тогда берется значение, ранее использованное перед этим, или значение по умолчанию (если команда еще не использовалась).
Формат задается одним символом, который либо совпадает с символами формата для команды print, либо дает один из дополнительных форматов s или i. Формат i задает вывод в виде машинных команд, изображаемых ассемблерными мнемокодами, а формат s соответствует ASCIIZ, т.е. символьной строке, заканчивающейся нулевым кодом.
В качестве символов размера информационного элемента допустимо использовать символы b, h и w. Символ b обозначает байты, h обозначает полуслова (два байта), а w - слова (четыре байта) - это и есть начальное значение по умолчанию.
Для продолжения автоматического выполнения команд с текущей точки приостановки (и обычно до следующей встречающейся точки приостановки) служит команда отладчика continue, задаваемая обычно единственным символом c.
Универсальный символический отладчик gdb имеет еще множество возможностей, как для отладки высокоуровневых программ, так и пригодных для ассемблерных программ. В частности, он включает средства, позволяющие отлаживать многопоточные приложения и параллельные процессы. В данном пособии нет технической возможности останавливаться на них.