Gdb调试方法总结

 
Category: Debug

写在前面

Ubuntu 20.04 x86_64 gdb: 9.2

总结一下 C/C++ 代码调试的艺术, 这本书讲了 gdb 和 vc 的调试方法, 虽然有一些小错误, 但是不影响看, 突击面试确实是很方便的.

啃文档的话可以, 不过时间来不及了, 虽然技术是要慢慢沉淀的…

当然我还看了 软件调试的艺术 这本书, 中外作者的文笔还是有所不同的..

gdb 主要功能

总览:

支持的功能 描述 命令
断点管理 设置断点, 查看断点, 条件断点 b(break), condition
调试执行 逐语句, 逐过程执行 r(run), n(next),
s(step), c(continue)
查看数据 查看变量数据, 内存数据 p(print), bt, i(info)
运行时修改变量值 调试状态下修改变量的值  
显示源代码 查看对应的源码 l(list)
搜索源代码 查找源码 search
调用堆栈管理 堆栈信息 f(frame)
线程管理 多线程调试, 查看, 线程间跳转 thread, i(info)
进程管理 调试多个进程  
核心转储文件分析 分析 core dumped 文件  
调试启动方式 不同方式调试进程(加载参数启动, 附加到进程, 通过 PID) -p

其他常见命令

  • q: quit 退出
  • set args : 设置命令行参数(传入程序的命令行参数)
  • gdb attach <pid>: 附加到进程(通过 ps aux | grep <exe_file> 来查看PID)

杂项

通用的 Makefile

EXECUTABLE:= main
LIBDIR:=
LIBS:=pthread
INCLUDES:=.
SRCDIR:=

CC:=g++
CFLAGS:= -g -Wall -O0 -static -static-libgcc -static-libstdc++ 
CPPFLAGS:= $(CFLAGS)
CPPFLAGS+= $(addprefix -I,$(INCLUDES))
CPPFLAGS+= -I.
CPPFLAGS+= -MMD

RM-F:= rm -f

