首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >GDB 调试日志:追踪一个 elusive 的栈溢出问题

GDB 调试日志:追踪一个 elusive 的栈溢出问题

原创
作者头像
熊猫钓鱼
发布2025-08-27 17:41:51
发布2025-08-27 17:41:51
4170
举报

GDB(GNU Debugger)是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许你比较喜欢图形界面方式的调试工具,如VC、BCB等IDE的调试器,但在UNIX平台下做开发,GDB这个命令行调试工具却有着图形化工具所不能比拟的强大功能。

GDB主要帮忙你完成下面四个方面的功能:

  1. 启动你的程序,可以按照你的自定义要求运行程序。
  2. 让被调试的程序在你所指定的断点处停住(断点可以是条件表达式)。
  3. 当程序被停住时,可以检查此时你的程序中所发生的事。
  4. 动态地改变你程序的执行环境。

使用GDB调试

代码语言:txt
复制
hchen/test> gdb tst <---------- 启动GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-suse-linux"...

(gdb) l <-------------------- l命令相当于list,从第一行开始列出源码。
1       #include <stdio.h>
2       
3       int func(int n)
4       {
5           int sum=0,i;
6           for(i=0; i<n; i++)
7           {
8               sum+=i;
9           }
10          return sum;
(gdb) <-------------------- 直接回车表示重复上一次命令
11      }
12      
13      
14      main()
15      {
16          int i;
17          long result = 0;
18          for(i=1; i<=100; i++)
19          {
20              result += i; 
(gdb) break 16 <-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看断点信息。
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048496 in main at tst.c:16
2       breakpoint     keep y   0x08048456 in func at tst.c:5
(gdb) r <--------------------- 运行程序,run命令简写
Starting program: /home/hchen/test/tst

Breakpoint 1, main () at tst.c:17 <---------- 在断点处停住。
17          long result = 0;
(gdb) n <--------------------- 单条语句执行,next命令简写。
18          for(i=1; i<=100; i++)
(gdb) n
20              result += i;
(gdb) n
18          for(i=1; i<=100; i++)
(gdb) n
20              result += i;
(gdb) c <--------------------- 继续运行程序,continue命令简写。
Continuing.

result[1-100] = 5050 <---------- 程序输出。

Breakpoint 2, func (n=250) at tst.c:5
5           int sum=0,i;
(gdb) n
6           for(i=1; i<=n; i++)
(gdb) p i <--------------------- 打印变量i的值,print命令简写。
$1 = 134513808
(gdb) n
8               sum+=i;
(gdb) n
6           for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8               sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6           for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函数堆栈。
#0  func (n=250) at tst.c:5
#1  0x080484e4 in main () at tst.c:24
#2  0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函数。
Run till exit from #0  func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24          printf("result[1-250] = %d \n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 继续运行。
Continuing.

result[1-250] = 31375 <---------- 程序输出。

Program exited with code 027. <-------- 程序退出,调试结束。
(gdb) q <--------------------- 退出gdb。
hchen/test>

场景:深陷崩溃的泥潭

项目中的一个核心数据处理服务在运行一段时间后,总会毫无征兆地发生 Segmentation fault (core dumped)。

最棘手的是,这个崩溃无法稳定复现,有时几小时,有时几天才出现一次。日志里除了这条冰冷的报错信息,几乎没有其他有用线索。

面对这种“玄学”问题,传统的 printf 大法效率极低。是时候请出终极武器——GDB,结合 Core Dump 文件进行事后分析了。

第一反应:保护现场,获取 Core Dump

问题发生后的第一要务是保存“犯罪现场”。我立刻在生产环境服务器上设置了 Core Dump 文件生成:

代码语言:txt
复制
# 检查当前core dump设置
ulimit -c
# 如果显示0,表示不生成core文件,需要解除限制
ulimit -c unlimited

# 指定core文件生成路径和命名格式(确保路径有写权限)
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

%e: 可执行文件名

%p: 进程PID

%t: 崩溃时间戳

这样,下次服务再崩溃时,就会在 /tmp/ 目录下生成一个包含丰富信息的 core 文件,例如 core-my_service-12345-1629780000。

操作步骤:开启侦探模式

重现与捕获:耐心等待,问题终于再次出现。我立刻将生成的 core 文件和对应的二进制程序(务必是当时编译的那个,且带 -g 选项!)备份到本地开发机。

启动调查:使用 GDB 加载程序和 core 文件,直接“穿越”到程序崩溃的那一刻。

代码语言:txt
复制
gdb ./my_service /tmp/core-my_service-12345-1629780000

查看案发现场:GDB 加载后,第一件事就是查看崩溃时的调用堆栈(Backtrace)

代码语言:txt
复制
(gdb) bt
#0  0x00007f8a5b1a5f25 in raise () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007f8a5b18f897 in abort () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007f8a5b1e2fba in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x00007f8a5b1ec4dc in __libc_message () from /lib/x86_64-linux-gnu/libc.so.6
#4  0x00007f8a5b1f3d4f in __fortify_fail () from /lib/x86_64-linux-gnu/libc.so.6
#5  0x00007f8a5b1f3d10 in __stack_chk_fail () from /lib/x86_64-linux-gnu/libc.so.6
#6  0x000055f8a9c2a1d3 in process_packet (data=0x7f8a4c00008c, len=1024) at src/packet_processor.c:147

关键线索出现了! 堆栈最顶层是 __stack_chk_fail,这是一个非常明确的信号:栈溢出(Stack Smashing) 或者栈保护机制被触发。而最后一条属于我们代码的帧指向了 process_packet 函数的第 147 行。

勘察局部环境:我立刻切换到 process_packet 函数的栈帧,并查看当时的局部变量和源代码上下文。

代码显示第 147 行是一个简单的赋值语句,看起来人畜无害。但 info locals 的输出显示,其中一个栈上的缓冲区 char buffer[1024] 的值看起来非常混乱,且其后的其他局部变量(如 int packet_id)的值也被篡改了。这几乎是栈溢出的铁证——某个操作写穿了 buffer,覆盖了相邻的变量和函数返回地址,最终被栈保护机制检测到并终止了进程。

深入疑点函数:问题肯定出在对 buffer 的操作上。我向上查看代码,发现一个可疑的调用:

代码语言:txt
复制
// src/packet_processor.c:143
parse_custom_header(buffer, incoming_data, incoming_data_len);

parse_custom_header 是一个解析协议头的函数,它接受 buffer 作为目标缓冲区。我需要深入这个函数。

代码语言:txt
复制
(gdb) disassemble /m parse_custom_header

通过反汇编并混合显示源码(/m 选项),我快速浏览了这个函数的逻辑。发现内部调用了 memcpy。

在定位到memcpy参数错误后,可使用GDB的硬件观察点验证溢出过程:

代码语言:txt
复制
(gdb) watch *buffer+1023  # 监控缓冲区边界
Hardware watchpoint 3: *buffer+1023
(gdb) run  # 重新运行程序触发故障

当程序执行到memcpy时,观察点会捕获越界写入:

代码语言:txt
复制
Hardware watchpoint 3: *buffer+1023
Old value = 0x00000000
New value = 0x41414141  # 'AAAA'(模糊测试注入的数据)

釜底抽薪:检查 memcpy 的参数:虽然无法在 core dump 中重新执行,但我可以检查传递给 memcpy 的参数值。我通过反汇编代码找到了 memcpy 调用点的地址,然后使用 print 命令检查当时寄存器中的参数。

代码语言:txt
复制
(gdb) p incoming_data_len
$1 = 1520
(gdb) p sizeof(buffer)
$2 = 1024

破案了! 源数据的长度是 1520 字节,而目标栈缓冲区 buffer 只有 1024 字节。memcpy 执行了缓冲区溢出,直接破坏了栈结构。

通过GDB的Python API自动提取关键参数:

代码语言:txt
复制
# .gdbinit中添加
define trace_memcpy
  set $dst = (char *)$arg0
  set $src = (char *)$arg1
  set $len = (size_t)$arg2
  printf "memcpy(dst=%p, src=%p, len=%zu)\n", $dst, $src, $len
end

break memcpy
commands
  silent
  trace_memcpy
end

核心思路与总结

这次调试经历完美体现了 GDB 的核心价值:“时空回溯”。

思路:面对随机崩溃,不要盲目猜测。首要任务是获取现场(Core Dump),然后像法医一样对其进行解剖分析(GDB)。分析的核心是:

bt(回溯):找到案发现场。

frame & info locals:勘察现场环境。

list & disassemble:追溯案发过程。

print:查验关键证据(变量值、参数大小)。

工具链:ulimit + core_pattern + gdb 是 Linux 下定位疑难杂症的黄金组合。

教训:

永远不要假设数据是安全的。即使协议规定长度,也要在代码中显式地进行边界检查。

使用更安全的函数:将 memcpy(dst, src, len) 替换为 memcpy(dst, src, min(len, dst_size)) 或者使用 snprintf 代替 sprintf。

让工具帮你发现问题:编译时加入 -fstack-protector-all(GCC) 等标志,让编译器插入栈保护代码,可以在溢出发生时第一时间崩溃并给出明确信号,而不是让数据污染继续传播导致更诡异的行为。

最终,修复方案就是在 memcpy 前增加一行简单的边界检查:

代码语言:txt
复制
// 修复后
size_t copy_len = incoming_data_len < sizeof(buffer) ? incoming_data_len : sizeof(buffer) - 1;
memcpy(buffer, incoming_data, copy_len);
buffer[copy_len] = '\0'; // 确保字符串终止

通过这次经历,我再次深刻体会到,GDB 远不止一个“单步调试器”。它更是一个强大的离线事故分析工具,能够将那些最隐蔽、最随机的问题瞬间定格、放大,让它们无处遁形。这种从 core dump 中抽丝剥茧、最终锁定真凶的过程,无疑是程序员能体验到的最具成就感的“破案”经历之一。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用GDB调试
    • 场景:深陷崩溃的泥潭
    • 第一反应:保护现场,获取 Core Dump
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档