简介

GDB,又称 GNU 调试器,是用来帮助调试我们程序的工具。

GDB有如下两种使用方式

  1. 调试会崩溃、有逻辑错误的程序;
  2. 调试程序崩溃时自动生成的 core dump;

GDB 可以干以下几件事:

  1. 给程序设置(特定条件下的)断点,包括某块内存的内容改变时触发断点,如果某个变量不知被哪段代码意外篡改了,可以使用 GDB 打数据断点抓到肇事者。
  2. 当程序停在断点处时,我们查看所有变量、寄存器的值
  3. 当程序停在断点处时,我们除了能查看所有变量的值以外,还能在不改变源代码的情况下改变这些值

安装

1
sudo apt-get install gdb

编译被测程序

被测程序需修改 Makefile,将 CFLAGS 添加 -O0 -g 重新编译并且保证不去符号。如果不加 -g ,gdb 加载后将提示 No symbol table is loaded。缺少调试信息,调试起来会比较麻烦。

如果不使用 -O0 而是-O1-O2-O3,代码优化后,给调试带来难度。

make 编译时,后面需要加 STRIP=/bin/true,当然如果 Makefile 里没有调用 strip 去符号,可以不用加。

1
time make -j STRIP=/bin/true

加载被测程序

设被测程序名为 hello。

使用 gdb 运行:

1
2
3
gdb ./hello

Reading symbols from ./hello... # 如果未输出此行,则说明 hello 没有加 -g 编译

普通断点

在 main 函数打普通断点:

1
(gdb) b main

然后运行

1
(gdb) r

运行到 main 函数时程序暂停。

条件断点

顾名思义,这种断点是当满足一定条件时才会触发,比较适合进行异常排查。设置方式

1
(gdb) b line-or-function if (condition)

如:

1
(gdb) b src/main.cpp:127 if count==10

监视

设置监视也必须是在程序运行后才行。如:

1
2
3
(gdb) watch *地址    # 当地址所指内容发生变化时断点
(gdb) watch var # 当 var 值变化时,断点
(gdb) watch (condition) # 当条件符合时,断点

监视也被称为硬件断点。可以监测栈变量和堆变量值的变化,当被监测变量值发生变化时,程序被停住。

调试 coredump

coredump 叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。

该文件也是二进制文件,可以使用 gdb、elfdump、objdump 进行打开分析里面的具体内容。

ulimit

虽然我们知道进程在 coredump 的时候会产生core文件,但是有时候却发现进程虽然崩溃了,但是我们却找不到coredump 文件。

在 Linux 下是需要进行设置的。

ulimit -c 可以设置core文件的大小,如果这个值为0.则不会产生core文件,这个值太小,则 core 文件也不会产生,因为core文件一般都比较大。

使用ulimit -c unlimited 来设置无限大,则任意情况下都会产生 core 文件。Ubuntu 18.04,coredump会默认产生在以下目录

1
/var/lib/apport/coredump/

gdb 分析 coredump 的简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

void dumpCrash()
{
char *pStr = "test_content";
free(pStr);
}

int main(void)
{
dumpCrash();
return 0;
}

如上代码,pStr 指针指向的是字符串常量,字符串常量是保存在常量区的,free 释放常量区的内存肯定会导致coredump。

首先把上面的代码保存为 dumpTest.c 文件,gcc 编译:

1
gcc dumpTest.c -o dumpTest

设置 core 文件大小为无限制大小:

1
ulimit -c unlimited

运行 dumpTest 产生 core 文件。

gdb 调试 coredump 初步尝试

gdb打开 core 文件的格式为

gdb 程序名(包含路径) core*(core文件名和路径)

gdb 打开 core 文件时,有显示没有调试信息,因为之前编译的时候没有带上 -g 选项,没有调试信息是正常的,实际上它也不影响调试 core 文件。因为调试 core 文件时,符号信息都来自符号表,用不到调试信息。

查看 coredump 时的堆栈

查看堆栈使用 bt 或者 where 命令。

在带上调试信息的情况下,我们实际上是可以看到 core 的地方和代码行的匹配位置。

但往往正常发布环境是不会带上调试信息的,因为调试信息通常会占用比较大的存储空间,一般都会在编译的时候把 -g 选项去掉。

没有调试信息的情况下,打开 coredump 堆栈,并不会直接显示 core 的代码行。

