【Linux 项目管理工具】GDB 调试是现成 C/C++ 项目的 “造影剂”,用来分析项目的架构原理

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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 能"回放"崩溃现场。

它实际上做了什么?(简略流程)

  1. 启动:你输入 gdb a.out
  2. 附着:你输入 run 后,GDB 会 fork() 出一个子进程,在这个子进程中调用 ptrace(PTRACE_TRACEME, ...),然后子进程再 execve("a.out", ...) 加载你的程序。从此,子进程的一举一动都被操作系统通过 ptrace 报告给父进程 GDB
  3. 统治循环: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 之类的东西,我们有可能看到清晰的报文响应)。

因此,什么是重要的呢?函数调用链,链上的信息载体变量的具体内容。我们也可以在断点处切换线程,看看其余线程在执行什么样的接口函数。这些信息足够我们去勾勒一个服务器程序的运行架构了。

相关推荐
物理与数学2 小时前
linux 交换分区(Swap)
linux·linux内核
呼啦啦5612 小时前
【C++入门】
c++
苦藤新鸡2 小时前
19.旋转输出矩阵
c++·算法·leetcode·力扣
南工孙冬梅2 小时前
【久久派】Linux 文件系统制作配置 基于buildroot
linux
小范馆2 小时前
C++ 编译方法对比:分步编译 vs 一步到位
java·开发语言·c++
云泽8082 小时前
C++ 继承进阶:默认成员函数、多继承问题与继承组合选型
开发语言·c++
一颗青果2 小时前
C++下的atomic | atmoic_flag | 内存顺序
java·开发语言·c++
宴之敖者、2 小时前
Linux——指令(下)
linux
抠脚学代码2 小时前
Qt与Linux
linux·数据库·qt