X86指令系统¶
真题中的 X86 指令大题¶
【2017-44】¶
(10分)在按字节编址的计算机M上,题43中f1的部分源程序(阴影部分)与对应的机器级代码(包括指令的虚拟地址)如下图所示。
int f1(unsigned n)
1 00401020 55 push ebp
... ... ...
for (unsigned i = 0; i <= n-1; i++)
... ... ...
20 0040105E 39 4D F4 cmp dword ptr [ebp-0Ch],ecx
... ... ...
{ power *= 2;
... ... ...
23 00401066 D1 E2 shl edx,1
... ... ...
return sum;
... ... ...
35 0040107F C3 ret
其中,机器级代码行包括行号、虚拟地址、机器指令和汇编指令。
请回答下列问题。
(1) 计算机M是RISC还是CISC?为什么?
(2) f1的机器指令代码共占多少字节?要求给出计算过程。
(3) 第20条指令cmp通过i减n-1实现对i和n-1的比较。执行f1(0)过程中,当i=0时,cmp指令执行后,进/借位标志CF的内容是什么?要求给出计算过程。
(4) 第23条指令shl通过左移操作实现了power*2运算,在f2中能否也用shl指令实现power*2?为什么?
本题需结合前面一题题目条件来做:
已知\(f(n)=\sum_{i=0}^n2^i=2^{n+1}-1=\overbrace{11\cdots 1}^{n+1\text{位}}{\rm B}\) ,计算f(n)的C语言函数f1如下:
将f1中的int都改为float,可得到计算f(n)的另一个函数f2。假设unsigned和int型数据都占32位,float采用IEEE754单精度标准。
解析
(1) 计算机M是 CISC(复杂指令集计算机)。原因:
- 指令长度可变:从给出的机器级代码可以看出,不同指令占用的字节数不同。例如,
push(55)占字节,
cmp(39 4D F4)占字节,
shl(D1 E2)占字节。而 RISC 架构通常采用定长指令。
- 寻址方式复杂(直接访存):第20条指令
cmp dword ptr [ebp-0Ch],ecx允许直接对内存地址([ebp-0Ch])中的操作数进行处理。在 RISC 架构中,通常只有专用的Load/Store指令才能访问内存,其他运算指令均在寄存器之间进行。
(2) f1的机器指令代码共占 96 字节(十六进制为 60H 字节)。
计算过程:函数 f1 的第一条指令 push ebp 的起始虚拟地址为 00401020H,函数 f1 的最后一条指令 ret 的起始虚拟地址为 0040107FH,根据代码片段中的机器指令,ret 对应的机器码为 C3,占用 个字节。代码占用的连续内存空间总长度 = 最后一条指令的地址 - 第一条指令的地址 + 最后一条指令占用的字节数 =
0040107FH - 00401020H + 1H = 60H = 96 字节。
(3) 标志位 。
- 题目已知
为
unsigned(无符号)类型。在执行f1(0)时,。
- 循环条件为
i <= n-1,由于是无符号数,
会发生下溢出。在 32 位计算机中,
的无符号数机器码表示为全 1,即
。
- 当
时,
cmp指令通过做减法来比较和
,即执行运算
。
- 在无符号数减法运算中,当被减数(
)小于减数(
)时,必须向高位借位,因此会产生借位标志。所以进/借位标志
。
(注:从底层加法器实现的角度来看,。加法运算的最高位进位
。对于减法,
。)
(4) 不能。(1 分)f2中不能用shl指令实现power*2。因为shl指令用来将一个整数的所有有效数位作为一个整体左移,但f2中的变量power是float型,按照IEEE754标准,其机器数中不包含最高有效数位,但包含了阶码部分,将其作为一个整体左移时并不能实现乘以2的功能,所以f2中不能用shl指令实现 power*2。(2 分)浮点数运算比整型运算要复杂,耗时也较长。
【2019-45】¶
(16分)已知\(f(n)=n!=n×(n-1)×(n-2)×…×2×1\),计算f(n)的C语言函数f1的源程序(阴影部分)及其在32位计算机M上的部分机器级代码如下:
int f1(int n){
1 00401000 55 push ebp
... ... ...
if(n>1)
11 00401018 83 7D 08 01 cmp dword ptr [ebp+8],1
12 0040101C 7E 17 jle f1+35h (00401035)
return n*f1(n-1);
13 0040101E 8B 45 08 mov eax, dword ptr [ebp+8]
14 00401021 83 E8 01 sub eax, 1
15 00401024 50 push eax
16 00401025 E8 D6 FF FF FF call f1 (00401000)
... ... ...
19 00401030 0F AF C1 imul eax, ecx
20 00401033 EB 05 jmp f1+3Ah (0040103a)
else return 1;
21 00401035 B8 01 00 00 00 mov eax, 1
}
... ... ...
26 00401040 3B EC cmp ebp, esp
... ... ...
30 0040104A C3 ret
其中,机器级代码行包括行号、虚拟地址、机器指令和汇编指令,计算机M按字节编址,int型数据占32位。请回答下列问题:
(1) 计算f(10)需要调用函数f1多少次?执行哪条指令会递归调用f1?
(2) 上述代码中,哪条指令是条件转移指令?哪几条指令一定会使程序跳转执行?
(3) 根据第16行的call指令,第17行指令的虚拟地址应是多少?已知第16行的call指令采用相对寻址方式,该指令中的偏移量应是多少(给出计算过程)?已知第16行的call指令的后4字节为偏移量,M是采用大端方式还是采用小端方式?
(4) f(13)=6227020800,但f1(13)的返回值为1932053504,为什么两者不相等?要使f1(13)能返回正确的结果,应如何修改f1的源程序?
(5) 第19行的imul指令(带符号整数乘)的功能是R[eax]←R[eax]×R[ecx],当乘法器输出的高、低32位乘积之间满足什么条件时,溢出标志OF=1?要使CPU在发生溢出时转异常处理,编译器应在imul指令后应加一条什么指令?
解析
(1) 函数调用次数与递归指令
- 调用次数:计算 f(10) 需要调用函数 f1 共 10 次(分别在 n 为 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 时被调用)。
- 递归指令:执行第 16 行的
call f1 (00401000)指令会进行递归调用。
(2) 转移指令分析
- 条件转移指令:第 12 行的
jle f1+35h (00401035)(小于或等于则跳转)是条件转移指令。 - 必然跳转的指令:以下三条指令一定会使程序跳转执行:
- 第 16 行的
call指令(无条件过程调用) - 第 20 行的
jmp指令(无条件直接跳转) - 第 30 行的
ret指令(无条件过程返回)
(3) 虚拟地址、偏移量与端序
- 第17行的虚拟地址:0040102A。
计算过程:第 16 行指令起始地址为 00401025H,该机器指令(E8 D6 FF FF FF)共占 5 个字节。因此下一条指令(即第 17 行指令)的地址为 00401025H + 5H = 0040102AH。
- 偏移量:FFFFFFD6H。
计算过程:在相对寻址中,转移目标地址 = 转移指令下一条指令的地址 (PC) + 偏移量。
已知目标地址为 00401000H,PC 为 0040102AH。
偏移量 = 目标地址 - PC = 00401000H - 0040102AH = FFFFFFD6H。
- 小端方式
分析:由机器码 E8 D6 FF FF FF 可知,E8 为操作码,偏移量 FFFFFFD6H 在内存中按从低地址到高地址存放的顺序为 D6 FF FF FF,即最低有效字节(D6)存放在了最低地址处,符合小端存储机制。
(4) 结果不相等的原因与修改方法
- 不相等的原因:发生了整数溢出。32 位带符号整数(
int)的最大表示范围是(即 2147483647)。f(13) 的正确结果 6227020800 超过了此范围,导致乘法运算后高位发生截断,从而输出了错误的结果 1932053504。
- 修改方法:将 f1 函数的返回值类型由 32 位整数(
int)修改为能够容纳更大数值的类型,例如 64 位整型(long long)或双精度浮点型(double)。修改后的函数头示例:long long f1(int n)
(5) 溢出标志OF的判断与异常处理指令
- OF=1 的条件:当乘法器输出的 64 位乘积中,高 32 位不是低 32 位的符号扩展时(非全 0 或非全 1),溢出标志 OF=1。
对于带符号整数乘法,如果结果的高位部分仅仅是低位部分符号位的延伸(即低 32 位最高位为 0 时,高 32 位全为 0;最高位为 1 时,高 32 位全为 1),则表明未溢出;反之则溢出。
- 应加的指令:编译器应该在imul指令后加一条“溢出自陷指令”,使得CPU自动查询溢出标志OF,当OF=1时调出“溢出异常处理程序”。
溢出自陷指令 ( 例如 x86 架构中的 into 指令,即 Interrupt on Overflow ) 是一种处理整数运算溢出的机器指令。它通常由处理器提供,并用于捕捉和处理算术运算中发生的溢出错误。当执行一条溢出自陷指令时,处理器会检测当前的算术运算结果是否发生了溢出。如果发生了溢出,处理器会触发一个异常或中断,将控制权转移到相应的异常处理程序中。溢出自陷指令的目的是提供一种机制,用于检测和处理数值运算的溢出。当进行加法、减法、乘法等算术运算时,如果结果超过了数据类型能够表示的范围,就会发生溢出。溢出可能导致错误的计算结果和无效的程序行为,因此通过溢出自陷指令可以及时地进行检测和处理。
【2023-44】¶
(9 分)题43中C程序段在计算机M上的部分机器级代码如下,每个机器级代码行中依次包含指令序号、虚拟地址、机器指令和汇编指令。
for(i=0; i<24; i++)
1 00401072 C7 45 F8 00 00 00 00 mov[ebp-8], 0
2 00401079 EB 09 jmp 00401084h
3 0040107B 8B 55 F8 mov eax, [ebp-8]
... ... ...
7 00401088 7D 32 jge 004010bch
for(j=0; j<64; j++)
8 0040108A C7 45 FC 00 00 00 00 mov[ebp-4], 0
... ... ...
a[i][j]=10;
... ... ...
19 004010AE C7 84 82 00 20 42 00 0A 00 00 00 mov[ecx+edx*4+00422000h], 0Ah
20 ... ... ...
请回答下列问题。
(1) 第20条指令的虚拟地址是多少?
(2) 已知第2条jmp和第7条jge都是跳转指令,其操作码分别是EBH和7DH,跳转地址分别为0040 1084H、0040 10BCH,这两条指令都采用什么寻址方式?给出第2条指令jmp的跳转目标地址计算过程。
(3) 已知第19条mov指令的功能是“a[i][j]←10”,其中ecx和edx为寄存器名,0042 2000H是数组a的首地址,指令中源操作数采用什么寻址方式?已知edx中存放的是变量j,ecx中存放的是什么?根据该指令的机器码判断计算机M采用的是大端还是小端方式。
(4) 第一次执行第19条指令时,取指令过程中是否会发生缺页异常?为什么?
解析
(1)第20条指令的虚拟地址是 0040 10B9H。
计算过程:第19条指令的起始地址为 0040 10AEH,通过数第19条指令的字节数可知,它共占用 11 个字节(十六进制为 0BH)。第20条指令紧跟在第19条之后,因此其起始地址 = 第19条指令起始地址 + 第19条指令占用的字节数 = 0040 10AEH + 0BH = 0040 10B9H。
(2) 这两条指令(jmp 和 jge)均采用 相对寻址。
第2条指令 jmp 的跳转目标地址计算过程:在相对寻址中,跳转目标地址 = 转移指令下一条指令的地址 (PC) + 偏移量。
- 第2条指令 jmp 所在的虚拟地址为
0040 1079H,指令占 2 个字节(机器码EB 09),因此下一条指令的地址(即 PC 值)为0040 107BH。 - 从机器码
EB 09中提取出偏移量为09H。 - 目标地址 =
0040 107BH + 09H = 0040 1084H。
(3) 源操作数寻址方式、ecx内容及大小端判断
- 源操作数寻址方式:立即寻址。
分析:指令 mov[ecx+edx*4+00422000h], 0Ah 中的源操作数是常数 0Ah(即十进制的 10),它直接包含在指令的机器码中。
- ecx 中存放的是什么:ecx 中存放的是
(即数组第
行首地址相对于数组首地址的字节偏移量)。
分析:二维数组 a[i][j] 在内存中按行优先连续存放。要计算 a[i][j] 的地址,公式为:数组首地址 + (i * 列数 + j) * 元素大小。题目代码中有 for(j=0; j<64; j++),可知每行有 64 个元素;根据目标地址乘以 4(edx*4),可知每个 int 元素占 4 个字节。将公式展开为:数组首地址 + i * 64 * 4 + j * 4。指令中的地址表达式为 00422000h + ecx + edx * 4。已知 edx 存放的是 j,00422000h 是数组首地址,所以 ecx 对应 i * 64 * 4,即 。
- 计算机M采用的是 小端方式。
分析:观察第19条指令的机器码,数组首地址 0042 2000H 在机器码中表示为 00 20 42 00,立即数 0000 000AH 在机器码中表示为 0A 00 00 00。可以看出,数据的低有效字节存放在了低地址端,符合小端存储机制。
(4) 不会发生缺页异常。
原因分析:缺页异常发生在 CPU 访问的虚拟地址所在的页面不在物理内存中时。
- 第19条指令的虚拟地址为
0040 10AEH。 - 程序的执行是按顺序进行的(或者经过循环跳转)。在第一次执行第19条指令之前,CPU 已经取了同处于一个循环代码块中的前面几条指令(例如第1、2、3条指令,地址在
0040 1072H附近)。 - 无论系统采用多大的页面大小(通常至少为 4KB,即
1000H),地址0040 1072H和0040 10AEH都必然同属于同一个页面(它们的页号都是00401H)。 - 既然前面的指令已经成功取指并执行,说明该代码段所在的页面已经被装入主存,因此取第19条指令时,该页面必定在内存中,不会发生缺页异常。
X86 中的常见寄存器¶
X86处理器的寄存器主要分为基本寄存器、系统寄存器、浮点寄存器等几类。这里我们仅介绍与汇编代码密切相关的基本寄存器,包括通用寄存器、指令指针寄存器、和标志寄存器,这些都是 程序员可见寄存器 。

- 指令指针寄存器:
EIP寄存器是32位X86机器上的指令指针寄存器(也就是我们所说的 \(PC\) 寄存器),它始终指向下一条指令的地址,并且由CPU自动设置,无法手动干预。\(call、jmp、ret\) 等指令可以改变EIP的值,使得程序跳转执行。(它是用户可见的寄存器,汇编程序员可以通过指定待执行指令的地址例如通过 \(jmp\) 指令来改变 \(pc\) 的值) -
标志寄存器
EFLAGS: -
内存操作数
说明操作数存储在内存之中,由数据寻址的方式可知,操作数的写法可以有多种方式,在下面我们一起介绍。
示例:
通用寄存器¶
通用寄存器是指没有专门用途、可以存放各类定点操作数的寄存器。它们的长度为 32 位,但可根据操作数长度(字节、字、双字)灵活存取最低 8 位、最低 16 位或全部 32 位。
| 32 位 | 16 位 | 8 位(高/低) | 名称与用途 |
|---|---|---|---|
| EAX | AX | AH / AL | 累加器。主要用来存放操作数 |
| EBX | BX | BH / BL | 基址寄存器。主要用来存放操作数 |
| ECX | CX | CH / CL | 计数寄存器。主要用来存放操作数 |
| EDX | DX | DH / DL | 数据寄存器。主要用来存放操作数 |
| ESP | SP | — | 栈指针寄存器,被用来指示栈顶,主要用于实现函数调用 |
| EBP | BP | — | 基址指针寄存器,一般被用来指示栈底 |
| ESI | SI | — | 源变址寄存器。主要存放变址值或指针 |
| EDI | DI | — | 目标变址寄存器。主要存放变址值或指针 |
使用要点:
- EAX、EBX、ECX、EDX:这 4 个寄存器以存放操作数为主。例如,当操作数是字节时,可只使用 AL/BL/CL/DL;是字时使用 AX/BX/CX/DX;是双字时则使用完整的 32 位 EAX~EDX。
- ESP、EBP、ESI、EDI:这 4 个寄存器以存放变址值或指针为主。其中 ESP 专用于指向栈顶,EBP 常用于指向栈帧基址,ESI 和 EDI 则在串操作等场合分别作为源和目标变址指针。
这里面的\(E\)代表\(Extended\) ,表示相当于对
AX等16位寄存器进行了扩展,是32位的寄存器。汇编语言对寄存器的大小写不敏感,例如考试中通常把EAX寄存器写成eax。
指令指针寄存器 EIP¶
- EIP 是从 16 位 IP 扩展而来的 32 位寄存器。
- 它与程序计数器 PC 功能完全一样(只是名称不同),专门用来存放将要执行的下一条指令的地址。
- 实地址模式时,CPU 使用 16 位的 IP;保护模式时,则使用 32 位的 EIP。
标志寄存器 EFLAGS¶
它是一个32位的标志寄存器(即我们常说的程序状态字寄存器\(PSW\)),它由控制CPU操作或反映CPU某些运算结果的相互独立的二进制位构成,这些二进制位可以分为控制标志、状态标志和系统标志。通常当某标志位为1时称该标志位被“置位”,当某标志位为0时称该标志位被“清零”。我们主要关注\(CF、OF、ZF、SF\)即可。

EFLAGS 按功能分为 6 个条件标志和 3 个控制标志:
6 个条件标志(由硬件自动设定,又称条件码)
| 标志 | 全称 | 含义 |
|---|---|---|
| OF | Overflow Flag(溢出标志) | 反映带符号数运算结果是否超出数值范围。如字节运算超出 -128 ~ +127、字运算超出 -32768 ~ +32767,则 OF = 1,否则 OF = 0。 |
| SF | Sign Flag(符号标志) | 反映带符号数运算结果的符号。负数为 1,否则为 0。 |
| ZF | Zero Flag(零标志) | 反映运算结果是否为 0。结果为 0 则 ZF = 1,否则 ZF = 0。 |
| CF | Carry Flag(进/借位标志) | 反映无符号整数加/减运算后的进位或借位情况。有进(借)位则 CF = 1,否则 CF = 0。 |
| AF | Auxiliary Carry Flag(辅助进位标志) | 反映低 4 位向高 4 位的进位或借位(BCD 运算用)。 |
| PF | Parity Flag(奇偶标志) | 反映运算结果低 8 位中 1 的个数是否为偶数。 |
特别注意:OF 和 SF 对无符号数运算没有意义;CF 对带符号整数运算没有意义。
3 个控制标志(由软件设定,用于控制 CPU 操作)
| 标志 | 全称 | 含义 |
|---|---|---|
| DF | Direction Flag(方向标志) | 确定串操作指令执行时,变址寄存器 SI(ESI)和 DI(EDI)的内容是自动递增还是递减。DF = 1 为递减,否则为递增。可用 std 指令置 1,cld 指令清 0。 |
| IF | Interrupt Flag(中断允许标志) | IF = 1 表示允许响应中断,否则禁止。它仅对外部可屏蔽中断起作用,对非屏蔽中断和内部异常不起作用。可用 sti 置 1,cli 清 0。 |
| TF | Trap Flag(陷阱标志) | 控制单步执行。TF = 1 时,CPU 每执行完一条指令后就把当前机器状态(各寄存器和存储单元值等)显示出来,便于调试。没有专门指令直接修改它,但可通过栈操作指令 pushf/pushfd 和 popf/popfd 来改变其值。 |
x86 汇编语言的指令格式¶
在底层机器码层面,一条指令通常由操作码(Opcode)和操作数(Operand)构成。为了方便人类阅读和编写,汇编语言将其抽象成了文本格式。一条完整的 x86 汇编指令通常包含以下四个基本部分: [标号: ] 指令助记符 操作数 [; 注释]
标号 (Label) - 【可选】¶
标号主要用来标记代码或数据在内存中的地址,相当于高级语言中的“变量名”或“行号”。
-
代码标号:必须以冒号
:结尾。通常放在一条指令的前面,作为控制流指令(如jmp、call)跳转的目标地址。 -
示例:
target: mov eax, ebx(可以独占一行,也可以和指令同在一行)。 -
数据标号:不能以冒号结尾。通常放在变量定义的前面,表示该变量的起始地址。
-
示例:
count DWORD 100(定义了一个 4 字节的变量 count,初始值为 100)。
指令助记符 - 【必需】¶
指令助记符是指令操作性质的英文缩写,它直接对应底层的机器指令操作码。
- 示例:
MOV(数据传送)、ADD(加法)、JMP(无条件跳转)等。
操作数 - 【通常是必需的】¶
操作数是指令操作的对象,或者指明操作对象存放的地址。一条指令通常可以有 0 到 3 个操作数。当有多个操作数时,Intel 格式通常分为目的操作数(在前)和源操作数(在后),中间用逗号隔开。
操作数的类型和寻址方式是汇编语言的核心考点。按照教材约定(用 \(R[r]\) 表示寄存器的值,用 \(M[addr]\) 表示内存地址处的值),常见寻址方式如下:
| 操作数类型 | 寻址方式 | 汇编书写举例 | 实际引用的值 | 说明 |
|---|---|---|---|---|
| 立即数 | 立即数寻址 | 0xff |
\(255\) | 直接将常数写在指令中 |
| 寄存器 | 寄存器寻址 | eax |
\(R[eax]\) | 数据存放在寄存器中 |
| 内存 | 直接寻址 | [var] |
\(M[var]\) | 直接给出内存变量名/地址 |
| 内存 | 寄存器间接寻址 | [eax] |
\(M[R[eax]]\) | 寄存器中存放的是内存地址 |
| 内存 | 基址寻址 | [ebx+4] |
\(M[R[ebx]+4]\) | 基址寄存器 + 固定偏移量 |
| 内存 | 基址变址寻址 | [ebx+esi+4] |
\(M[R[ebx]+R[esi]+4]\) | 基址 + 变址 + 偏移量 |
| 内存 | 比例变址寻址 | [ebx+eax*4+3] |
\(M[R[ebx]+R[eax] \times 4+3]\) | 用于数组访问:基址 + 索引*元素大小 + 偏移 |
重要提示 (PTR 操作符):\ 当操作数涉及内存读写,且 CPU 无法自动推断出要读写多少个字节时,必须使用
PTR指明操作数的位宽:
BYTE PTR:单字节(8位)WORD PTR:字(16位)DWORD PTR:双字(32位)- 示例:
MOV DWORD PTR [ebx], 1(指明向 ebx 指向的内存写入一个 32 位的数字 1)。
注释 - 【可选】¶
以分号 ; 开始,分号后面的所有内容都是注释。汇编器在编译时会完全忽略这部分内容,仅用于帮助程序员理解代码。
扩展补充:Intel 格式 vs AT\&T 格式¶
虽然考试和大多数教材以 Intel 格式为主,但在 Linux 和 GCC 编译器底层常常使用 AT\&T 格式。两者的主要区别如下:
-
操作数方向:
-
Intel:
助记符 目的操作数, 源操作数 -
AT\&T:
助记符 源操作数, 目的操作数(完全相反) -
寄存器与立即数前缀:
-
Intel:直接写
eax和常数100。 -
AT\&T:寄存器前加
%(如%eax),立即数前加$(如$100)。 -
内存寻址符号:
-
Intel:使用中括号
[ebx]。 -
AT\&T:使用小括号
(%ebx)。 -
指令后缀:
-
Intel:依靠寄存器大小或
PTR指明操作长度。 - AT\&T:在助记符后加后缀
b(byte, 8位),w(word, 16位),l(long, 32位),q(quad, 64位)。例如movl。
X86 常用指令介绍¶
这里默认采用常见的 Intel语法,即操作数顺序为
目标操作数, 源操作数,且以32位架构为例)。
数据传送指令¶
MOV 指令(真题中出现最多)¶
MOV 指令(Move)是 x86 汇编中最基础、使用频率最高的数据传送指令。它的核心功能是将数据从一个位置“复制”到另一个位置(注意:是复制,源位置的数据不会丢失)。
- 指令格式 (Intel 语法)
作用:将 SRC 的内容赋值给 DEST。相当于高级语言中的 DEST = SRC;。
-
操作数可能的类型
-
reg (寄存器, Register):如 EAX, EBX, CL 等。
- mem (内存, Memory):用方括号
[]表示的内存地址,如[EBX],[VarName]。 -
imm (立即数, Immediate):直接写出的常数,如 1, 0Ah, 100。
-
MOV 指令允许的组合(数据流向)
-
MOV reg, reg(寄存器 -> 寄存器) MOV reg, imm(立即数 -> 寄存器)MOV mem, imm(立即数 -> 内存)MOV reg, mem(内存 -> 寄存器)MOV mem, reg(寄存器 -> 内存)
绝对禁止的组合(硬件限制):
- 不能
MOV mem, mem:x86 不允许直接将数据从一个内存地址传送到另一个内存地址。必须用寄存器中转(先 MOV 到寄存器,再 MOV 到目标内存)。 - 目的操作数不能是立即数:你不能写
MOV 1, EAX,这在逻辑上也是说不通的。
真题相关指令讲解
示例 1:获取函数参数 :mov eax, dword ptr [ebp+8]
- 指令含义:将内存地址
EBP + 8处连续的 4 个字节(DWORD,双字)数据,读取并存入寄存器 EAX 中。 - 寻址方式分析:目的操作数 (eax):寄存器寻址 。操作数直接存放在寄存器中;源操作数 ([ebp+8]): 基址寻址。以 EBP 作为基址寄存器,加上偏移量 8 得到最终的有效地址。
在 C 程序的底层,这几乎是读取函数参数的标准动作。在建立标准栈帧后,
[ebp+8]通常指向函数调用者压入栈的第一个参数。
示例 2:寄存器赋初值:mov eax, 1 //将常数值 1 存入寄存器 EAX 中。
- 寻址方式分析:目的操作数 (eax):寄存器寻址; 源操作数 (1):立即数寻址。操作数本身直接包含在指令的机器码中。
常用于变量初始化。或者在函数返回前,用
MOV EAX, 1来设置函数的返回值(C语言约定整数返回值存放在 EAX 中,这里相当于return 1;)。
示例 3:局部变量赋初值:mov dword ptr [ebp-4], 0
- 指令含义:将常数值 0 写入到内存地址
EBP - 4开始的 4 个字节空间中。
这也是栈帧操作中的标准动作,用于初始化局部变量。在函数内部声明的局部变量(如
int a = 0;),通常会被分配在 EBP 之下,[ebp-4]就是第一个局部变量的位置。
示例 4:复杂数组寻址
- 指令含义:计算地址
ECX + (EDX * 4) + 00422000h,并将立即数 10 (0Ah) 写入该内存地址处的 4 个字节中。 - 寻址方式分析:源操作数 (0Ah):立即数寻址;目的操作数 ([...]):带比例的基址变址寻址。
- 基址寄存器 (Base):
ECX(比如代表某个对象的起始地址)。 - 变址寄存器 (Index):
EDX(比如代表数组的当前索引i)。 - 比例因子:
4(因为是 32位 DWORD 数组,每个元素占 4 字节)。 - 偏移量:
00422000h(比如代表全局数组的首地址,或者是结构体中某个数组成员的偏移)。
这种寻址方式完美对应了高级语言中的复杂数组或结构体操作。例如 C 语言中的
obj->array[i] = 10;或全局数组GlobalArray[i] = 10;,编译器可以用这一条指令就完成所有复杂的地址计算和赋值动作。
PUSH - 压栈指令¶
PUSH 的核心使命是将数据安全地存入内存中的“栈(Stack)”区。栈的特点是“后进先出 (LIFO)”,并且在 x86 中,栈是向低地址方向生长的。

