Предыдущая моя статья получилась довольно объемной, а нераскрытых тем осталось довольно много. Поэтому,было принято решение написать ещё одну статью, в которой я опишу особенности многофайлового дебага с помощью 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"
На этом, пожалуй, завершу обзор дебага крупных многофайловых проектов. Всем приятного дебага!