栈溢出首先破坏栈帧中存储的寄存器值(如rbp、rip),因溢出数据覆盖相邻栈空间;仅当访问未映射内存或越权地址时,mmu才触发段错误。

为什么栈溢出会导致寄存器损坏而不是直接段错误
栈溢出本身不一定会立刻触发 Segmentation fault,尤其在多线程场景下:每个线程有独立栈(通常 2MB 或 8MB),但内核只在栈“触底”时才映射新页;若溢出发生在已映射栈页内,就可能默默覆盖相邻内存——包括该线程的 %rbp、%rsp 甚至 %rax~%r15 的保存区(比如被 push 指令压入的寄存器值)。更隐蔽的是,溢出可能破坏当前栈帧的 %rbp 链,导致 bt 显示错乱、info registers 中 %rbp 和 %rsp 差值异常小或为负。
用 gdb 定位多线程栈溢出的三步关键操作
不要依赖崩溃后的 bt——它可能已不可信。必须在疑似线程运行中主动检查:
- 先用
info threads确认所有线程状态,重点关注running或blocked但栈深度异常大的线程 - 对可疑线程执行
thread <n></n>切换,再运行info frame查看Stack level和sp值;对比info proc mappings中该线程栈地址范围(如0x7ffff7a00000-0x7ffff7c00000),若$rsp接近下界(如0x7ffff7a01234),说明栈已几乎耗尽 - 用
x/16xg $rsp向上查看栈顶内存,如果发现大量重复值(如全0x0000000000000000或0xdeadbeefdeadbeef)、非指针值混杂在本该是返回地址的位置,基本可判定栈被写坏
watch 对栈指针和 canary 的实际效果很有限
有人想对 $rsp 或 canary 地址设 watch,但现实很骨感:
-
watch $rsp在 x86_64 上几乎无效——$rsp每条指令都可能变,GDB 会频繁中断,根本无法推进 - canary 存在栈上,但位置随函数变化(如
%rbp-8),且 GCC 默认启用-fstack-protector-strong,不是每个函数都插 canary;即使有,watch *(char**)(($rbp)-8)也需先确保$rbp本身没被破坏,否则地址计算就错了 - 真正有效的做法是:编译时加
-fstack-check(插入每页校验)或运行时用ulimit -s限制栈大小,让溢出更快暴露为SIGSEGV
避免误判:区分栈溢出与栈破坏的典型痕迹
栈溢出(stack overflow)和栈破坏(stack corruption)常被混淆,但调试线索完全不同:
- 栈溢出:崩溃前线程长时间高 CPU、
info threads显示某线程running却无日志输出;cat /proc/<pid>/maps | grep stack</pid>可见栈区域远大于默认值(说明被mmap扩展过) - 栈破坏(如 buffer overrun):崩溃点常在
__stack_chk_fail,bt能清晰看到上层函数(如func1),且disassemble func1可见校验逻辑;此时应重点查func1内局部数组访问是否越界 - 寄存器损坏若伴随
$rbp == $rsp或$rbp指向非法地址(如0x1),大概率是栈帧链断裂,而非单纯溢出——要检查是否有setjmp/longjmp或信号处理函数干扰了栈布局
真正难啃的是那种“栈没溢出但寄存器值莫名改变”的情况:它往往藏在信号 handler 里修改了备用栈,或第三方库用了 makecontext/swapcontext 却没清理寄存器。这时候 info registers 的输出必须和 disassemble 当前指令逐字节对齐,否则一个 mov %rax,%rbp 就可能让你以为 %rax 是无辜的。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











