Опубликован: 01.03.2016 | Уровень: для всех | Доступ: платный
Лекция 6:

Отладчик GDB

< Лекция 5 || Лекция 6

Цель отладки программы - устранение ошибок в её коде. Для этого вам, скорее всего, придётся исследовать состояние переменных во время выполнения, равно как и сам процесс выполнения (например, отслеживать условные переходы). Тут отладчик - наш первый помощник. Конечно же, в Си достаточно много возможностей отладки без непосредственной остановки программы: от простогоprintf(3) до специальных систем ведения логов по сети и syslog. В ассемблере такие методы тоже применимы, но вам может понадобиться наблюдение за состоянием регистров, образ (dump) оперативной памяти и другие вещи, которые гораздо удобнее сделать в интерактивном отладчике. В общем, если вы пишете на ассемблере, то без отладчика вы вряд ли обойдётесь.

Начать отладку можно с определения точки останова (breakpoint), если вы уже приблизительно знаете, какой участок кода нужно исследовать. Этот способ используется чаще всего: ставим точку останова, запускаем программу и проходим её выполнение по шагам, попутно наблюдая за необходимыми переменными и регистрами. Вы также можете просто запустить программу под отладчиком и поймать момент, когда она аварийно завершается из-за segmentation fault, - так можно узнать, какая инструкция пытается получить доступ к памяти, подробнее рассмотреть приводящую к ошибке переменную и так далее. Теперь можно исследовать этот код ещё раз, пройти его по шагам, поставив точку останова чуть раньше момента сбоя.

Начнём с простого. Возьмём программу Hello world и скомпилируем её с отладочной информацией при помощи ключа компилятора -g:

[user@host:~]$ gcc -g hello.s -o hello
[user@host:~]$ 

Запускаем gdb:

[user@host:~]$ gdb ./hello
GNU gdb 6.4.90-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and 
you are welcome to change it and/or distribute copies of it under 
certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for 
details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db 
library "/lib/tls/libthread_db.so.1".

(gdb) 

GDB запустился, загрузил исследуемую программу, вывел на экран приглашение (gdb) и ждёт команд. Мы хотим пройти программу "по шагам" (single-step mode). Для этого нужно указать команду, на которой программа должна остановиться. Можно указать подпрограмму - тогда остановка будет осуществлена перед началом исполнения инструкций этой подпрограммы. Ещё можно указать имя файла и номер строки.

(gdb) b main
Breakpoint 1 at 0x8048324: file hello.s, line 17.
(gdb) 

b - сокращение от break. Все команды в GDB можно сокращать, если это не создаёт двусмысленных расшифровок. Запускаем программу командой run. Эта же команда используется для перезапуска ранее запущенной программы.

(gdb) r
Starting program: /tmp/hello

Breakpoint 1, main () at hello.s:17
17      movl  $4, %eax   /* поместить номер системного вызова write = 4
Current language:  auto; currently asm
(gdb) 

GDB остановил программу и ждёт команд. Вы видите команду вашей программы, которая будет выполнена следующей, имя функции, которая сейчас исполняется, имя файла и номер строки. Для пошагового исполнения у нас есть две команды: step (сокращённо s) и next (сокращённо n). Команда step производит выполнение программы с заходом в тела подпрограмм. Команда next выполняет пошагово только инструкции текущей подпрограммы.

(gdb) n
20      movl  $1, %ebx          /* первый параметр - в регистр %ebx  */
(gdb) 

Итак, инструкция на строке 17 выполнена, и мы ожидаем, что в регистре %eax находится число 4. Для вывода на экран различных выражений используется команда print (сокращённо p). В отличие от команд ассемблера, GDB в записи регистров использует знак $ вместо %. Посмотрим, что в регистре %eax:

(gdb) p $eax
$1 = 4
(gdb) 

Действительно 4! GDB нумерует все выведенные выражения. Сейчас мы видим первое выражение ($1), которое равно 4. Теперь к этому выражению можно обращаться по имени. Также можно производить простые вычисления:

(gdb) p $1
$2 = 4
(gdb) p $1 + 10
$3 = 14
(gdb) p 0x10 + 0x1f
$4 = 47
(gdb) 

Пока мы играли с командой print, мы уже забыли, какая инструкция исполняется следующей. Команда info line выводит информацию об указанной строке кода. Без аргументов выводит информацию о текущей строке.

(gdb) info line
Line 20 of "hello.s"  starts at address 0x8048329 <main+5> and ends at 
0x804832e <main+10>.
(gdb) 

Команда list (сокращённо l) выводит на экран исходный код вашей программы. В качестве аргументов ей можно передать:

  • номер_строки - номер строки в текущем файле;
  • файл:номер_строки - номер строки в указанном файле;
  • имя_функции - имя функции, если нет неоднозначности;
  • файл:имя_функции - имя функции в указанном файле;
  • *адрес - адрес в памяти, по которому расположена необходимая инструкция.

Если передавать один аргумент, команда list выведет 10 строк исходного кода вокруг этого места. Передавая два аргумента, вы указываете строку начала и строку конца листинга.

