本文仅用于技术研究,禁止用于非法用途。
Author:枷锁
零基础 Pwn 学习指南:手把手教你用 GDB 调教 C 程序
如果你刚刚踏入网络安全、尤其是 Pwn(二进制漏洞利用)或逆向工程的大门,你一定会频繁听到一个名字------GDB (GNU Debugger)。它就像 Pwn 选手的"第一把手术刀",能帮你剖开二进制程序的"五脏六腑",看清程序运行时的内存、寄存器、指令流转,甚至能"篡改"程序运行逻辑,为后续漏洞挖掘和利用打下最坚实的基础。
很多小白第一次打开 GDB 时,面对黑乎乎的终端和毫无生机的命令行,内心是崩溃的:不知道输什么命令、看不懂输出的乱码、程序一跑就结束,完全无从下手。别怕!今天这篇博客,我们就用最简单的大白话,抛弃复杂的理论堆砌,加上一个专门为零基础新手定制的"靶机程序",带你从零开始,敲下每一个命令,看懂每一行输出,一步一步上手,轻松拿捏 GDB 的基本操作,迈出 Pwn 学习的第一步。
一、 准备工作:打造我们的"靶机"
学 GDB 光看不练是没用的,就像学外科手术不能只看视频不碰手术刀一样。我们需要一个简单、无多余干扰、能清晰展示程序运行流程的"小白鼠"------也就是我们的测试程序(靶机),后续所有操作都围绕它展开,确保你每一步都能看到明确的效果。
1. 创建并编写测试程序 test.c
首先,你需要一个 Linux 环境(这是 Pwn 学习的基础,没有 Linux 环境的同学,优先安装 Ubuntu 20.04/22.04 或 Kali Linux,虚拟机、WSL 都可以,这里不展开环境搭建,后续可单独出一期教程)。
打开你的 Linux 终端(Ubuntu、Kali 等都可以),默认会进入用户目录(路径一般是 /home/你的用户名/),我们就在这个目录下操作,避免复杂路径带来的麻烦。
如果你不知道怎么写代码文件,完全不用慌,跟着我做就行,我们用最简单的 nano 编辑器(系统默认自带,无需额外安装),输入以下命令打开编辑器并创建 test.c 文件:
nano test.c
按下回车后,终端会变成一个简洁的文本编辑器(没有多余按钮,纯键盘操作)。此时,把下面这段非常简单的 C 语言代码复制并粘贴进去(注意:复制时不要漏行,尤其是头文件和函数括号,新手最容易栽在语法错误上):
c
#include <stdio.h>
// 一个简单的加法函数,模拟"隐藏功能"
void secret_func(int a) {
int b = a + 100; // 计算a+100,模拟程序内部逻辑
printf("Secret value is: %d\n", b); // 输出计算结果
}
int main() {
int target = 10; // 定义一个变量,赋值为10
printf("Hello GDB!\n"); // 打印提示信息,标记程序执行节点
// 调用隐藏函数,传递target作为参数
secret_func(target);
printf("Program finished.\n"); // 程序结束提示
return 0; // 程序正常退出
}

保存退出步骤(重点!新手必看)
粘贴完成后,不要直接关闭终端,按以下步骤保存并退出 nano 编辑器,否则代码会丢失:
-
按下键盘上的 Ctrl + O(注意:是字母 O,不是数字 0),此时底部会出现"File Name to Write: test.c"的提示,这是让你确认保存的文件名。

-
直接按 Enter(回车) 确认(不要修改文件名,否则后续编译会找不到文件),底部会提示"Wrote X lines",说明保存成功。
-
按下键盘上的 Ctrl + X,退出 nano 编辑器,回到我们熟悉的"黑乎乎"的终端命令行。
退出后,输入 ls 命令(列出当前目录下的文件),如果能看到"test.c"这个文件,说明第一步已经成功了!

