gdb调试的一些技巧

函数

列出函数名

1
2
3
4
info functions

# 支持正则
info functions <regex>

进入函数

使用gdb调试遇到函数时,使用step命令(缩写为s)可以进入函数(函数必须有调试信息,没有调试信息可以执行set step-mode on
可以使用next命令(缩写为n)不进入函数,gdb会等函数执行完
当单步调试一个函数时,如果不想继续跟踪下去了,可以有两种方式退出:(1)finish,函数会执行完并打印返回值,然后等待输入(2)return,函数不会继续执行下面的语句,而是直接返回,也可以用return <expression>指定返回值

参考:gdb手册

直接执行函数

使用gdb调试程序时,可以使用“call”或“print”命令直接调用函数执行

参考:gdb手册

打印函数堆栈帧信息

1
2
3
4
5
6
7
8
9
10
i frame

i registers

# 查看func函数汇编代码
disassemble <func>

# 输出尾调用的相关信息(设置`debug entry-values`选项为非0值)
# 尾调用gdb(https://sourceware.org/gdb/onlinedocs/gdb/Tail-Call-Frames.html)
set debug entry-values 1

参考:gdb手册

断点

设置断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 设置函数断点
b Foo::foo
# 设置匿名空间断点
b (anonymous namespace)::bar

# 在程序地址上设置断点
# 当调试汇编程序,或者没有调试信息的程序时,经常需要在程序地址上打断点,方法为`b *address`
# 可使用`readelf -h a.out`或gdb中`info files`获得入口地址
b *0x400522

# 在行号上设置断点
b file.cc:123

# 设置临时断点
# tb a.c:15
tbreak a.c:15

# 设置条件断点
b aa.cc:10 if i == 101

注:几个有用的命令:file a.out;nm a.out;readelf -h a.out;strip a.out除去行号信息、重定位信息、符号表、调试段、typchk 段和注释段

忽略断点

在设置断点以后,可以忽略断点,命令是“ignore bnum count”:意思是接下来count次编号为bnum的断点触发都不会让程序中断,只有第count + 1次断点触发才会让程序中断

1
2
b aa.cc:15
ignore 1 5

保存设置断点

1
2
3
4
5
6
7
8
save breakpoints file-name-to-save

# 从文件恢复断点
source file-name-to-save

# 查看断点信息
# i b
info breakpoints

观察点

gdb可以使用watch命令设置观察点,也就是当一个变量值发生变化时,程序会停下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
watch a
watch *(data type*)address

# 查看观察点
info watchpoints

# 特定线程观察点
# gdb可以使用“watch expr thread threadnum”命令设置观察点只针对特定线程生效,也就是只有编号为threadnum的线程改变了变量的值,程序才会停下来,其它编号线程改变变量的值不会让程序停住
i threads
wa a thread 2

# gdb可以使用“rwatch”命令设置读观察点,也就是当发生读取变量行为时,程序就会暂停住;只对硬件观察点才生效
# 缩写是rw
rwatch a

# 读写观察点
# 所写是aw
awatch a

如果系统支持硬件观测的话,当设置观测点是会打印如下信息: Hardware watchpoint num: expr

如果不想用硬件观测点的话可如下设置: set can-use-hw-watchpoints

参考:gdb手册

Catchpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
catch fork
catch vfork
catch exec

# 只触发一次
tcatch fock

# 可以使用catch syscall [name | number]为关注的系统调用设置catchpoint
catch syscall mmap
# 可以使用系统调用的编号
# 系统调用和编号可以参考/usr/local/share/gdb/syscalls/adm64-linux.xml
catch syscall 9

# 为所有系统调用设置catchpoint
catch syscall

# 通过为ptrace调用设置catchpoint破解anti-debugging的程序
# 破解这类程序的办法就是为ptrace调用设置catchpoint,通过修改ptrace的返回值,达到目的
catch syscall ptrace
r
c
set $rax = 0
c

参考:gdb手册

打印

打印ASCII和宽字符串

1
2
3
4
5
6
7
8
# 用gdb调试程序时,可以使用“x/s”命令打印ASCII字符串
x/s str1

# 打印宽字符串
# 4字节
x/ws str2
# 2字节
x/hs str2

参考: gdb手册

打印数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 打印数组中任意连续元素的值,可以使用“p array[index]@num”命令(p是print命令的缩写)。其中index是数组索引(从0开始计数),num是连续多少个元素。
p array
p array[60]@10
# 打印从数组开头连续元素值
p *array@10

# 限制大数组的打印数,默认不设置最多显示200
set print elements number-of-elements
# 设置为0或unlimited为不限制
set print elements unlimited
p array

# 打印数组索引下标
set print array-indexes on

打印函数局部变量值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bt
# backtrace缩写是bt
backtrace full

# `bt full n`,意思是从内向外显示n个栈桢,及其局部变量
bt full 2

# `bt full -n`,意思是从外向内显示n个栈桢,及其局部变量
bt full -2

# 打印当前函数局部变量值
info locals

# 查看变量类型
whatis val

# 查看变量详细类型
ptype val

# 查看该变量的文件
i variables val
i variables ^val$

参考: gdb手册

打印进程内存信息

1
2
3
4
5
# 查看进程的内存映射信息,可以使用“i proc mappings”命令(i是info命令缩写)
i proc mappings
# 可以更详细地输出进程的内存信息
i files
i target

参考: gdb手册

打印内存值

例子

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

int main(void) {
int i = 0;
char a[100];

for (i = 0; i < sizeof(a); i++)
{
a[i] = i;
}

return 0;
}

gdb中使用“x”命令来打印内存的值,格式为“x/nfu addr”。含义为以f格式打印从addr开始的n个长度单元为u的内存值。参数具体含义如下:
a)n:输出单元的个数。
b)f:是输出格式。比如x是以16进制形式输出,o是以8进制形式输出,等等。
c)u:标明一个单元的长度。b是一个byteh是两个byte(halfword),w是四个byte(word),g是八个byte(giant word)。

