2、GDB基础入门:启动GDB、加载程序、设置断点、运行、单步执行、查看变量与内存

GDB(GNU Debugger)是Linux/Unix环境下最经典的调试器,也是逆向工程与漏洞分析工作中不可或缺的基础工具。无论是分析一个崩溃的守护进程,还是定位缓冲区溢出漏洞的触发点,GDB都提供了从“程序外部观察”到“指令级控制”的完整能力。本章将围绕GDB最核心的操作闭环——启动、加载、断点、运行、单步、查看变量与内存——展开系统讲解,帮助读者建立扎实的调试基本功。

GDB 基础调试闭环 ① 启动与加载 gdb ./target file /path/to/binary ② 设置断点 break main break *0x401234 ③ 运行与暂停 run [args] Ctrl+C 中断 ④ 单步执行 next / step nexti / stepi ⑤ 查看变量与内存 print var / x/20gx $rsp info registers / p/x $rip ⑥ 分析判断 栈帧布局 / 寄存器状态 数据流 / 控制流 ⑦ 调整断点/继续调试 delete / disable / enable continue / until / finish 迭代循环:根据观察结果调整断点策略,重新运行或继续执行 核心能力:符号解析 · 断点管理 · 指令级控制 · 内存/寄存器查看 · 栈回溯 GDB 基础命令是后续反汇编分析(objdump/gef/pwndbg)与漏洞利用开发的前提

2.1 启动GDB与加载目标程序

核心原理:GDB通过ptrace系统调用与目标进程建立调试关系。启动时有两种典型方式:直接以gdb ./program方式启动,此时GDB会解析ELF文件中的符号表(.symtab)和调试信息(.debug_info),为后续的符号级调试(如按函数名设置断点)提供基础;另一种方式是gdb -p <pid>附加到已运行的进程,常用于调试服务型程序或分析正在运行的恶意样本。加载程序后,GDB并不会立即执行目标代码,而是停留在等待命令的状态,此时可以预先设置断点、修改环境变量或传递参数。

操作流程:

# 方式一:直接加载可执行文件
gdb ./vuln_program

# 方式二:带参数启动
gdb --args ./vuln_program arg1 arg2

# 方式三:附加到运行中的进程(需ptrace权限)
gdb -p 12345

# 在GDB内部加载文件(若已启动但未加载)
(gdb) file ./vuln_program

# 设置运行参数
(gdb) set args arg1 arg2

# 设置环境变量
(gdb) set env LD_PRELOAD=/path/to/lib.so

案例拆解:假设我们有一个存在栈溢出的程序stack_demo,需要分析其输入处理逻辑。启动时使用gdb -q ./stack_demo(-q表示安静模式,不显示版本信息)。此时GDB会显示类似Reading symbols from ./stack_demo...的提示。如果程序是strip过的(无符号表),GDB会提示(No debugging symbols found in ./stack_demo),此时只能使用地址级调试,这对后续漏洞分析是常见场景。

经验提醒:对于漏洞分析,建议保留符号表(编译时加-g选项),但也要习惯在无符号环境下通过info file、info functions等命令获取ELF段信息和导出函数。使用checksec(pwndbg/gef插件)可快速查看防护机制(NX、PIE、RELRO、Canary),这决定了后续利用策略。

2.2 设置断点:从源码行到指令地址

核心原理:断点的本质是在目标地址处将指令替换为int3(0xCC)软中断指令。当CPU执行到该地址时触发SIGTRAP信号,GDB捕获该信号后暂停程序,并恢复原始指令以便继续执行。GDB支持多种断点类型:源码行断点、函数名断点、地址断点、条件断点、硬件断点(针对数据访问)。在漏洞分析中,地址断点(break *0x401234)和条件断点(break func if var == 0x10)最为常用。

断点管理命令:

# 常用断点设置
(gdb) break main              # 在main函数入口
(gdb) break *0x401234         # 在地址处(反汇编后常用)
(gdb) break file.c:15         # 在源码第15行
(gdb) break func if len > 10  # 条件断点

# 查看与管理断点
(gdb) info breakpoints        # 列出所有断点
(gdb) delete 1                # 删除编号为1的断点
(gdb) disable 2               # 禁用编号为2的断点
(gdb) enable 2                # 重新启用

# 临时断点(命中一次后自动删除)
(gdb) tbreak main

实战场景:在分析strcpy调用时,我们往往希望在调用前查看源和目标地址。反汇编后得到call 0x401030 <strcpy@plt>,此时设置break *0x401030,运行后使用info registers查看rdi(目标)和rsi(源)寄存器值,即可确认拷贝方向与缓冲区边界。

风险说明:在共享库(如libc)中设置断点时,需注意ASLR(地址空间布局随机化)导致库加载地址每次运行不同。建议使用start命令(在main处暂停)后,通过info sharedlibrary查看库的实际加载基址,再计算目标函数地址。或者使用break *<function>@plt方式在PLT桩处下断。

2.3 运行控制:run、continue与单步执行

核心原理:GDB的运行控制基于信号与ptrace的暂停/恢复机制。run启动程序并运行至第一个断点或程序结束;continue从当前暂停点继续运行;单步执行则通过设置CPU的陷阱标志(TF)或使用ptrace(PTRACE_SINGLESTEP)实现指令级暂停。理解next与step的区别至关重要:next在源码级别执行一行,遇到函数调用时跳过(不进入函数体);step则会进入函数内部。对应的指令级命令是nexti(跳过调用指令)和stepi(进入调用指令)。

操作流程与对比:

