写在前面
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
- 函数重载情况下, 会为每一个同名函数都设置断点, 如果需要指定函数, 可以加上函数签名
(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 事件
用于以下几种情况
- throw: C++抛出异常
- catch: C++捕获之后的语句块
- exec,fork,vfork: C 系统调用
- 动态链接库相关
线程管理
查看线程
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