SRCS:= $(wildcard *.cpp) $(wildcard $(addsuffix /*.cpp, $(SRCDIR)))
OBJS:= $(patsubst %.cpp,%.o,$(SRCS))
DEPS:= $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS:= $(filter-out $(wildcard $(DEPS)),$(DEPS))
#MISSING_DEPS_SOURCES:= $(wildcard $(patsubst %.d,%.cpp,$(MISSING_DEPS)))


.PHONY : all deps objs clean
all:$(EXECUTABLE)
deps:$(DEPS)

objs:$(OBJS)
clean:
	@$(RM-F) *.o
	@$(RM-F) *.d

ifneq ($(MISSING_DEPS),)
$(MISSING_DEPS):
	@$(RM-F) $(patsubst %.d,%.o,$@)
endif
-include $(DEPS)
$(EXECUTABLE) : $(OBJS)
	$(CC) -o $(EXECUTABLE) $(OBJS) $(addprefix -L,$(LIBDIR)) $(addprefix -l,$(LIBS))

人生苦短, 我用 cmake

窗口管理

layout src
layout asm
layout split
layout regs

python 集成

直接用python 或者py即可使用 python 命令.

一个简单的例子如下:

(gdb) py print("this is python")
this is python

当然主要是用来使用gdb库的.

shell 集成

shell 命令

快捷键

  • C-x a: 切换 TUI 模式(Text User Interface), 展示源码
  • C-n/C-p: 下一个/上一个命令
  • C-L: redraw 窗口, 刷新显示
  • C-x 1:
  • C-x 2:

程序运行管理

attach到进程

ps aux | grep a.out

gdb attach -p <PID>

运行

启动程序

r
run

继续运行

命中断点之后继续运行

c
cont
continue

继续运行并跳过断点 N 次

c N
continue N

继续运行直到当前函数结束(直接到函数调用位置)

fin
finish

单步执行

s
step

逐过程执行(跳过函数)

n
next

逐指令执行

// 从第一条指令开始(可以看到被 strip 的程序的 entry point 信息, 比较有用的一条命令
starti
// 单指令执行  Step one instruction exactly.
si 
stepi

跳转执行

jump 位置

位置: 代码行或者函数地址

一定要让 jump 之后程序执行仍有意义(正确执行), 就像 C 的 goto 一样, 最好不要轻易使用.

assert 宏

定义了NDEBUG之后, assert 不会中断程序, 用于调试

查看/修改信息

查看源码

l
list
layout src

还可以指定函数名

l main

设置一下每次显示代码的行数:

(gdb) set listsize 1
(gdb) l main
5	int main(void) {

搜索源码

search 正则表达式
forward-search 正则表达式
reverse-search 正则表达式

查看函数参数

i args
info args

查看变量的值

p variable_name
print variable_name

查看内存大小

p sizeof (int)
p sizeof (struct sockaddr)

查看数组

(gdb) l
1	int main(int argc, char *argv[]) {
2	    int a[10] = {0};
3
4	    return 0;
5	}
(gdb) p a
$2 = {-134536472, 32767, 1431654832, 21845, 0, 0, 1431654496, 21845,
  -8496, 32767}
(gdb) set print array on // 优化打印
(gdb) p a
$3 =   {-134536472,
  32767,
  1431654832,
  21845,
  0,
  0,
  1431654496,
  21845,
  -8496,
  32767}
(gdb) n
2	    int a[10] = {0};
(gdb) n
4	    return 0;
(gdb) p a
$5 =   {0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0}

查看变量类型

ptype 可选参数 变量或类型

可选参数:

  • /r: raw 原始数据显示, 不替换 typedef
  • /m: member 不显示类的方法, 仅显示成员变量
  • /M: 显示类的方法
  • /t: typedef 不打印类中的 typedef 数据
  • /o: offset 打印结构体字段偏移量和大小
whatis 变量或者表达式

信息较为简略

查看结构体

例如下面这个经典的二叉树节点结构体:

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right)
        : val(x), left(left), right(right) {}
};

int main(int argc, char *argv[]) {
    TreeNode t1;
    auto t2 = new TreeNode(1);
    return 0;
}

编译一波:

g++ tree.cpp -g
gdb a.out

然后就可以直接打印节点信息:

(gdb) start
Temporary breakpoint 1 at 0x555555555149: file tree.cpp, line 12.
Starting program: /home/zorch/code/book_debug/chapter_3.3/a.out

Temporary breakpoint 1, main (argc=21845, argv=0x7ffff7fb22e8 <__exit_funcs_lock>) at tree.cpp:12
12	int main(int argc, char *argv[]) {
(gdb) p t1
$1 = {val = 0, left = 0x555555555060 <_start>, right = 0x7fffffffded0}
(gdb) set print pretty // 格式更漂亮
(gdb) p t1
$2 = {
  val = 0,
  left = 0x555555555060 <_start>,
  right = 0x7fffffffded0
}

指针变量就用: p *pTreeNode来打印.

Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string) and z(hex, zero padded on the left). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).

自动显示变量值

display 

例子:

(gdb) l
1	int t1(int i) {
2	    if (i == 1) return 1;
3	    return i + t1(i - 1);
4	}
5	int main(void) {
6	    int ans = t1(10);
7	    return 0;
8	}

(gdb) start // main 函数处命中临时断点
Temporary breakpoint 1 at 0x1159: file x.c, line 5.
Starting program: /home/zorch/code/book_debug/chapter_3.3/a.out

Temporary breakpoint 1, main () at x.c:5
5	int main(void) {
    
(gdb) b 2 // 第二行设断点
Breakpoint 2 at 0x555555555138: file x.c, line 2.

(gdb) c // 往下走
Continuing.

Breakpoint 2, t1 (i=10) at x.c:2
2	    if (i == 1) return 1;

(gdb) display i // 设置自动变量
1: i = 10
(gdb) n
3	    return i + t1(i - 1);
1: i = 10
(gdb) // 回车默认执行上一条命令

Breakpoint 2, t1 (i=9) at x.c:2
2	    if (i == 1) return 1;
1: i = 9
(gdb)
3	    return i + t1(i - 1);
1: i = 9
(gdb)

Breakpoint 2, t1 (i=8) at x.c:2
2	    if (i == 1) return 1;
1: i = 8

(gdb) i display // 显示自动显示的变量
Auto-display expressions now in effect:
Num Enb Expression
1:   y  i
(gdb) undisplay 1 // 取消自动显示
(gdb) n
3	    return i + t1(i - 1);
(gdb)

Breakpoint 2, t1 (i=6) at x.c:2
2	    if (i == 1) return 1;
(gdb)
3	    return i + t1(i - 1);

还可以用delete display 1 删除第一个自动显示变量, 或者(关闭/开启自动显示变量, 不删除)

disable display 1
enable display 1

查看内存

x /option address

具体选项包括:nfu 即,

  • n, number: 显示的单元数量, 默认 1 个单元(u 选项保证)

  • f, format: 格式

    Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string) and z(hex, zero padded on the left). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).

    • 十六进制 x(默认),
    • 八进制 o,
    • 二进制 t,
    • null 结尾字符串 s,
    • 机器指令 i
  • u, unit size:

    • 单元长度: 可选为b(字节),
    • h(半字,2 字节),
    • w(一字, 4 字节, 默认),
    • g(八字节)

都用默认的话就只加/即可

x / &node

查看寄存器

i r
info r
info registers
i all-registers

查看调用栈

当程序进行函数调用时, 这些调用信息(where, how)称为栈帧(frame).

每一个栈帧的内容还包括调用函数的参数, 局部变量等.

所有这些栈帧组成的信息称为调用栈.

bt (可选参数, 指定显示数量)
backtrace

i f
i frame // 栈信息
i locals // 查看局部变量
i args // 查看当前帧的所有函数参数

切换栈帧:

f 栈帧号
f ad 栈帧地址
frame 栈帧号
frame ad 栈帧地址
// 切换
up
down

MISC

如果需要指定 entry point 然后执行调试, 需要使用 starti 命令, 然后使用i files, 即可查看 strip 之后的程序 的 entry-point, 否则直接用i files显示的并不是真实的 entry point

断点

普通断点: break

源码某行设置断点

break file_name:row_number
// 例如
break test.cpp:23 // 在第 23 行设置断点

与此同时, 可以通过行号偏移量设置断点, 如下:

b +offset
b -offset

函数设置断点

break func_name // (可通过 tab 补全)
// 例如
break main
  1. 函数重载情况下, 会为每一个同名函数都设置断点, 如果需要指定函数, 可以加上函数签名(int) 或者类作用域限定::

通过正则表达式设置断点

rb <regex>
rbreak <regex>
// 例如: 
rb func*

指令地址

如果没有调试信息(编译时未添加`-g), 需要通过地址信息来设置条件断点

p func // 获取函数地址
b * 0x304f0b

条件断点($\bigstar$)

基于行号

b Breakpoint condition
// 例如
b test.cpp:80 if i==10

基于函数名

b func if a==10

临时断点

只命中一次, 就被自动销毁, 后续即使代码被调用多次也不会再次命中.

tb breakpoint
tbreak breakpoint

事实上start命令就是相当于在main处设置临时断点然后开始执行

断点管理

查看断点信息

i b
i break
i breakpoint
info b
info break
info breakpoint

得到断点编号, 下面会用到.

或者用

info b 断点编号

开启/禁用断点

enable 编号
disable 编号

可以使用范围:

enable 4-6 // 启用编号为 4~6 的断点

开启一次

类似于临时断点, 只命中一次, 与临时断点的不同在于, 开启一次的断点命中后不会被删除, 而是处于禁用状态

enable once 断点编号

启用断点并删除

相当于把一个被禁用的断点转换为临时断点

enable delete 断点编号

启用断点并命中 N 次

enable count N 断点编号

忽略前 N 次命中

ignore 断点编号 N

删除断点

删除所有

delete

删除指定编号断点

delete 断点编号 断点编号 ... 
delete 5-7 // 指定范围

删除指定行号的断点

clear main.cpp:23

删除指定函数的断点

clear func_name // 存在重载则全部删除

观察点/捕捉点

很多时候, 程序只在一些特定条件下才出现 bug, 观察点就可以用来发现或者定位该类型的 bug.

观察点 可以设置为监控一个变量或者一个表达式的值, 当这个值或者表达式的值发生变化时, 程序会暂停, 而不需要提前设置断点.

设置观察点

watch 条件
// e.g.:
watch count==5

读取/读写观察点

rwatch 变量或表达式 // 读取观察点
awatch 变量或表达式 // 读写观察点

查看观察点

i watchpoints

捕获点

catch 事件

用于以下几种情况

  1. throw: C++抛出异常
  2. catch: C++捕获之后的语句块
  3. exec,fork,vfork: C 系统调用
  4. 动态链接库相关

线程管理

查看线程

i threads // 查看当前进程所有线程的信息

切换线程

thread 线程 ID // 通过 `i threads` 查看

指定线程设断点

b 断点 thread 线程 ID

指定线程运行命令

thread apply 线程 ID 线程 ID(可以有多个) 命令

例子:

thread apply 2 3 i locals

核心转储文件调试

Ubuntu20.04 为例, 需要先写入 core 文件格式:

  sudo systemctl disable apport.service # 关闭系统的日志分析
  echo "core-%e-%p-%t"> /proc/sys/kernel/core_pattern
  
  # 开 limit:
  ulimit -c unlimited
  ulimit -a
  -t: cpu time (seconds)              unlimited
  -f: file size (blocks)              unlimited
  -d: data seg size (kbytes)          unlimited
  -s: stack size (kbytes)             8192
  -c: core file size (blocks)         unlimited

可以备份一下默认的 core 格式:

  // Ubuntu
  |/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
  
  // ArchLinux
  |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
  • %e: 可执行程序名称
  • %p: PID
  • %t: 时间戳

随便访问一下空地址, 就爆了:

struct P{
    int a;
    char b;
};

int main(){
    P *p = 0;
    p->a;
}

结果:

$ gcc aa.c && ./a.out
Segmentation fault (core dumped)

编译加上-g, 然后开gdb:

gdb a.out

接着调试就好

死锁调试

bt

th

f

l

基本常用的就这么几个

动态库调试

分为静态加载和动态加载(dlopen), 如果有调试信息直接加载, 没有的话需要看堆栈

内存调试

主要通过 Asan 在编译阶段定位内存问题, 除此之外就是自己下断点设置自动变量(display)查看变量(p)和内存(x)的值, 进一步分析

配置文件

set disassembly-flavor intel