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 默认为1

  • down <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. 调试过程执行函数

  • 可以使用 callprint 命令直接调用函数执行,不过其返回值不会写入 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