以上面程序为例:
(1) 以16进制格式打印数组前a16个byte的值:

1
2
3
(gdb) x/16xb a
0x7fffffffe4a0: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x7fffffffe4a8: 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f

(2) 以无符号10进制格式打印数组a前16个byte的值:

1
2
3
(gdb) x/16ub a
0x7fffffffe4a0: 0 1 2 3 4 5 6 7
0x7fffffffe4a8: 8 9 10 11 12 13 14 15

(3) 以2进制格式打印数组前16个abyte的值:

1
2
3
(gdb) x/16tb a
0x7fffffffe4a0: 00000000 00000001 00000010 00000011 00000100 00000101 00000110 00000111
0x7fffffffe4a8: 00001000 00001001 00001010 00001011 00001100 00001101 00001110 00001111

(4) 以16进制格式打印数组a前16个word(4个byte)的值:

1
2
3
4
5
(gdb) x/16xw a
0x7fffffffe4a0: 0x03020100 0x07060504 0x0b0a0908 0x0f0e0d0c
0x7fffffffe4b0: 0x13121110 0x17161514 0x1b1a1918 0x1f1e1d1c
0x7fffffffe4c0: 0x23222120 0x27262524 0x2b2a2928 0x2f2e2d2c
0x7fffffffe4d0: 0x33323130 0x37363534 0x3b3a3938 0x3f3e3d3c

参考: gdb手册.

打印源代码行号

1
2
3
4
5
6
list 25
l 24
l main
l-
l+
l 1,10

其他打印技巧

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
# pretty打印
set print pretty on

# 按照派生类型打印
set print object on

# "x"命令会把最后检查的内存地址值存在“$_”这个“convenience variable”中,并且会把这个地址中的内容放在“$__”这个“convenience variable”
# https://sourceware.org/gdb/onlinedocs/gdb/Convenience-Vars.html
p $_
p $__

# 打印程序动态分配内存信息
define mallocinfo
set $__f = fopen("/dev/tty", "w")
call malloc_info(0, $__f)
call fclose($__f)
end
mallocinfo

# 打印函数中变量,c++函数名需要使用单引号括起来
p '(anonymous namespace)::SSAA::handleStore'::n->pi->inst->dump()

# 打印寄存器值
i registers
i all-gegisters
i register eax
p $eax

改变程序执行

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
# 改变字符串的值
# https://stackoverflow.com/questions/19503057/in-gdb-how-can-i-write-a-string-to-memory
set main::p1="Bill"
p &p1
set {char[4]} 0x80477a4 = "Ace"

# 设置变量的值,set var variable=expr
set var i = 8
# set {type}address=expr,通过地址给变量赋值
p &i
set {int}0x8047a54 = 8

# 修改PC寄存器的值
disassemble main
info line 6
info line 7
p $pc
set var $pc=0x08050949

# 跳转到制定位置执行
# https://sourceware.org/gdb/onlinedocs/gdb/Jumping.html#Jumping
j 15

# 使用断点命令改变程序的执行
# https://sourceware.org/gdb/onlinedocs/gdb/Break-Commands.html#Break-Commands
b fun
command 1
>silent
>set variable n = 0
>continue
>end
r

# 修改被调试程序的二进制文件
# https://sourceware.org/gdb/onlinedocs/gdb/Patching.html#Patching
gcc-write ./a.out # 命令后选项
set write on
file ./a.out
disassemble /mr fun
set variable *(short*)0x400651=0x0ceb
disassemble /mr fun

信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 查看gdb如何处理信号
i signals
i handle

# 可以用“handle signal stop/nostop”命令设置当信号发生时,是否暂停程序的执行
handle SIGHUP nostop
# handle signal print/noprint设置是否打印信息
handle SIGHUP noprint

# handle signal pass(noignore)/nopass(ignore) 设置信号是否丢给程序处理
handle SIGHUP nopass

# 直接发送信号给调试的程序
signal SIGHUP

# 使程序重新运行,但不发送任何信号
signal 0

# 读信号信息
ptype $_siginfo
ptype $_siginfo._sifields._sigfault
p $_siginfo._sifields._sigfault.si_addr

参考: gdb手册-signals

gdb配置文件