- 指令格式:
PUSH 源操作数 - 底层微观动作 (分两步) 以压入 32 位数据为例:
- 指针先下移:栈顶指针寄存器
ESP = ESP - 4。 - 数据后存入:将
SRC的值写入到当前ESP所指向的内存地址中。 -
经典示例
-
示例 1:保存寄存器现场:
push eax //将 EAX 中的 32 位数据压入栈中保存。
应用场景:在调用函数前,或者在函数内部使用 EAX 前,先把 EAX 原本的值存起来,防止被覆盖。
- 示例 2:传递常数参数:
push 10 //将数值 10 压入栈中。
应用场景:C 语言调用函数
func(10)时,底层就是先把参数 10 压栈。
- 示例 3:传递内存中的参数:
push dword ptr [ebp+8]去内存地址EBP + 8处读取 4 个字节,然后把这 4 个字节压入栈中。
应用场景:当前函数拿到了调用者传来的参数
[ebp+8],现在它又要调用另一个子函数,于是把这个参数原封不动地再次压栈传给子函数。
POP - 出栈指令¶
POP 是 PUSH 的逆操作,负责把栈顶的数据取出来,放回指定的目的地,并清理栈空间。
- 指令格式:
POP 目的操作数 (DEST) -
操作数可能的类型
-
reg (寄存器):如 EAX, EBX
- mem (内存):如
[ECX],[EBP-4] -
⚠️ 绝对禁止:不能是立即数(不能把数据弹出到一个常数里)。
-
底层微观动作 (分两步)
- 数据先取出:将当前
ESP所指向的内存地址中的 4 个字节数据,复制给DEST。 - 指针后回退:栈顶指针寄存器
ESP = ESP + 4(向高地址退回)。 -
经典示例与寻址方式
-
示例 1:恢复寄存器现场:
pop eax //把栈顶的数据弹出来,赋值给 EAX
应用场景:与
push eax对应,函数执行完毕准备返回前,把当初保存的值恢复到 EAX 中。
- 示例 2:弹出数据到内存:
pop dword ptr [ebp-4] //把栈顶的数据弹出来,写入到 EBP - 4 这个内存地址中
应用场景:有时候不想用寄存器中转,直接把栈顶的数据赋值给某个局部变量(
ebp-4通常是第一个局部变量)。
LEA 指令 (Load Effective Address) - 装入有效地址¶
目前还未出现过,简单了解一下
LEA 指令看似是传送数据,但实际上它是用来计算地址的。它将源操作数(通常是一个内存引用)的内存地址(即 EA 有效地址),而不是该地址里的数据,加载到目标寄存器中。
- 指令格式:
LEA 目标寄存器 (DEST), 源操作数 (SRC) - 指令作用:计算 SRC 的有效地址,并将这个地址值存入 DEST。
- 指令示例
LEA EAX, [EBX+ECX*4+10] ; 计算地址 EBX + ECX*4 + 10,并将这个地址值存入 EAX
LEA EDX, [EBP-8] ; 获取栈帧中局部变量的地址,存入 EDX
-
⚠️ 注意事项 (LEA 与 MOV 的核心区别)
-
如果执行
MOV EAX, [EBX+8]:CPU 会去计算EBX+8的地址,去内存中读取该地址存放的数据,然后放入 EAX。 - 如果执行
LEA EAX, [EBX+8]:CPU 只计算EBX+8这个地址数值本身,并把这个地址值放入 EAX,不会去访问内存。 - 由于
LEA本质上是在做快速的加法和乘法运算且不访问内存,编译器在优化代码时,经常会巧妙地使用LEA指令来代替普通的算术指令(如ADD或MUL)进行快速的数值计算。
算术和逻辑运算指令¶
$ \def\arraystretch{1.5} \begin{array}{|l|c|c|} \hline \text{指令} & \text{效果} & \text{描述} \ \hline
\text{inc D} & \text{D} + 1 \rightarrow \text{D} & \text{加 1} \ \text{dec D} & \text{D} - 1 \rightarrow \text{D} & \text{减 1} \ \text{neg D} & -\text{D} \rightarrow \text{D} & \text{取负} \ \text{not D} & \sim\text{D} \rightarrow \text{D} & \text{取补} \ \hline \text{add D, S} & \text{D} + \text{S} \rightarrow \text{D} & \text{加} \ \text{sub D, S} & \text{D} - \text{S} \rightarrow \text{D} & \text{减} \ \text{imul D, S} & \text{D} * \text{S} \rightarrow \text{D} & \text{乘} \ \text{xor D, S} & \text{D} \mathbin{\hat{}} \text{S} \rightarrow \text{D} & \text{异或} \ \text{or D, S} & \text{D} \mid \text{S} \rightarrow \text{D} & \text{或} \ \text{and D, S} & \text{D} \mathbin{\&} \text{S} \rightarrow \text{D} & \text{与} \ \hline \text{sal D, k} & \text{D} \ll \text{k} \rightarrow \text{D} & \text{左移} \ \text{shl D, k} & \text{D} \ll \text{k} \rightarrow \text{D} & \text{左移(等同于 sal)} \ \text{sar D, k} & \text{D} \gg \text{k} \rightarrow \text{D} & \text{算术右移} \ \text{shr D, k} & \text{D} \gg \text{k} \rightarrow \text{D} & \text{逻辑右移} \ \hline \end{array} $
ADD / SUB 指令¶
- 指令格式
- 操作数可能的类型组合\
与
MOV指令极其相似,支持reg, reg、reg, mem、mem, reg、reg, imm、mem, imm。\ 绝对禁止:mem, mem(内存到内存的直接加减是不允许的)。 - 底层微观动作:执行二进制加法或减法。
关键点:这俩指令的执行会疯狂更新 EFLAGS 状态标志寄存器(ZF, SF, OF, CF 等标志位),为后面的
jcond跳转指令提供判断依据。
-
经典示例与寻址方式
-
示例 1:累加器加常数:
add eax, 100 //EAX = EAX + 100 - 示例 2:内存减去寄存器:
sub dword ptr [ebp-8], ebx//将局部变量[ebp-8]的值减去 EBX,结果存回[ebp-8]
自增与自减指令:INC / DEC¶
用于将操作数加 1 或减 1。
- 指令格式
-
底层微观动作与特殊性
-
特殊规则:
INC和DEC会更新 ZF, SF, OF,但绝对不会改变 CF(进/借位标志)!这是为了在循环中方便地使用DEC控制循环次数,而不破坏原本可能需要保留的进位信息。 -
经典示例与寻址方式
-
示例 1:循环计数器递减:
dec ecx - 示例 2:内存数据自增:
inc dword ptr [esi+ecx*4]//将数组中当前元素的值加 1
乘法与除法:IMUL / IDIV (有符号)¶
这里我们重点讲有符号运算(带 I 的指令),因为它们在高级语言中对应标准的 int 类型运算。
IMUL (有符号乘法) 的三种形态¶
-
单操作数(隐式双倍字长):
IMUL mem/reg -
隐含被乘数是
EAX。执行EAX * 操作数。 -
结果是 64 位:高 32 位存入
EDX,低 32 位存入EAX。 -
双操作数(最常用):
IMUL reg, mem/reg/imm -
DEST = DEST * SRC。结果被截断为 32 位。 -
三操作数:
IMUL reg, mem/reg, imm -
DEST = SRC1 * SRC2。
IDIV (有符号除法)¶
指令格式:IDIV 除数 (SRC) (除数可以是 reg 或 mem,不能是 imm)。
- 底层动作:它要求被除数必须是 64 位,隐式存放在
EDX:EAX中。 - 除法执行后:商存放在
EAX,余数存放在EDX。
逻辑运算指令:AND / OR / XOR / NOT(与/或/异或/非)¶
这些是按位(Bitwise)操作的指令。
-
指令格式与规则
-
AND / OR / XOR DEST, SRC(双操作数,支持 reg/mem/imm 组合)。 NOT DEST(单操作数,支持 reg/mem)。-
标志位影响:
AND/OR/XOR执行后,会强制清除 CF 和 OF (置 0),并更新 ZF, SF。NOT不影响任何标志位。 -
经典示例与底层妙用
-
示例 1:掩码清零特定位 (AND)
- 示例 2:寄存器高效清零 (XOR)
- 指令含义:自己跟自己异或,结果必为 0。
- 示例 3:按位取反 (NOT)
取负指令 NEG¶
- 指令格式:
NEG 操作数 (DEST) - 底层逻辑\
将操作数求补,也就是取相反数。在底层,CPU 是通过用 0 减去该操作数(
0 - DEST)来实现的。 - 示例
移位指令:SHL / SHR (逻辑移位)¶
移位指令非常强大,常被编译器用来替代极其耗时的乘除法。
SHL(Shift Left):逻辑左移,右边空出的位补 0。-
SHR(Shift Right):逻辑右移,左边空出的位补 0。 -
指令格式:
SHL 目的操作数 (DEST), 移位次数 (COUNT)
操作数要求:
DEST可以是 reg 或 mem。COUNT只能是常数 (立即数),或者放在 CL 寄存器中(当移位次数是动态变量时,只能用 CL,不能用其他寄存器!)。
- 底层微观动作\
移位时,最后被移出边界的那个比特位(Bit),会被“掉”进进位标志
CF寄存器中。 -
经典示例与寻址方式
-
示例 1:乘以 2 的 N 次方 (SHL)
- 指令含义:将 EAX 左移 3 位,等价于 \(EAX \times 2^3 = EAX \times 8\)。比
IMUL速度快得多。 - 示例 2:动态除以 2 的 N 次方 (SHR)
控制流指令¶
在 x86 处理器中,程序通过指令指针寄存器 EIP(相当于 PC)来指示当前执行指令的地址。因为 EIP 不能被直接访问或修改,所以必须依赖以下这些控制流指令结合标签(label)来实现程序的分支、循环和函数调用。
状态设置指令:cmp 与 test¶
控制流往往需要根据条件来判断,而 cmp 和 test 指令就是专门用来“刺探”状态并设置标志位的先锋指令。
- 核心特点:这两个指令只负责更新程序状态字寄存器(EFLAGS,即 psw 寄存器)中的条件码,绝对不会改变原始操作数的值。
- cmp 指令:底层执行减法(
sub)运算。常用于比较两个数的大小。例如cmp eax, ebx,通过计算eax - ebx来设置状态标志。 - test 指令:底层执行按位与(
and)运算。常用于测试某个寄存器是否为 0,或者测试特定的位。例如test eax, eax,如果eax为 0,则标志位 ZF 会被置 1。

