gdb笔记
GDB 笔记
1. 启动 GDB
gdb <program>
:programe 即可执行文件,一般在当前目录下gdb <program> core
:用 gdb 同时调试一个运行程序和 core 文件,core 是程序非法执行后 core dump 后产生的文件,这个文件名也可以自己规定(见后文)gdb <program> <PID>
:让 gdb 调试器附着到一个正在运行的进程上,program 应该在 PATH 环境变量中搜索得到# 例如 a.out 为一个正在运行的程序, 其 PID 为 15344 # 则可以让 gdb 附着在其上 (gdb) gdb a.out 15344 # 如果不想继续调试了, 可以使用 detach 命令脱离进程 (gdb) detach
2. 暂停机制
- 断点:通知 GDB 在程序中的特定位置暂停执行
- 监视点:通知 GDB 当特定内存位置(或者涉及一个或多个位置的表达式)的值发生变化时暂停执行
- 捕获点:通知 GDB 当特定事件发生时暂停执行
2.1 设置断点
break function
(gdb) break main
break line_number
# 指定当前活动源文件的行号 (gdb) break 20
break filename:line_number
(gdb) break src/test.c:30
break filename:function
(gdb) break test.c:hellofunc
break namespace::func
# 对指定命名空间的函数设置断点 (gdb) break Foo::foo # 对匿名空间的函数设置断点 (gdb) break (anonymous namespace)::bar
当设置一个断点时,该断点的有效性会持续到删除、禁用或退出 GDB 时,这样可以方便我们更改程序后重新调试时不用再重复打断点,临时断点在首次到达后消失,命令为 tbreak
,其使用方式同 break
2.2 查看断点信息
info breakpoints
(gdb) info b (output) Num Type Disp Enb Address What 1 breakpoint keep y 0x25362345 in main at test.c:10 # 1. 标识符(Num):断点的唯一标识符 # 2. 类型(Type):指出该断点是断点(breakpoint)、监视点(watchpoint)还是捕获点(catchpoint) # 3. 部署(Disp):指示断点下次引起 GDB 暂停程序的执行后该断点上会发生什么事, 有三种可能 # 保持(keep):下次到达断点后不改变断点(默认) # 删除(del) :下次到达断点后删除该断点(tbreak) # 禁用(dis) :下次到达断点后禁用该断点(enable once) # 4. 启用状态(Enb):说明断点当前是启用还是禁用的 # 5. 地址(Address):这是内存中设置断点的位置 # 6. 位置(What):显示了断点所在位置的行号和文件名
GDB 会给断点赋予一个唯一的编号来标识断点(Num)
2.3 删除和禁用断点
删除断点表示之后都不会用这个断点了,禁用表示暂时另断点不作用,之后可以重新启用
GDB 删除断点有两个命令,delete
命令用来基于标识符删除断点,clear
命令使用与创建断点相同的语法删除断点
delete breakpoint_list
# 使用系统赋予的标识符删除, 可同时删除多个断点(2, 3) (gdb) delete 2 3
delete
# 删除所有断点 (gdb) delete
clear
# 清除 GDB 将执行的下一个指令处的断点 (gdb) clear
clear function、clear filename:function、clear line_number、clear filename:line_number
# 根据位置清除断定, 与 break 命令工作方式相似
如果要保留断点以便以后使用,同时又不希望 GDB 停止执行,可以禁用它们,在以后需要时再启用
disable breakpoint-list
(gdb) disable 3
enable breakpoint-list
(gdb) enable 3
disable
# 禁用所有现有断点 (gdb) disable
enable
# 启用所有现有断点 (gdb) enable
enable once breakpoint-list
# 与 tbreak 相似, 不过不是删除断点而是禁用断点 (gdb) enable once 3
2.4 条件断点
有时有必要告诉调试器只有当符合某种条件时才在断点处停止,比如当变量具有我们感兴趣的某个特定的值时
设置条件断点:
break break-args if (condition)
# 其中 break-args 是可以传递给 break 以指定断点位置的任何参数 # condition 的圆括号是可选的 (gdb) break main if argc > 1 (gdb) break if (i == 100) # 相等、逻辑和不相等运算符(<、<=、==、!=、>、>=、&&、||等) (gdb) break 12 if string==NULL && i < 0 # 按位和移位运算符 (&、|、^、>>、<<等) (gdb) break test.c:32 if (x & y) == 1 # 算术运算符(+、-、x、/、%) (gdb) break myfunc if i % (j + 3) != 0 # 你自己的函数, 只要它们被链接到程序中 break test.c:myfunc if !check(x) # 库函数, 只要其被链接到代码中 break 44 if strlen(str) == 0 # 如果没有调试信息, GDB 会假设函数的返回值是 int
为正常断点设置条件使其成为条件断点:
condition break-list (condition)
# 简写为 cond (gdb) cond 3 i == 3
删除断点的条件,使其成为普通断点:
condition break-list
(gdb) cond 3
2.5 断点命令列表
到达指定断点后执行一系列命令:
#(gdb) commands breakpoint-number #... #commands #... #end (gdb) commands 1 >silent # 可以使 GDB 更安静地触发断点 >printf "test %d.\n", v >end (gdb) # 如果命令列表中的最后一个命令是 continue, GDB 将在完成命令列表 # 中的命令后自动执行程序
定义宏
(gdb) define print_and_go >printf $arg0, $arg1 >continue >end # 使用宏 commands 1 >silent >print_and_go "test %d.\n", v >end
列出所有宏:
show user
2.6 保存设置的断点
可以将断点信息保存到一个文本文件中,下次重新调试时可以根据断点文件快速设置断点
(gdb) save breakpoints file_name
# 重新调试,根据断点文件设置断点
(gdb) source file_name
2.7 忽略断点
# 意思是接下来count次编号为bnum的断点触发都不会让程序中断
# 只有第count + 1次断点触发才会让程序中断
(gdb)ignore bnum count
2.8 监视点
监视点是一种特殊类型的断点,它类似于正常断点,是要求 GDB 暂停程序执行的指令,区别在于监视点没有“住在”某一行源代码中,而是指示 GDB 每当某个表达式改变了值就暂停执行,可以将监视点看做被“附加”在表达式上,当表达式的值改变时 ,GDB 会暂停程序的执行
监视点变量:
watch var
# GDB 实际上是在 var 的内存位置改变值时终端 (gdb) watch val
监视表达式:
watch expr
# 当表达式的值变化时会暂停程序执行 (gdb) watch val > 10
设置读监视点:
rwatch var
# 当发生读取变量行为时, 程序就会暂停 (gdb) rw val
设置读写监视点:
awatch var
# 当发生读取变量或改变变量值的行为时程序暂停 (gdb) aw val
2.9 捕获点
捕获点单次触发:
tcatch func
(gdb) tcatch fork
为特定函数调用设置
catchpoint
# 还可以 catch vfork, exec, syscall [name | number] (gdb) catch fork (gdb) catch syscall (gdb) catch syscall mmap
3. 恢复执行
单步进入(step into):
step
# 简写为 s, step 会进入函数内部, 如果没有函数的调试信息则不会进入 # 可以在 step 后面加上行数, 相当于多次执行 step 命令 (gdb) s 10
单步越过(step out):
next
# 简写为 n, next 不会进入函数内部, 而是直接跳过并得到返回结果 (gdb) n 10
恢复程序执行:
continue
# continue 会使程序继续运行直到遇到下一个断点后停止 # 简写为 c, 接受一个可选的整数参数 n, 要求 GDB 忽略下面 n 个断点 (gdb) c (gdb) c 10
恢复程序执行:
finish
# finish(简写为 fin)指示 GDB 恢复执行,直到恰好在当前栈帧完成之后位置 # finish 的一个常见用途是当不小心单步进入原本希望单步越过的函数时,使用 finish 可以将你 # 正好放回到使用 next 会到的位置(不能直接退出 main 函数), 如果在一个递归函数中 # finish 只会将你带到递归的上一层, 如果要在递归层次较高时完全退出递归函数,可以通过 # 临时断点及continue,或者用 until 命令 (gdb) fin
恢复程序执行:
until
# until(简写为u)通常用来在不进一步在循环中暂停的情况下完成正在执行的循环 # until 命令也可以接受源代码中的位置作为参数, 可以使程序运行到指定位置 (gdb) u (gdb) u 17 (gdb) u swap (gdb) u test.c:18 (gdb) u test.c:swap
不执行完函数直接返回:
return
# 例如函数 int add(int, int); # 在进入函数后可以执行 return 直接返回结果 (gdb) return 10
4. 检查和设置变量
打印变量的值:
print var
# 打印普通变量 (gdb) print v # 打印字符串指针地址及内容 (gdb) print str # 打印字符串的第一个字符 (gdb) print *str (gdb) print str[0] # 打印带命名空间的变量 (gdb) print dev::val
打印 STL 容器的内容:https://sourceware.org/gdb/wiki/STLSupport
打印表达式的值:
print expr
# 注意变量需要在可以访问到的作用域 (gdb) print val < 2
按不同进制输出:
print /f var
- x 按十六进制格式显示变量 - d 按十进制格式显示变量 - u 按十六进制格式显示无符号整型 - o 按八进制格式显示变量 - t 按二进制格式显示变量 - a 按十六进制格式显示变量 - c 按字符格式显示变量 - 按浮点数格式显示变量 # 按十六进制显示 (gdb) print /x digit # 按二进制打印变量地址 (gdb) print /t &val
打印大数组的内容
# 对于大数组缺省最多会显示 200 个元素 # 可以设置如下命令设置这个最大限制数 (gdb) set print elements number-of-elements # 元素为 0 表示没有限制 (gdb) set print elements 0 或 (gdb) set print elements unlimited
打印数组中任意连续元素的值:
print array[index]@num
# index 从 0 开始, num 是连续多少个元素 # 如打印 arr[9~18] 这连续10个元素 (gdb) print arr[9]@10
打印动态数组:
print *array@len
# print 简写为 p (gdb) p *arr@10 # 打印动态数组中的某个元素 (gdb) p arr[3] # 打印静态数组 (gdb) p arr # 打印数组时确认不打印索引下标, 可以通过如下命令设置(输出的索引是结果集从0开始) (gdb) set print array-indexes on
查看变量的类型
(gdb) whatis vec_int type = std::vector<int, std::allocator<int> >
浏览类或结构体的结构:
ptype var
(gdb) ptype node
监视局部变量:
info locals
# 列出当前栈帧中所有局部变量的值 (gdb) info locals
检查当前函数参数的值:
info args
(gdb) info args
每次暂停都打印变量:
display var
# display(简写disp)会在每次暂停时打印变量, 前提是在作用域内 (gdb) disp x # 查看所有显示项 (gdb) info disp # 禁用某个显示项 (gdb) dis disp 1 # 启用某个显示项 (gdb) enable disp 1 # 完全删除某个显示项 (gdb) undisp 1
查看内存:
examine addr
# examine(简写为x)来查看内存地址中的值 # x /n、f、u 是可选的参数 # n 是一个正整数,表示显示内存的长度,也就说从当前地址向后显示几个地址的内容 # f 表示显示的格式, 跟 print 的格式参数相同 # u 表示从当前地址往后请求的字节数,如果不指定的话,GDB 默认是 4 bytes, u参数可以用 # 下面的字符来代替, b 表示单字节, h 表示双字节, w 表示四字节, g 表示八字节 # 表示从内存地址 0x54320 读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示 (gdb) x /3uh 0x54320 # 根据字符串首地址打印其所有字符 const char* str = "hello world"; (gdb) call strlen(str) # 调用函数取得字符串长度 (gdb) x /11cb str 0x555555556005: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 119 'w' 111 'o' 0x55555555600d: 114 'r' 108 'l' 100 'd' # 按二进制格式打印6个单元, 每个单元占2个字节, 注意里考虑的字节序 (gdb) x /6th str 0x555555556005: 0110010101101000 0110110001101100 0010000001101111 0110111101110111 0110110001110010 0000000001100100
设置变量:
set var = val
# 可以改变正在运行的程序中变量的值 (gdb) set x = 10 # 但是如果要改变函数入参变量的值, 需要加上 var (gdb) set var x = 20
设置命令行参数:
set args v1 v2 v3 v4
# 在 run 之前设置 (gdb) set args 1 2 3 4 # 在 run 的同时设置 (gdb) run 1 2 3 4
设置寄存器的值,如
eax
寄存器用于保存函数的返回值(gdb) set $eax = 10
打印函数堆栈帧信息:
info frame
(gdb) info frame
打印寄存器信息:
info registers
(gdb) info registers
打印函数汇编指令:
disassemble <function_symbol>
(gdb) disasemble add
5. 处理程序崩溃
当程序因段错误崩溃时,系统会编写一个名为核心文件(core file)的文件,俗称转储核心。核心文件包含程序崩溃时对程序状态的详细描述:栈的内容(或者,如果程序是多线程的,则是各个线程的栈),CPU 寄存器的内存(同样,如果程序是多线程的,则是每个线程上的一组寄存器值),程序的静态分配变量的值(全局与 static 变量)等等。
很多情况下,调试过程都不涉及核心文件,如果程序发生了段错误,程序员只要打开调试器并在此运行程序就可以重建该错误,因此又由于核心文件比较大,所以大多数现代 shell 都会在一开始防止编写核心文件,故需要使用 ulimit
命令来控制核心文件的创建
ulimit -c n
:其中 n 是核心文件的最大大小,以千字节为单位,超过 nKB 的核心文件都不会被编写ulimit -c unlimited
:允许任意大小的核心文件
若未设置过 core 文件生成路径和名称,默认生成在可执行文件运行命令的统一路径下,名为 core,新的 core 文件生成将覆盖原来的 core 文件,因此有时我们需要设置 core 文件的名称格式。这可以通过编辑 /proc/sys/kernel/core_pattern
文件来设置,若没有设置绝对路径,则默认产生在可执行文件运行的路径下,若设置了绝对路径,则需要保证文件夹都已经被创建了,否则 core 文件无法生成到指定位置,常用命令为 echo "core-%e-%p-%t.core" > core_pattern
,可以配置如下选项:
%p
:进程 id%u
:用户 id%g
:用户组 id%s
:导致产生 core 的信号%t
:core 文件生成时的 UNIX 时间%h
:主机名%e
:导致产生 core 的命令名
5.1 查看栈信息
当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的,每当调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入栈中,可以用 backtrack
命令来查看当前的栈中的信息
backtrace
:简写 bt ,打印完整的调用堆栈backtrace <n>
:n是一个正整数,表示只打印栈顶上 n 层的栈信息backtrace <-n>
:-n 表示一个负整数,表示只打印栈底下 n 层的栈信息
如果要查看某一层的信息,需要切换当前的栈,调用 bt
命令查看调用栈时,最上层的栈就是当前栈,编号为0,往下依次递增,可以通过 frame
命令切换到其它栈
frame <n>
:简写为 f, n 表示栈帧编号(gdb) f 3 # 切换到编号为 3 的栈帧
up <n>
:表示向栈的上面移动 n 层, 并打印栈详细信息, n 默认为1down <n>
:表示向栈的下面移动 n 层,并打印栈详细信息,n 默认为1
# 如果不想打印栈信息, 可以通过如下命令
(gdb) select-frame <n> # 对应 frame
(gdb) up-silently <n> # 对应 up
(gdb) down-silently <n> # 对应 down
5.2 使用 core 文件调试程序
通过 core 文件可以复现可能需要很长一段时间才会发生的程序错误,有些程序的行为取决于随机的环境事件,比如客户现场出现了崩溃问题,但是难以复现,因此只能通过核心文件来找到当时出现问题的地方
# 通过 core 启动 gdb
(gdb) gdb a.out core
# 如果启动后出现??,说明没有动态库调试信息, 可以在 run 之前设置动态库搜索路径
# 多个路径之间用 ':' 分隔
(gdb) set solib-absolute-prefix /lib:/usr/lib # 库的绝对路径前缀
(gdb) set solib-search-path ../lib:/usr/lib # 设置库的搜索路径, 可以是相对路径
# 例子
(gdb) gdb a.out core
# 如果发现出现问题地方的函数名为 ??, 用 info sharedlibrary 命令查看缺少的动态库
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
No /root/gdb_learn/build/libsvc_add.so
0x00007f60ed5fe160 0x00007f60ed6e6452 Yes (*) /lib/x86_64-linux-gnu/libstdc++.so.6
0x00007f60ed390630 0x00007f60ed50527d Yes /lib/x86_64-linux-gnu/libc.so.6
0x00007f60ed22c3c0 0x00007f60ed2d2fa8 Yes /lib/x86_64-linux-gnu/libm.so.6
0x00007f60ed756100 0x00007f60ed778684 Yes /lib64/ld-linux-x86-64.so.2
0x00007f60ed2075e0 0x00007f60ed218045 Yes (*) /lib/x86_64-linux-gnu/libgcc_s.so.1
(*): Shared library is missing debugging information.
# 这里在 a.out 运行的目录下缺少 libsvc_add.so 库
(gdb) set solib-search-path ../lib # 这里设置动态库的路径
# 重新加载 core 文件
(gdb) core-file core
# 查看调用堆栈 backtrace
(gdb) bt
# 在栈帧间切换并打印栈中的变量值进行分析
(gdb) f 3
(gdb) print x
6. 线程相关命令
info threads
:给出关于当前所有线程的信息thread apply all bt
:打印所有线程的调用堆栈thread id
:切换当前调试所在线程, id 可以通过info threads
看到break line-num thread id
:当指定 id 的线程到达源代码 line-num 行时停止执行break line-num thread id if condition
:当到达指定位置并满足条件时停止(gdb) break 32 thread 2 if x == y
watch expr thread thread id
:设置观察点只针对特定线程生效
7. 调试过程执行函数
可以使用
call
或print
命令直接调用函数执行,不过其返回值不会写入eax
寄存器(gdb) call add(2, 3) $1 = 5 (gdb) print sizeof(int) $2 = 4
8. 共享库相关
列出所有加载的共享链接库信息:
info sharedlibrary
# 如果程序里用到了运行时加载动态库, 可以通过此命令观察加载前后的动态库列表 (gdb) i sharedlibrary #后面可以跟正则表达式匹配
设置动态库搜索路径, 多个动态库路径用
:
隔开# 设置绝对搜索路径 (gdb) set solib-absolute-prefix /xxx/lib # 设置搜索路径(绝对或相对) (gdb) set solib-search-path ../xxx/lib:/xxx/lib