
Лекции / GL03
.DOC
3. Отладчик Debug
3.1. Пример работы с отладчиком.
Мы уже посмотрели примеры работы с отладчиком в предыдущей главе. Теперь на очень простом примере продемонстрируем типичный сеанс работы с отладчиком.
Предположим, мы хотим изучить работу команды сложения двух целых чисел. Первое слагаемое хранится в слове со смещением 200h (в текущем сегменте данных) — оно будет загружено в регистр AX, второе слагаемое — число 2 — будет загружено в регистр BX, сумму требуется разместить в AX. Напишем с этой целью простую программу, содержащую всего три команды.
mov ax,[200h] |
; Поместить в аккумулятор содержимое |
|
; слова по адресу DS:200 |
mov bx,2 |
; Поместить в регистр BX число 2 ( BX = 2) |
add ax,bx |
; Сложить содержимое AX и BX |
|
; и поместить результат в AX ( AX += BX) |
Комментарии отделены от текста программы точкой с запятой. (Рекомендуется каждую фразу комментария начинать с прописной буквы. Если комментарий продолжается на следующей строке, то продолжение фразы начинается со строчной буквы.) В скобках последние две команды записаны с использованием нотации языка Си — так проще запомнить, что первый операнд является приемником, а второй — источником. Следует понимать, что на самом деле эти три команды являются только фрагментом программы на языке ассемблера. Если мы собираемся использовать транслятор (например MASM или TASM), то к этим командам нужно добавить еще ряд директив. Однако для отладчика этих команд достаточно. Еще заметим, что вместо последних двух команд можно было написать одну: add ax,2.
Вызовем отладчик. Для этого наберем в командной строке имя утилиты debug
D:\USER\>debug
нажмем Enter и получим приглашение к диалогу — дефис.
1) Ассемблирование текста программы. В отладчике имеется мини-ассемблер. Он переводит мнемонику команд в их машинные коды. Практически все команды отладчика вызываются набором одной буквы, далее, возможно, указываются параметры команды. Ассемблирование начинается по команде A (Assemble). Команды можно набирать и большими и малыми буквами. Команда A имеет параметр: адрес в памяти, начиная с которого будут размещены машинные коды. (Здесь необходимо уточнение: полный адрес представлен в виде segment:offset — сегмент:смещение. Как правило, мы будем указывать только смещение). Разместим коды команд, начиная со смещения 100h (этот выбор не случаен: ниже смещения 100h что-либо записывать не рекомендуется, пока вы не изучите структуру области от начала сегмента до смещения 100h — она носит название префикс программного сегмента (Program Segment Prefix — PSP)).
-a100
22B6:0100 mov ax,[200] (суффикс h не указываем!)
22B6:0103 mov bx,2
22B6:0106 add ax,bx
22B6:0108 nop
22B6:0109
В ответ на ввод команды a100 отладчик выдает адрес в формате сегмент:смещение (на вашей машине сегментная часть адреса может быть другой — в дальнейшем это не оговаривается) и ожидает ввод команды. Набираем команду и нажимаем клавишу Enter. Мнемоника команды немедленно переводится мини-ассемблером в машинный код (чуть ниже мы увидим, как его посмотреть) и отладчик выводит следующий свободный адрес — 103. Следовательно, код команды mov ax,[200] занимает три байта (с адресами 100, 101, 102). Мы вводим следующую команду и т.д. Завершаем программу командой nop (нет операции). Удобство ее включения в текст программы станет ясным из дальнейшего. В ответ на адрес 109 нажимаем Enter и получаем приглашение отладчика. Обратите внимание, что все числа в debug интерпретируются как 16-ричные, поэтому суффикс h не указывается.
Если какая-либо команда будет введена неправильно, отладчик сообщит об этом и выдаст тот же адрес (естественно — он же не смог транслировать команду):
-a100
22B6:0100 mov ax[200] (пропущена запятая)
^ Error
22B6:0100
Код нашей программы занимает байты с 100-го по 108-й, т.е. девять байтов.
2) Дисассемблирование кода (восстановление мнемоники команд по машинному коду).
-u100L9
22B6:0100 A10002 MOV AX,[0200]
22B6:0103 BB0200 MOV BX,0002
22B6:0106 01D8 ADD AX,BX
22B6:0108 90 NOP
Команда расшифровывается так: дисассемблировать (U — Unassemble), начиная с 100-го смещения код длиной (L — Length) 9 байтов. (Можно было набрать команду в иной форме: U100 108, т.е. указать начальный и конечный адреса кода).
Полученные в результате строки имеют формат:
сегмент:смещение машинный код мнемоника
В кодах команд можно увидеть коды операндов в "перевернутом виде": Расшифруем код A10002. A1 — код пересылки содержимого ячейки памяти в аккумулятор, 00 — младший байт и 02 — старший байт операнда-источника. Аналогично расшифровывается код второй команды. Можно убедиться в том, что машинные коды имеют разную длину: в нашем примере — 1, 2 и 3 байта.
3) Заполнение памяти. Перед выполнением программы нужно занести в слово памяти, расположенное по адресу DS:200, число 1. Можно было добавить это действие в нашу программу в качестве первой команды (чуть позже мы узнаем, как это сделать), но мы воспользуемся средствами отладчика. Команда E (Enter — ввод) позволяет занести в память новое содержимое (здесь то, что выводит debug, подчеркнуто):
-e200
22B6:0200 4D.01 E2.00
(нажимаем пробельную клавишу)
Итак, мы вводим команду E и адрес. Отладчик сообщает, что в байте по адресу 200 записано число 4Dh, точка — приглашение к вводу. Набираем 01 (можно и без ведущего нуля) и нажимаем пробельную клавишу, чтобы получить содержимое следующего байта (если бы старое значение байта нас устраивало, мы сразу нажали бы пробел). По адресу 201 записано число 0E2h — заносим нуль. Завершаем команду E, нажимая Enter.
Команду E можно было ввести и в другой форме, указывая в одной строке адрес и список вводимых байтов (т.е. на этот раз мы не видим прежнее содержимое ячеек памяти):
-e200 1 0
Проверим результат занесения уже знакомой командой
-d200L2
22B6:0200 01 00
Записано слово 0001 (младший байт 01, старший байт 00).
Для ввода слова проще воспользоваться уже знакомой командой A:
-a200
22B6:0200 dw 1 (Define Word — задать слово)
Здесь есть одна тонкость. Команда E offset заносит данные по адресу DS:offset, а команда A offset — по адресу CS:offset. Но сейчас содержимое сегментных регистров CS и DS совпадает.
4) Содержимое регистров. Перед тем как выполнять программу посмотрим содержимое регистров центрального процессора. Введем команду R (Register):
-r
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0100 NV UP EI PL NZ NA PO NC 22B6:0100 A10002 MOV AX,[0200] DS:0200=0001
Отладчик вывел три строки. В первой строке перечислены регистры общего назначения и показано их содержимое. Начальное содержимое регистров — нулевое. Только указатель стека SP содержит FFEEh, т.е. показывает на адрес, близкий к концу сегмента. Во второй строке присутствуют сегментные регистры DS, ES, SS, CS — в них хранится одинаковый сегментный адрес; счетчик команд IP содержит смещение 100h; далее перечислены текущие значения флажков в регистре флагов. Отдельные биты этого регистра устанавливаются (1) или сбрасываются (0) по результату выполнения команд. Но отладчик показывает не содержимое регистра флагов в шестнадцатеричном представлении, а значения битов в закодированной форме в виде двухбуквенных сокращений. Сведем их в таблицу 3.1.
Таблица 3.1. Кодирование значений флагов в debug.
Флаг |
Наименование |
Установлен (‘1’) |
Сброшен (‘0’) |
OF |
переполнение |
OV (OVerflow) |
NV (Non oVerflow) |
|
|
Есть знаковое переполнение |
Нет знакового переполнения |
DF |
направление |
DN (DowN) |
UP |
|
|
автоуменьшение указателя при выполнении строковых команд |
автоувеличение указателя при выполнении строковых команд |
IF |
прерывание |
EI (Enable Interrupt) |
DI (Disable Interrupt) |
|
|
внешние прерывания разрешены |
внешние прерывания запрещены |
SF |
знак |
NG (NeGativ) |
PL (PLus) |
|
|
результат отрицательный |
результат неотрицательный |
ZF |
ноль |
ZR (ZeRo) |
NZ (Non Zero) |
|
|
результат нулевой |
результат ненулевой |
AF |
вспомогательный |
AC (Adjust Carry) |
NA (Non Adjust carry) |
|
перенос |
произошел перенос из младшей тетрады |
переноса из младшей тетрады не было |
PF |
четность |
PE (Parity Even) |
PO (Parity Odd) |
|
|
сумма битов младшего байта четная |
сумма битов — нечетная |
CF |
перенос |
CY (Carry Yes) |
NC (Non Carry) |
|
|
установлен флаг переноса |
флаг переноса сброшен |
В третьей строке представлены адрес, код и мнемоника команды, которая будет выполняться первой при запуске на выполнение. Ее сегментный адрес хранится в CS, а смещение в IP (именно этой информацией воспользовался отладчик, чтобы отобразить команду). Кроме того, в третьей строке показано содержимое ячейки памяти, на которую ссылается один из операндов команды.
5) Выполнение программы "по шагам". В этом режиме после каждой команды выполнение будет приостанавливаться. Для этого вводим команду T (Trace — трассировка). Выполняется команда, адрес которой содержится в IP, и показывается информация, как при подаче команды R.
-t
AX=0001 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0103 NV UP EI PL NZ NA PO NC 22B6:0103 BB0200 MOV BX,0002
Обратите внимание, что изменилось содержимое только двух регистров: AX — в него загружено значение 1, и IP — в нем адрес следующей выполняемой команды. Сама команда показана в третьей строке. Флаги не изменились.
-t
AX=0001 BX=0002 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0106 NV UP EI PL NZ NA PO NC 22B6:0106 01D8 ADD AX,BX
-t
AX=0003 BX=0002 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0108 NV UP EI PL NZ NA PE NC 22B6:0108 90 NOP
Только теперь (после команды add ax,bx) изменилось содержимое регистра флагов. Правда это коснулось только одного флага PF (в младшем байте результата (03h=00000011b) установлено два бита, то есть четное число битов).
6) Внесение изменений в программу: в регистр BX запишем –1. Заменим вторую команду нашей программы.
-a103
22B6:0103 mov bx,-1
22B6:0106
Проверим результат изменений (на всякий случай, хотя и так видно, что длина команды не изменилась):
-u100L9
22B6:0100 A10002 MOV AX,[0200]
22B6:0103 BBFFFF MOV BX,FFFF (дополнительный код числа –1)
22B6:0106 01D8 ADD AX,BX
22B6:0108 90 NOP
Вновь выполним трассировку. Но сейчас в IP содержится 108h, поэтому первая команда трассировки выглядит иначе, чем раньше: после знака равенства указываем стартовый адрес.
-t=100
AX=0001 BX=0002 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0103 NV UP EI PL NZ NA PE NC 22B6:0103 BBFEFF MOV BX,FFFE
-t
.............
7) Изменение содержимого регистров. Если наша цель — изучение команды add, то проще изменять значения регистров средствами отладчика непосредственно перед выполнением команды add. Вновь воспользуемся командой R, но теперь укажем имя регистра:
-rax
AX 0000
:-2
^ Error
Отладчик показывает содержимое AX и запрашивает новое содержимое. Вводим число –2. Отладчик воспримет это как ошибку. Нужно сначала получить дополнительный код –2. Его можно узнать с помощью отладчика. Команда H <1-е число> <2-е число> вычисляет сумму и разность аргументов‑слов:
-h0 2
0002 FFFE (т.е. 0 + 2 = 0002 и 0 – 2 = 0FFFEh)
Теперь введем в AX дополнительный код числа –2.
-rax
AX 0000
:fffe
Изменим BX
-rbx
BX FFFE
:3
А теперь выполним трассировку только команды сложения.
-t=106
AX=0001 BX=0003 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0108 NV UP EI PL NZ AC PO CY 22B6:0108 90 NOP
8) Выполнение всей программы:
-g=100 108
AX=0000 BX=FFFF CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0108 NV UP EI PL ZR AC PE CY 22B6:0108 90 NOP
Итак, использована команда G (Go — прогон), далее после знака равенства указываем стартовый адрес, затем адрес точки останова (теперь мы видим пользу команды nop — именно ее адрес служит адресом точки останова). Заметим, что команда g=100 приведет скорее всего к зависанию программы (лучше не проверять это!). Действительно, после наших четырех команд в оперативной памяти хранится "мусор", который будет интерпретироваться процессором как коды команд.
9) Сохранение файла с программой на диске. Для этого выполним последовательность действий:
а) дадим файлу имя pr1.com. Введем команду N (Name — имя)
-npr1.com
(отладчик создает исполняемые файлы только типа .com — не .exe!)
б) в регистры BX и CX занесем длину программы в байтах. BX:CX — длинное беззнаковое целое: в BX — старшие разряды, в CX — младшие разряды. Типичная ошибка: забывают занести в BX нуль. В результате на диске образуется очень большой файл.
-rbx
BX FFFF
:0
-rcx
CX 0000
:9
Еще раз напомним, что диапазон адресов 100 – 108 имеет длину 9 байтов.
в) введем команду записи на диск W (Write — писать).
-w
Writing 00009 bytes
Отладчик сообщил количество записанных байтов.
10) Выход из отладчика. Вводим команду Q (Quit — мы с отладчиком "квиты")
-q
D:\USER\>_
Убедитесь, что в текущем каталоге действительно имеется файл pr1.com размера 9 байтов. Запускать его на выполнение из командной строки бессмысленно по двум причинам:
-
программа не отображает результаты работы на экране, проследить ее работу можно только средствами отладчика;
-
в программе нет команд, обеспечивающих возврат в DOS, поэтому после наших четырех команд начнет выполняться "мусор" с непредсказуемыми последствиями.
11) Отладка программы, хранящейся в файле. Мы хотим еще поработать с нашей программой, записанной в файле pr1.com. Начать работу можно двумя способами.
а) Указать имя файла в командной строке при запуске debug:
D:\USER\> debug pr1.com
-r
AX=0000 BX=0000 CX=0009 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=22B6 ES=22B6 SS=22B6 CS=22B6 IP=0100 NV UP EI PL NZ NA PO NC 22B6:0100 B80100 MOV AX,0001
В BX:CX записан размер считанной программы.
б) Загрузить нужный файл в оперативную память с помощью команд debug:
D:\USER\> debug
-n pr1.com (назовем имя)
-L (Load — загрузка содержимого файла в оперативную память)
Программа считана с диска. С ней можно работать. После внесения изменений в программу может возникнуть желание сохранить новый вариант программы на диске. Для записи на диск достаточно установить нужные BX:CX и ввести команду W, т.к. имя файла уже задано.
Теперь, после того как мы получили представление о возможностях отладчика и типовых "связках" команд, дадим систематическое изложение. Отладчики, которые мы будем изучать позже, не будут сопровождаться столь подробным изложением, т.к. для них можно получить информацию в Справке (Help) программного продукта.
3.2. Параметры команд отладчика.
Запуск отладчика возможен в двух вариантах:
1) D:\USER\> debug
2) D:\USER\> debug <имя файла>
При втором варианте запуска отладчик загружает содержимое файла в нижний доступный сегмент (при этом файл с расширением .com загружается в этом сегменте со смещением 100h).
После запуска debug на выполнение пользователь получает приглашение (-) и может вводить команды. Формат команды:
буква [параметр(ы)] Здесь буква — первая буква команды, например команда F — (Fill —заполнять). параметр(ы) — перечень параметров, которые могут быть разделены пробелами или запятыми. Разделитель обязателен между последовательностью шестнадцатеричных величин. Буквы могут быть прописными и строчными.
Перечислим типы параметров: величина — от одной до четырех шестнадцатеричных цифр (суффикс h не указывается); байт — две шестнадцатеричные цифры; адрес — можно задать тремя способами:
1) сегмент:смещение, например, 057D:0020, или 57D:20 (ведущие нули можно не указывать);
2) сегментный регистр:смещение, например, cs:109
3) смещение. Если команда работает с машинными инструкциями (команды A,G,L,T,U,W), подразумевается сегментный регистр CS, если работа идет с данными — DS.
область — диапазон адресов; его можно задать двумя способами
1) адрес смещение, например, cs:100 110 (Неверно: cs:100 cs:110 — второй параметр не является смещением).
2) адресLвеличина, где величина — размер области в байтах, например cs:100L11 задает точно такую же область длиной 17 байтов, как и в предыдущем примере.
строка — цепочка символов, заключенная в одинарные или двойные кавычки (если кавычки встречаются внутри строки, их следует удваивать).
Примеры: 'Эта ''строка'' правильная'
"this ""string"" is okay"
список — последовательность байтов и/или строк.
пример: "Hello!",d,a,'$'
3.3. Команды отладчика
Сгруппируем команды по их функциональному назначению.
1) Работа с данными.
1.1) |
D [область] |
Dump — дамп |
Эта команда бала разобрана ранее.
1.2) |
E адрес [список] |
Enter — ввод |
Ввод данных по указанному адресу.
а) E адрес (список опущен)
-e200
1C36:0200 01.02 12. 1B.0
вводим пробел для перехода к следующему байту
Результат:
-d200L3
1C36:0200 02 12 00
Итак, при нажатии пробельной клавиши отладчик выводит очередной байт и запрашивает его изменение, выводя точку. Для перехода к предыдущему байту нужно ввести дефис (попробуйте!).
Упражнение. С помощью команды E можно просматривать память. Посмотрите, что будет происходить, если вы подадите команду efff0 и несколько раз нажмете пробельную клавишу. Перейдете ли Вы границу сегмента? А что произойдет, если вы наберете команду dfff0?
б) E адрес список
e200 02 12 00
Результат тот же, что и в предыдущем примере, но на этот раз замена данных ведется "вслепую".
Слова приходится заносить в память побайтно. Байты заносятся в обратном порядке. Например, для ввода по адресу (смещению) 120 слова 1234 подаем команду:
-e120 34 12
Пример ввода строки символов:
-e130 'string for example',D,A,'$'
(0Dh — код возврата каретки, 0Ah — код перевода строки. $ — терминатор (ограничитель) строки, который нужен при использовании системной функции вывода строки на экран. Эту функцию мы изучим позднее.)
1.3) |
R [имя регистра] |
Register — регистр |
а) R (без параметра) — выводятся имена всех регистров и их содержимое. Последняя строка вывода имеет формат:
адрес код команды мнемоника команды [содержимое памяти]
1С36:0100 A10002 MOV AX,[0200] DS:0200=01E4
б) R имя регистра — debug выводит содержимое регистра, а на следующей строке помещает двоеточие — приглашение к вводу нового значения. К сожалению, можно указывать только 16-разрядные регистры.
-rbx
1107
: 1 (вводим новое содержимое регистра)
<Enter>
-rah
br ERROR (недопустимое имя регистра)
в) RF выводит содержимое регистра флагов
NV UP DI NG NZ AC PE NC -
После дефиса можно ввести новые значения флагов: - PLCY (если задано более чем один код значения для флага, появляется сообщение об ошибке df error если задан код значения, отсутствующий в списке — сообщение bf error).
1.4) |
F область список |
Fill—заполнение |
Если размер области превосходит длину списка, то список используется повторно, пока область не будет заполнена. Остаток списка усекается.
Пример. Перед созданием программы в отладчике полезно заполнить участок памяти кодом команды nop (90h) (если это не сделать, "мусор" в памяти сбивает с толку начинающего пользователя).
-f100L100 90
Упражнение. Выполнить f400L10 1 2 3 и изучить результат.
2) Работа с кодами команд.
2.1) |
A [адрес] |
Assemble — ассемблировать |
Пример работы этой команды мы уже видели. Если в начале сеанса работы с debug не указывать адрес, то ассемблирование начнется с адреса XXXX:0100.
Отметим, что с использованием мини-ассемблера заполнение памяти данными осуществить даже проще, чем с помощью команды E:
-a200
1C36:0200 dw ffd0,12,a1c7
1C36:0206
-
(В результате [200]=FFD0h, [202]=0012h, [204]=A1C7h.)
-a200
1C36:0200 db "String",D,A,'$'
1C36:0209
Здесь использованы директивы dw (Define Word — задать слово) и db (Define Byte — задать байт).
2.2) |
U [область] |
Unassemble — дисассемблировать |
Команда переводит коды из области памяти в мнемонику команд.
Если перевод невозможен, то выводится ???? или DB число. Если параметр область опущен, то команда дисассемблирует 20 байт, начиная с текущего значения IP. Вновь введем U — следующие 20 байт и т.д. (аналогично команде D без параметров).
3) Выполнение программы.
3.1) |
T[=стартовый адрес] [количество повторений] |
Trace — трассировка |
После выполнения каждой команды выводится информация, как при подаче команды R. Приведем примеры:
а) t — выполнить одну команду, адрес которой в CS:IP;
б) t5 — выполнить 5 команд, начиная с текущего адреса;
в) t=106 8 — выполнить 8 команд, начиная с адреса CS:106.
3.2) |
G[=стартовый адрес] [адрес останова] |
Go — прогон |
а) g — выполнить программу, начиная с адреса, хранящегося в IP. Программа должна заканчиваться инструкциями завершения (эти инструкции мы изучим позднее — сейчас упоминаем о них только в справочных целях), например, INT 20h или MOV AH,4Ch /INT 21h. После выполнения INT 20h debug выводит сообщение:
Program terminated normally (Выполнение программы завершено) Теперь повторный запуск программы возможен только после выполнения команды L (см. ниже).
б) g=100 адрес команды nop
Мы рекомендуем на начальном этапе обучения завершать учебные программы, набираемые в debug, командой nop и использовать ее адрес в качестве адреса останова.
4) Работа с файлами и дисками.
4.1) |
N имя файла |
Name — имя |
Задание имени файла. Расширение указывать обязательно. Используется перед следующими двумя командами.