文章目录
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
前言(逆向思维)
我之前接触 GDB 的时候,只知道它可以作为调试工具而存在,即我写完了一个程序,发现运行有问题,诶,那就在编译的时候,插入 gdb 调试选项
c
gcc -o test myCwork.c -g -ggdb
为什么 gdb 有两个编译参数选项呢?
| 功能特性 | -g | -ggdb | 说明 |
|---|---|---|---|
| 基本断点 | ✅ | ✅ | 两者都支持 |
| 变量查看 | ✅ | ✅ | 两者都支持 |
| 堆栈跟踪 | ✅ | ✅ | 两者都支持 |
| 宏展开 | ❌ | ✅ | -ggdb可以查看宏定义 |
| 预处理器信息 | ❌ | ✅ | 查看#ifdef等条件编译上下文 |
| 详细类型信息 | 基本 | 详细 | -ggdb有更完整的类型展开 |
| GDB脚本扩展 | ❌ | ✅ | 支持GDB Python脚本等扩展 |
| 兼容其他调试器 | ✅ | ⚠️ | -ggdb可能包含GDB专用扩展 |
然后,就可以进入调试流程了。
bash
gdb test
现在,我们反过来,用逆向思维,如果一个项目被验证为是正确的,功能正常运行正常,而且性能优异。那么,此时作为一个优秀的好学者,我们想要学习别人优秀经验,想要拆解别人的设计架构,怎么办呢?那当然是使用 gdb 调试工具,通过函数的调用顺序关系,来描绘项目的运行架构。如果我们把这个架构摸清了,并且以形象的图示法描述,那么,我们才算是真正学会了这个项目。
gdb 的本质
八股文:GDB,即 GNU 调试器,是 GNU 项目的一部分,是一个功能强大的、可移植的、基于命令行的源代码级调试器。它支持多种编程语言,尤其广泛应用于 C 和 C++ 程序的调试。
- 源代码级:能对应到你的代码行,不只是看机器码。
- 可移植:能在 Linux、Windows(通过 MinGW 或 Cygwin)、macOS 等多种平台使用。
- 设置断点:在特定条件(如某行代码、函数入口、变量被改变)下让程序暂停。
- 单步执行:一条条指令或一行行代码地执行。
- 检查与修改:读/写内存和寄存器。
- 核心转储:程序崩溃时产生的内存快照,GDB 能"回放"崩溃现场。
它实际上做了什么?(简略流程)
- 启动:你输入
gdb a.out。 - 附着:你输入
run后,GDB 会fork()出一个子进程,在这个子进程中调用ptrace(PTRACE_TRACEME, ...),然后子进程再execve("a.out", ...)加载你的程序。从此,子进程的一举一动都被操作系统通过ptrace报告给父进程GDB。 - 统治循环:
GDB进入一个主循环,等待你的命令。
你输入break main:GDB 计算main函数地址,通过ptrace向该地址写入一条特殊指令(如 int 3,触发断点异常)。
你输入continue:GDB 用ptrace(PTRACE_CONT, ...)告诉操作系统"让我的子进程继续跑",直到遇到断点或信号。
程序在断点停下:操作系统通过 ptrace 通知 GDB。GDB 将刚才的 int 3 指令恢复为原指令,并让你感觉程序"恰好停在了那一行"。
你输入print variable:GDB 根据调试符号信息找到变量地址,通过 ptrace 从子进程内存中读取数据并显示给你。
简单来说,gdb 是一个主进程,我们将要调试的程序是被他 fork 出来的分叉子进程,gdb 控制这个子进程。