当gdb启动时,会读取HOME目录和当前目录下的的配置文件,执行里面的命令。这个文件通常为“.gdbinit”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# gdb配置文件`.gdbinit`
# 打印STL容器中的内容
python
import sys
sys.path.insert(0, "/home/xmj/project/gcc-trunk/libstdc++-v3/python")
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end

# 保存历史命令
set history filename ~/.gdb_history
set history save on

# 退出时不显示提示信息
set confirm off

# 按照派生类型打印对象
set print object on

# 打印数组的索引下标
set print array-indexes on

# 每行打印一个结构体成员
set print pretty on

其他gdb技巧

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
116
117
118
119
120
121
# 调试已经运行的进程
gdb program -p=10210
gdb program processID
gdb program --pid=10210

# attach已经运行的进程
attach 10210
detash

# 调试多进程程序时,gdb默认会追踪父进程,以下命令可是其追踪子进程
set follow-fork-mode child

# 调试多进程程序,同时调试父进程和子进程
set detach-on-fork off
i inferior
inferior 2

# 调试多进程程序,让父进程和子进程同时运行
set schedule-multiple on

# 查看线程信息
i threads

# 打印所有线程堆栈信息
# https://sourceware.org/gdb/onlinedocs/gdb/Threads.html
thread apply all bt

# 不显示线程启动和退出信息
set print thread-events off

# 用gdb调试多线程程序时,一旦程序断住,所有的线程都处于暂停状态。此时当你调试其中一个线程时(比如执行“step”,“next”命令),所有的线程都会同时执行
# 如果想在调试一个线程时,让其它线程暂停执行,可以使用“set scheduler-locking on”命令
set scheduler-locking on

# `$_thread`变量表示线程号
# https://sourceware.org/gdb/onlinedocs/gdb/Threads.html
wa a
command 2
printf "thread id=%d\n", $_thread
end
c

# `$_exitcode`程序退出时的exit code
p $_exitcode

# 为调试进程产生core dump文件
# https://sourceware.org/gdb/onlinedocs/gdb/Core-File-Generation.html
generate-core-file
gcore

# 调试core文件,也可gdb启动后用`file`命令来读取可执行文件的符号表信息,而`core`命令制定core dump文件的位置
gdb path/to/the/executable path/to/the/coredump
# 其他调试用到的命令
readelf -S mainO3 # 查看段
objcopy --only-keep-debug mainO3 mainO3.symbol # 拷贝出一个符号表文件
objcopy --strip-debug mainO3 mainO3.bin # 拷贝出一个执行文件

# on为反汇编后面要执行的代码,off关闭
set disassemble-next-line on
# auto为后面的代码没有源码的情况下才反汇编
set disassemble-next-line auto
si

# 将源程序和汇编指令映射起来,disas为disassemble命令缩写
# https://sourceware.org/gdb/onlinedocs/gdb/Machine-Code.html
disas /m fun
# 只想看某一行对应的地址范围
i line 13
# 只看这一条语句对应的汇编代码
disassemble 0x4004e9, 0x40050c

# 显示将要执行的汇编指令
# https://sourceware.org/gdb/onlinedocs/gdb/Auto-Display.html
display /i $pc
display /3i $pc
undisplay

# 显示程序原始机器码
# https://sourceware.org/gdb/onlinedocs/gdb/Machine-Code.html
disassemble /r main
disassemble /r 0x0000000000400534,+4

# 显示共享链接库信息`info sharedlibrary regex`,gerex为正则或空
# https://sourceware.org/gdb/current/onlinedocs/gdb/Files.html#index-shared-libraries
info sharedlibrary
info sharedlibrary hiredi*

# 设置源文件查找路径
directory ../ki/
gdb -q a.out -d /search/code/some

# 图形化调试
# https://sourceware.org/gdb/onlinedocs/gdb/TUI-Commands.html
# 输入以下命令启动,或gdb中使用"crtl+X+A",退出也是此快捷键
# layout asm显示汇编代码窗口
# layout regs显示寄存器窗口
gdb -tui program
tui reg float
tui reg system
tui reg general

# 使用`gcc -g`编译的程序不包含预处理宏信息,可使用`gcc -g3`编译
# https://sourceware.org/gdb/onlinedocs/gdb/Macros.html#Macros
p NAME
# gcc debug相关:https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html#Debugging-Options

# gdb启动参数
# https://sourceware.org/gdb/onlinedocs/gdb/Arguments.html#Arguments
gdb -args ./a.out a b c
set args a b c
r a b c
set args

# 设置被调试程序的环境变量
set env LD_PRELOAD=/lib/x86_64-linux-gnu/libpthread.so.0

# 记录执行gdb过程
# https://sourceware.org/gdb/onlinedocs/gdb/Logging-Output.html
# 用gdb调试程序时,可以使用“set logging on”命令把执行gdb的过程记录下来,方便以后自己参考或是别人帮忙分析。默认的日志文件是“gdb.txt”,也可以用“set logging file file”改成别的名字
# set logging overwrite on可以让输出覆盖之前的日志文件
# set logging redirect on让gdb的日志不会打印在终端

参考

https://github.com/hellogcc/100-gdb-tips/blob/master/src/index.md