(gdb) l main
12                                 за пределами этого файла          */
13      .type  main, @function  /* main - функция (а не данные)      */
14
15
16      main:
17              movl  $4, %eax  /* поместить номер системного вызова 
18                                 write = 4 в регистр %eax          */
19
20              movl  $1, %ebx  /* первый параметр поместить в регистр 
21                                 %ebx; номер файлового дескриптора 
22                                 stdout = 1                        */
(gdb) l *$eip
0x8048329 is at hello.s:20.
15
16      main:
17              movl  $4, %eax  /* поместить номер системного вызова 
18                                 write = 4 в регистр %eax          */
19
20              movl  $1, %ebx  /* первый параметр поместить в регистр 
21                                 %ebx; номер файлового дескриптора 
22                                 stdout = 1                        */
23              movl  $hello_str, %ecx  /* второй параметр поместить в 
24                                 регистр %ecx; указатель на строку */
(gdb) l 20, 25
20              movl  $1, %ebx  /* первый параметр поместить в регистр 
21                                 %ebx; номер файлового дескриптора 
22                                 stdout = 1                        */
23              movl  $hello_str, %ecx  /* второй параметр поместить в 
24                                 регистр %ecx; указатель на строку */
25
(gdb) 

Запомните эту команду: list *$eip. С её помощью вы всегда можете просмотреть исходный код вокруг инструкции, выполняющейся в текущий момент. Выполняем нашу программу дальше:

(gdb) n
23              movl  $hello_str, %ecx  /* второй параметр поместить в 
                                           регистр %ecx
(gdb) n
26              movl  $hello_str_length, %edx /* третий параметр 
                                           поместить в регистр %edx
(gdb) 

Не правда ли, утомительно каждый раз нажимать n? Если просто нажать Enter, GDB повторит последнюю команду:

(gdb) 
29              int   $0x80     /* вызвать прерывание 0x80           */
(gdb) 
Hello, world!
31              movl  $1, %eax  /* номер системного вызова exit = 1  */
(gdb) 

Ещё одна удобная команда, о которой стоит знать - info registers. Конечно же, её можно сократить до i r. Ей можно передать параметр - список регистров, которые необходимо напечатать. Например, когда выполнение происходит в защищённом режиме, нам вряд ли будут интересны значения сегментных регистров.

(gdb) info registers
eax            0xe      14
ecx            0x804955c        134518108
edx            0xe      14
ebx            0x1      1
esp            0xbfabb55c       0xbfabb55c
ebp            0xbfabb5a8       0xbfabb5a8
esi            0x0      0
edi            0xb7f6bcc0       -1208566592
eip            0x804833a        0x804833a <main+22>
eflags         0x246    [ PF ZF IF ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) info registers eax ecx edx ebx esp ebp esi edi eip eflags
eax            0xe      14
ecx            0x804955c        134518108
edx            0xe      14
ebx            0x1      1
esp            0xbfabb55c       0xbfabb55c
ebp            0xbfabb5a8       0xbfabb5a8
esi            0x0      0
edi            0xb7f6bcc0       -1208566592
eip            0x804833a        0x804833a <main+22>
eflags         0x246    [ PF ZF IF ]
(gdb)

Так, а кроме регистров у нас ведь есть ещё и память, и частный случай памяти - стек. Как просмотреть их содержимое? Команда x/формат адрес отображает содержимое памяти, расположенной по адресу в заданном формате. Формат - это (в таком порядке) количество элементов, буква формата и размер элемента. Буквы формата: o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) и s(string). Размер: b(byte), h(halfword), w(word), g(giant, 8 bytes). Например, напечатаем 14 символов строки hello_str:

(gdb) x/14c &hello_str
0x804955c <hello_str^gt;: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 44 ', ' 
                       32 ' ' 119 'w'
0x8049564 <hello_str+8>: 111 'o' 114 'r' 108 'l' 100 'd' 33 '! '  10 '\n'
(gdb) 

То же самое, только в шестнадцатеричном виде:

(gdb) x/14xb &hello_str
0x804955c <hello_str>:   0x48  0x65  0x6c  0x6c  0x6f  0x2c  0x20  0x77
0x8049564 <hello_str+8>: 0x6f  0x72  0x6c  0x64  0x21  0x0a
(gdb) 

Напечатаем 8 верхних слов (4 байта) из стека (для "погружения в стек" читаем слева направо и сверху вниз):

(gdb) x/8xw $esp
0xbfd8902c:  0xb7e14ea8      0x00000001      0xbfd890a4      0xbfd890ac
0xbfd8903c:  0x00000000      0xb7f2dff4      0x00000000      0xb7f53cc0
(gdb) 

Было бы хорошо, если бы GDB отображал значение какого-то выражения автоматически. Это делает команда display/формат выражение. Если в формате будет указан размер, то принцип действия аналогичен x. Если размер не указан, команда ведёт себя как print.

(gdb) display/4xw $esp
1: x/4xw $esp
0xbf8fdb9c:  0xb7e4dea8      0x00000001      0xbf8fdc14      0xbf8fdc1c
(gdb) display/x $eax
2: /x $eax = 0xe
(gdb) n
32              movl  $0, %ebx  /* передать 0 как значение параметра */
2: /x $eax = 0x1
1: x/4xw $esp
0xbf8fdb9c:  0xb7e4dea8      0x00000001      0xbf8fdc14      0xbf8fdc1c
(gdb) 
< Лекция 5 || Лекция 6
Константин Белюстин
Константин Белюстин
Украина, г. Киев
Максим Барашков
Максим Барашков
Россия, Якутск