逆向学习手记 #1
发布于 2026-10-09 09:49:25
参考链接:汇编语言简述(逆向工程)
汇编理解
寄存器
寄存器是CPU里的高速缓存区,为了提高效率,人们想到在CPU和内存之间加入高速缓存,提升效率
所以在编写汇编代码的时候,要先将数据写入寄存器中,CPU才能进一步曹祖偶
而寄存器又分为:
-
通用寄存器
-
指针和变址寄存器,指针寄存器又分为:基址指针寄存器、堆栈指针寄存器等
-
段寄存器
-
指令指针寄存器(PC)
-
标志寄存器
在做逆向分析过程中,关注最多的一般是通用寄存器、堆栈指针寄存器、标志寄存器
大端小端
内存中数据存储的顺序是需要我们注意的
计算机存储是以1个byte为单位存储的,但是很多数据要大于一个字节
-
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
-
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
大端模式(网络字节序):
内存地址
0x00
0x01
0x02
0x03
内容
12
34
56
78
看到的就是 12 34 56 78,非常符合直觉。
小端模式(x86/x64 架构):
内存地址
0x00
0x01
0x02
0x03
内容
78
56
34
12
看起来反过来了,但 CPU 处理起来效率极高。
栈和堆
1️⃣ 抽象层面:数据结构上的“栈”
栈是一种**“后进先出”(LIFO, Last In First Out)**的线性表。
生活比喻:就像一摞盘子。你只能从最上面放盘子(压栈 Push),也只能从最上面拿盘子(出栈 Pop)。你想拿最底下的盘子,必须先拿走上面所有的盘子。
核心特征:
-
只允许在“栈顶”进行操作。
-
数据存取顺序是反的(最后进去的最先出来)。
-
在C语言里,函数的局部变量、参数传递,就是利用这种结构来管理的。
2️⃣ 内存层面:计算机里的“调用栈”(Call Stack)
函数调用的全过程(叠盘子和查字典)
假设我们在 main 函数里,调用了 func(10, 20)。此时栈是满的,我们只关注新增的动作。
第一步:传参(把食材扔进篮子) 程序会把参数从右到左(x86 cdecl约定)压入栈。
-
汇编动作:
push 20,push 10 -
内存变化:栈顶(ESP)往下(低地址)移动,
10和20被放到了栈里。
第二步:保存返回地址(记住回来的路)
准备跳进 func 了,但跳进去之后怎么回来?CPU 自动把 main 里下一条指令的地址(返回地址)压入栈。
-
汇编动作:
call func -
内存变化:ESP 继续往下移,压入返回地址。
第三步:建立新基地(原 EBP 入栈 + 更新 EBP)
进入 func 内部,它要建立自己的地盘。
-
汇编动作:
push ebp(先把main的基地保存起来,将来好回家),然后mov ebp, esp(让 EBP 指向当前栈顶,作为func的新基地)。 -
内存变化:此时,EBP 的位置就固定下来了。EBP 往上的地址(高地址)存着返回地址和参数;EBP 往下的地址(低地址)用来放
func自己的局部变量。
第四步:分配局部变量(占用空间)
-
汇编动作:
sub esp, 0x40(把栈顶往上拉 0x40 字节,相当于给func留出放局部变量的空位)。 -
内存变化:ESP 往下走。你会发现,局部变量的地址比 EBP 小,所以在 IDA 里写成了
[rbp-8h];而参数比 EBP 大,写成[rbp+8h]或[rbp+10h]。 这就是为什么你在 IDA 里看到的是加号和减号!
第五步:执行函数体
函数开始干活。它想拿参数,就去 [ebp+8]、[ebp+12] 拿;想用局部变量,就去 [ebp-4]、[ebp-8] 拿。
第六步:退场(恢复现场)
函数干完活了,要回 main。
-
汇编动作:
leave(等价于mov esp, ebp; pop ebp。把 ESP 拉回 EBP 的位置,并且把之前保存的main的 EBP 恢复回去)。 -
汇编动作:
ret(把之前压入的返回地址弹给 EIP,CPU 根据这个地址跳回main)。 -
内存变化:栈里的垃圾数据被清理(其实就是 ESP 移动了),但物理内存里还留着残影,直到被下次压入覆盖。
栈
栈:大家都知道,栈是一种数据结构,数据按照先进后出的方式进行存储,汇编里也是如此,但一般只用于保存函数内的局部变量。在调用一个函数的时候,先预留足够数量的栈空间,供函数内部使用,在函数结束时,又需要移动栈指针空间,使之与进入函数前的地址保持一致,即所谓的栈平衡。
堆
堆:堆也是用于存放变量的,但不同于栈但是,栈用于保存申明方式定义的局部变量,而堆用于保存alloc或new方式申请的变量。由于是运行时才使用内存空间,就导致编译器无法知道你需要多大的空间,所以在C/C++里这种方式申请的内存需要自己手动释放。
我们写递归函数时,如果遇到死循环抛出异常,为什么叫StackOverflow异常?就是因为死循环调用函数时,会一直移动SP下移,最终栈内存会和堆内存相遇,这时候就是我们遇到的栈内存溢出异常了。
汇编语言的特点
-
没有申请、释放内存的概念,对内存操作没有限制,拿到内存地址就读写。不同于C/C++等语言里,局部变量的内存的分配和释放都是编译器在编译的时候完成的。
-
没有函数、方法等概念,高级语言中所有的函数、方法对应到汇编语言中都是一个个代码片段。而在汇编里实现高级语言的函数调用是:保存当前代码地址的下一个指令地址
addr_a -
转跳到代码片段
fun_a的地址addr_b -
代码片段
fun_a执行完后,转跳到地址addr_a
x86_64寄存器
16个通用寄存器
用途
用途
RAX
通常作为函数返回值
R8
通常用作参数5
RBX
被调用者保护的寄存器,或用作基指针
R9
通常用作参数6
RCX
通常用作参数4
R10
临时寄存器
RDX
通常用作参数3
R11
临时寄存器
RSI
通常用作参数2
R12
临时寄存器
RDI
通常用作参数1
R13
临时寄存器
RBP
被调用者保护的寄存器,或用作帧指针
R14
临时寄存器
RSP
栈指针寄存器,指向栈顶
R15
临时寄存器
常用指令
这里列举一些常见的指令,用于基本的逆向分析:
Name
Description
Name
Description
MOV
移动数据
INTO
溢出中断指令
PUSH
数据入栈
SHL
逻辑左移
POP
数据出栈
SHR
逻辑右移
ADD
加法运算
SAL
算术左移
SUB
减法运算
SAR
算术右移
INC
操作数加1
ROL
循环左移
DEC
操作数减1
ROR
循环右移
ADC
多字节加法,带进位(CF)做加法
RCL
带进位循环左移
SBB
多字节减法,带借位(CF)做减法
RCR
带进位循环右移
CMP
数据比较,只改变标志寄存器
LEA
取偏移地址指令
AND
逻辑与
JMP
转移指令
OR
逻辑或
JE
等于则转跳
XOR
逻辑异或
JNE
不等于则转跳
NOT
逻辑非
JG
大于则转跳
TEST
逻辑与运算类似,但只改变标记寄存器
JGE
大于等于则转跳
CALL
子程序调用指令
JL
小于则转跳
RET
返回指令
JLE
小于等于则转跳
INT
中断指令
壳(尤其是加密壳/压缩壳)本身也是一段程序,它要负责把被压缩/加密的原始程序“解压/解密”到内存里,再跳过去执行。为了完成这个任务,它必须调用系统 API,比如 LoadLibrary、GetProcAddress、VirtualAlloc** 等。所以即使原始程序没用到这些函数,加壳后的程序也会出现它们。**