无条件转移指令:jmp¶
遇到该指令时,程序不需要任何判断,直接跳转到目标地址。
- 指令格式:
jmp <label> - 底层机制:在汇编代码中,
label只是一个助记符。经过汇编器翻译成机器码后,它会被转换为一个相对偏移量。 - 偏移量计算:偏移量 = 目标地址 - 下一条指令的起始地址(因为执行当前
jmp指令时,EIP 已经自动指向了下一条指令)。
条件转移指令:jcond (jcondition)¶
这是实现 if-else 分支和 for/while 循环的核心。它们根据cmp或test刚刚设置的标志位,决定是跳转到label处,还是继续顺序执行下一条指令。
可以将其分为三大类,这里的跳转条件逻辑表达式是历年考试的重要考点:
1. 基于单个状态标志的跳转
je <label> # 相等时转移 (jump when equal)
jz <label> # 结果为零时转移 (jump when zero)
jne <label> # 不相等时转移 (jump when not equal)
jnz <label> # 结果非零时转移 (jump when not zero)
js <label> # 结果为负数时转移 (jump when sign)
jns <label> # 结果为非负数时转移 (jump when not sign)
2. 针对“有符号数”的跳转 (判断 Greater / Less)\ 此类指令需综合判断符号标志 SF 和溢出标志 OF,以应对运算溢出导致的符号位反转问题。
jg <label> # 大于时转移 (jump when greater)
jnle <label> # 不小于等于时转移 (jump when not less or equal)
jge <label> # 大于等于时转移 (jump when greater or equal)
jnl <label> # 不小于时转移 (jump when not less)
jl <label> # 小于时转移 (jump when less)
jnge <label> # 不大于等于时转移 (jump when not greater or equal)
jle <label> # 小于等于时转移 (jump when less or equal)
jng <label> # 不大于时转移 (jump when not greater)
3. 针对“无符号数”的跳转 (判断 Above / Below)\ 此类指令不关心符号位,只关注进位/借位标志 CF 和零标志 ZF。
ja <label> # 高于/超过时转移 (jump when above)
jnbe <label> # 不低于等于时转移 (jump when not below or equal)
jae <label> # 高于等于时转移 (jump when above or equal)
jnb <label> # 不低于时转移 (jump when not below)
jb <label> # 低于时转移 (jump when below)
jnae <label> # 不高于等于时转移 (jump when not above or equal)
jbe <label> # 低于等于时转移 (jump when below or equal)
jna <label> # 不高于时转移 (jump when not above)
\( \def\arraystretch{1.5} \begin{array}{|c|l|l|l|l|} \hline \textbf{序号} & \textbf{指令} & \textbf{转移条件 } & \textbf{逻辑表达式} & \textbf{说明} \\ \hline 1 & \text{jc label} & \text{CF = 1} & CF & \text{有进位/借位} \\ \hline 2 & \text{jnc label} & \text{CF = 0} & \overline{CF} & \text{无进位/借位} \\ \hline 3 & \text{je/jz label} & \text{ZF = 1} & ZF & \text{相等/等于零} \\ \hline 4 & \text{jne/jnz label} & \text{ZF = 0} & \overline{ZF} & \text{不相等/不等于零} \\ \hline 5 & \text{js label} & \text{SF = 1} & SF & \text{是负数} \\ \hline 6 & \text{jns label} & \text{SF = 0} & \overline{SF} & \text{是非负数} \\ \hline 7 & \text{jo label} & \text{OF = 1} & OF & \text{有溢出} \\ \hline 8 & \text{jno label} & \text{OF = 0} & \overline{OF} & \text{无溢出} \\ \hline 9 & \text{ja/jnbe label} & \text{CF = 0 AND ZF = 0} & \overline{CF} \land \overline{ZF} & \text{无符号整数 } A > B \\ \hline 10 & \text{jae/jnb label} & \text{CF = 0} & \overline{CF} & \text{无符号整数 } A \ge B \\ \hline 11 & \text{jb/jnae label} & \text{CF = 1} & CF & \text{无符号整数 } A < B \\ \hline 12 & \text{jbe/jna label} & \text{CF = 1 OR ZF = 1} & CF \ | \ ZF & \text{无符号整数 } A \le B \\ \hline 13 & \text{jg/jnle label} & \text{SF = OF AND ZF = 0} & \overline{SF \oplus OF} \land \overline{ZF} & \text{带符号整数 } A > B \\ \hline 14 & \text{jge/jnl label} & \text{SF = OF} & \overline{SF \oplus OF} & \text{带符号整数 } A \ge B \\ \hline 15 & \text{jl/jnge label} & \text{SF} \neq \text{OF} & SF \oplus OF & \text{带符号整数 } A < B \\ \hline 16 & \text{jle/jng label} & \text{SF} \neq \text{OF OR ZF = 1} & (SF \oplus OF) \ | \ ZF & \text{带符号整数 } A \le B \\ \hline \end{array} \)
函数调用与返回指令:call / ret¶
这两条指令是高级语言中函数调用机制的底层核心,它们负责转移控制权的同时,妥善管理返回地址。
call 指令:¶
- 首先,将下一条指令的地址(即函数执行完毕后应该回到的地址)压入堆栈(栈指针下移)。
- 然后,将
label处的子程序起始地址复制到 EIP 寄存器中,实现向子程序的跳转。
ret 指令:¶
- 子程序执行完毕后,从堆栈中弹出之前保存的返回地址。
- 将弹出的地址送入 EIP 寄存器,程序控制权完美交接回调用者。
x86 过程(函数)调用的机器级表示¶
寄存器使用规则¶
x86 架构中寄存器资源有限,为了防止调用者(Caller)与被调用者(Callee)在同时使用寄存器时发生数据覆盖冲突,现代编译器(如 GCC)严格遵守以下调用约定:
1. 调用者保存寄存器
- 寄存器:
EAX,ECX,EDX(这些寄存器主要用于传递参数或临时计算)。 - 规则: 如果调用者(Caller)希望调用子程序后这些寄存器的值保持不变,调用者必须自己保存(如压栈),并在子程序返回后恢复。被调用者(Callee)可以自由使用这些寄存器,无需在返回前恢复它们的原值。
- 特殊用途:
EAX通常还固定用于存放函数的返回值。
2. 被调用者保存寄存器
- 寄存器:
EBX,ESI,EDI,EBP(这些寄存器通常用于保存重要数据或作为指针。) - 规则: 当过程 P 调用过程 Q 时,P 默认这些寄存器在调用前后保持不变。如果 Q 需要用到其中某一个寄存器,Q 必须在正式执行自身业务逻辑前,将它们压栈备份,并在返回(执行
ret)前弹出恢复。 - 指针寄存器:
EBP(基址指针)和ESP(栈指针)同样由被调用者维护和恢复。
运行栈与栈帧(Stack Frame)结构¶
x86 的运行栈是一个向低地址增长的内存区域(即高地址为栈底,低地址为栈顶)。每个正在执行的函数在栈中都拥有一个专属区域,称为栈帧(Stack Frame)。
- EBP (Base Pointer - 基址指针): 指向当前栈帧的“底部”(高地址端,紧邻返回地址和旧 EBP),在整个函数执行期间保持不变。通过
[ebp + 偏移量]访问参数,通过[ebp - 偏移量]访问局部变量。 - ESP (Stack Pointer - 栈指针): 始终指向当前运行栈的“真正栈顶”(低地址端),随着数据的入栈(
push)和出栈(pop)动态变化。

