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,干净利落。
退出就简单了:输入 quit 或 q,回车。如果调试的进程还在运行,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
什么时候用硬件断点?我总结了三类场景:
- 固件有代码校验机制
- 调试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的操作远不止这些,但上面这些——启动退出、断点、单步、寄存器内存——是你在固件逆向中每天都要用的基本功。多练几次,形成肌肉记忆,后面分析复杂固件时才能游刃有余。