FAQ-14:段错误(segmentation fault)
预计阅读时间:18 分钟
📖 目录
问题速查表
| 问题 | 解决章节 |
|---|---|
| 程序崩溃,需要确认段错误记录 | 第一步:确认段错误信息 |
| 需要定位崩溃的具体代码位置 | 第二步:生成并分析 core dump |
| 怀疑内存硬件故障导致随机崩溃 | 第三步:检查内存硬件 |
| 动态链接库缺失或版本不兼容 | 第四步:检查动态链接库 |
| 栈空间不足(递归过深或大数组) | 第五步:检查栈空间 |
| 开发阶段排查内存错误 | 第六步:使用 Address Sanitizer(开发场景) |
Segmentation fault(通常简写为 segfault)是 Linux 系统中最常见的程序崩溃信号。当程序试图访问不允许访问的内存区域(如访问未分配的内存、写入只读内存、或超出进程地址空间范围)时,内核会向该进程发送 SIGSEGV 信号,默认行为是终止进程并生成 core dump 文件。
段错误的本质是操作系统的内存保护机制在起作用。每个进程都运行在自己的虚拟地址空间中,内核通过页表和 MMU(内存管理单元)确保进程只能访问属于自己的内存区域。当程序违反这个规则时,硬件会触发一个异常,内核捕获后向违规进程发送 SIGSEGV 信号。这种保护防止了一个程序的错误操作影响其他进程或整个系统。
段错误可能出现在任何使用 C/C++、Rust 等需要手动管理内存的编程语言编写的程序中,也可能出现在 Python、Java 等语言的 C 扩展模块中。对于系统管理员来说,理解如何诊断段错误是排查服务崩溃的关键技能。
原因分析
空指针解引用是最常见的原因之一。当程序尝试通过一个值为 NULL(0x0)的指针访问内存时,由于该地址通常不在进程的地址空间内,会触发段错误。这通常是因为函数返回了错误结果但没有检查,或者变量未正确初始化。
缓冲区溢出是另一个主要来源。当程序向一个固定大小的缓冲区写入超过其容量的数据时,可能会覆盖相邻的内存区域,包括返回地址、函数指针或其他关键数据,最终导致段错误。
使用已释放的内存(use-after-free)在复杂的程序中很常见。当程序释放了一块内存后,又通过之前的指针访问它,如果该内存已被重新分配给其他用途,就会产生不可预测的行为,包括段错误。
栈溢出通常由无限递归或过大的局部数组引起。每个线程的栈空间有限(通常为 8MB),当递归调用层数过深或在栈上分配了过大的数据结构时,会超出栈边界触发段错误。
内存硬件故障虽然相对少见,但在服务器环境中确实存在。损坏的内存条可能导致程序在正常运行时随机崩溃,且症状与软件 bug 类似,增加了排查难度。
动态链接库版本不兼容也会导致段错误。当程序链接的共享库版本与编译时不一致,或者库文件损坏,可能导致函数调用约定不匹配,进而引发内存访问异常。
排查决策树
段错误的根因分布很广,按下面的决策树从"有没有证据"到"证据指向哪里"逐层推进,核心原则是:先拿到 core dump 再用 gdb 说话,不要瞎猜:
程序崩溃:Segmentation fault (core dumped)
│
├─ ① 先确认崩溃记录与是否有 core 文件
│ │ dmesg | grep segfault / coredumpctl list
│ │ ├─ 有 core → 进入 ② 用 gdb 分析
│ │ └─ 无 core(ulimit -c 0 或 core_pattern 未配置)
│ │ → 先启用 core dump,再复现一次
│
├─ ② gdb 栈回溯指向哪里
│ │ gdb 中 bt / bt full
│ │ ├─ 指向应用自身函数 → 代码 bug
│ │ │ ├─ 空指针 / 越界 / use-after-free
│ │ │ │ └─ 开发环境用 ASan/Valgrind 复现定位
│ │ │ └─ 递归过深 / 大数组 → 栈溢出 → ulimit -s / 改堆分配
│ │ └─ 指向共享库 → 进入 ③
│
├─ ③ 动态库检查
│ │ ldd 程序 | grep "not found"
│ │ ├─ 缺失 → 安装 / 重装对应库
│ │ ├─ LD_LIBRARY_PATH 指向错误版本 → 修正环境变量
│ │ └─ 库都正常 → 进入 ④
│
└─ ④ 环境性因素
├─ 崩溃随机、复现率低 → 怀疑内存硬件
│ └─ memtester / memtest86+ 检测,journalctl -k 看 MCE 记录
└─ 崩溃与其他变更时间点重合 → 回查升级记录、配置变更
排查步骤
第一步:确认段错误信息
# 查看内核日志中的段错误记录
dmesg | tail -20 | grep segfault
# 输出示例:
# [12345.678] myapp[1234]: segfault at 0xdeadbeef ip 0x7f1234567890 sp 0x7ffabc123456 error 4
# 使用 systemd-coredump 查看 core dumpcoredumpctl list
coredumpctl info 程序名
coredumpctl gdb 程序名 # 直接用 gdb 分析
第二步:生成并分析 core dump
# 启用 core dump
ulimit -c unlimited
# 设置 core dump 文件名模式
echo "core.%e.%p" > /proc/sys/kernel/core_pattern
# 运行程序并等待崩溃
./myapp
# 查看生成的 core 文件
ls -lh core*
# 用 gdb 分析 core dump
sudo apt install gdb
gdb ./myapp core.1234
# 在 gdb 中常用命令
(gdb) bt # 查看完整调用栈
(gdb) bt full # 查看调用栈及局部变量
(gdb) info registers # 查看寄存器状态
(gdb) print 变量名 # 查看变量值
第三步:检查内存硬件
# 使用 memtester 进行内存测试
sudo apt install memtester
sudo memtester 1G 1 # 测试 1GB 内存,运行 1 轮
# 或重启后进入 memtest86+(在 GRUB 启动菜单中选择)
# 查看硬件错误日志
sudo journalctl -k | grep -i "memory error\|mce\|EDAC\|hardware error"
# 查看 MCE(Machine Check Exception)
sudo mcelog --client
第四步:检查动态链接库
# 检查程序依赖的共享库
ldd /path/to/program
# 确认所有库文件都存在且版本匹配
# 输出中如果有 "not found" 表示缺失库文件
# 检查库文件完整性
ldconfig -p | grep 库名
# 重新安装缺失的库
sudo apt install --reinstall lib库名
第五步:检查栈空间
# 查看当前栈大小限制
ulimit -s
# 输出单位为 KB,默认通常是 8192(8MB)
# 临时增大栈大小
ulimit -s 65536 # 改为 64MB
# 永久修改(/etc/security/limits.conf)
# * soft stack 65536
第六步:使用 Address Sanitizer(开发场景)
# 编译时启用 ASan(GCC/Clang)
gcc -fsanitize=address -g -o myapp myapp.c
# 或 C++
g++ -fsanitize=address -g -o myapp myapp.cpp
# 运行程序,ASan 会检测内存错误并输出详细报告
./myapp
# 输出示例:
# ==1234==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
# SUMMARY: AddressSanitizer: stack-buffer-overflow myapp.c:42 in main
解决方案
针对空指针解引用
// 在代码中添加 NULL 检查
void process(char *data) {
if (data == NULL) {
fprintf(stderr, "Error: data is NULL\n");
return;
}
// 安全使用 data
}
// 使用编译器警告
gcc -Wall -Wextra -o myapp myapp.c
针对缓冲区溢出
// 使用安全的字符串操作函数
// 避免:strcpy(buf, src);
// 推荐:
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// 或使用 snprintf
snprintf(buf, sizeof(buf), "%s", src);
// 编译时启用栈保护
gcc -fstack-protector-strong -o myapp myapp.c
针对 use-after-free
// 释放后将指针置为 NULL
free(ptr);
ptr = NULL;
// 使用 Valgrind 检测内存问题
valgrind --tool=memcheck --leak-check=full ./myapp
// 或使用 Address Sanitizer(性能更好)
gcc -fsanitize=address -g -o myapp myapp.c
针对栈溢出
// 检查递归终止条件
int factorial(int n) {
if (n <= 1) return 1; // 确保有终止条件
return n * factorial(n - 1);
}
// 将大数组改为动态分配(堆上)
// 避免:
void func() {
int arr[1000000]; // 栈上分配可能溢出
}
// 推荐:
void func() {
int *arr = malloc(1000000 * sizeof(int));
// 使用完毕后 free(arr);
}
真实案例
案例 A:ulimit -c 0 导致无 core 可分析,启用后 gdb 栈回溯秒定位
现象:自研 C++ 数据处理服务上线后频繁崩溃,但只拿到一段 dmesg 记录,没有 core 文件:
$ dmesg | tail -5
[432109.87] process_data[2456]: segfault at 0x7f8a2c000000 ip 0x55a3f1d2a1b3 sp 0x7f8a0ffffc30 error 4
$ ls core* 2>/dev/null
ls: cannot access 'core*': No such file or directory
$ ulimit -c
0
根因:ulimit -c 0 禁用了 core dump。dmesg 虽然给出了崩溃地址(ip 0x55a3...),但没有 core 文件就无法用 gdb 还原调用栈,等于只有结论没有证据。
修复:启用 core dump 并固定输出路径:
$ ulimit -c unlimited
$ echo 'kernel.core_pattern=/var/crash/core.%e.%p' | sudo tee /etc/sysctl.d/99-core.conf
$ sudo sysctl -p /etc/sysctl.d/99-core.conf
$ ./process_data # 复现崩溃
$ ls -lh /var/crash/
-rw------- 1 root root 128M /var/crash/core.process_data.2456
验证(gdb 栈回溯):
$ gdb ./process_data /var/crash/core.process_data.2456
(gdb) bt
#0 memcpy (dest=0x7f8a2c000000, src=0x55a3f2000ab0, n=4096) at memcpy.S
#1 0x000055a3f1d2a1b3 in parse_chunk (data=0x0, len=4096) at process.c:87
#2 0x000055a3f1d2a402 in main (argc=2, argv=0x7ffd...) at process.c:152
(gdb) frame 1
(gdb) print data
$1 = (char *) 0x0
栈回溯显示 parse_chunk 在 process.c:87 把 NULL 指针直接传给了 memcpy,空指针解引用实锤。修复:
// process.c:85
if (data == NULL) {
log_error("parse_chunk: NULL data, len=%zu", len);
return -1;
}
memcpy(dst, data, len);
重新编译部署后观察一周无崩溃。后续把 core 配置写进部署脚本,避免新环境再次踩坑。
案例 B:LD_LIBRARY_PATH 指向旧版动态库,ldd 揪出元凶
现象:升级系统库后,第三方商业软件(/opt/vendor/bin/analysis)启动即崩溃:
$ /opt/vendor/bin/analysis
Segmentation fault (core dumped)
排查过程:先看程序依赖的共享库,注意加载路径:
$ ldd /opt/vendor/bin/analysis | grep -E "not found|ssl|crypto"
libssl.so.3 => /usr/local/lib/libssl.so.3 (0x00007f...)
libcrypto.so.3 => /usr/local/lib/libcrypto.so.3 (0x00007f...)
$ echo $LD_LIBRARY_PATH
/usr/local/lib
根因:环境变量 LD_LIBRARY_PATH=/usr/local/lib 让程序加载了用户源码编译的旧版 OpenSSL(符号表与软件预期不一致),函数调用约定错位导致崩溃。系统自带的 /usr/lib/x86_64-linux-gnu/libssl.so.3 才是兼容版本。
修复:清空该环境变量并从配置文件中移除:
$ unset LD_LIBRARY_PATH
$ sed -i '/LD_LIBRARY_PATH/d' ~/.bashrc
$ ldd /opt/vendor/bin/analysis | grep ssl
libssl.so.3 => /usr/lib/x86_64-linux-gnu/libssl.so.3 (0x00007f...)
验证:
$ /opt/vendor/bin/analysis && echo OK
OK
$ ldd -r /opt/vendor/bin/analysis # -r 检查所有符号可解析,无报错即通过
经验:LD_LIBRARY_PATH 是全局影响的环境变量,只应在临时调试时使用;生产环境优先用 rpath 或把库安装到标准路径,升级库后对关键软件做一次 ldd -r 回归检查。
案例 C:硬件内存故障导致随机 segfault
现象:服务器上的 Java 应用不定时崩溃,segfault 地址每次不同,没有固定规律。
# dmesg 中多次出现不同地址的 segfault
$ dmesg | grep segfault
[432109.87] java[2456]: segfault at 0x7f8a2c000000 ip 0x55a3f1d2a1b3
[433215.42] java[3012]: segfault at 0x7f3b1a000000 ip 0x55a3f1d2a1b3
[434567.89] java[2789]: segfault at 0x7fc5e8000000 ip 0x55a3f1d2a1b3
# 每次崩溃地址不同 → 不是代码 bug,疑似硬件问题
# 运行 memtest86+ 检测内存
$ sudo memtester 4G 3 # 测试 4GB 内存,跑 3 轮
FAIL: 0x5a5a5a5a != 0x00000000 at address 0x1a2b3c4d
FAIL: Memory write/read mismatch
根因:物理内存条的某个区域损坏,导致该区域写入的数据读取时位翻转。Java 堆分配到损坏地址时触发 segfault,地址随机是因为每次分配位置不同。
修复:更换故障内存条。运行 memtester 或 memtest86+ 可以精确定位故障地址范围。
案例 D:Valgrind 检测出隐藏的内存泄漏导致程序逐渐崩溃
现象:一个长期运行的 C 程序在运行数小时后崩溃,但崩溃前没有任何明显错误:
# 程序运行一段时间后崩溃
$ ./long-running-app
... 正常运行 3 小时 ...
Segmentation fault (core dumped)
# 查看内存使用趋势
$ while true; do ps -p $(pgrep long-running-app) -o rss=; sleep 60; done
524288
655360
786432
917504
1048576 # 内存持续增长,最终耗尽
排查过程:使用 Valgrind 检测内存问题:
# 使用 Valgrind 检测内存泄漏
$ valgrind --tool=memcheck --leak-check=full --track-origins=yes ./long-running-app 2>&1 | head -100
==12345== Memcheck, a memory error detector
==12345== HEAP SUMMARY:
==12345== in use at exit: 1,048,576 bytes in 4,096 blocks
==12345== total heap usage: 524,288 allocs, 520,192 frees, 125,829,120 bytes allocated
==12345==
==12345== 1,048,576 bytes in 4,096 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x1091B3: process_data (app.c:142)
==12345== by 0x10945B: main (app.c:200)
根因:程序在 process_data 函数中每次调用都分配内存,但某些错误路径下没有释放,导致内存泄漏。运行数小时后耗尽地址空间,触发 segfault。
修复:
// app.c:142 附近的代码
void *process_data(const char *input) {
char *buffer = malloc(BUF_SIZE);
if (buffer == NULL) return NULL;
int ret = parse_input(buffer, input);
if (ret < 0) {
// 错误:这里没有释放 buffer 就返回了
return NULL; // ← 修复:改为 goto cleanup
}
// ... 处理数据 ...
free(buffer);
return result;
cleanup:
free(buffer);
return NULL;
}
预防:使用 -fsanitize=address 编译并在 CI 中运行测试。对长期运行的程序定期用 Valgrind 做内存检测。
案例 E:多线程竞态条件导致随机段错误
现象:多线程程序在高并发时随机崩溃,崩溃位置不固定:
# 崩溃时的堆栈
$ gdb ./threaded-app core.1234
(gdb) bt
#0 0x00007f... in __memmove_avx2 () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x000055... in update_counter (ctx=0x55...) at stats.c:87
#2 0x000055... in worker_thread (arg=0x55...) at worker.c:45
排查过程:使用 Thread Sanitizer 检测竞态条件:
# 编译时启用 Thread Sanitizer
$ gcc -fsanitize=thread -g -o threaded-app threaded-app.c -lpthread
# 运行程序
$ ./threaded-app
WARNING: ThreadSanitizer: data race (pid=1234)
Write of size 4 at 0x7f... by thread T1:
#0 update_counter stats.c:87
#1 worker_thread worker.c:45
Previous read of size 4 at 0x7f... by thread T2:
#0 read_counter stats.c:92
#2 worker_thread worker.c:50
根因:多个线程同时读写共享的计数器变量,没有使用互斥锁保护,导致数据竞争和随机崩溃。
修复:
// stats.c 中添加互斥锁
static pthread_mutex_t counter_lock = PTHREAD_MUTEX_INITIALIZER;
static int shared_counter = 0;
void update_counter(int value) {
pthread_mutex_lock(&counter_lock);
shared_counter += value;
pthread_mutex_unlock(&counter_lock);
}
int read_counter(void) {
int val;
pthread_mutex_lock(&counter_lock);
val = shared_counter;
pthread_mutex_unlock(&counter_lock);
return val;
}
预防:使用 -fsanitize=thread 编译并在 CI 中运行并发测试。对共享资源的访问始终使用适当的同步机制。
案例 F:内核模块导致整个系统段错误
现象:加载自定义内核模块后,系统随机崩溃,dmesg 中出现内核级别的 segfault:
# 内核日志
$ dmesg | grep -i "segfault\|oops\|bug"
[ 123.456] BUG: unable to handle page fault at ffffffff82000000
[ 123.456] PGD 0 P4D 0
[ 123.456] Oops: 0000 [#1] SMP NOPTI
[ 123.456] CPU: 2 PID: 1234 Comm: mymodule Tainted: G W O 5.15.0-generic
[ 123.456] RIP: 0010:my_func+0x1a/0x50 [mymodule]
排查过程:
# 查看加载的模块
$ lsmod | grep mymodule
mymodule 16384 0
# 检查模块信息
$ modinfo mymodule
filename: /lib/modules/5.15.0/kernel/drivers/mymodule.ko
description: My custom module
license: GPL
# 查看模块日志
$ journalctl -k | grep mymodule
根因:内核模块代码中存在内存访问错误(如空指针解引用、越界访问)。内核级别的错误会导致整个系统崩溃,比用户空间程序更危险。
修复:卸载有问题的模块,修复代码后重新编译:
# 卸载模块
$ sudo rmmod mymodule
# 修复代码后重新编译
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载修复后的模块
$ sudo insmod mymodule.ko
预防:内核模块开发必须极其谨慎。使用 KASAN(Kernel Address Sanitizer)检测内存错误。在测试环境中充分验证后再部署到生产环境。
预防措施
- 编译时始终启用警告:
-Wall -Wextra -Werror,将警告视为错误 - 定期使用 Valgrind 或 Address Sanitizer 进行内存检测
- 使用静态分析工具(如 cppcheck、clang-tidy)在编码阶段发现潜在问题
- 对于生产环境的关键服务,启用 core dump 并配置 coredumpctl 便于事后分析
- 硬件层面定期运行 memtest86+ 检测内存故障
- CI 流水线中接入 gdb + core dump 自动分析步骤,代码合并前崩溃即可定位到行
- 禁止全局设置
LD_LIBRARY_PATH,库路径统一通过 ldconfig 配置,升级库后对关键软件做ldd -r回归 - 对承载关键服务的物理机每季度做一次内存巡检(memtest86+ / memtester),硬件更换后必测
进阶排查技巧
使用 gdb 进行事后调试
core dump 是分析段错误的金矿,gdb 可以还原崩溃现场:
# 启用 core dump
ulimit -c unlimited
echo '/var/crash/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern
# 运行程序并复现崩溃
./myapp
# 使用 gdb 分析 core dump
gdb ./myapp /var/crash/core.myapp.1234
# gdb 常用命令
(gdb) bt # 查看调用栈
(gdb) bt full # 查看调用栈及局部变量
(gdb) frame 1 # 切换到栈帧 1
(gdb) print variable # 查看变量值
(gdb) info registers # 查看寄存器状态
(gdb) disassemble # 反汇编当前函数
使用 coredumpctl 管理 core dump
# 列出所有 core dump
coredumpctl list
# 查看特定程序的 core dump
coredumpctl info myapp
# 用 gdb 分析
coredumpctl gdb myapp
# 导出 core dump 文件
coredumpctl dump -o /tmp/core.myapp
使用 dmesg 追踪段错误模式
# 查看所有段错误记录
dmesg | grep segfault
# 分析崩溃地址模式
dmesg | grep segfault | awk '{print $6}' | sort | uniq -c | sort -rn
# 查看内核日志中的内存错误
dmesg | grep -i "memory\|mce\|edac\|hardware error"
使用 Address Sanitizer 运行时检测
# 编译时启用 ASan
gcc -fsanitize=address -g -o myapp myapp.c
# 运行程序(ASan 会自动检测内存错误)
./myapp
# ASan 输出示例:
# ==1234==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
# ==1234==READ of size 4 at 0x... thread T0
# ==1234== #0 0x... in func myapp.c:42
常见误区与陷阱
- 不要忽略编译器警告:
-Wall -Wextra可以在编译期发现很多潜在的段错误 - 不要在生产环境使用 Valgrind:性能开销太大,只在开发/测试环境使用
- 不要盲目禁用 core dump:core dump 是事后分析的关键证据
- 不要忽略随机崩溃:随机的 segfault 很可能是硬件问题或内存泄漏
- 不要在没有调试信息的情况下分析 core dump:编译时加
-g保留调试符号
故障复现与验证清单
修复段错误问题后,使用以下清单验证:
# 1. 编译时启用所有警告
gcc -Wall -Wextra -Werror -o myapp myapp.c
# 2. 使用 Valgrind 检测内存问题
valgrind --tool=memcheck --leak-check=full ./myapp
# 3. 使用 Address Sanitizer 测试
gcc -fsanitize=address -g -o myapp-asan myapp.c
./myapp-asan
# 4. 长时间运行测试
./myapp & # 运行 24 小时以上
# 监控内存使用:watch -n 60 'ps -p $(pgrep myapp) -o rss='
# 5. 检查 core dump 配置
ulimit -c # 应为 unlimited
cat /proc/sys/kernel/core_pattern
最佳实践
- 编译选项:开发阶段使用
-fsanitize=address -g -Wall -Wextra,发布时使用-O2 -fstack-protector-strong - core dump 管理:确保
ulimit -c unlimited已设置,并配置/proc/sys/kernel/core_pattern以便自动收集 core 文件 - 版本控制:使用包管理器安装的程序,确保版本与系统其他组件兼容;手动编译的程序记录编译选项
- 日志关联:将 segfault 事件与应用日志、系统日志关联分析,找出崩溃前的操作序列
- 防御性编程:在代码中加入必要的空指针检查、边界检查、返回值检查,宁可冗余也不冒险
延伸阅读
- 2.4:进程管理 进程管理——信号与进程终止
- 4.6:Linux 性能调优 Linux 性能调优——内存分析与调优
附录:内存调试工具速查表
| 工具 | 用途 | 安装命令 |
|---|---|---|
| gdb | core dump 分析、实时调试 | sudo apt install gdb |
| Valgrind | 内存泄漏检测、非法访问检测 | sudo apt install valgrind |
| Address Sanitizer | 运行时内存错误检测 | 编译选项 -fsanitize=address |
| Thread Sanitizer | 竞态条件检测 | 编译选项 -fsanitize=thread |
| memtester | 内存硬件检测 | sudo apt install memtester |
| memtest86+ | 全面内存硬件测试 | GRUB 启动菜单选择 |
| coredumpctl | core dump 管理 | systemd 自带 |
| strace | 系统调用跟踪 | sudo apt install strace |
附录:gdb 常用命令速查
# 启动 gdb
gdb ./program
gdb ./program core.1234
# 常用命令
(gdb) run # 运行程序
(gdb) bt # 查看调用栈
(gdb) bt full # 查看调用栈及局部变量
(gdb) frame N # 切换到栈帧 N
(gdb) print variable # 查看变量值
(gdb) print *pointer # 查看指针指向的内容
(gdb) info registers # 查看寄存器状态
(gdb) info locals # 查看所有局部变量
(gdb) disassemble # 反汇编当前函数
(gdb) list # 显示源代码
(gdb) break function # 设置断点
(gdb) continue # 继续执行
(gdb) step # 单步执行(进入函数)
(gdb) next # 单步执行(跳过函数)
(gdb) quit # 退出 gdb
附录:Valgrind 使用示例
# 基本内存检测
valgrind --tool=memcheck ./program
# 检测内存泄漏
valgrind --tool=memcheck --leak-check=full ./program
# 检测非法内存访问
valgrind --tool=memcheck --track-origins=yes ./program
# 输出到文件
valgrind --log-file=/tmp/valgrind.log ./program
# 常见错误类型:
# Invalid read/write of size N # 非法读写
# Use of uninitialised value # 使用未初始化值
# Conditional jump depends on... # 依赖未初始化值
# LEAK SUMMARY: # 内存泄漏摘要
附录:Address Sanitizer 输出解读
# 编译启用 ASan
gcc -fsanitize=address -g -o program program.c
# 运行程序
./program
# 输出示例:
# ==1234==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
# ==1234==READ of size 4 at 0x... thread T0
# #0 0x... in func program.c:42
# #1 0x... in main program.c:100
# ==1234==ABORTING
# 错误类型:
# heap-buffer-overflow # 堆缓冲区溢出
# stack-buffer-overflow # 栈缓冲区溢出
# heap-use-after-free # 堆释放后使用
# stack-use-after-return # 栈返回后使用
# double-free # 双重释放
# memory-leak # 内存泄漏
案例 G:共享库符号版本不匹配导致运行时崩溃
现象:升级 OpenSSL 后,依赖旧版 OpenSSL 的程序启动时崩溃:
$ /opt/legacy-app/bin/app
Segmentation fault (core dumped)
# 检查依赖的库
$ ldd /opt/legacy-app/bin/app
libssl.so.1.1 => /usr/lib/x86_64-linux-gnu/libssl.so.1.1
libcrypto.so.1.1 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1
排查过程:
# 检查库文件是否存在
$ ls -la /usr/lib/x86_64-linux-gnu/libssl.so.1.1
ls: cannot access '/usr/lib/x86_64-linux-gnu/libssl.so.1.1': No such file or directory
# 系统上只有 OpenSSL 3.x
$ ls /usr/lib/x86_64-linux-gnu/libssl.so.*
/usr/lib/x86_64-linux-gnu/libssl.so.3
# 用 gdb 分析 core dump
$ gdb /opt/legacy-app/bin/app core.1234
(gdb) bt
#0 0x00007f... in SSL_CTX_new () from /usr/lib/x86_64-linux-gnu/libssl.so.3
#1 0x000055... in init_ssl (ctx=0x0) at app.c:45
根因:程序编译时链接的是 OpenSSL 1.1,但系统升级到 OpenSSL 3.x 后,旧版库文件被删除。虽然 ldd 通过符号链接找到了 libssl.so.3,但函数签名和数据结构已改变,导致运行时崩溃。
修复:
# 方案一:安装兼容版本的旧库
sudo apt install libssl1.1
# 方案二:重新编译程序,链接新版 OpenSSL
cd /opt/legacy-app && make clean && make
# 方案三:使用容器运行旧版程序
docker run --rm -v /opt/legacy-app:/app openssl:1.1 /app/bin/app
预防:系统升级后对关键软件做 ldd -r 回归检查。使用容器化部署可以隔离系统库依赖。