汇编基础:逻辑运算、寄存器与堆栈

一、逻辑运算(位运算)

引入:计算机只认识二进制(0和1),它是如何进行复杂的加减乘除、加密解密等运算的呢?
答案就是通过逻辑运算(位运算) 来实现。这是CPU运算的基石。

1.1 基本逻辑运算

或运算(OR)

  • 规则:两个操作位中,只要有一个为1,结果就为1。相当于电路中的“并联”开关,任一开关闭合,灯泡就会亮。
  • C语言符号|
  • 汇编指令OR

1
2
3
4
   1011 0001 0101 0101
OR 0101 0101 0101 0101
———————————————————————
1111 0101 0101 0101

与运算(AND)

  • 规则:两个操作位中,都为1时,结果才为1。相当于电路中的“串联”开关,必须两个开关都闭合,灯泡才会亮。
  • C语言符号&
  • 汇编指令AND

1
2
3
4
    1011 0001 0101 0101
AND 0101 0101 0101 0101
———————————————————————
0001 0001 0101 0101

异或运算(XOR)

  • 规则:两个操作位,不同(相异)时为1,相同(都为0或都为1)时为0
  • 核心特性:一个数A与另一个数B异或两次(A XOR B XOR B),结果会还原为A本身。这一特性使其在加密算法中非常常用。
  • C语言符号^
  • 汇编指令XOR
1
2
3
4
    1011 0001 0101 0101
XOR 0101 0101 0101 0101
———————————————————————
1110 0100 0000 0000

非运算(NOT)

  • 规则:单操作数运算,按位取反。1变成0,0变成1
  • 汇编指令NOT

1
2
3
NOT 1011 0001 0101 0101
———————————————————————
0100 1110 1010 1010

左移运算(SHL)

  • 规则:将二进制位整体向左移动指定位数。高位丢弃,低位补0。每左移一位,相当于乘以2。
  • 汇编指令SHL
1
2
3
4
   1011 0001 0101 0101
SHL 1 (左移一位)
———————————————————————
0110 0010 1010 1010

1.2 逻辑运算的应用

1. CPU如何做加法?(以 2+3 为例)

CPU只会做逻辑运算,所有算术运算(加、减、乘、除)都必须分解成逻辑运算和移位来实现。以下是 2+3 的计算过程:

  • 2 的二进制:0010
  • 3 的二进制:0011

第一步:不考虑进位的加法(异或)

1
2
3
4
    0010
XOR 0011
————————
0001 (结果存入R)

第二步:计算进位(与运算 + 左移)

1
2
3
4
5
6
7
    0010
AND 0011
————————
0010 (与运算结果)
SHL 1 (左移一位,得到进位值)
————————
0100 (进位值,存入Y)

第三步:判断运算是否结束
判断第二步得到的进位值(0100)是否为 0。若不为 0,则需将第一步的结果(0001)和这个进位值(0100)作为新的加数,重复上述过程。

新一轮运算:

  • 新加数X:0001
  • 新加数Y:0100

重复第一步:

1
2
3
4
    0001
XOR 0100
————————
0101 (新结果,存入R)

重复第二步:

1
2
3
4
5
6
7
   0001
AND 0100
————————
0000 (与运算结果为0)
SHL 1
————————
0000 (新进位值为0)

此时,新进位值为 0,循环结束。最终结果就是 R 中的值 0101,即十进制的 5

2. 一个简单的加密/解密算法(异或加密)

利用异或的特性,可以设计一个最简单的加密方案。

  • 客户端(发送方)与服务器端(接收方)需要事先约定一个密钥
  • 加密过程密文 = 明文 XOR 密钥
  • 解密过程明文 = 密文 XOR 密钥

示例:加密“20”
假设我们要发送的明文是 20,密钥是 26

  1. 明文 20 的二进制0010 0100
  2. 密钥 26 的二进制0001 1010
  3. 加密(XOR)
1
2
3
4
    0010 0100  (明文: 20)
