null

Дебаг сложных проектов в gdb

Предыдущая моя статья получилась довольно объемной, а нераскрытых тем осталось довольно много. Поэтому,было принято решение написать ещё одну статью, в которой я опишу особенности многофайлового дебага с помощью gdb.

Рассматривать будем сразу же проект из двух файлов со статическими функциями.
Стоит обратить особое внимание, когда компилируете статические функции, т.к. при должном уровне оптимизации, компилятор их инлайнит.
В нашем случае, скомпилируем код без оптимизаций. Далее приведены исходные файлы и листинг компиляции:
main.c
#include <unistd.h>
extern int far(void);

static int f() {
   write(2, "f1\n", 3);
   return 0;
}

int main(int argc, char *argv[]) {   
   f();
   far();
   
   return 0;
}

far.c
#include <unistd.h>

static int f() {
   write(2, "f2\n", 3);
   return 0;
}

int far() {
   f();
   return 0;
}
 
$ cat Makefile 
CC = gcc
RM = rm
CFLAGS = -Wall -g -ansi -pedantic
FILES = main far
OBJS = $(FILES:=.o)

all: $(OBJS)
   $(CC) $(CFLAGS) -o main $(OBJS)

$(OBJS):
   $(CC) $(CFLAGS) -c $(@:.o=.c)

clean:
   $(RM) -f $(OBJS) $(FILES)
$ gmake
gcc -Wall -g -ansi -pedantic -c main.c
gcc -Wall -g -ansi -pedantic -c far.c
gcc -Wall -g -ansi -pedantic -o main main.o far.o
Запустим код на отладку и посмотрим, какие функции нам видны: 
$ gdb -q main
(gdb) info sources
Source files for which symbols have been read in:



Source files for which symbols will be read in on demand:

far.c, main.c
(gdb) info functions 
All defined functions:

File main.c:
int main(int, char **);
static int f();

File far.c:
int far();
static int f();

Non-debugging symbols:
0x0000000000400460  .init
0x0000000000400460  _init
0x0000000000400474  .plt
0x0000000000400484  atexit@plt
0x0000000000400494  _init_tls@plt
0x00000000004004a4  exit@plt
0x00000000004004b4  write@plt
0x00000000004004d0  .text
0x00000000004004d0  _start
0x0000000000400580  __do_global_dtors_aux
0x00000000004005c0  frame_dummy
0x0000000000400680  __do_global_ctors_aux
0x00000000004006b4  .fini
0x00000000004006b4  _fini
Отсюда нас интересуют функции с одинаковым прототипом: static int f(void);
В крупных проектах сложность представляет найти необходимый символ и остановить выполнение на нём.
Если поступить традиционно (указать команде break имя символа):
(gdb) break f
Breakpoint 1 at 0x4005f4: file main.c, line 5.
То отладчик установит точку останова на функции f() в файле main.c
Но что если требуется остановиться на функции f() в файле far.c ?
Этот случай предусмотрен в gdb и существует расширенный способ передачи аргументов. Сделать это можно следующим образом:
(gdb) break far.c:f
Breakpoint 2 at 0x400644: file far.c, line 4.
Теперь отладчик остановит выполнение программы при обоих вызовах данной функции. 
Нераскрытым вопросом остается дебаг инлайн функций. В большинстве случаев, отладчику успешно удается установить точку останова на вызове такой функции. Т.к. фактически инструкции call для неё нету, отладчик находит её начало и устанавливает точку останова на первой из её инструкций. Однако, иногда ему не удается этого сделать (напимер, когда функция объявлена в заголовочном файле). В таком случае, придется читать ассемблерный листинг. Для этого подойдет команда disas, либо встроенная короткая команда x (предположительно, от eXamine), которая умеет выводить дамп памяти в определённом формате. Ниже представлены синтаксис команды и примеры использования:
x/count_format addr
(gdb) x/16x 0x00000000004004d0
0x4004d0 <_start>:   0xe5894855  0x8d4c5541  0x5441086f  0xec834853
0x4004e0 <_start+16>:   0x481f8b08  0x042d3d83  0x48000020  0x8d4cc363
0x4004f0 <_start+32>:   0x4810c764  0x041e058b  0x0f490020  0xdb85c444
0x400500 <_start+48>:   0x11058948  0x7e002004  0x578b4838  0xd2854808
(gdb) x/4i _start
0x4004d0 <_start>:   push   %rbp
0x4004d1 <_start+1>: mov    %rsp,%rbp
0x4004d4 <_start+4>: push   %r13
0x4004d6 <_start+6>: lea    0x8(%rdi),%r13
 
Другими интересными фичами gdb являются условные точки останова. Синтаксис использования таков:
break <addr> if <condition>
В качестве условия могут быть выражения с локальными переменными или регистрами.
Также полезной функцией gdb является command.
Это выражение принимает в качестве аргумента номер точки останова и позволяет указать, какие команды выполнять при останове.
Вот один из примеров использования этих замечательных функций:
(gdb) break main if argc == 1
Breakpoint 1 at 0x40061f: file inline.c, line 5.
(gdb) command 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>print argc
>print *argv
>end
(gdb) run
Starting program: /tmp/debug/main 

Breakpoint 1, main (argc=1, argv=0x7fffffffd950) at main.c:5
5     f();
$1 = 1
$2 = 0x7fffffffdc20 "/tmp/debug/main"
 
На этом, пожалуй, завершу обзор дебага крупных многофайловых проектов. Всем приятного дебага!
korg

 

Коротко о себе

Работаю в компании Tune-IT, администрирую инфраструктуру компании и вычислительную сеть кафедры Вычислительной ТехникиСПбНИУ ИТМО.

Интересы: администрирование UNIX и UNIX-like систем и активного сетевого оборудования, написание shell- и perl-скриптов, изучение технологий глобальных сетей.
Люблю собирать GNU/Linux и FreeBSD, использовать тайлинговые оконные менеджеры и писать системный софт.