gdb
1.调试代码
GDB 调试分三步:编译时带调试信息 → 启动 GDB → 用断点/单步/打印/栈回溯定位问题。
1. 先编译成 debug 版本
g++ -g -O0 -o demo demo.cpp
-g:生成调试信息,GDB 才能看到行号、函数名、变量。-O0:关闭优化,避免变量被优化掉或执行顺序乱跳。
CMake 项目可以:
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
2. 启动 GDB
gdb ./demo
如果程序需要参数:
gdb --args ./demo arg1 arg2
也可以附加到正在运行的进程:
gdb -p <PID>
3. 常用命令
表格
| 场景 | 命令 | 作用 |
|---|---|---|
| 运行 | run / r |
启动程序 |
| 断点 | break main |
在函数处打断点 |
| 断点 | break demo.cpp:10 |
在某一行打断点 |
| 条件断点 | break 10 if i == 5 |
满足条件才停 |
| 单步 | next / n |
执行下一行,不进入函数 |
| 进入函数 | step / s |
执行下一行,进入函数 |
| 跳出函数 | finish |
执行完当前函数并返回 |
| 继续 | continue / c |
继续跑到下一个断点 |
| 打印变量 | print x / p x |
查看变量值 |
| 查看内存 | x/16xb addr |
查看某地址内存 |
| 查看栈 | backtrace / bt |
看调用栈 |
| 查看局部变量 | info locals |
看当前帧局部变量 |
| 查看参数 | info args |
看当前函数参数 |
| 退出 | quit / q |
退出 GDB |
4. 典型调试流程
比如程序段错误:
bash
gdb ./demo
(gdb) run
# 程序崩溃后
(gdb) bt
(gdb) frame 0
(gdb) list
(gdb) print ptr
bt 会显示崩溃时完整调用栈,frame 切到对应栈帧,list 看源码,print 看变量值。
如果是逻辑错误,比如某个变量值不对:
bash
(gdb) break demo.cpp:20
(gdb) run
(gdb) next
(gdb) print count
(gdb) continue
2.利用Core Dump文件进行事后分析
当程序在生产环境崩溃时,可通过core dump文件回溯现场。 确保系统开启core dump:
执行 ulimit -c unlimited 。 编译时保留调试符号:必须加 -g 选项。
不加 -g 能否定位崩溃?
- 能加载 core dump,但几乎无法定位源码级问题。
- GDB 会显示内存地址和函数名(如果符号表未 strip),但不会显示文件名、行号、变量名。
- 你只能看到类似
#0 0x00000000004005a6 in ?? ()的输出,无法知道是哪一行代码出错。 - 若程序经过
strip处理,连函数名都会丢失,只能看到纯地址。 - 结论:生产环境务必保留
-g编译版本(或单独保留调试符号文件),否则 core dump 分析价值极低。
什么是栈帧?
每次函数调用时,系统会在栈上分配一块内存区域,用于保存该函数的局部变量、参数、返回地址等信息,这块区域就叫"栈帧"。GDB 用编号管理这些栈帧:
#0:当前正在执行的函数(崩溃点)#1:调用#0的函数#2:调用#1的函数,依此类推
示例代码
cpp
#include <stdio.h>
#include <string.h>
void crash_func(char *buf) {
strcpy(buf, "This is a very long string that will overflow the buffer!"); // 越界写入
}
void middle_func() {
char small_buf[10];
crash_func(small_buf); // 传入小缓冲区
}
int main() {
middle_func();
return 0;
}
编译并运行生成 core dump
bash
gcc -g -o test_crash test_crash.c
ulimit -c unlimited
./test_crash # 触发段错误,生成 core.xxx
使用 GDB 分析
bash
gdb ./test_crash core.xxx
进入 GDB 后执行:
bash
(gdb) bt
#0 0x00007f8b8c0a3d20 in __GI___strcpy_sse2_unaligned () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00000000004005a6 in crash_func (buf=0x7ffd5b3a2a50 "") at test_crash.c:5
#2 0x00000000004005c3 in middle_func () at test_crash.c:10
#3 0x00000000004005d4 in main () at test_crash.c:14
此时你看到崩溃发生在 crash_func 的第 5 行,但你想确认传入的 buf 是什么、middle_func 里的 small_buf 大小是多少。
切换到 #1 帧查看 crash_func 的参数:
bash
(gdb) frame 1
#1 0x00000000004005a6 in crash_func (buf=0x7ffd5b3a2a50 "") at test_crash.c:5
(gdb) print buf
$1 = 0x7ffd5b3a2a50 ""
(gdb) info locals
No locals.
切换到 #2 帧查看 middle_func 的局部变量:
bash
(gdb) frame 2
#2 0x00000000004005c3 in middle_func () at test_crash.c:10
(gdb) print small_buf
$2 = '\000' <repeats 9 times>
(gdb) info locals
small_buf = '\000' <repeats 9 times>
你现在可以确认:middle_func 定义了一个 10 字节的 small_buf,却传给了 crash_func 去写入超长字符串,导致栈溢出崩溃。
asan
asan可以用来检测,下面这几类错误
| 错误类型 | 典型写法 | ASan 报错 |
|---|---|---|
| 堆越界 | new int[10]; p[12] |
heap-buffer-overflow |
| 栈越界 | char buf[4]; strcpy(buf,"overflow") |
stack-buffer-overflow |
| 全局越界 | int g[10]; g[10]=1 |
global-buffer-overflow |
| 释放后使用 | delete p; *p=1 |
heap-use-after-free |
| 重复释放 | delete p; delete p |
double-free |
| 内存泄漏 | malloc 后忘记 free |
LeakSanitizer: detected memory leaks |
举个例子
模拟一个真实 bug
假设你写一个简单协议解析函数:收到一段原始字节,前面 4 字节是长度,后面是 payload。
cpp
#include <cstdlib>
#include <cstring>
#include <iostream>
struct Packet {
int id;
char payload[64];
};
Packet* parse_packet(const char* data, int data_len) {
if (data_len < 4) return nullptr;
int payload_len = *(const int*)data; // 前4字节是长度
Packet* pkt = (Packet*)malloc(sizeof(Packet));
pkt->id = 1;
// 危险:没有校验 payload_len,可能超过 64
memcpy(pkt->payload, data + 4, payload_len);
return pkt;
}
int main() {
// 构造一个"恶意"输入:payload_len = 128,但缓冲区只有 64
char buf[4 + 128];
*(int*)buf = 128;
memset(buf + 4, 'A', 128);
Packet* pkt = parse_packet(buf, sizeof(buf));
if (pkt) {
std::cout << "id=" << pkt->id << std::endl;
free(pkt);
}
}
这个 bug 很典型:没有校验长度就直接 memcpy 。
没有 ASan 时,它可能偶尔不崩,但会悄悄破坏堆上的元数据,后面再 malloc/free 时才会崩溃,定位起来非常痛苦。
用 ASan 编译
bash
g++ -g -O1 -fsanitize=address -fno-omit-frame-pointer packet.cpp -o packet
这里:
-g:保留调试信息,报错时能显示行号;-O1:兼顾性能和可读性;-fsanitize=address:开启 ASan;-fno-omit-frame-pointer:让调用栈更完整。
运行后看报错
./packet
ASan 会输出类似:
bash
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 128 at 0x...
#0 0x... in parse_packet packet.cpp:15
#1 0x... in main packet.cpp:25
0x... is located 0 bytes after 64-byte region [0x...,0x...)
allocated by thread T0 here:
#0 malloc
#1 0x... in parse_packet packet.cpp:12
这份报告可以分三层读:
表格
| 报告片段 | 含义 |
|---|---|
heap-buffer-overflow |
堆缓冲区越界写 |
WRITE of size 128 |
写了 128 字节 |
packet.cpp:15 |
越界发生在 memcpy 那一行 |
allocated by ... packet.cpp:12 |
这块内存是在 malloc(sizeof(Packet)) 分配的 |
0 bytes after 64-byte region |
写入位置正好在合法区域后面,说明 payload 超出了 payload[64] |
到这里,根因就很清楚了:payload_len 没有做上限校验,导致 memcpy 写穿了 pkt->payload。
Valgrind
| 问题类型 | 典型表现 | Valgrind 能力 |
|---|---|---|
| 内存泄漏 | new/malloc 后没 delete/free |
✅ 强项 |
| 未初始化内存 | 读了没赋值的变量 | ✅ 比 ASan 强 |
| 非法读写 | 越界、野指针 | ✅ 能抓 |
| use-after-free | 释放后继续访问 | ✅ 能抓 |
| double-free | 重复释放 | ✅ 能抓 |
举个例子
1.准备代码
新建 valgrind_demo.cpp:
cpp
#include <cstdlib>
#include <cstring>
#include <iostream>
struct Node {
int id;
char name[16];
};
void leak_example() {
// 这块内存没有释放,会产生内存泄漏
Node* node = (Node*)malloc(sizeof(Node));
node->id = 1;
// name 没有初始化,后面读它就是"使用未初始化内存"
std::cout << "name[0]=" << (int)node->name[0] << std::endl;
// 故意越界写:name 只有 16 字节,这里写了 32 字节
memset(node->name, 'A', 32);
}
int main() {
leak_example();
return 0;
}
这个程序故意写了三个问题:
malloc后没有free;- 读了未初始化的
name[0]; memset写穿了name[16]。
2. 编译
cpp
g++ -g -O0 -o valgrind_demo valgrind_demo.cpp
这里:
-g:保留调试信息;-O0:关闭优化,方便定位行号。
3. 用 Valgrind 跑
cpp
valgrind --leak-check=full --show-leak-kinds=all ./valgrind_demo
4. 看输出
运行后大概会看到三类信息。
第一类:Invalid write
bash
Invalid write of size 1
at 0x...: memset (in ...)
by 0x...: leak_example() (valgrind_demo.cpp:17)
Address 0x... is 0 bytes after a block of size 20 alloc'd
at 0x...: malloc (in ...)
by 0x...: leak_example() (valgrind_demo.cpp:9)
意思是:
valgrind_demo.cpp:17这一行发生了非法写;- 写的位置在
malloc出来的块后面; - 也就是
memset(node->name, 'A', 32)写越界了。
第二类:Use of uninitialised value
bash
Use of uninitialised value of size 8
at 0x...: leak_example() (valgrind_demo.cpp:14)
意思是:
valgrind_demo.cpp:14这一行读了一个没有初始化的值;- 对应
node->name[0],它从来没被赋值。
第三类:内存泄漏
LEAK SUMMARY:
definitely lost: 20 bytes in 1 blocks
再看详细部分:
20 bytes in 1 blocks are definitely lost in loss record 1 of 1
at 0x...: malloc (in ...)
by 0x...: leak_example() (valgrind_demo.cpp:9)
意思是:
valgrind_demo.cpp:9这里malloc出来的 20 字节没有释放;- 属于"确定泄漏"。
总结
日常开发可以先开 ASan ,把越界、UAF、double free 这些高频内存错误拦住。
如果怀疑有泄漏、未初始化值,或者 ASan 没报错但行为仍然诡异,再用 Valgrind 做深度检查。
如果程序已经崩溃,或者需要看变量、栈帧、内存内容、多线程状态,就用 GDB。
开发阶段:ASan 常驻
发布前:Valgrind 跑一轮
崩溃时:GDB / core dump 定位