参考链接:汇编语言简述(逆向工程)

汇编理解

寄存器

寄存器是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

中断指令