一、逻辑运算(位运算)
引入:计算机只认识二进制(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 的计算过程:
第一步:不考虑进位的加法(异或)
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)作为新的加数,重复上述过程。
新一轮运算:
重复第一步:
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。
- 明文
20 的二进制: 0010 0100
- 密钥
26 的二进制: 0001 1010
- 加密(XOR):
1 2 3 4
| 0010 0100 (明文: 20) XOR 0001 1010 (密钥: 26) —————————————————————————— 0000 1110 (密文: 14)
|
客户端将加密后的 14 发送给服务器。服务器收到后,用同样的密钥 26 进行解密:
- 解密(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位,能表示的地址编号范围是
0x00000000 到 0xFFFFFFFF。每个地址指向一个字节,因此最大能直接管理的物理内存是 2^32 字节,即 4GB。
- 64位CPU:同理,能管理的理论内存上限远超4GB。
理解误区:不是因为“寄存器是32位的”才叫32位计算机,因为有些寄存器(如段寄存器)位数远超32位。
3.2 内存读写(寻址公式)
要读写内存,必须知道它的“门牌号”,即内存地址。以下是五种获取内存地址和读写数据的寻址方式。
说明:在编写汇编指令时,段寄存器前缀规则如下:
- 地址如果是直接的数字(立即数):用
DS。
- 地址中包含了
ESP 或 EBP:用 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(压栈/入栈):将数据放入堆栈。
ESP = ESP - 4(栈顶向低地址移动4字节)。
- 将数据存入
[ESP] 指向的新位置。
- POP(弹栈/出栈):从堆栈取出数据。
- 将
[ESP] 当前指向的数据取出。
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 变形的艺术
逆向分析者通常会重点监控 PUSH、POP 等标志性指令。指令变形的核心思想是:使用其他功能等效的指令组合来代替这些敏感指令,增加分析难度,这是动态变形壳的基础技术
分析 PUSH EAX 到底做了什么?
ESP 的值减4(栈顶提升)。
- 将
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怎么变形
五、课后作业
逐步画出以下代码执行时,每一行指令结束后,堆栈区域的变化图。需清晰标注出 ESP 和 EBP 的位置,以及内存中被修改的部分。
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)以及栈帧的建立和销毁。这个练习是后续学习缓冲区溢出和函数调用约定的绝对基础。