此时只能通过 disassemble 命令打开该帧函数的反汇编代码进行分析。具体的分析方法后续再补充完善。

在实际问题中,C 程序的很多 coredump 问题都是和指针相关的,很多 segmentfault 都是由于指针被误删或者访问空指针、或者越界等造成的。

多进程调试

提示

在入职培训之初的 C 语言作业,不涉及多进程,下面的内容简单浏览,有个印象即可。后续正式开发工作中可能需要使用。

gdb调试多进程的命令:

set follow-fork-mode mode 设置调试器的模式

mode 参数可以是

parent: fork之后调试原进程,子进程不受影响,这是缺省的方式

child: fork之后调试新的进程,父进程不受影响。

show follow-fork-mode 显示当前调试器的模式

set detach-on-fork mode 设置gdb在fork之后是否detach进程中的其中一个,或者继续保留控制这两个进程

on 子进程(或者父进程,依赖于follow-fork-mode的值)会detach然后独立运行,这是缺省的mode

off 两个进程都受gdb控制,一个进程(子进程或父进程,依赖于follow-fork-mode)被调试,另外一个进程被挂起

info inferiors 显示所有进程

inferiors processid 切换进程

detach inferiors processid detach 一个由指定的进程,然后从fork 列表里删除。这个进程会被

允许继续独立运行。

kill inferiors processid 杀死一个由指定的进程,然后从fork 列表里删除。

catch fork 让程序在fork,vfork或者exec调用的时候中断

调试范例

范例1

编译并构建程序,加上调试选项 -g,若为 make 构建,修改 Makefile 的 CFLAGS。

1
gcc -g main.c -o test.out

用 GDB 来运行程序:

1
gdb test.out

在 main 函数入口处设置一个断点:

1
b main

运行程序:

1
r

不断按 s 单步运行到第N行:

1
s

查看 balance, rate, interest 变量的值:

1
2
3
p balance
p rate
p interest

范例2

假设要调试程序 、/bin/hello 的wlanAC.c 中的 wacAddWhitelist() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/******************************************************************************
* FUNCTION : wacAddWhitelist()
* DESCRIPTION : 添加一个站点设备MAC地址到白名单。
* INPUT :
* OUTPUT : N/A
* RETURN :
* HISTORY :
******************************************************************************/
LOCAL int wacAddWhitelist(WAC_WHITELIST_ITEM *item)
{
int res = ERROR;
UINT32 index = 0;

if (NULL == item || INVALID_MAC(item->staMac))
{
return res;
}

/* 添加一个条目到白名单列表里。*/
WACLOCK(wacWhitelistMutex);
if (gWacWhitelist.count < WAC_WHITELIST_SIZE)
{
/* 是否有重复添加的,没有则允许加入。*/
for (index = 0; index < gWacWhitelist.count; index++)
{
if (0 == memcmp(gWacWhitelist.list[index].staMac, item->staMac, WAC_MAC_SIZE))
{
WACUNLOCK(wacWhitelistMutex);
return res;
}
}

memcpy(&gWacWhitelist.list[gWacWhitelist.count], item, sizeof(WAC_WHITELIST_ITEM));
gWacWhitelist.count++;
res = OK;
}
WACUNLOCK(wacWhitelistMutex);

return res;
}

试图在变量 index 的值发生变化时触发断点,相关命令和输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
root@TDMP:~# gdb /bin/hello
GNU gdb (GDB) 8.3.1
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "arm-brcm-linux-gnueabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /bin/hello...

(gdb) set detach-on-fork off # 设置 gdb 在 fork 之后继续保留控制这两个进程

(gdb) r # 等效于 run 命令,开始运行
Starting program: /bin/hello
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[New inferior 2 (process 5488)]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[Inferior 1 (process 5485) exited normally]

(gdb) info inferiors # 显示所有进程
Num Description Executable
* 1 <null> /bin/hello
2 process 5488 /bin/hello