命令 粒度 遇到函数调用时 典型用途
next 源码行 不进入,整行执行 快速跳过库函数调用
step 源码行 进入函数内部 跟踪自定义函数逻辑
nexti 单条指令 不进入call指令 反汇编调试时跳过函数
stepi 单条指令 进入call指令 分析函数序言/内部指令
until 源码行 运行至指定位置 跳出循环
finish 函数级 运行至当前函数返回 快速结束函数分析

案例拆解:在分析一个格式化字符串漏洞时,我们跟踪printf调用。使用stepi进入printf@plt,再使用x/10i $rip查看PLT桩指令,通过stepi跟踪跳转到libc中的实际实现。此时若使用nexti则会跳过整个printf执行,无法观察其内部对格式化字符串的解析过程。对于漏洞验证,通常使用next跳过printf,直接观察栈上数据变化。

调试技巧:在循环中调试时,until <line>可以运行至指定行,避免多次next。例如until 25表示运行至第25行。若需跳出当前函数,使用finish。对于无限循环,可先break循环条件判断处,再continue。

2.4 查看变量与内存:print、x与寄存器

核心原理:GDB通过读取目标进程的虚拟内存映射(/proc/pid/maps)和寄存器上下文来获取数据。print(简写p)按符号类型格式化输出变量值;x命令直接按指定格式查看内存内容;info registers显示所有通用寄存器。在漏洞分析中,查看内存布局(栈、堆、BSS段)和寄存器状态是判断溢出偏移、ROP链构建的基础。

常用查看命令:

# 查看变量(需有调试符号)
(gdb) print buf
(gdb) p &buf              # 查看变量地址
(gdb) p/x var             # 十六进制显示

# 查看内存(x/格式 地址)
(gdb) x/20gx $rsp         # 查看栈顶20个8字节(g=giant,8字节)
(gdb) x/20wx $rsp         # 以4字节十六进制查看
(gdb) x/10i $rip          # 查看当前指令附近10条汇编
(gdb) x/s 0x402000        # 查看字符串

# 查看寄存器
(gdb) info registers      # 所有通用寄存器
(gdb) p/x $rax            # 单个寄存器
(gdb) p/x $rsp-0x20       # 寄存器运算

# 查看栈回溯
(gdb) bt                  # backtrace,查看调用栈
(gdb) frame 1             # 切换到第1层调用帧

实战场景:假设程序在strcpy后崩溃,我们执行bt查看调用栈,发现main调用vuln_func。使用frame 1切换到vuln_func帧,info locals查看局部变量,x/20gx $rsp-0x40查看栈上缓冲区内容。通过对比输入数据在栈上的位置与返回地址的偏移,可精确计算溢出长度。

常见误区:使用print查看无符号表程序的全局变量会失败,此时需通过info proc mappings找到数据段基址,再结合反汇编中的偏移计算实际地址。另外,x命令的格式字符必须正确:g表示8字节,w表示4字节,b表示1字节,i表示指令。误用格式会导致数据解读错误,例如将8字节值拆成两个4字节查看。

2.5 综合案例:分析一个栈缓冲区溢出

案例背景:目标程序vuln存在一个简单的栈溢出漏洞,函数vuln_func使用gets()读取输入。程序编译时关闭了栈保护(-fno-stack-protector),但开启了NX(数据执行保护)。我们需要通过GDB确定溢出偏移,并验证能否控制返回地址。

初始分析:

# 启动GDB并加载
gdb -q ./vuln
(gdb) info functions          # 查看函数列表
(gdb) disassemble vuln_func   # 反汇编目标函数

反汇编结果显示gets调用位于0x4011a9,缓冲区起始地址为rbp-0x30(即48字节)。返回地址存储在rbp+8处,因此从缓冲区到返回地址的偏移为0x30 + 8 = 56字节。

验证过程:

# 在gets调用后设置断点
(gdb) break *0x4011a9
(gdb) run < input.txt        # 输入56字节'A' + 8字节'B'

# 查看栈布局
(gdb) x/20gx $rsp
0x7fffffffe3a0: 0x4141414141414141  0x4141414141414141
0x7fffffffe3b0: 0x4141414141414141  0x4141414141414141
0x7fffffffe3c0: 0x4141414141414141  0x4141414141414141
0x7fffffffe3d0: 0x4141414141414141  0x4242424242424242  <- 返回地址被覆盖

# 继续运行,观察崩溃点
(gdb) continue
Program received signal SIGSEGV, Segmentation fault.
0x0000424242424242 in ?? ()

结果分析:程序崩溃在0x42424242424242处,证实了偏移为56字节,且返回地址可控。由于NX开启,不能直接执行栈上shellcode,后续需考虑ROP链。通过info proc mappings查看libc基址,结合ROPgadget工具寻找gadget,最终构造ROP链调用system("/bin/sh")。

复盘经验:GDB基础命令的熟练度直接决定漏洞分析效率。建议将disassemble、x/、info registers、bt这四个命令组合使用,形成“反汇编→看寄存器→查栈→回溯调用”的标准分析流程。同时,善用GDB脚本(source script.gdb)自动化重复操作,例如在断点处自动打印关键寄存器值。

总结复盘:本章核心是建立GDB调试的闭环思维:启动加载→设置断点→运行暂停→单步追踪→查看状态→调整策略。对于漏洞分析,还需额外掌握:checksec查看防护、vmmap(pwndbg)查看内存映射、cyclic模式生成偏移量。GDB是动态分析的基石,后续章节的反汇编与漏洞利用都将基于这些基础命令展开。