- GDB的干预能力:
暂停/继续:GDB可以让内核暂停某个线程,不给它CPU时间片
单步执行:GDB说"只执行一条指令",内核会:
------1、恢复线程,让它执行一条指令
------2、立即触发一个陷阱(trap)
------3、内核捕获陷阱,再次暂停线程并通知GDB
GDB不直接访问被调试进程的内存。它必须通过内核,因为每个进程有自己的虚拟地址空间,GDB进程的 "0x1234" 和被调试进程的 "0x1234" 映射到不同的物理地址,只有内核有权限跨进程访问内存。
常用的命令
我所需要用到的命令并不多。
锁定目标调试程序
第一种方法是直接调试目标程序
bash
$ gcc -o [输出文件] [源文件列] -g
$ gdb [输出文件]
第二种方法是依附进程
bash
$ ps -ef | grep [目标程序]
获取对应的进程号
$ sudo gdb attach [目标程序的进程号]
如果目标程序还需要输入参数,或者输入配置文件的话,比如 -c 3600 之类的参数设定
bash
(gdb) set args "[参数1]" "[参数2]"
在调试之前,打断点,确定调试的对象命令
如果我们之前打过断点,并且生成过记录断点的文本文件,那么我们是可以用 source [断点文本文件] 命令去加载他,在此基础之上,还可以使用命令 b [文件]:[行数/函数] 继续打断点,之后呢?还是可以继续更新原来的断点文本文件。使用 save breakpoints [断点文本文件]
bash
(gdb) source mysql_pool.txt
Breakpoint 1 at 0x151e5: file SQLOperation.cpp, line 7.
Breakpoint 2 at 0x134bd: file MySQLWorker.cpp, line 32.
Breakpoint 3 at 0xd8bb: file MySQLConnPool.cpp, line 48.
(gdb) b main.cpp:117
Breakpoint 4 at 0x8562: file main.cpp, line 117.
(gdb) save breakpoints mysql_pool.txt
对已经设置的断点,我们还有以下的操作去管理断点
bash
(gdb) info break # 定期检查
(gdb) disable break_num # 临时禁用(不是删除)
(gdb) enable break_num # 重新启用
(gdb) delete break_num # 确认无用后删除
启动与运行
启动命令 start,启动程序,作必要的初始化,直至到程序的入口------main 函数
bash
(gdb) start
运行命令 run,不同于 run,他是直接运行下去的,直至遇到断点才停下来。
bash
(gdb) run
执行数量固定的命令
只执行一行命令
bash
(gdb) next
运行若干条命令,直至遇上某一个行才停止
bash
(gdb) u [行号]
过程变量的查询、源码查看
在调试过程,往往会伴随着函数和其传入的参数,以及返回值。我们会对这些变量感兴趣的
查询变量取值
bash
(gdb) p [变量名]
查询变量的声明定义
bash
(gdb) ptype [变量名]
在运行到的断点处查看部分源码
bash
(gdb) l +
回溯命令
bt 这是一个回溯命令 "backtrace" 的缩写
bash
(gdb) bt
我举一个例子来说明它的功能。以下的例子是,指针的解引用错误。
c
#include <stdio.h>
void deep_inside() {
int *p = NULL;
*p = 42; // 这里将会导致段错误(访问空指针)
}
void middle() {
deep_inside();
}
void start() {
middle();
}
int main() {
start();
return 0;
}
在编译后
c
gcc -g example.c -o example
gdb ./example
...进入调试程序的若干提示输出
(gdb) r
...程序报错的输出
(gdb) bt
#0 0x0000555555555159 in deep_inside () at example.c:5
#1 0x000055555555516e in middle () at example.c:10
#2 0x000055555555517e in start () at example.c:14
#3 0x0000555555555192 in main () at example.c:18
我们可以发现,这个错误是从最内层的开始一步步找到其调用链的,最下面的打印是,最外层的调用。这个调用,不仅仅是用户态的函数调用,也会有内核态的调用,所以会有奇奇怪怪的函数调用出现,大家也不用惊慌
我们再用 "逆向思维",非得是出现了错误才能使用吗?不一定,这个命令可以用来查看,一个命令背后的调用链。
多线程调试
列出当前所有的线程,并且用星号 * 指明当前正在观察哪一条线程
bash
(gdb) info thread
Id Target Id Frame
* 1 Thread 0x7ffff6d36740 (LWP 1097556) "main" main () at main.cpp:117
2 Thread 0x7ffff6d35640 (LWP 1097557) "main" SQLOperation::Execute (this=0x7ffff0000fb0, conn=0x55555558b9f0) at SQLOperation.cpp:7
3 Thread 0x7ffff6534640 (LWP 1097623) "main" __futex_abstimed_wait_common64 (private=0, cancel=true, abstime=0x0, op=393, expected=0, futex_word=0x55555558b524) at ./nptl/futex-internal.c:57
4 Thread 0x7ffff5d33640 (LWP 1097638) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
5 Thread 0x7ffff5532640 (LWP 1097639) "main" 0x00005555555618c4 in MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
6 Thread 0x7ffff4d31640 (LWP 1097640) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
7 Thread 0x7fffeffff640 (LWP 1098381) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
还可以使用 t [线程id] 命令来切换线程,还可以知道所切换到的线程在做什么
bash
(gdb) t 2
[Switching to thread 2 (Thread 0x7ffff6d35640 (LWP 1097557))]
#0 SQLOperation::Execute (this=0x7ffff0000fb0, conn=0x55555558b9f0) at SQLOperation.cpp:7
7 {
(gdb) info thread
Id Target Id Frame
1 Thread 0x7ffff6d36740 (LWP 1097556) "main" main () at main.cpp:117
* 2 Thread 0x7ffff6d35640 (LWP 1097557) "main" SQLOperation::Execute (this=0x7ffff0000fb0, conn=0x55555558b9f0) at SQLOperation.cpp:7
3 Thread 0x7ffff6534640 (LWP 1097623) "main" __futex_abstimed_wait_common64 (private=0, cancel=true, abstime=0x0, op=393, expected=0, futex_word=0x55555558b524) at ./nptl/futex-internal.c:57
4 Thread 0x7ffff5d33640 (LWP 1097638) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
5 Thread 0x7ffff5532640 (LWP 1097639) "main" 0x00005555555618c4 in MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
6 Thread 0x7ffff4d31640 (LWP 1097640) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
7 Thread 0x7fffeffff640 (LWP 1098381) "main" MySQLConnPool::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void (std::unique_ptr<sql::ResultSet, std::default_delete<sql::ResultSet> >)>&&) (this=0x55555558b2d0, sql="SELECT U_NAME FROM TBL_USER", cb=...) at MySQLConnPool.cpp:48
查看进程的调度器锁
bash
(gdb) show scheduler-locking
Mode for locking scheduler during execution is "replay".
实际上有 4 种调度器锁的模式
- off(关闭,默认)
行为:当暂停一个线程时,其他线程继续运行 - on(开启)
行为:当暂停一个线程时,暂停所有线程 - step(单步模式)
行为:只在单步执行时暂停所有线程,其他时间遵循off模式
bash
(gdb) set scheduler-locking step
(gdb) break producer
(gdb) run
情况1:命中普通断点
- producer暂停,其他线程继续运行(像off模式)
情况2:执行step/next命令
- 所有线程暂停(像on模式)
- 执行单步
- 恢复其他线程
- replay(重放模式)
行为:程序早就运行过了,我们现在所谓的调试只是在看回访而已,遇上断点其余线程也一样会中断。这个其实很好用。
GDB "造影" 的技巧
该技巧只针对那些 "服务器" 程序,服务器在未接收到请求的时候,通常会挂起阻塞,就算我们加入 GDB 调试,也一样是会被挂起。这个时候呢,我们需要把所想要测试的功能相关的接口全部找出来,不要找太多,刚好就行,对这些接口实现打上断点。(必须要服务和客户分离)
当我们把请求发送过去的时候,首先遇到的断点,就是处理请求接口链的头节点,如此类推,可以一步步地找到调用链的调用顺序。查看断点处调用栈可以搞清楚,这个断点的接口是被哪些函数所嵌套,一步步的把它的栈找出来。
当我们运行到某个断点的时候,如果我们感兴趣这个接口函数,我们应该使用
bash
(gdb) l +
先把该接口函数里面的情况打印出来,然后选择某一行去打断点,之后在运行
bash
(gdb) b [断点所在文件]:[显示的行号]
...
(gdb) n 或者 u [某一行]
而不是直接运行 n 命令,否则我们的视线就只会停留在主函数里面。
记录过程所用到的数据结构、变量的取值也是很重要的,因为这事关信息传递的载体(比如 buffer 之类的东西,我们有可能看到清晰的报文响应)。
因此,什么是重要的呢?函数调用链,链上的信息载体变量的具体内容。我们也可以在断点处切换线程,看看其余线程在执行什么样的接口函数。这些信息足够我们去勾勒一个服务器程序的运行架构了。