(gdb) inferior 2 # 切换到进程2
[Switching to inferior 2 [process 5488] (/bin/hello)]
[Switching to thread 2.1 (process 5488)]
Reading symbols from /bin/hello...
Reading symbols from /usr/lib/libjson.so.0...
Reading symbols from /lib/libpthread.so.0...
Reading symbols from /lib/libsecurity.so...
Reading symbols from /usr/lib/libcyassl.so.5...
Reading symbols from /usr/lib/libz.so.1...
Reading symbols from /lib/libbcm_flashutil.so...
Reading symbols from /lib/libbcm_boardctl.so...
Reading symbols from /lib/libbcm_util.so...
Reading symbols from /lib/libgen_util.so...
Reading symbols from /lib/libsys_util.so...
Reading symbols from /lib/libethswctl.so...
Reading symbols from /lib/libc.so.6...
Reading symbols from /lib/ld-linux.so.3...
Reading symbols from /lib/libm.so.6...
#0 0xf75ae2c8 in fork () from /lib/libc.so.6

(gdb) b wacAddWhitelist # 等效于 break wacAddWhitelist, 在 wacAddWhitelist 函数开头打断点
Breakpoint 1 at 0x2311c4: wacAddWhitelist. (2 locations)

(gdb) c
Continuing.
[New Thread 0xf73c3460 (LWP 22[ 487.008626] gpio 17 request failed.
14)]
[New Th[ 487.013850] gpio 10 request failed.
......
60 (LWP 2215)]
[New Thread 0xf63c3460 (LWP 2216)]
......
[New Thread 0xf5bc3460 (LWP 2217)]
......
Can not resume the parent process over vfork in the foreground while
holding the child stopped. Try "set detach-on-fork" or "set schedule-multiple".

(gdb) bt # 等效于 backtrace,打印栈回溯信息,可知在 vfork 处暂停
#0 0xf75ae560 in vfork () from /lib/libc.so.6
#1 0x0029ab14 in doSystemExec (cmdline=0x33bf84 "insmod slp_gpio.ko", sync=1)
at systemOps.c:83
#2 0x0029ad50 in systemExec (cmdline=0x33bf84 "insmod slp_gpio.ko")
at systemOps.c:142
#3 0x002917b4 in gpioInit () at gpio.c:215
#4 0x002db7c8 in __libc_csu_init ()
#5 0xf7528b08 in __libc_start_main () from /lib/libc.so.6
#6 0x0003f2e4 in _start ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

(gdb) set detach-on-fork on # 子进程(或者父进程,依赖于 follow-fork-mode 的值)会 detach 然后独立运行

(gdb) c # 继续运行直到在 wacAddWhitelist 入口处暂停
Continuing.
......
[Detaching after vfork from child process 4737]

Thread 2.1 "hello" hit Breakpoint 2, wacAddWhitelist (item=0xfffefad8)
at wlanAC.c:98
98 wlanAC.c: No such file or directory.

(gdb) bt ## 栈回溯显示确实是在 wacAddWhitelist 暂停
#0 wacAddWhitelist (item=0xfffefad8) at wlanAC.c:98
#1 0x00232740 in wacSyncWhitelistFromFlash () at wlanAC.c:631
#2 0x00231838 in wacEnableOperation () at wlanAC.c:302
#3 0x00233b34 in wacStart () at wlanAC.c:1034
#4 0x002431e4 in ctrlModuleStart () at control.c:672
#5 0x002436d0 in ctrlPowerOn () at control.c:895
#6 0x002da9b0 in main (argc=1, argv=0xfffefe54) at main.c:103

(gdb) watch index # 设置当 index 变量发生变化时暂停
Watchpoint 2: index

(gdb) c # 继续运行
Continuing.

Thread 2.1 "hello" hit Watchpoint 5: index

Old value = 4147980344
New value = 0
wacAddWhitelist (item=0xfffefad8) at wlanAC.c:101
101 in wlanAC.c

GDB 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
r   直接调到断点处,没有设置断点的话直接运行程序
b fun 设置一个断点breakpoint在函数”fun”的最开始
b N 在当前运行源文件的第N行设置断点
b file.c:N 在当前源文件file.c的第N行设置断点
d N 删掉delete第N行的断点
info break 显示所有断点信息
c 继续(continue)运行程序,一直到下一个断点或程序结束
f 运行直到当前函数(function)结束
s 按step调试1行,会进入函数体
s N 按step调试接下来的N行
n 调试1行,与按s命令不同的是此处不进入函数体
p var 输出(print)变量”var”的值
set var=val 设置变量”var”的值
bt 打印调用堆栈(stack trace)
q 退出gdb

官方文档

以上为常见的调试命令,若需要更高级的技巧,可以参阅 GDB 官方参考资料:https://sourceware.org/gdb/current/onlinedocs/gdb/

GDB 的调试原理

参见 GDB 的调试原理