栈帧内容(从高地址到低地址):
- 参数:由调用者压入。
- 返回地址:由
call指令自动压入(即call下一条指令的地址)。 - 旧的 EBP:保存上一层函数的 EBP。
- 局部变量:由当前函数分配。
- 保存的寄存器:如果有使用被调用者保存寄存器(如 EBX),会在这里保存。
过程调用流程详解:假设过程 P 调用过程 Q。¶
调用前 (Caller P)¶
- 参数传递:若 EAX/ECX/EDX 在使用中且调用后仍需使用,将其值保存到栈里,将参数从右至左依次入栈(最后一个参数先压栈,第一个参数最后压栈,因此第一个参数在栈内地址最小)
- 调用指令:执行
call Q。 - 隐含操作:将返回地址(
call的下一条指令地址)压入栈中。 - 跳转:PC 指向 Q 的第一条指令。
进入子程序 (Callee Q )¶
- 保存旧 EBP:
push ebp。 - 建立新栈帧:
mov ebp, esp。此时 EBP 指向新栈帧的底部(高地址)。 - 分配局部变量空间:
sub esp, N(N 为字节数, 提升栈顶(减小 ESP)为局部变量分配空间 )。 - 保存被调用者寄存器:如果需要使用 EBX/ESI/EDI,使用
push ebx这种指令保存寄存器的值 等。
执行主体¶
- 通过
[ebp + 8],[ebp + 12]等访问参数。 - 通过
[ebp - 4],[ebp - 8]等访问局部变量。 - 计算结果通常保存在
EAX中。
返回前 (Callee Q )¶
- 保存返回值:将结果放入
EAX。 - 恢复被调用者寄存器:
pop ebx/esi/edi等。 - 释放局部变量空间:通常使用
leave指令(等价于mov esp, ebp然后pop ebp)。 - 返回:执行
ret指令。隐含操作:从栈顶弹出返回地址到 PC,跳转回 Caller。
返回后 (Caller P)¶
- 获取返回值:读取
EAX。 - 清理栈中参数:
add esp, M(M 为参数总字节数,调整 ESP,无需实际清除数据)。 - 恢复所保存的调用者保存寄存器
自定义示例:函数调用¶
场景:主函数 caller 调用子函数 calculate。
// 子函数:计算 (x - y) * z
int calculate(int x, int y, int z) {
int diff = x - y; // 局部变量
return diff * z;
}
// 调用者
int caller() {
return calculate(10, 5, 3);
}
; --- Caller 部分 ---
caller:
push ebp ; 保存 caller 的 ebp
mov ebp, esp ; 设置 caller 的 ebp
; 准备参数 (从右向左压栈: 3, 5, 10)
push 3 ; 参数 z
push 5 ; 参数 y
push 10 ; 参数 x
call calculate ; 调用子程序 (自动压入返回地址)
add esp, 12 ; 清理栈中参数 (3个参数 * 4字节)
; 此时 EAX 中保存了计算结果
pop ebp ; 恢复 ebp
ret
; --- Callee 部分 ---
calculate:
push ebp ; 1. 保存调用者 (caller) 的 ebp
mov ebp, esp ; 2. 设置 calculate 的 ebp (指向新栈帧基址)
sub esp, 4 ; 3. 分配局部变量空间 (1个int, 4字节)
; 此时栈结构 (低地址 -> 高地址):
; [ebp-4] : diff (局部变量)
; [ebp] : Old EBP (caller的ebp)
; [ebp+4] : Return Address
; [ebp+8] : x (参数1)
; [ebp+12]: y (参数2)
; [ebp+16]: z (参数3)
; 4. 执行主体: diff = x - y
mov eax, [ebp+8] ; 取出 x
sub eax, [ebp+12] ; 减去 y
mov [ebp-4], eax ; 存入 diff
; 5. 执行主体: return diff * z
mov eax, [ebp-4] ; 取出 diff
imul eax, [ebp+16] ; 乘以 z (结果存入 eax)
; 6. 返回前清理
; 不需要恢复被调用者寄存器 (因为没用 EBX/ESI/EDI)
leave ; 等价于 mov esp, ebp (释放局部变量) + pop ebp (恢复旧ebp)
ret ; 返回 caller


