gdb、asan、Valgrind

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 定位

相关推荐
yyuuuzz1 小时前
云服务器部署 web 项目的进程常驻记录
运维·服务器·前端·阿里云·云计算·aws
曹牧1 小时前
VS: Visual Studio 里把未包含的文件加进项目2
linux·运维·算法
微软技术分享1 小时前
NexShell 1.0|大模型驱动的智能 SSH 运维终端,永久免费,赋能 Linux 服务器管理
运维·服务器·ssh
坤坤子吖2 小时前
Linux 线程概念与控制——线程创建、退出与等待
linux·开发语言·c++·笔记·学习
Chenyu_3102 小时前
NUMA 基础:从 CPU、内存到 Node 的整体理解
linux·服务器·网络·缓存
小白的码BUG之路10 小时前
linux -- ssh两台机器相连
运维·服务器·ssh
缓慢更新11 小时前
表单引擎的存储建模:宽表与文档库的取舍
运维·服务器·网络·数据库·低代码·oracle·适配器模式
续写烂尾12 小时前
贴片 SD NAND、eMMC、TF 卡规格怎么定?硬件工程师选型清单
运维·服务器·网络
不昀12 小时前
网口Link Up后仍然掉线,怎样区分物理链路和数据通路问题?
linux·以太网