普通断点抓不到内存覆盖问题,因其只停执行流而不监控内存写入;必须用硬件断点(如GDB的watch)在返回地址被写入的瞬间中断,关键是在函数入口准确获取并监控栈上该地址。

为什么普通断点抓不到内存覆盖问题
因为内存覆盖是写操作,而函数返回地址被改是覆盖的后果——等你单步到 ret 指令时,栈上返回地址早就被写坏了,源头早已过去。普通软件断点(如 break func 或行断点)只停执行流,不监控内存写入,完全错过篡改瞬间。
必须用硬件断点(Hardware Watchpoint),让 CPU 在指定内存地址被写入时立即中断。GDB 的 watch 命令底层就依赖 x86 的调试寄存器(DR0–DR3),对栈上返回地址位置设写监控最直接。
- 先得算出返回地址在栈上的确切地址:可在函数开头用
info registers rsp+x/2gx $rsp看栈顶,返回地址通常在$rsp或$rsp+8(x64,调用约定影响) - 不要对整个栈帧设 watch——太宽泛,会频繁触发;只盯准那个 8 字节地址(x64)或 4 字节(x86)
-
watch * (long long*)0x7fffffffeabc这种写法容易因地址未映射或权限问题失败,优先用watch *(char(*)[8])0x7fffffffeabc避免类型推导歧义
怎么在 GDB 中准确设置栈上返回地址的硬件写断点
关键不是“下断点”,而是“在正确时机、对正确地址、用正确尺寸”下。函数 prologue 一执行,call 指令就把返回地址压栈了,此时立刻读取并监控它。
- 在疑似出问题的函数入口处下断点:
break vulnerable_func - 运行到后,用
x/gx $rsp查看压入的返回地址值,再用x/16gx $rsp-0x20确认周围栈布局,排除 red zone 干扰 - 假设返回地址在
$rsp,执行:watch *(long long*)$rsp—— 注意必须带解引用*,否则监控的是寄存器值变化 - 如果提示
Cannot watch memory at address ...,说明地址不可写或不在当前映射页,试试watch *(char(*)[8])$rsp或降级为 4 字节:watch *(int*)$rsp(x86)
硬件断点触发后如何快速定位越界写来源
断下来时,watch 断点停在执行写操作的那条指令上,但这条指令未必是 bug 本身——可能是 memcpy、循环赋值、或某个看似合法的数组索引。重点看上下文。
立即学习“C++免费学习笔记(深入)”;
- 用
x/i $pc看触发写的汇编,注意是不是mov %rax,(%rdx)这类间接写,%rdx就是罪魁地址 - 用
info registers rdx rsi rdi rcx快速检查涉及地址计算的寄存器值 - 用
bt看调用栈,但注意:此时栈可能已被部分破坏,bt可能截断或错乱;更可靠的是info frame看当前栈帧基址和保存的rbp - 如果写操作来自库函数(如
strcpy),用finish返回到调用方,再结合源码检查传入的 src/dst 长度
常见失效场景和绕过方法
硬件断点不是万能的。x86 最多支持 4 个同时生效的硬件 watchpoint,且不支持监控物理地址或大页内存;某些优化后的内联函数会让返回地址位置不稳定。
- 若
watch提示Could not insert hardware watchpoint,说明调试寄存器已满——先info watchpoints,用delete N清掉不用的,或改用awatch(access watchpoint,读写都停,有时更稳) - ASLR 或栈随机化会导致每次运行返回地址不同,别硬编码地址;务必在每次 run 后重新
x/gx $rsp获取 - 函数被内联后没有独立栈帧,返回地址压在调用者栈上——得在调用点设断点,再查调用者的
$rsp - 实在抓不到,退一步用 AddressSanitizer 编译:
g++ -fsanitize=address,它会在越界写发生时直接报错并打印栈踪迹,比手动设硬件断点快得多
硬件断点有效,但前提是地址可访问、调试寄存器有空位、且你盯住了那个精确的字节。多数人卡在第一步:没确认返回地址当前到底在栈上哪个位置,就盲目 watch 一个猜测的地址——结果当然什么也抓不到。


















