程序、汇编与寄存器
程序、汇编与寄存器
本文适合
刚开始读二进制程序、能运行程序但看不懂汇编和寄存器的新手。学完你能:用调用约定、比较指令和寄存器状态判断输入在哪里被处理,并完成一次最小动态验证。
逆向工程的基本问题是:程序拿到输入后,经过哪些指令处理,最后为什么判断对或错。
源码编译后会变成机器指令。汇编是机器指令的可读表示。寄存器是 CPU 当前正在使用的高速存储位置。
一句话判断
当你看到程序把输入送进 strcmp、memcmp、循环比较、异或、加减或查表逻辑时,寄存器和关键汇编就是还原判断条件的入口。
逆向入门不要求逐条翻译所有汇编,而是先回答三个问题:输入在哪个寄存器或内存里,和什么值比较,比较结果决定跳到成功还是失败。
题目中常见信号
- 程序运行后要求输入 password、serial、key、flag。
strings能看到Correct、Wrong、success、try again。- 反编译结果里出现
strcmp、memcmp、strlen、scanf、read、fgets。 - 汇编里出现
cmp、test、je、jne、jg、jl等条件跳转。 - 动态调试时
rdi、rsi、rdx或 Windows x64 的rcx、rdx指向可疑字符串。 - 成功失败分支明显,但不知道如何让分支走向成功。
核心概念
汇编读法的核心是数据流和控制流。数据流回答“值从哪里来到哪里去”,控制流回答“程序根据什么条件跳转”。
入门阶段重点掌握:
- 调用约定:函数参数通过寄存器或栈传递,返回值常在
rax/eax。 - 比较结果:
cmp、test不直接保存结果,而是设置标志位。 - 条件跳转:
je/jne/jg/jl根据标志位选择成功或失败路径。 - 内存引用:
[rbp-0x20]、[rax+rcx]通常是局部变量、数组或结构字段。 - 地址计算:
lea常用于计算地址或简单算术,不一定真的访问内存。
看懂这些,就能把“输入 -> 处理 -> 比较 -> 跳转”的最小链条还原出来。
最小分析流程
- 用
file确认架构和位数,区分 Linux/Windows、x86/x64/ARM。 - 用
strings找成功、失败、输入提示和可疑常量。 - 在 IDA/Ghidra 中对关键字符串按交叉引用,找到附近函数。
- 找输入函数和比较函数,记录参数寄存器或栈位置。
- 在比较指令或函数调用处下断点,用不同输入观察寄存器和内存变化。
- 根据
cmp/test后的条件跳转判断成功条件,再写脚本或手算还原输入。
最小验证示例
如果程序调用 strcmp(input, target),在 GDB 中验证两个参数:
gdb ./challenge
break strcmp
run命中断点后:
x/s $rdi
x/s $rsi
finish
p/d $rax如果 $rdi 是你的输入,$rsi 是目标字符串,且 rax=0 时后续进入成功分支,就完成了最小验证。后续需要做的不是继续猜,而是让输入等于目标字符串,或追踪目标字符串如何生成。
常见利用 / 解题路线
路线总览:
- 字符串比较路线:断在
strcmp/memcmp/strncmp,读参数,直接得到目标或比较缓冲区。 - 长度检查路线:先过
strlen或长度cmp,确认 flag 长度,再逐位还原。 - 循环变换路线:找到对输入每个字节的
xor/add/sub/rol,提取常量写逆运算脚本。 - 分支 patch 验证路线:临时改跳转或标志位确认成功分支是否真实,再回头还原逻辑。
- 寄存器追踪路线:从
rax/eax返回值和参数寄存器追踪关键函数输入输出。 - 平台调用约定路线:Linux x64 看
rdi/rsi/rdx,Windows x64 看rcx/rdx/r8/r9,32 位优先看栈。
为什么要看寄存器
程序运行时,函数参数、返回值、临时变量经常会经过寄存器。
在 x86-64 Linux 常见调用约定中:
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
返回值:rax如果你看到程序调用 strcmp,就要关注 rdi 和 rsi 分别指向哪个字符串。
常见指令
mov 表示数据移动。
cmp 表示比较两个值。
jmp 表示无条件跳转。
je、jne 表示根据比较结果跳转。
call 表示调用函数。
ret 表示函数返回。
入门阶段不需要背完整指令集,先能看懂数据从哪里来、在哪里比较、跳到哪里就够了。
一个最小判断逻辑
call strcmp
test eax, eax
jne fail
call successstrcmp 返回 0 表示两个字符串相等。test eax, eax 检查返回值是否为 0。jne fail 表示不等就跳到失败分支。
这段逻辑说明:如果想成功,要让 strcmp 的两个参数相等。
静态分析时看什么
先看字符串。成功提示和失败提示常常能带你找到关键分支。
再看交叉引用。字符串在哪里被使用,附近通常就是判断逻辑。
最后看比较前的数据。输入是否被异或、加减、Base64、哈希或查表。
x86-64 寄存器全览
64 位模式下寄存器更丰富,调用约定也不同于 32 位:
通用寄存器:
rax - 返回值 / 临时值
rbx - 被调用者保存
rcx - 第4个参数
rdx - 第3个参数
rsi - 第2个参数
rdi - 第1个参数
rbp - 栈帧基址(可作通用)
rsp - 栈指针
r8 - 第5个参数
r9 - 第6个参数
r10 - 临时值
r11 - 临时值
r12-r15 - 被调用者保存
特殊寄存器:
rip - 指令指针(当前执行地址)
rflags - 状态标志(ZF/CF/OF/SF)Windows x64 调用约定
Windows 使用不同的调用约定:
前4个参数:rcx, rdx, r8, r9
返回值:rax
必须预留 32 字节影子空间32 位 Windows 使用 cdecl/stdcall,参数从右到左压栈。
更多常用指令
算术运算:
add dst, src ; dst = dst + src
sub dst, src ; dst = dst - src
imul dst, src ; dst = dst * src(有符号)
idiv src ; rax = rax / src,余数在 rdx
位运算:
and dst, src ; 按位与
or dst, src ; 按位或
xor dst, src ; 按位异或
not dst ; 按位取反
shl dst, cnt ; 左移
shr dst, cnt ; 逻辑右移
sar dst, cnt ; 算术右移(保留符号位)
比较与跳转:
cmp a, b ; 设置标志位(a-b 的结果)
test a, b ; 设置标志位(a AND b 的结果)
je / jz ; 相等 / ZF=1 时跳转
jne / jnz ; 不等 / ZF=0 时跳转
jg / jnle ; 大于(有符号)
jl / jnge ; 小于(有符号)
ja / jnbe ; 大于(无符号)
jb / jnae ; 小于(无符号)
jle / jng ; 小于等于(有符号)
栈操作:
push src ; rsp -= 8, [rsp] = src
pop dst ; dst = [rsp], rsp += 8
数据传送:
mov dst, src ; 数据复制
lea dst, [src] ; 计算地址(不访问内存)
movzx dst, src ; 零扩展
movsx dst, src ; 符号扩展IDA Pro 实战技巧
1. 打开程序后先看 Strings 窗口(Shift+F12)
2. 双击字符串跳转,再按 X 查看交叉引用
3. 按 F5 生成伪代码(需要 Hex-Rays 插件)
4. 按 N 重命名变量/函数
5. 按 Y 修改函数签名
6. 按 X 在引用间跳转
7. 按 G 跳转到指定地址在伪代码中关注:
// 典型的 flag 检查逻辑
int check_flag(char *input) {
if (strlen(input) != 32) return 0;
for (int i = 0; i < 32; i++) {
if ((input[i] ^ key[i]) != expected[i])
return 0;
}
return 1;
}GDB 动态调试
# 启动调试
gdb ./challenge
# 常用命令
b main # 在 main 设断点
b *0x401234 # 在地址设断点
r # 运行
r arg1 arg2 # 带参数运行
ni # 单步(不进入函数)
si # 单步(进入函数)
c # 继续执行
# 查看寄存器
info registers # 所有寄存器
p $rax # 打印 rax
p/x $rax # 十六进制打印
# 查看内存
x/10x $rsp # 栈上10个字(十六进制)
x/s $rdi # 查看 rdi 指向的字符串
x/20i $rip # 查看后续20条指令
# 修改
set $rax = 1 # 修改寄存器
set {int}0x601040 = 0x1 # 修改内存
# 使用 pwndbg/peda 增强
# 安装: git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh常见失败原因
- 试图一口气读完整个程序:先锁定输入、比较和成功失败分支,初始化和库函数可以跳过。
- 看到汇编就逐行翻译:每条指令都要服务于问题“它影响输入、比较或跳转了吗”。
- 只看成功分支:失败分支前的条件通常更接近真正判断逻辑。
- 忽略函数参数寄存器:断在函数入口时先看参数寄存器,再看反编译变量名。
- Linux/Windows 调用约定混用:Windows x64 第一个参数是
rcx,不是rdi。 - 以为 patch 成功就是解题:patch 只能验证路径,最终还要还原真实输入或算法。
- 不用不同输入验证:至少用两组输入观察寄存器变化,避免把常量误认为目标。
迷你案例
一个 ELF 程序运行后提示 Input password:,随便输入后输出 Wrong。用 strings 看到 Correct 和 Wrong,在 IDA 对 Correct 按交叉引用,附近有:
call strcmp
test eax, eax
jne fail
call success在 GDB 中断到 strcmp:
break strcmp
run
x/s $rdi
x/s $rsi$rdi 是你输入的 aaaa,$rsi 是 r3v_basic_2026。重新运行输入这个字符串,程序进入 Correct。这个案例的闭环是:字符串定位 -> 交叉引用 -> 调用约定读参数 -> 成功条件验证。