XOR 0001 1010 (密钥: 26)
——————————————————————————
0000 1110 (密文: 14)

客户端将加密后的 14 发送给服务器。服务器收到后,用同样的密钥 26 进行解密:

  1. 解密(XOR)
1
2
3
4
    0000 1110  (密文: 14)
XOR 0001 1010 (密钥: 26)
————————————————————————————————————
0010 0100 (还原为明文: 20)

二、寄存器与内存

引入:我们知道了CPU怎么算,但计算的数据和结果该保存在哪里呢?
计算机中有两个核心的存储部件:CPU内部的寄存器和主板上的内存。它们本质都是存储0和1的“容器”,没有区别。

  • 寄存器:集成在CPU内部,读写速度极快但容量小、价格昂贵。
  • 内存:独立于CPU,读写速度相对慢但容量大、价格便宜。

2.1 32位通用寄存器

x86架构CPU提供了8个32位的通用寄存器,用于临时存储数据。它们是:

寄存器名 约定用途(了解即可,无需硬背)
EAX 累加器,常用于存放函数返回值
ECX 计数器,常用于循环
EDX 数据寄存器
EBX 基址寄存器
ESP 堆栈指针,始终指向栈顶
EBP 基址指针,常用于访问堆栈内的参数和局部变量
ESI 源变址寄存器
EDI 目的变址寄存器

“32位”意味着每个寄存器都是一个能存放32个二进制位(即4字节)的容器。

2.2 如何使用寄存器(基础汇编指令)

1
2
3
4
5
6
7
8
9
10
11
12
; 1. 将立即数存入寄存器
MOV EAX, 12345678H ; 将十六进制数12345678H 放入 EAX

; 2. 加法指令
ADD EAX, 1 ; EAX = EAX + 1

; 3. 寄存器间运算
MOV ECX, 2
ADD EAX, ECX ; EAX = EAX + ECX

; 4. 减法指令
SUB EAX, 3 ; EAX = EAX - 3

三、内存

当数据量远超8个寄存器的容量时,就必须使用内存。

3.1 什么是32位/64位计算机?

这个位数的核心指的是CPU的寻址宽度,即它能访问的内存地址的最大范围。

  • 32位CPU:地址总线宽度为32位,能表示的地址编号范围是 0x000000000xFFFFFFFF。每个地址指向一个字节,因此最大能直接管理的物理内存是 2^32 字节,即 4GB
  • 64位CPU:同理,能管理的理论内存上限远超4GB。

理解误区:不是因为“寄存器是32位的”才叫32位计算机,因为有些寄存器(如段寄存器)位数远超32位。

3.2 内存读写(寻址公式)

要读写内存,必须知道它的“门牌号”,即内存地址。以下是五种获取内存地址和读写数据的寻址方式。

说明:在编写汇编指令时,段寄存器前缀规则如下:

  • 地址如果是直接的数字(立即数):用 DS
  • 地址中包含了 ESPEBP:用 SS
  • 地址中包含了 EDI:用 ES
    (和16汇编有不同,在32位平坦模式下,段寄存器的作用主要是标识内存属性,与寻址本身关系不大,初学时可先按此规则套用。)

寻址公式一:[立即数]

1
2
3
4
5
6
7
8
9
10
; 读取内存的值(将地址0x213456里的4字节内容放到EAX中)
MOV EAX, DWORD PTR DS:[0x213456]
; 注意:EAX得到的是地址0x213456里存放的数值,而不是地址编号0x213456本身。

; 向内存写入数据
MOV DWORD PTR DS:[0x12321], 12345678H

; 获取内存地址(将地址编号0x123456本身放到ECX中)
LEA ECX, DWORD PTR DS:[0x123456]
; LEA = Load Effective Address

寻址公式二:[reg]

1
2
3
4
5
6
7
8
; reg可以是任意通用寄存器
MOV ECX, 0x13FFD0 ; 先将地址编号存入ECX
MOV EAX, DWORD PTR DS:[ECX] ; 从ECX存的地址里读取值