2. 编译程序(Pwn 选手的必修课,重点!)
我们刚才写的 test.c 是"文本代码",计算机看不懂,需要通过编译器(gcc)把它转换成"二进制可执行程序",才能运行和调试。这一步是 Pwn 学习的核心基础------后续我们调试的,都是编译后的二进制程序,而编译参数的设置,直接影响调试效果。
在终端里输入以下"编译命令"(建议手动敲一遍,加深记忆,复制也可以,但要注意符号不要漏):
gcc -g -fno-stack-protector -no-pie test.c -o test

按下回车后,如果终端什么都没显示,直接弹出了新的一行命令提示符(比如 user@kali:~$),恭喜你,说明编译完美成功!此时再输入 ls 命令,目录下会多一个绿色的(Kali 中)或白色的(Ubuntu 中)叫 test 的文件,这就是我们最终要调试的"靶机程序"。

如果编译时出现报错(比如"error: expected ';' before '}' token"),大概率是你粘贴代码时漏了括号、分号,回到 nano 编辑器(nano test.c)检查代码,修正后重新编译即可。
魔法参数深度解析(新手必懂,避免后续踩坑)
刚才的编译命令中,-g、-fno-stack-protector、-no-pie 这三个参数不是随便加的,它们是 Pwn 新手调试的"黄金参数",每一个都有不可替代的作用:
-
-g:灵魂参数! 告诉编译器"把源代码的信息(比如行号、变量名)保留在编译后的二进制程序里"。没有它,你在 GDB 里只能看到天书般的机器码(全是 0x 开头的十六进制数),根本对应不上我们写的 C 代码;有了它,你才能直接在 GDB 里看 C 语言源码、按行调试、查看变量值------这是零基础新手能看懂 GDB 的关键。
-
-fno-stack-protector:关闭栈保护。栈保护是编译器的一种安全机制,会在程序运行时检测栈溢出(Pwn 中最常见的漏洞),一旦检测到就会终止程序。新手初期的调试目标是"看懂程序运行逻辑、篡改数据",开启栈保护会增加不必要的干扰,关掉它,让程序更"听话"。
-
-no-pie:关闭地址随机化。PIE(位置无关可执行程序)会让程序每次运行时,代码、变量的内存地址都随机变化。这对 Pwn 新手来说是"灾难"------你这次调试找到的变量地址,下次运行就变了,根本无法重复调试。关掉它后,程序每次运行的内存地址都是固定的,你找到的地址可以反复使用,方便我们做实验、验证想法。
注意:这三个参数仅适用于新手调试练习!在真实的 CTF Pwn 比赛中,题目给出的二进制程序,通常是开启栈保护、开启 PIE 的(增加漏洞利用难度),后续我们学会基础操作后,会专门讲解如何应对这些保护机制。
二、 GDB 基础四步曲:进门、定身、步进、透视
编译好靶机程序后,我们正式进入 GDB 的世界!深呼吸,不用紧张,GDB 的核心操作就4步:进门(启动 GDB)、定身(设断点)、步进(控制程序执行)、透视(查看/篡改数据)。我们一步步来,每一步都敲命令、看效果,确保你能跟着做、能看懂。
首先,启动 GDB 并加载我们的靶机程序,在终端输入以下命令:
gdb ./test

按下回车后,屏幕上会刷出一大段版权免责声明(全是英文,不用看,直接忽略)。只要你在最后一行看到了 pwndbg> 这个提示符,并且光标在闪烁,就说明你已经成功进入了 GDB 的世界!接下来,所有操作都在 pwndbg> 后面输入命令,按回车执行。

