






















当你进入汇编级别(Assembly level)的调试时,通常是因为 C 源代码已经无法解释当前的现象(比如你遇到的栈被踩烂、寄存器污染或 ??() 这种断层)。
在 GDB 中,从“源码级”切换到“汇编级”调试,主要依靠以下三板斧:
不要一行行输入命令,直接进入 GDB 的 TUI (Text User Interface) 模式,让你边看代码边看汇编。
layout split: 同时显示源代码和汇编代码。
layout regs: 显示寄存器状态和汇编代码。
focus asm: 将键盘焦点切换到汇编窗口,方便翻页。
layout next:切换到下一个布局方案layout prev:切换到上一个布局方案小技巧:在 TUI 模式下,当前执行的那一行汇编会被高亮。配合
stepi极其直观。
在 GDB 的 TUI(文本用户界面)模式中,想要退出 layout asm 或关闭图形窗口回到纯命令行模式,有几种常用的方式:
1). 彻底退出 TUI 模式
如果你想关闭所有窗口(汇编、寄存器等),回到最原始的命令行界面:
快捷键: Ctrl + X 紧接着按 A
这是一个开关操作:如果当前在 TUI 模式,按它可以退出;如果在命令行模式,按它可以重新进入上一次的布局。
2). 切换回源码布局
如果你只是不想看汇编了,想切回 C/C++ 源码界面:
命令: layout src
注意:这需要你的程序在编译时带了
-g选项,否则窗口会显示找不到源码。
常见问题:界面乱了怎么办?
在 TUI 模式下,如果程序运行输出(printf)污染了汇编窗口,导致界面字符错位,不需要退出,只需输入:
refresh 或者按下 Ctrl + L
这会重新绘制屏幕,修复显示乱码。
提个醒: 如果你发现按方向键无法查看历史命令,而是移动了汇编窗口,可以输入 focus cmd 将输入焦点切回命令行窗口。
在汇编调试中,传统的 n (next) 和 s (step) 颗粒度太粗了,你需要:
stepi (或 si): 执行一条机器指令。如果这行指令是 call,它会跳进函数内部。
nexti (或 ni): 执行一条机器指令。如果这行指令是 call,它会直接执行完该函数,停在下一条指令。
until *0x地址: 运行到指定的内存地址处停下。
既然是汇编,寄存器就是你的变量。
info registers (或 i r): 查看所有通用寄存器的值。
p $rax: 打印特定寄存器(如 rax)的值。在 64 位系统中,通常 rax 存放函数返回值。
display /i $pc: 每次停下时,自动显示下一条要执行的汇编指令。
x/gx $rsp: 检查栈顶。对于调试递归引起的 ??(),这是看谁把返回地址改了的唯一方法。
当你遇到 ??() 且 bt 失效时,在汇编级你可以这样排查:
(gdb) p /x $rip
如果 rip 指向了一个不属于代码段的地址(比如 0x0 或一个巨大的随机数),说明你的程序执行了 ret 指令,但从栈上弹出的返回地址是错的。
使用 disassemble 查看当前函数的开头:
(gdb) disassemble my_func
你会看到类似这样的指令:
push %rbp
mov %rsp,%rbp
sub $0x40,%rsp <-- 这里在分配栈空间
如果递归太深,执行到 sub $0x40,%rsp 后,栈指针 $rsp 可能会越界进入不可写的内存页,直接触发 SIGSEGV。
disassemble /m: 将汇编代码和源代码穿插显示(非常适合理解 C 代码如何转译成机器码)。
disassemble /r: 显示原始的十六进制机器码(Opcode)。
set disassembly-flavor intel: 如果你看补惯 AT&T 格式(mov %src, %dest),可以切换到 Intel 格式(mov dest, src)。
如递归问题,在汇编层级看其实就是:
大量的 push %rbp 将旧的帧指针压栈。
不断的 call 指令跳转回函数开头。
最终 $rsp 撞到了内存管理的墙(Guard Page)。
进阶挑战: 下次在 GDB 里,试着在递归崩溃前执行 x/5i $pc,看看崩溃那一刻 CPU 到底是在执行 push(压栈失败)还是 mov(访问非法地址)。
参考资料:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。