int f1(int n){
; ================== 函数序言 ==================
1 00401000 55 push ebp ; 保存调用者的旧 EBP
00401001 8B EC mov ebp, esp ; 设置当前 f1 的新 EBP 基址
; ================== if(n > 1) 条件测试 ==================
11 00401018 83 7D 08 01 cmp dword ptr [ebp+8], 1 ; 比较参数 n 与 1 (n 位于 [ebp+8])
12 0040101C 7E 17 jle f1+35h (00401035) ; 【反向跳转】若 n <= 1,则跳转到 else 分支
; ================== return n * f1(n-1); 递归准备 ==================
13 0040101E 8B 45 08 mov eax, dword ptr [ebp+8]; eax = n
14 00401021 83 E8 01 sub eax, 1 ; eax = n - 1 (计算递归参数)
15 00401024 50 push eax ; 【传参】将参数 n - 1 压栈
16 00401025 E8 D6 FF FF FF call f1 (00401000) ; 【递归调用】调用自身,并将返回地址压栈
; 返回后,f1(n-1) 的计算结果保留在 EAX 中
; ================== 递归返回后的现场恢复与乘法 =====================
0040102A 83 C4 04 add esp, 4 ; 【平栈】释放压入的参数空间 (4字节)
0040102D 8B 4D 08 mov ecx, dword ptr [ebp+8]; 【恢复现场】将当前栈帧的 n 重新加载到 ecx 中
19 00401030 0F AF C1 imul eax, ecx ; eax = eax * ecx (即 f1(n-1) * n)
20 00401033 EB 05 jmp f1+3Ah (0040103a) ; 跳转到尾部统一退出
; ================== else return 1; 分支 ==================
21 00401035 B8 01 00 00 00 mov eax, 1 ; 递归基(Base Case):返回值 eax = 1
; ================== 函数尾声 (Epilogue) & 栈检查 ==================
0040103A 8B E5 mov esp, ebp ; 恢复调用者的 ESP (释放局部空间)
0040103C 5D pop ebp ; 恢复调用者的 EBP
26 00401040 3B EC cmp ebp, esp ; 编译器生成的栈平衡检查 (防止栈溢出)
00401042 75 01 jne 00401045h ; 若不平衡则触发异常处理
00401045 ... ...
30 0040104A C3 ret ; 弹出返回地址,返回上一层递归或主调函数
}
x86 选择语句的机器级表示¶
核心底层机制(标志位与跳转)¶
在机器级,if-else 语句的执行依赖于 CPU 的程序状态字寄存器(PSW / EFLAGS)中的条件码。
1. 核心条件码(标志位)
任何算术或逻辑运算(如 sub, add)都会自动影响以下标志位,408 极常在选择题中考察它们的组合:
ZF(Zero Flag): 零标志。结果为 0 则置 1;否则置 0(常用于判断相等)。SF(Sign Flag): 符号标志。结果为负数则置 1;为正数或 0 则置 0(即运算结果的最高位)。OF(Overflow Flag): 溢出标志。针对有符号数运算,若发生补码溢出则置 1(正+正=负,负+负=正)。CF(Carry Flag): 进位/借位标志。针对无符号数运算,最高位产生进位或借位则置 1。
2. 专门的测试指令(详细描述见3.3.1 状态设置指令:cmp 与 test)
cmp src2, src1: 核心功能是计算src1 - src2。不保存计算结果,纯粹为了根据差值去改变上述四个标志位。test src2, src1: 核心功能是计算src1 & src2(按位与)。常用于执行test eax, eax来检查寄存器中的值是 0、正数还是负数。
跳转指令的辨析¶
编译器在翻译 if 中的比较条件时,会根据操作数是有符号数(int)还是无符号数(unsigned int),编译出完全不同的跳转指令。这是 408 极为致命的挖坑点:
- 有符号条件跳转(基于 SF、OF、ZF 的组合)
有符号数的“大小”受符号位和溢出位共同制约:
jg(Jump if Greater,大于): 条件为ZF == 0 && SF == OF。jge(Jump if Greater or Equal,大于等于): 条件为SF == OF(只要没发生意料之外的符号翻转,或者翻转了但溢出了,就说明是大于等于)。jl(Jump if Less,小于): 条件为SF != OF。-
jle(Jump if Less or Equal,小于等于): 条件为ZF == 1 || SF != OF。 -
无符号条件跳转(基于 CF、ZF 的组合)
无符号数不考虑符号,只看进位/借位:
ja(Jump if Above,大于): 条件为CF == 0 && ZF == 0。jb(Jump if Below,小于): 条件为CF == 1(减法产生了借位,说明被减数更小)。
\( \def\arraystretch{1.5} \begin{array}{|c|l|l|l|l|} \hline \textbf{序号} & \textbf{指令} & \textbf{转移条件 } & \textbf{逻辑表达式} & \textbf{说明} \\ \hline 1 & \text{jc label} & \text{CF = 1} & CF & \text{有进位/借位} \\ \hline 2 & \text{jnc label} & \text{CF = 0} & \overline{CF} & \text{无进位/借位} \\ \hline 3 & \text{je/jz label} & \text{ZF = 1} & ZF & \text{相等/等于零} \\ \hline 4 & \text{jne/jnz label} & \text{ZF = 0} & \overline{ZF} & \text{不相等/不等于零} \\ \hline 5 & \text{js label} & \text{SF = 1} & SF & \text{是负数} \\ \hline 6 & \text{jns label} & \text{SF = 0} & \overline{SF} & \text{是非负数} \\ \hline 7 & \text{jo label} & \text{OF = 1} & OF & \text{有溢出} \\ \hline 8 & \text{jno label} & \text{OF = 0} & \overline{OF} & \text{无溢出} \\ \hline 9 & \text{ja/jnbe label} & \text{CF = 0 AND ZF = 0} & \overline{CF} \land \overline{ZF} & \text{无符号整数 } A > B \\ \hline 10 & \text{jae/jnb label} & \text{CF = 0} & \overline{CF} & \text{无符号整数 } A \ge B \\ \hline 11 & \text{jb/jnae label} & \text{CF = 1} & CF & \text{无符号整数 } A < B \\ \hline 12 & \text{jbe/jna label} & \text{CF = 1 OR ZF = 1} & CF \ | \ ZF & \text{无符号整数 } A \le B \\ \hline 13 & \text{jg/jnle label} & \text{SF = OF AND ZF = 0} & \overline{SF \oplus OF} \land \overline{ZF} & \text{带符号整数 } A > B \\ \hline 14 & \text{jge/jnl label} & \text{SF = OF} & \overline{SF \oplus OF} & \text{带符号整数 } A \ge B \\ \hline 15 & \text{jl/jnge label} & \text{SF} \neq \text{OF} & SF \oplus OF & \text{带符号整数 } A < B \\ \hline 16 & \text{jle/jng label} & \text{SF} \neq \text{OF OR ZF = 1} & (SF \oplus OF) \ | \ ZF & \text{带符号整数 } A \le B \\ \hline \end{array} \)
最好会推导这个表格里面的转移条件!!!
标准汇编控制流转换(反向跳转逻辑)¶
为了让流水线执行得更顺畅,现代编译器在生成 if-else 的汇编时,通常采用“反向条件跳转”的范式。
现代编译器实际转换成的goto拓扑流:在汇编中,编译器会判断 x > y 的反向条件(即 x <= y)。如果 x <= y 成立,直接跳过去执行 Else 分支;如果反向条件不成立,则顺序向下执行 Then 分支。
示例讲解¶
编写一个计算两个数绝对差值的函数 abs_delta。
int abs_delta(int x, int y) {
int result;
if (x > y) {
result = x - y;
} else {
result = y - x;
}
return result;
}
假设参数 x 位于 [ebp+8],y 位于 [ebp+12]:
abs_delta:
push ebp ; 建立栈帧 prologue
mov ebp, esp
mov eax, dword ptr [ebp+8] ; 将参数 x 加载到寄存器 eax
mov edx, dword ptr [ebp+12] ; 将参数 y 加载到寄存器 edx
cmp eax, edx ; 【核心比对】评估 eax - edx,设置 ZF, SF, OF 标志位
jle .L_false_branch ; 【分支预测/反向跳转】如果 x <= y,则跳转到 Else 分支
; jle 的触发条件:ZF==1 或 SF!=OF
; -------------------- Then 分支 (x > y) --------------------
sub eax, edx ; eax = eax - edx (即 x - y,返回值保留在 eax 中)
jmp .L_done ; Then 执行完毕,必须无条件跳过 Else 分支
; -------------------- Else 分支 (x <= y) -------------------
.L_false_branch:
sub edx, eax ; edx = edx - eax (即 y - x)
mov eax, edx ; 将结果送入结果寄存器 eax
; -------------------- 汇合终点 ------------------------------
.L_done:
pop ebp ; 撤销栈帧 epilogue
ret ; 返回调用者
x86循环结构语句的机器级表示¶
Note
循环结构是指可重复执行的一组语句。C语言提供的主要循环结构有 for、while 和 do-while。
x86汇编语言中没有直接对应这些高级循环结构的指令,编译器通过条件测试(如 cmp、test)和条件转移(如 je、jne、jg、jle 等)指令的组合来实现循环逻辑。
编译器在把循环语句转换为机器级表示时,会先将循环结构翻译为 goto 语句,再把 goto 语句转换成更低级的测试和条件跳转的组合。
do-while 循环¶
- 通用形式
do-while 是一种"后测试"型循环,先执行循环体,再判断循环条件。
- 翻译为 goto 语句
loop:
body_statement // 执行循环体语句(首次进入时必然执行)
t = test_expr; // 计算循环条件
if(t) // 若条件为真(t≠0)
goto loop; // 跳回 loop 标签,继续下一轮迭代
由于循环体在条件判断之前执行,因此无论 test_expr 的初始值是什么,body_statement 至少会被执行一次。
- 示例:累加求和
int sum_do_while(int n) {
int sum = 0;
int i = 1;
do {
sum += i;
i++;
} while(i <= n);
return sum;
}
翻译为 goto 语句:
int sum_do_while_goto(int n) {
int sum = 0;
int i = 1;
loop:
sum += i;
i++;
if(i <= n)
goto loop;
return sum;
}
x86汇编代码:
sum_do_while:
push ebp
mov ebp, esp
mov eax, 0 ; sum = 0
mov ecx, 1 ; i = 1
loop:
add eax, ecx ; sum += i
add ecx, 1 ; i++
cmp ecx, [ebp+8] ; 比较 i 与 n
jle loop ; 若 i <= n,跳回 loop
pop ebp
ret
while 循环¶
- 通用形式
while 循环是一种"前测试"型循环,与 do-while 不同,while 循环在第一次执行 body_statement 之前就会测试 test_expr 的值。如果初始条件为假(test_expr = 0),那么循环体一次都不会执行。
- 翻译方法
编译器通常采用"先判断、再进入循环体"的策略,可以将 while 循环转换为一个带前置判断的 do-while 式结构:
方法一:jump to middle 方法
方法二:guarded-do 方法
- 示例:计算阶乘
C语言代码:
翻译为 goto 语句(guarded-do 方法):
int factorial_while_goto(int n) {
int result = 1;
if(!(n > 1))
goto done;
loop:
result *= n;
n--;
if(n > 1)
goto loop;
done:
return result;
}
x86汇编代码:
factorial_while:
push ebp
mov ebp, esp
mov eax, 1 ; result = 1
mov ecx, [ebp+8] ; ecx = n
cmp ecx, 1
jle done ; 若 n <= 1,跳到 done
loop:
imul eax, ecx ; result *= n
sub ecx, 1 ; n--
cmp ecx, 1
jg loop ; 若 n > 1,跳回 loop
done:
pop ebp
ret
for 循环¶
- 通用形式
init_expr:初始化表达式,用于初始化循环计数器(如i = 0)test_expr:条件判断表达式,每次执行循环体前测试(如i < 10)-
update_expr:更新表达式,每次循环体执行完后执行(如i++) -
翻译为 while 形式
从执行逻辑上看,for 循环完全等价于以下 while 循环代码:
init_expr; // 初始化(只执行一次)
while(test_expr) { // 每次循环前判断条件
body_statement // 执行循环体
update_expr; // 更新循环变量
}
- 翻译为 goto 语句
init_expr; // 执行初始化
t = test_expr; // 测试初始条件
if(!t)
goto done; // 若初始条件为假,跳过循环
loop:
body_statement // 执行循环体
update_expr; // 执行更新操作
t = test_expr; // 重新测试条件
if(t)
goto loop; // 若条件仍为真,跳回循环体
done:
- 示例:数组元素求和
C语言代码:
int array_sum_for(int arr[], int n) {
int sum = 0;
for(int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
翻译为 goto 语句:
int array_sum_for_goto(int arr[], int n) {
int sum = 0;
int i = 0;
if(!(i < n))
goto done;
loop:
sum += arr[i];
i++;
if(i < n)
goto loop;
done:
return sum;
}
x86汇编代码:
array_sum_for:
push ebp
mov ebp, esp
mov eax, 0 ; sum = 0
mov ecx, 0 ; i = 0
mov edx, [ebp+8] ; edx = arr(数组指针)
mov ebx, [ebp+12] ; ebx = n
cmp ecx, ebx
jge done ; 若 i >= n,跳到 done
loop:
mov esi, [edx+ecx*4]; esi = arr[i]
add eax, esi ; sum += arr[i]
add ecx, 1 ; i++
cmp ecx, ebx
jl loop ; 若 i < n,跳回 loop
done:
pop ebp
ret
三种循环结构对比总结¶
| 循环类型 | 测试时机 | 最少执行次数 | 翻译策略 |
|---|---|---|---|
| do-while | 后测试 | 1次 | 直接翻译为 loop + 条件跳转 |
| while | 前测试 | 0次 | jump to middle 或 guarded-do |
| for | 前测试 | 0次 | 先转为while,再按while方法翻译 |
; =========================================================
; f1 函数的完整汇编代码还原
; =========================================================
00401020 55 push ebp ; 【函数进入】保存调用者的基址指针
00401021 8B EC mov ebp, esp ; 设置当前函数的栈帧基址
00401023 83 EC 0C sub esp, 0Ch ; 在栈上分配 12 字节空间,用于存储局部变量(如 i)
; --- 1. 初始化局部变量 ---
00401026 B8 01 00 00 mov eax, 1 ; sum = 1 (用 eax 存储 sum,也是后续的返回值)
0040102B BA 01 00 00 mov edx, 1 ; power = 1 (用 edx 存储 power)
00401030 C7 45 F4 00 mov dword ptr [ebp-0Ch], 0 ; i = 0 (局部变量 i 驻留在栈 [ebp-0Ch] 处)
; --- 2. 提前计算循环终点 n - 1 ---
00401037 8B 4D 08 mov ecx, dword ptr [ebp+8] ; 从栈中读取参入参数 n
0040103A 83 E9 01 sub ecx, 1 ; ecx = n - 1 (提前算好边界,避免每次循环都做减法)
; --- 3. 循环条件测试(前测试型/Guarded-do) ---
0040103D EB 1F jmp 0040105E ; 首次执行,直接跳转到 0040105E 进行条件检查
; --- 4. 循环体内部 (Loop Body) ---
00401066 D1 E2 shl edx, 1 ; power *= 2 (逻辑左移 1 位高效代替乘法)
00401068 03 C2 add eax, edx ; sum += power (将当前的 power 累加到 eax 中)
; --- 5. 循环步进 (Update) ---
0040106A 83 45 F4 01 add dword ptr [ebp-0Ch], 1 ; i++
; --- 6. 循环条件判定 (Test) ---
0040105E 39 4D F4 cmp dword ptr [ebp-0Ch], ecx ; 比较 i 与 n-1 的大小
00401061 76 03 jbe 00401066 ; 如果 i <= n-1,则跳转回 00401066 继续循环
; (76 是无符号小于等于跳转 jbe 的机器码)
; --- 7. 循环结束,函数返回 ---
00401063 8B E5 mov esp, ebp ; 【函数退出】恢复栈指针
00401065 5D pop ebp ; 恢复调用者的 ebp
0040107F C3 ret ; 函数返回,此时 sum 的值就在 eax 中