第一步:进门与查看 (List) ------ 摸清手中的"牌"
进入 GDB 后,不要盲目输入 run 让程序跑起来(否则程序会瞬间执行完,你什么都看不到)。我们先看看我们的"底牌"------也就是我们写的 C 源代码,确认 GDB 能正常识别源码(这就是 -g 参数的作用)。
在 pwndbg> 后面输入 list(或者简写l),然后回车:
pwndbg> list
此时 GDB 会输出我们的 C 代码,默认显示10行:
plain
1 #include <stdio.h>
2
3 // 一个简单的加法函数,模拟"隐藏功能"
4 void secret_func(int a) {
5 int b = a + 100; // 计算a+100,模拟程序内部逻辑
6 printf("Secret value is: %d\n", b); // 输出计算结果
7 }
8
9 int main() {
10 int target = 10; // 定义一个变量,赋值为10

小技巧
- 如果想继续往下看后续代码,直接再按一次回车键,GDB 会自动重复上一个命令(list)或者(l),显示下10行代码,直到代码结束。

- 如果想查看指定行附近的代码,输入 list 行号(比如 list 14),GDB 会显示第14行附近的代码。

- 如果输入 list 后,GDB 提示"No symbol table is loaded. Use the "file" command.",说明你编译程序时忘了加 -g 参数!重新编译(加上 -g)后,再启动 GDB 即可。
第二步:定身术 (Breakpoints 设断点) ------ 让程序"停在"你想查看的地方
如果我们现在输入 run(或简写 r)让程序跑起来,它会瞬间执行完所有代码,打印出结果然后退出,我们根本来不及查看中间过程。所以,我们需要给程序"下命令",让它在关键位置停下来------这就是断点(Breakpoint),相当于给程序装了一个"暂停键"。
我们先在 main 函数开头下一个断点(main 是程序的入口,所有程序都会从 main 函数开始执行),确保程序一开始运行就停下来,方便我们一步步跟踪。
在 pwndbg> 后面输入 break main(或简写 b main),按回车:
pwndbg> b main
GDB 会输出:
Breakpoint 1 at 0x401146: file test.c, line 10.

输出解读
这句话的意思是:成功在 test.c 的第 10 行(int target = 10;)设置了一个断点,断点编号是 1(后续管理断点会用到这个编号)。这里的 0x401146 是该代码对应的机器码地址(新手暂时不用关注)。
接下来,我们再在 secret_func 函数内部下一个断点,方便我们查看函数的执行过程。我们选择第5行(int b = a + 100;),输入 b 5(简写,全称是 break 5):
pwndbg> b 5
GDB 会输出:
Breakpoint 2 at 0x401165: file test.c, line 5.

此时,我们已经设置了两个断点:编号1(main 函数第10行)、编号2(secret_func 函数第5行)。
断点管理小贴士(新手必会)
- 查看所有断点:输入 info breakpoints(简写 i b),GDB 会列出你设置的所有断点,包括断点编号、位置、状态等。

- 删除断点:如果断点下错了(比如下到了不存在的行号),输入 delete 断点编号(简写 d 断点编号),比如 d 1,就可以删除编号为1的断点。如果想删除所有断点,输入 delete(不加编号)即可。

- 禁用/启用断点:如果不想删除断点,只是暂时不用,输入 disable 断点编号(禁用)、enable 断点编号(启用),比如 disable 2,编号2的断点就会暂时失效,程序不会在那里停下来。
现在,断点已经设置好了,我们让程序跑起来!输入 run(或简写 r):
pwndbg> r
GDB 会输出:
Starting program: /home/user/test
Breakpoint 1, main () at test.c:10
10 int target = 10;


看!程序被死死地定在了第10行,并且 GDB 高亮显示了"即将要执行"的这一行代码。这说明我们的断点生效了------程序从 main 函数开始执行,遇到第10行的断点,就暂停了,等待我们的下一步指令。
第三步:步步为营 (Next / Step / Continue) ------ 掌控程序的执行节奏
程序停住了,接下来我们就要"指挥"它一步步执行,看清每一步的变化。目前程序停在第10行(int target = 10;),这一行代码还没有执行(GDB 显示的是"即将执行"的行),我们接下来通过三个核心命令,掌控程序的执行节奏:Next(单步跳过)、Step(单步步入)、Continue(继续执行)。
1. 单步跳过(遇到函数不进去):next (简写 n)
next 命令的作用是:执行当前行代码,然后停在下一行代码。如果当前行是"调用函数"(比如 secret_func(target)),next 会直接执行完整个函数,跳过函数内部的细节,直接停在函数调用后的下一行------适合我们不想看函数内部、只想跟踪整体流程的场景。
现在,我们输入 n 并回车:
pwndbg> n
GDB 会输出:
11 printf("Hello GDB!\n");

解读:程序执行了第10行代码(把 10 赋值给变量 target),然后停在了第11行(即将执行 printf 语句)。
再按一次 回车键(GDB 自动重复上一个命令 n):
pwndbg>回车
此时,终端会打印出:
Hello GDB!

同时,GDB 会显示:
14 secret_func(target);

解读:程序执行了第11行的 printf 语句(所以打印出了"Hello GDB!"),然后停在了第14行(即将调用 secret_func 函数)。
2. 单步步入(遇到函数钻进去):step (简写 s)
现在,程序停在了第14行(secret_func(target);),这一行是调用 secret_func 函数。如果我们继续输入 n,程序会直接执行完 secret_func 函数的所有代码,跳过内部细节;但我们想进去看看函数内部是怎么执行的、变量 a 和 b 的值是怎么变化的------这时候就需要用 step 命令。
step 命令的作用是:执行当前行代码,如果当前行是调用函数,就钻进函数内部,停在函数的第一行可执行代码------适合我们想查看函数内部逻辑、跟踪变量变化的场景。
输入 s 并回车:
pwndbg> s
GDB 会输出:
Breakpoint 2, secret_func (a=10) at test.c:5
5 int b = a + 100;


这样我们不仅成功钻进了 secret_func 函数内部,GDB 还贴心地告诉我们:传进来的参数 a 的值是 10(这就是我们在 main 函数中定义的 target 的值)。同时,因为我们之前在第5行设置了断点,程序刚好停在了第5行(即将执行 int b = a + 100;)。

小区别:next 和 step 的核心区别的是"是否进入函数"。新手可以记住:想跳过函数,用 next;想进入函数,用 step。
3. 继续狂奔:continue (简写 c)
如果代码很长,我们不想一步步用 next/step 执行(太麻烦),可以用 continue 命令:解除单步状态,让程序继续全速运行,直到遇到下一个断点,或者程序正常结束。
比如现在,我们已经看完了 secret_func 函数的第5行,想让程序继续执行,直到下一个断点(如果没有下一个断点,就执行到结束),输入 c 并回车即可。后续我们篡改变量值后,会用到这个命令。

第四步:火眼金睛 (Print / Set / X) ------ Pwn 的核心!查看并篡改数据
程序停住的最大意义,不是"看代码",而是"看数据"------查看程序运行时变量、内存、寄存器的值,甚至"篡改"这些值,让程序按照我们的想法运行。这就是 Pwn 的核心思维:掌控程序的运行逻辑和数据。
现在,我们的程序停在第5行(int b = a + 100;),重点注意:停在这一行,意味着这一行代码还没有执行(b 还没有被赋值,当前的值是内存中的垃圾值)。我们接下来用三个核心命令,实现"查看数据"和"篡改数据"。
1. 查看变量的值:print (简写 p)
print 命令是 GDB 中最常用的命令之一,作用是:查看指定变量、表达式的值,支持直接计算表达式(比如 a+100)。
实操演练
-
查看参数 a 的值:输入 p a(简写,全称 print a),按回车:
pwndbg> p a
GDB 输出:$1 = 10

解读:1 是 GDB 给这个查询结果分配的编号(后续可以用 1 直接引用这个值),a 的值是 10(和我们在 main 函数中传递的 target 的值一致)。
-
查看变量 b 的值:输入 p b,按回车:
pwndbg> p b
GDB 输出:$2 = 0(不同环境下,这个数字可能不一样,比如 0xfffffff、4194304 等)

解读:因为第5行代码还没有执行(b 还没有被赋值为 a+100),所以 b 的值是内存中的"垃圾值"(随机值),这是正常现象。
-
执行第5行代码,再查看 b 的值:输入 n(单步跳过,执行第5行),按回车,GDB 会停在第6行(printf("Secret value is: %d\n", b);),然后输入 p b:
pwndbg> n
6 printf("Secret value is: %d\n", b);

pwndbg> p b
GDB 输出:$3 = 110

解读:第5行代码执行后,b 被赋值为 a+100(10+100=110),所以此时 b 的值是 110,和我们预期的一致。
小技巧
print 命令支持直接计算表达式,比如输入 p a+200,GDB 会直接输出 210;输入 p &a(& 是取地址符号),会输出变量 a 在内存中的地址(后续查看内存会用到)。
2. 神技:动态修改变量的值:set ------ 让程序"听话"
这是新手最容易兴奋的一步!print 只能"查看"数据,而 set 命令可以"篡改"数据------在程序运行时,直接修改变量、内存、寄存器的值,让程序按照我们的想法执行,而不用修改源代码、重新编译。这就是 Pwn 漏洞利用的核心思路之一:篡改程序运行时的数据,劫持程序执行流程。
实操演练(开挂篡改 b 的值)
现在,b 的值是 110(10+100),假设这是一个游戏的分数,我们觉得太低了,想"开挂"把它改成 9999,怎么做?
-
在 GDB 中直接输入以下命令(注意格式:set var 变量名 = 新值):
pwndbg> set var b = 9999
没有任何输出,说明修改成功(GDB 中,成功执行的命令通常不提示,只有错误才会提示)。
-
验证修改结果:输入 p b,查看 b 的值:
pwndbg> p b
GDB 输出:$4 = 9999

完美!我们已经成功篡改了变量 b 的值,从 110 改成了 9999。
-
让程序继续执行,查看最终结果:输入 c(continue),让程序继续运行,直到结束:
pwndbg> c
GDB 输出:
Continuing.
Secret value is: 9999
Program finished.
[Inferior 1 (process 5566) exited normally]

看!终端打印出来的 Secret value 变成了 9999,而不是原来的 110!这说明我们的篡改生效了------程序按照我们修改后的值执行了,而不是按照源代码中的逻辑(a+100)执行。
恭喜你!你已经完成了人生中第一次简单的内存数据篡改!这就是 Pwn 的本质之一:通过调试工具,篡改程序运行时的数据,让程序按照你的想法运行。后续我们学习栈溢出、格式化字符串等漏洞,本质上都是"更高级的篡改"------篡改程序的指令指针(rip),让程序执行我们写的恶意代码。
3. Pwn 选手的透视眼:Info / X ------ 查看寄存器和内存
在 Pwn 调试中,只看变量名是不够的------变量名是编译器给我们的"便利",而程序真正运行时,数据是存在内存中的,指令是存在 CPU 寄存器中的。Pwn 选手需要直接和内存、寄存器打交道,所以我们需要掌握两个核心命令:info registers(查看寄存器)、x(查看内存)。
我们先重新启动 GDB 调试(因为刚才程序已经执行结束了),输入 r(run),程序会再次停在我们下好的 main 函数断点处(第10行)。

(1)摸清 CPU 寄存器:info registers (简写 i r)
寄存器是 CPU 中用来临时存储数据和指令的"高速缓存",程序运行时,所有操作都会通过寄存器完成。Pwn 的终极目标(比如栈溢出),就是劫持 rip 寄存器(指令指针寄存器),让它指向我们的恶意代码。
在 pwndbg> 后面输入 info registers(简写 i r),按回车:
pwndbg> i r
GDB 会输出所有寄存器的值(不同环境下,地址会不一样,正常现象):

plain
rax 0x40118a 4198794
rbx 0x0 0
rcx 0x403e18 4210200
rdx 0x7fffffffde78 140737488346744
rsi 0x7fffffffde68 140737488346728
rdi 0x1 1
rbp 0x7fffffffdd50 0x7fffffffdd50
rsp 0x7fffffffdd40 0x7fffffffdd40
r8 0x7ffff7e1bf10 140737352154896
r9 0x7ffff7fc9040 140737353912384
r10 0x7ffff7fc3908 140737353890056
r11 0x7ffff7fde660 140737353999968
r12 0x7fffffffde68 140737488346728
r13 0x40118a 4198794
r14 0x403e18 4210200
r15 0x7ffff7ffd040 140737354125376
rip 0x401196 0x401196 <main+12>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
新手必记的3个核心寄存器(重中之重)
-
rip(指令指针寄存器):最核心的寄存器!它永远指向程序"下一步要执行的机器码地址"。程序的执行流程,就是由 rip 控制的------rip 指向哪里,CPU 就执行哪里的指令。Pwn 漏洞利用的终极目标,就是修改 rip 的值,让它指向我们的恶意代码。
-
rsp(栈顶指针寄存器):记录了当前"栈"的最高位置(栈是程序运行时存储局部变量、函数参数的地方,比如我们的 target、a、b 变量,都存在栈里)。后续学习栈溢出时,我们会频繁和 rsp 打交道,计算栈的偏移量。
-
rbp(栈底指针寄存器):记录了当前栈的最低位置,和 rsp 一起构成"当前栈帧"(可以理解为"当前函数的内存空间"),用来维护函数调用时的上下文。
(2)终极透视万能公式:查看内存 x 命令
变量是"包装好的内存",而 x 命令可以直接"撕开包装",查看内存中的原始数据------这是 Pwn 选手最常用的命令之一,尤其是在没有源码、只能看机器码的时候,x 命令能帮我们摸清程序的内存布局。
x 命令的万能公式(记死!):x/nfu <内存地址>
每个参数的含义(用大白话解释,拒绝专业术语):
-
n:你想查看"几个单位"的内存(比如 1 个、5 个)。
-
f:用什么"格式"查看内存(新手常用3种:x=十六进制、s=字符串、i=汇编指令)。
-
u:每个"单位"的大小(新手常用3种:b=1字节、w=4字节、g=8字节)。
-
<内存地址>:你想查看的内存地址(可以是变量的地址、寄存器的值,比如 rsp、rip 的值)。
实操演练:查看 target 变量的内存
我们现在想看看 main 函数中的 target 变量(值为10),在内存中到底长什么样(原始的十六进制形式),步骤如下:
-
第一步:获取 target 变量的内存地址。输入 p &target(& 是取地址符号,意思是"获取 target 变量的内存地址"):
pwndbg> p &target
GDB 输出:$6 = (int *) 0x7fffffffdd4c

解读:target 变量的内存地址是 0x7fffffffdd4c(不同环境下地址不一样,正常),int * 表示这是一个 int 类型变量的地址。
-
第二步:用 x 命令查看这个地址的内存。我们选择"查看1个单位、十六进制格式、每个单位4字节"(因为 target 是 int 类型,默认4字节),输入命令:
pwndbg> x/1xw 0x7fffffffdd4c
GDB 输出:0x7fffffffdd4c: 0x0000000a
输出解读
0x7fffffffe4dc 是我们查看的内存地址,后面的 0x0000000a 是这个地址中的原始数据(十六进制)。而 0x0000000a 转换成十进制,就是 10------和我们 target 变量的值一致!
这就是内存的原始样子:所有数据都以十六进制的形式存储,我们平时看到的"10",只是编译器帮我们转换成的十进制形式。
新手常用 x 命令组合(记熟)
-
x/1xw 地址:查看指定地址处1个4字节的十六进制数据(查看 int 类型变量)。
-
x/5xb 地址:查看指定地址处5个1字节的十六进制数据(查看字节数组、字符)。
-
x/1s 地址:查看指定地址处的字符串(比如查看 printf 输出的字符串)。
-
x/10i rip:查看 rip 指向地址处的10条汇编指令(没有源码时,查看程序下一步要执行的指令)。
三、 高手进阶:抛弃源码,直面汇编(适配 Pwndbg 版本)
我们前面所有的操作,都是基于"有 C 源码、有 -g 参数"的理想情况------但在真实的 CTF Pwn 比赛中,题目通常只有编译好的二进制程序(没有 test.c 源码,也没有 -g 参数),我们无法看到 C 语言代码,只能看到程序的"底层语言"------汇编代码。
而 Pwndbg 作为 GDB 最常用的 Pwn 调试插件,对汇编调试做了大量优化:默认集成"寄存器+汇编+栈+源码"全能视图,自动补全关键注释,高亮核心指令,远比原生 GDB 更高效。这一部分结合 Pwndbg 实际界面,讲解汇编调试的核心操作,适配实战场景。
1. Pwndbg 原生全能汇编视图(默认最优,无需手动调整)
你当前的 Pwndbg 界面已经是最适合 Pwn 调试的汇编视图,启动程序后默认就会显示,无需额外操作:
Bash
# 启动 Pwndbg 并加载程序,自动进入全能汇编视图
gdb ./test
pwndbg> b main
pwndbg> r
启动后会看到集成式视图(和实际调试界面一致),核心区域解读(新手必看):

| 区域名称 | 核心内容 | 实战作用 |
|---|---|---|
| REGISTERS | 实时寄存器值(rip/rsp/rbp 等关键寄存器标红) | 直接观察指令指针、栈指针的变化,无需手动输入 i r 查看寄存器 |
| DISASM | 汇编代码(当前要执行的指令标 ► 高亮) |
清晰看到每一条汇编指令,Pwndbg 自动标注指令的伪代码含义 |
| SOURCE (CODE) | 关联的 C 源码(有 -g 编译参数时显示) | 汇编和源码对照,新手快速理解指令对应的高级语言逻辑 |
| STACK | 栈内存布局(实时显示栈上数据) | 栈溢出漏洞调试的核心,直接看栈帧和数据分布 |
| BACKTRACE | 函数调用栈 | 清晰追踪程序的函数调用流程,知道当前处于哪个函数、由谁调用 |
Pwndbg 视图操作核心技巧
- 寄存器实时监控:无需切换布局,REGISTERS 面板会自动高亮变化的寄存器(比如执行指令后 rip/rsp 变动会标红),全程掌握寄存器状态;
- 刷新卡顿视图 :如果界面显示异常,输入
refresh或按Ctrl+L,即可重新渲染所有面板; - 调整视图显示 :若想临时隐藏某块面板(比如源码),可输入
set show-source off,输入set show-source on可恢复。
2. 查看指定函数的汇编:disassemble(Pwndbg 增强版)
Pwndbg 对 disassemble 命令做了核心增强------自动添加指令注释、函数调用关系,比原生 GDB 更易读,是实战中查看汇编的核心命令。
实操演练
- 查看 main 函数完整汇编:输入
disassemble main
Plain
pwndbg> disassemble main
Pwndbg 输出增强版汇编(带实战注释,高亮当前指令):

Plain
Dump of assembler code for function main:
0x000000000040118a <+0>: endbr64 ; 开启地址空间随机化保护(现代CPU安全指令)
0x000000000040118e <+4>: push rbp ; 函数栈帧初始化:保存旧rbp寄存器值
0x000000000040118f <+5>: mov rbp,rsp ; 新栈帧建立:将rbp指向当前rsp,固定栈底
0x0000000000401192 <+8>: sub rsp,0x10 ; 开辟16字节栈空间(用于存储局部变量)
=> 0x0000000000401196 <+12>: mov DWORD PTR [rbp-0x4],0xa ; [rbp-0x4] = 10 → 局部变量target = 10
0x000000000040119d <+19>: lea rax,[rip+0xe75] # 0x402019 ; 取字符串地址(Hello GDB!)存入rax
0x00000000004011a4 <+26>: mov rdi,rax ; 传参:将字符串地址存入rdi(puts函数第一个参数)
0x00000000004011a7 <+29>: call 0x401050 <puts@plt> ; 调用puts函数 → 打印字符串
0x00000000004011ac <+34>: mov eax,DWORD PTR [rbp-0x4] ; 将局部变量target的值(10)存入eax
0x00000000004011af <+37>: mov edi,eax ; 传参:将target的值存入edi(secret_func函数参数)
0x00000000004011b1 <+39>: call 0x401156 <secret_func> ; 调用secret_func函数,传入参数10
0x00000000004011b6 <+44>: lea rax,[rip+0xe67] # 0x402024 ; 取字符串地址(Program finished.)存入rax
0x00000000004011bd <+51>: mov rdi,rax ; 传参:将字符串地址存入rdi
0x00000000004011c0 <+54>: call 0x401050 <puts@plt> ; 调用puts函数 → 打印结束提示
0x00000000004011c5 <+59>: mov eax,0x0 ; 设置程序返回值eax = 0(正常退出)
0x00000000004011ca <+64>: leave ; 销毁栈帧:等价于mov rsp,rbp; pop rbp
0x00000000004011cb <+65>: ret ; 函数返回:rip跳回调用main函数的位置
End of assembler dump.
Pwndbg 汇编输出核心解读(新手必看)
- 指令地址 :左边
0x000000000040118a是指令的虚拟地址(与你实际汇编中main函数入口地址一致),和rip寄存器值一一对应(Pwn 中劫持程序执行流,核心就是修改 rip 指向目标指令地址); - 偏移量 :
<+0>是指令相对于函数入口(0x000000000040118a)的偏移,后续计算漏洞偏移、构造 payload 时会频繁用到(如你汇编中<+12>,即相对于入口偏移12字节); - 自动注释 :Pwndbg 会标注指令的实际作用(比如你汇编中
push rbp→ 栈帧初始化,endbr64→ 现代CPU安全指令),新手不用死记汇编语法也能理解逻辑; - 当前指令 :
=>箭头标红的是当前rip指向的指令(如你汇编中0x0000000000401196 <+12>),即程序下一步要执行的操作。
Pwn 实战高频汇编命令(Pwndbg 专属)
| 命令 | 作用 | 实战场景 |
|---|---|---|
disassemble secret_func |
快速反汇编 secret_func 函数 | 查看自定义函数的汇编逻辑,分析内部计算/判断逻辑 |
disassemble main 0x401160 |
反汇编 main 函数到 0x401160 地址 | 精准查看某段核心指令(比如函数调用前的传参逻辑) |
3. 退出 Pwndbg(GDB)
调试结束后,输入 quit(简写 q)即可退出,Pwndbg 保留简洁的退出提示:
Plain
pwndbg> q

若程序仍在运行,Pwndbg 会提示:
Plain
A debugging session is active.
Inferior 1 [process 12345] will be killed.
Quit anyway? (y or n)
输入 y 确认即可退出,回到系统终端命令行。
四、 课后作业与拓展
学习 GDB 没有捷径,只有多练、多敲命令、多观察输出,才能真正掌握。下面给你布置一个小作业,检验一下你的学习成果;同时预告后续的拓展内容,帮你规划下一步的学习方向。
作业小挑战
重新启动 GDB 调试(gdb ./test),按照以下步骤操作,看看你能不能成功:
-
把断点打在 main 函数的第 11 行(printf("Hello GDB!\n");),也就是 b 11。
-
运行程序(r),让程序停在这个断点处。
-
不要修改 C 语言代码,也不要重新编译,直接用 set 命令,把 target 变量的值从 10 篡改成 100(提示:set var target = 100)。
-
用 print 命令验证 target 的值已经变成 100。
-
用 next/step 步进执行,直到程序结束,看看 secret_func 打印出来的结果是不是变成 200 了(100+100=200)?
如果能成功打印出 200,说明你已经掌握了 GDB 的核心基础操作!如果遇到问题,回头再看看博客中的步骤,排查一下是不是命令输错了、断点下错了。
拓展预告(下一步学习方向)
原生的 GDB 确实有点"反人类"------尤其是在查看内存、跟踪栈变化的时候,需要手动输入很多命令,效率很低。在真正的 Pwn 世界里,没有哪个选手会用原生 GDB 调试,大家都会给 GDB 安装极其强大的辅助插件,让调试效率翻倍!
常用的 GDB 插件有3个(三巨头),后续我们会详细讲解安装和使用方法:
-
Pwndbg:最受新手欢迎的插件,轻量、简洁,自动显示栈结构、寄存器状态、汇编指令上下文,支持一键计算偏移量、查看内存布局,简直是新手的"福音"。
-
GDB-PEDA:功能强大,支持多种漏洞利用辅助(比如栈溢出、格式化字符串),自动检测程序保护机制,适合进阶学习。
-
GEF:跨平台、功能全面,支持多架构调试(x86、x64、ARM 等),适合后续学习多架构 Pwn 漏洞利用。
装上这些插件之后,你的 GDB 会"脱胎换骨"------每次单步执行,都会自动在屏幕上画出漂亮的内存栈结构图、寄存器状态和代码上下文,不用再手动输入 x、i r 等命令,简直是降维打击!