MOV EDX, 0x13FFD0
MOV DWORD PTR DS:[EDX], 0x12345678 ; 向EDX存的地址里写入值

LEA EAX, DWORD PTR DS:[EDX] ; 获取EDX中的地址编号

寻址公式三:[reg + 立即数]

1
2
3
4
5
6
7
8
9
MOV ECX, 0x13FFD0
; 读取地址 (ECX里的值 + 4) 处的内存数据
MOV EAX, DWORD PTR DS:[ECX + 4]

; 向地址 (ECX里的值 + 8) 处写入数据
MOV DWORD PTR DS:[ECX + 8], 0x12345678

; 获取地址编号 (ECX里的值 + 4) 本身
LEA EAX, DWORD PTR DS:[ECX + 4]

寻址公式四:[reg + reg * {1,2,4,8}]

注意:乘数只能是 1, 2, 4, 8

1
2
3
4
5
6
7
8
9
10
MOV EAX, 0x13FFD0
MOV ECX, 2
; 读取地址 (0x13FFD0 + 2*4) 处的内存数据
MOV EDX, DWORD PTR DS:[EAX + ECX * 4]

; 向该地址写入数据
MOV DWORD PTR DS:[EAX + ECX * 4], 0x12345678

; 获取该地址编号
LEA EAX, DWORD PTR DS:[EAX + ECX * 4]

寻址公式五:[reg + reg * {1,2,4,8} + 立即数]

1
2
3
4
MOV EAX, 0x13FFD0
MOV ECX, 2
; 读取、写入或获取地址,公式同上,再加一个立即数偏移
MOV EDX, DWORD PTR DS:[EAX + ECX * 4 + 10H]

四、堆栈

堆栈的本质就是一块被特殊管理的内存区域,用于存放函数调用过程中的临时变量、参数和返回地址等。

  • 关键特性:堆栈的操作遵循 后进先出 的原则。
  • 核心寄存器
    • ESP(堆栈指针):始终指向栈顶(低地址)。
    • EBP(基址指针):常用来作为基准点,访问当前函数栈帧内的参数和局部变量。
  • 重要备注:对于正向开发,堆栈细节常被编译器“透明化”;但对于逆向分析,堆栈是必须攻克的核心高地

这里放堆栈图

4.1 基本堆栈操作

  • PUSH(压栈/入栈):将数据放入堆栈。
    1. ESP = ESP - 4(栈顶向低地址移动4字节)。
    2. 将数据存入 [ESP] 指向的新位置。
  • POP(弹栈/出栈):从堆栈取出数据。
    1. [ESP] 当前指向的数据取出。
    2. ESP = ESP + 4(栈顶向高地址回退4字节)。
1
2
3
4
5
6
7
8
9
; 观察下面的操作对ESP和EBP的影响
PUSH 0x12345678
PUSH EAX
PUSH EBX

POP ECX ; ECX得到EBX压入的值,ESP+4
POP EDX ; EDX得到EAX压入的值,ESP+4
POP EAX ; EAX得到0x12345678,ESP+4
; 结论:POP操作只影响ESP,EBP保持不变。

这里放执行图

4.2 STOS与REP指令组合

  • STOS指令:将 EAX 的值拷贝到 EDI 寄存器所指向的内存地址,并自动修改 EDI
    • STOS DWORD PTR ES:[EDI] -> [EDI] = EAX; EDI 自动加/减4。
  • REP指令:重复执行紧跟的字符串指令(如STOS),重复次数由 ECX 决定。
1
2
3
4
5
6
7
8
9
; 组合用法示例:将EDI指向的内存区域,连续ECX次填充为EAX的值
MOV EAX, 0x88888888 ; 要填充的值
MOV ECX, 2 ; 重复2次
LEA EDI, DWORD PTR SS:[ESP] ; 从栈顶开始
REP STOS DWORD PTR ES:[EDI]
; 执行效果:从[ESP]开始,连续8个字节(2个双字)都被填充为0x88888888。
; EDI的值会根据EFLAG寄存器中的DF位(方向位)自动变化:
; DF=0(默认):EDI自动递增,向高地址覆盖。
; DF=1:EDI自动递减,向低地址覆盖。

