Отладчик EDB (как и любой другой отладчик) позволяет увидеть, что происходит «внутри» программы в момент ее выполнения, или что делает программа в момент краха, однако реализует эти возможности через наглядный графический интерфейс. Синтаксис команды для запуска отладчика имеет следующий вид:
edb [ --attach <ID процесса>] [--run <имя_файла> (аргументы)]
После запуска появляется графическое окно EDB, разделенное на четыре основные части: дизассемблер, стек, дамп памяти с вкладками, содержимое регистров.
Всю информацию и данные Evan’s Debugger отображает в меню и в окнах. Используются различные виды окон в зависимости от того, какого типа информация в них отображается. Все окна открываются и закрываются с помощью команд меню (или активных клавиш, соответствующих этим командам).
Окно регистров (Registers) отображает состояние регистров и флагов процессора, а также позволяет изменять их значения с помощью двойного нажатия мышкой. С помощью команд всплывающего (локального) меню можно попробовать перейти по адресу, хранящемуся в выбранном регистре, в окне стека или окне дампа памяти.
Окно дампа памяти (Data Dump) отображает построчное содержимое области памяти. Можно просматривать данные в виде шестнадцатеричных байтов, слов и двойных слов. Его можно использовать в тех случаях, когда желательно просмотреть некоторые исходные данные, не заботясь об остальном состоянии процессора. Во всплывающем меню имеются команды, которые позволяют модифицировать отображаемые данные, менять формат их отображения на экране и манипулировать блоками данных.
Окно стека (Stack) отображает текущее состояние стека, причем область первой вызванной функции будет находиться на дне стека, а всех последующих вызванных функций — в направлении вершины стека в последовательности их вызова. Во всплывающем меню имеются команды, которые позволяют модифицировать отображаемые данные, менять формат их отображения на экране, переходить по указанному адресу, по адресу из регистров ebp
или esp
.
Если есть файл с исходным текстом программы, а в исполняемый файл включена информация о номерах строк исходного кода, программу можно отлаживать, работая в отладчике непосредственно с ее исходным текстом. Чтобы программу можно было отлаживать на уровне строк исходного кода, она должна быть откомпилирована с ключом -g
.
Однако такая возможность есть не всегда, и в случае необходимости отладчик может дизассемблировать исполняемый код, изображая машинные команды в виде ассемблерных мнемоник.
Напомним, что существуют два режима отображения синтаксиса машинных команд: режим Intel, используемый в т.ч. в NASM, и режим ATT. По умолчанию в дизассемблере EDB принят режим Intel.
Установить точку останова можно путем двойного клика мышкой на нужной инструкции. Если точка останова установилась, напротив инструкции появится красная отметка:
Информацию о всех установленных точках останова можно посмотреть с помощью Breakpoint Manager, нажав Ctrl+M или открыв меню Plugins \rightarrow BreakpointManager \rightarrow Breakpoints:
Команда Run (F9) продолжает выполнение остановленной программы. Выполнение будет происходить, пока не встретится точка останова или программа не выполнится полностью.
Команда Step Into (F7) приводит к выполнению программы до тех пор, пока не будет достигнута следующая строка ее кода. Вызов процедуры трактуется отладчиком не как одна инструкция, а как передача управления на еще один блок ассемблерного кода, который тоже должен быть пройден по шагам.
Команда Step Over (F8) приводит к выполнению программы до тех пор, пока не будет достигнута следующая строка ее кода. В отличие от Step Into, вызов процедуры считается единой инструкцией.
Подпрограмма — это, как правило, функционально законченный участок кода, который можно многократно вызывать из разных мест программы. В отличие от простых переходов, из подпрограмм существует возврат на команду, следующую за вызовом.
Если в программе встречаются одинаковый участок кода, его можно оформить в виде подпрограммы, а во всех нужных местах поставить ее вызов. При этом подпрограмма будет содержаться в коде в одном экземпляре, что позволит уменьшить размер кода всей программы.
Основные моменты выполнения подпрограммы иллюстрируются на рис. [pic:l54]. В вызывающей подпрограмму программе выполняется инструкция call
, которая заносит адрес следующей инструкции в стек и загружает в регистр eip
адрес соответствующей подпрограммы, осуществляя таким образом переход. После этого подпрограмма выполняется, как любой другой код. В подпрограммах могут (часто это так и бывает) содержаться инструкции вызовов других подпрограмм.
Когда подпрограмма заканчивает работу, она вызывает инструкцию ret
, которая извлекает из стека адрес, занесенный туда соответствующей инструкцией call
, и заносит его в eip
. Это приводит к тому, что вызывающая программа возобновит выполнение с инструкции, следующей за инструкцией call
.
Например, следующая программа выводит на экран строку «Enter string:», ждёт ввода строки (например, «HELLO») и выводит на экран строку «Result:» и введённую строку (т. е. «Result: HELLO»). Для вывода строк вызывается подпрограмма PrintString
:
SECTION .data ; Константы ask1: DB 'Enter string: ' , 10 ask1_len: EQU $-ask1 result: DB 'Result: ' result_len: EQU $-result SECTION .bss buf1: RESB 80 SECTION .text ; Код программы GLOBAL _start ; Начало программы ;------------------------------------------------------------------------ ; Подпрограмма вывода на экран строки ; Входные данные: ; ecx - указатель на выводимую строку. ; Нарушаемые регистры: eax,ebx; ;------------------------------------------------------------------------ Print_string: mov eax, 4 mov ebx, 1 int 80h ret ; возврат в вызывающую программу ;------------------------------------------------------------------------ ; Подпрограмма ввода строки с клавиатуры ; Входные данные: ; ecx - указатель на буфер для входной строки. ; Нарушаемые регистры: eax,ebx; ;------------------------------------------------------------------------ Enter_string: mov eax, 3 mov ebx, 0 int 80h ret ; возврат в вызывающую программу _start: mov ecx, ask1 mov edx, ask1_len call Print_string mov ecx, buf1 mov edx, 6 call Enter_string mov ecx, result mov edx, result_len call Print_string mov ecx, buf1 mov edx, 6 call Print_string mov eax,1 ; Системный вызов для выхода (sys\_exit) mov ebx,0 ; Выход с кодом возврата 0 (без ошибок) int 80h ; Вывзов ядра
Подпрограмма PrintString
не настроена жестко на печать определенной строки. Она может печатать любую строку, на которую укажет регистр ecx
.
Инструкция ret
возвращает управление вызывающей программе. Для этого она извлекает из вершины стека четыре байта и заносит их в регистр счётчик команд eip
. После этого значение регистра esp
увеличится на 4. Если в процедуре занести что-то в стек и не извлечь, то на вершине стека окажется не адрес возврата, и это приведёт к ошибке выхода из процедуры.
Ассемблерная подпрограмма без команды возврата не вернется в точку вызова, а будет выполнять следующий за подпрограммой код, как будто он является ее продолжением.
Ввод информации с клавиатуры и вывод ее на экран осуществляется в символьном виде. Кодирование этой информации производится согласно кодовой таблице символов, где каждый символ (в простейшем случае) кодируется одним байтом. Однако в памяти компьютера любые числа, над которыми можно производить математические операции, записаны в двоичной системе счисления, и для вывода на экран необходимо преобразовать двоичное число в его символьную запись (а при вводе с клавиатуры — выполнить обратное преобразование).
Любое число X в позиционной системе счисления представляется в виде суммы произведений:
Здесь X — это число в системе с основанием p, имеющее n+1 цифру в целой части.
Так, при переводе кода введённого символа в десятичную систему (например, чтобы вывести на экран не символ, а численное значение его кода) надо разложить число на слагаемые, содержащие степени числа 10. Перевод кода символа производится путем последовательного деления на основание 10 с выделением остатков от деления до тех пор, пока частное не станет меньше делителя. Выписывая остатки от деления справа налево, получаем 10-чную запись числа.
Сначала рассмотрим деление 16-битового значения на 8-битовое. При беззнаковом делении 16-битового значения на 8-битовое, делимое должно быть записано в регистре ax
. 8-битовый делитель может храниться в любом 8-битовом общем регистре или переменной в памяти соответствующего размера. Инструкция div
всегда записывает 8-битовое частное в регистр al
, а 8-битовый остаток — в ah
. Например, после выполнения инструкций
. . .
mov ax,51
mov dl,10
div dl
. . .
результат 5 (51/10) будет записан в регистр al
, а остаток 1 (остаток от деления 51/10) — в регистр ah
.
Заметим, что частное представляет собой 8-битовое значение. Это означает, что результат деления 16-битового операнда на 8-битовый операнд не должен превышать 255. Если частное слишком велико, то генерируется прерывание 0 («деление на 0»). Инструкции
. . . mov ax,0fffh mov bl,1 div bl . . .
генерируют прерывание по делению на 0 (как можно ожидать, прерывание по делению на 0 генерируется также, если 0 используется в качестве делителя).
При делении 32-битового операнда на 16-битовый операнд делимое должно записываться в регистрах dx:ax
. 16-битовый делитель может находиться в любом из 16-битовых регистров общего назначения или в переменной в памяти соответствующего размера. Например, в результате выполнения инструкций
. . . mov ax,2 mov dx,1 ; загрузить в регистровую mov bx,10h ; пару \verb!dx:ax! 10002h div bx . . .
частное 1000h (результат деления 10002h на 10h) будет записано в регистре ax
, а 2 (остаток от деления) — в регистре dx
.
При делении имеет значение, используются операнды со знаком или без знака. Для деления беззнаковых операндов используется операция div
, а для деления операндов со знаком — idiv
.
Написать программу со следующим алгоритмом:
ввести символ с клавиатуры;
преобразовать полученный код в десятичную символьную запись;
вывести символ и его код.
Перевод числа в десятичную символьную запись оформить в виде подпрограммы.
Загрузить программу в отладчик. Это можно сделать двумя способами: написать в командной строке
edb --run имя_программы
или запуститьedb
и выбрать программу через пункт Open меню File.Выполнить программу по шагам, нажимая кнопку Step Over панели инструментов или клавишу F7 (находясь в основном окне отладчика), до конца.
Поместить в программу точку останова на инструкции, следующей после ввода символа с клавиатуры — щелкнув правой кнопкой по нужной строке дизассемблированного кода и выбрав пункт Add Breakpoint всплывающего меню. Выполнить программу до точки останова, нажав клавишу F9 или кнопку Run панели инструментов. Иметь в виду, что ввод текста с клавиатуры в выполняемую программу осуществляется в отдельном окне EDB Output, а не в основном окне отладчика.
Вывести в окне дампа памяти содержимое входного буфера, щелкнув в подокне Data Dump правой кнопкой мыши и выбрав пункт Goto Address всплывающего меню. Адрес вводить в шестрадцатиричной нотации Си (начиная с символов 0x).
Зайти в процедуру перевода числа в десятичную запись. Выполнить 2 прохода цикла по F7 (Step Into), контролируя значения регистров. Какие регистры изменяются в цикле?
Остальные проходы цикла выполнить по F8 (Step Over). В чем разница?
Определить физический адрес выходного буфера в ОЗУ .
Вывести ячейки памяти, соответствующие выходному буферу, в подокне Data Dump в шестнадцатиричном и в символьном виде.
Установить точку останова на инструкцию
div
. Выполнить программу, несколько раз нажав на F8 и наблюдая за изменением содержимого выходного буфера в подокне Data Dump. Каков результат? (Перевод чисел между шестнадцатиричной и десятичной системами счисления можно упростить, воспользовавшись программой Калькулятор, выбрав в ней пункт меню Вид \rightarrowПрограммирование).
Изменить содержимое входного буфера и проверить, как это отражается на выполнении программы.