3、GDB基础操作:启动、断点、单步与寄存器查看

各位好,我是你们的嵌入式安全讲师。今天咱们来聊聊GDB——这个在固件逆向中几乎天天都要打交道的调试器。

说实话,我刚开始接触GDB时也觉得它挺枯燥的,不就是个命令行调试工具嘛。但后来在一次路由器固件的漏洞分析中,我发现没有GDB,我根本没法定位那个堆栈溢出的精确位置。从那以后,我对GDB的态度就变了——它不只是工具,它是你在固件世界里的眼睛和手。

这一章,咱们就从头捋一遍GDB最核心的操作。你不需要记住所有命令,但下面这些,我建议你练到肌肉记忆。

3.1 GDB的启动与退出

启动GDB,说白了就两种场景:

  • 直接调试可执行文件gdb ./firmware.elf
  • 附加到正在运行的进程gdb -p 1234(1234是进程PID)

我个人习惯用第一种,因为固件逆向时我们通常没有完整的运行环境。但如果你在做嵌入式设备的动态调试,比如通过JTAG/SWD连接目标板,那第二种方式更常见。

小技巧: 启动时加 -q 参数可以跳过GDB的版本信息,直接进入调试界面。我每次都用 gdb -q ./target.elf,干净利落。

退出就简单了:输入 quitq,回车。如果调试的进程还在运行,GDB会问你是否要终止它。嗯,这里要注意——如果你只是挂载分析,不想影响目标进程,记得先 detach 再退出。

3.2 断点设置:硬件断点 vs 软件断点

断点是调试的灵魂。但很多人不知道,断点其实分两种。

软件断点(Software Breakpoint)

这是GDB默认的方式。原理很简单:GDB会把目标地址的指令替换成 0xCC(在x86上)或 BKPT(在ARM上)。当CPU执行到这里时,会触发一个异常,GDB就接管了控制权。

设置方法:

(gdb) break *0x08001234
(gdb) break main
(gdb) break firmware.c:42

我在项目中遇到过一个问题:有些固件会做代码完整性校验,比如CRC检查。软件断点一打上去,校验就失败了,程序直接崩溃。这时候就得用硬件断点。

硬件断点(Hardware Breakpoint)

硬件断点依赖CPU的调试寄存器,不修改代码内容。它直接在CPU内部设置一个地址比较器,当PC指向这个地址时,CPU自动暂停。

设置方法:

(gdb) hbreak *0x08001234
注意: 硬件断点数量有限。ARM Cortex-M系列通常只有4-6个,x86有4个。我曾经在调试一个多线程固件时,一口气设了8个硬件断点,结果后4个根本没生效——白忙活了一下午。

什么时候用硬件断点?我总结了三类场景:

  • 固件有代码校验机制
  • 调试ROM中的代码(不可写)
  • 需要在中断服务程序(ISR)中设断点

3.3 单步执行与继续执行

断点命中后,怎么控制程序走?

命令 作用 我的用法
stepi / si 单步执行一条汇编指令 逆向时几乎只用这个
nexti / ni 单步执行,遇到函数调用不进入 跳过库函数时用
step / s 单步执行一行C代码 有源码时才用
continue / c 继续执行,直到下一个断点 最常用

你想想看,在固件逆向中,我们面对的是赤裸裸的汇编代码。所以我个人建议:忘掉 step,记住 stepi。每次只走一条指令,你才能看清程序到底在干什么。

有一次我在分析一个加密算法的固件实现,用 stepi 一步步跟了300多步,终于发现了那个藏在循环里的常数——原来是个白盒AES的S盒。如果当时用 continue 跳过去,这个关键线索就丢了。

3.4 查看与修改寄存器/内存

调试的核心就是看状态、改状态。GDB给了我们两把刷子。

查看寄存器

(gdb) info registers    # 查看所有寄存器
(gdb) info registers r0 r1 pc  # 只看指定的几个
(gdb) print $r0         # 打印r0的值
(gdb) print/x $r0       # 十六进制显示

我习惯先看 pc(程序计数器)和 sp(栈指针),这两个寄存器能告诉你程序当前在哪儿、栈还够不够用。在固件漏洞分析中,pc 被篡改往往意味着控制流劫持攻击成功了。

查看内存

(gdb) x/10x 0x20001000   # 查看10个十六进制字
(gdb) x/20i $pc          # 查看当前PC附近的20条指令
(gdb) x/s 0x08001000     # 查看字符串

x 命令的格式是 x/[数量][格式][单位] 地址。格式有 x(十六进制)、d(十进制)、i(指令)、s(字符串)。单位有 b(字节)、h(半字)、w(字)、g(双字)。

实战经验: 在分析固件中的字符串时,我常用 x/s 配合 find 命令。比如 find /b 0x08000000, 0x08100000, "password",可以快速定位硬编码的敏感信息。

修改寄存器与内存

(gdb) set $r0 = 0x1234   # 修改寄存器
(gdb) set {int}0x20001000 = 0xdeadbeef  # 修改内存
(gdb) set {char[4]}0x20001000 = "ABCD"  # 修改字符串

为什么要修改?说白了,就是绕过某些检查。我曾经调试过一个固件,它在启动时检查某个标志位,如果不对就直接死循环。我用 set $r3 = 1 直接把这个标志位改了,程序顺利跑过了初始化——省去了逆向整个校验算法的功夫。

但这里要提醒一句:修改寄存器或内存后,程序的状态可能不一致。你跳过了检查,但后续代码可能依赖这个检查的结果。所以,改完之后要仔细观察程序行为,别让一个简单的 set 把你带沟里了。

知识体系总览

下面这张图,是我自己整理的GDB基础操作框架。每次带新人时,我都会先让他们看这张图——把整体脉络理清了,细节学起来才不迷糊。

GDB基础操作知识体系 GDB调试核心 启动与退出 gdb ./target.elf gdb -p PID quit / detach 断点设置 软件断点:break *addr 硬件断点:hbreak *addr 条件断点:break if cond 临时断点:tbreak 单步与继续 stepi / si(汇编级) nexti / ni(跳过调用) continue / c(继续) 寄存器与内存 info registers x/10x addr(查看内存) set $r0 = val(修改) set {int}addr = val 掌握这些,入门GDB

这张图把GDB基础操作分成了四个模块。你不需要一次全记住,但每次调试时,可以对照着看看自己卡在哪一步。我个人建议:先从断点和单步执行练起,这两个用得最频繁。

好了,这一章的内容就到这儿。GDB的操作远不止这些,但上面这些——启动退出、断点、单步、寄存器内存——是你在固件逆向中每天都要用的基本功。多练几次,形成肌肉记忆,后面分析复杂固件时才能游刃有余。


专注资料整理