这里放执行图

4.3 变形的艺术

逆向分析者通常会重点监控 PUSHPOP 等标志性指令。指令变形的核心思想是:使用其他功能等效的指令组合来代替这些敏感指令,增加分析难度,这是动态变形壳的基础技术

分析 PUSH EAX 到底做了什么?

  1. ESP 的值减4(栈顶提升)。
  2. EAX 的值存入新的栈顶地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
; 传统的 PUSH EAX
mov eax, 88888888H
push eax

; 等效的“变形”写法,可以达到同样效果:
mov eax, 88888888H
; 方式一:先存数据,再移动栈顶
mov dword ptr ds:[esp-4], eax
lea esp, dword ptr ss:[esp-4] ; LEA ESP,[ESP-4] 等同于 SUB ESP,4

; 方式二:先移动栈顶,再存数据
lea esp, dword ptr ss:[esp-4]
mov dword ptr ds:[esp], eax

同理,推导 POP ECX 的等效变形:

1
2
3
4
5
6
7
8
9
10
11
12
; 传统的 POP ECX
; 1. 从栈顶取出值给ECX
; 2. ESP = ESP + 4

; 等效变形写法:
; 方式一:先取值,再移动栈顶
mov ecx, dword ptr ss:[esp]
lea esp, dword ptr ss:[esp+4] ; LEA ESP,[ESP+4] 等同于 ADD ESP,4

; 方式二:先移动栈顶,再取值
lea esp, dword ptr ss:[esp+4]
mov ecx, dword ptr ss:[esp-4]

挑战:

push esp和pop esp怎么变形

五、课后作业

逐步画出以下代码执行时,每一行指令结束后,堆栈区域的变化图。需清晰标注出 ESPEBP 的位置,以及内存中被修改的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 假设执行前:ESP = 0x12FFC4, EBP = 0x12FFF0
push ebp ; 1. 压入旧的EBP
mov ebp, esp ; 2. 设置新的栈帧基址
sub esp, 40 ; 3. 为局部变量开辟40字节空间
push ebx ; 4. 保存现场:压入EBX
push esi ; 5. 保存现场:压入ESI
push edi ; 6. 保存现场:压入EDI
lea edi, dword ptr ss:[ebp-40] ; 7. EDI指向局部变量区的起始位置
mov ecx, 10 ; 8. 设置循环次数 (40字节 / 4字节 = 10次)
mov eax, 0CCCCCCCCH ; 9. 设置要填充的值 (0xCC是Debug模式下未初始化的栈空间填充值)
rep stos dword ptr es:[edi] ; 10. 将局部变量区填充为0xCC
mov eax, dword ptr ss:[ebp+8] ; 11. 获取第一个参数
add eax, dword ptr ss:[ebp+C] ; 12. 加上第二个参数(结果是返回值)
pop edi ; 13. 恢复现场
pop esi ; 14. 恢复现场
pop ebx ; 15. 恢复现场
mov esp, ebp ; 16. 恢复ESP,等价于 add esp, 40
pop ebp ; 17. 恢复旧的EBP
retn ; 18. 函数返回(从栈中弹出返回地址,并跳到该地址执行)

提示:这不是一个可直接调试的程序,这是一个独立的手绘练习。请将这段代码“放入”你画的内存图中,一步步模拟执行,这能帮你深入理解函数调用的标准过程(__stdcall)以及栈帧的建立和销毁。这个练习是后续学习缓冲区溢出和函数调用约定的绝对基础。

汇编基础:逻辑运算、寄存器与堆栈

https://blog.c8h10n4o2.asia/ebe25149/

作者

萌新知

发布于

2026-06-19

更新于

2026-07-20

许可协议