程序的机器级代码表示
TIP
2022 年之后新增机器代码考点,主要是 x86 汇编相关内容。 参考王道书
常用汇编指令介绍
相关寄存器
x86-32 (IA32) 体系结构包含 8 个 32 位的通用寄存器,它们都有特定的用途,但也可以通用。
| 32 位寄存器 | 16 位 | 8 位高/低 | 主要用途 |
|---|---|---|---|
| EAX | AX | AH / AL | 累加器 (Accumulator)。常用于算术运算、逻辑运算和函数返回值。 |
| ECX | CX | CH / CL | 计数器 (Counter)。常用于循环控制。 |
| EDX | DX | DH / DL | 数据寄存器 (Data)。常与 EAX 配合进行乘除法,或存放 I/O 端口地址。 |
| EBX | BX | BH / BL | 基址寄存器 (Base)。常作为内存地址的基址。 |
| ESI | SI | - | 源变址寄存器 (Source Index)。字符串操作中的源地址。 |
| EDI | DI | - | 目的变址寄存器 (Destination Index)。字符串操作中的目的地址。 |
| ESP | SP | - | 栈指针 (Stack Pointer)。始终指向栈顶。 |
| EBP | BP | - | 基址指针 (Base Pointer)。通常指向当前栈帧的底部。 |
汇编指令格式
AT&T 和 Intel 格式的区别
统考中要求掌握的是 Intel 格式,它们的区别如下:
- 操作数顺序:Intel 格式为
指令 目的操作数, 源操作数;AT&T 格式为指令 源操作数, 目的操作数。 - 寄存器名称:Intel 格式直接使用寄存器名(如
eax);AT&T 格式在寄存器名前加前缀%(如%eax)。 - 立即数:Intel 格式直接使用数值(如
123,十六进制用0Ah);AT&T 格式在立即数前加前缀$(如$123,$0xA)。 - 内存寻址:Intel 格式为
[基址 + 变址 * 比例因子 + 位移];AT&T 格式为位移(基址, 变址, 比例因子)。 - 指令后缀:Intel 格式根据操作数类型推断操作大小;AT&T 格式通常使用后缀
b(byte),w(word),l(long) 明确指出操作大小。
| AT&T 格式 | Intel 格式 | 含义 |
|---|---|---|
movl $5, %eax | mov eax, 5 | 将立即数 5 移动到寄存器 eax 中。 |
movl %ebx, %eax | mov eax, ebx | 将寄存器 ebx 的值移动到 eax 中。 |
movl 8(%ebp), %eax | mov eax, [ebp + 8] | 将内存地址 ebp+8 处的值移动到 eax 中。 |
leal (%eax, %eax, 4), %eax | lea eax, [eax + eax*4] | 将地址 eax*5 的值加载到 eax 中(计算地址)。 |
常用指令
| 记号 | 含义 |
|---|---|
<reg> | 表示任意寄存器,若带有数字,则指定其位数(如 <reg32>) |
<mem> | 表示内存地址(如 [eax], [var + 4]) |
<con> | 表示 8、16 或 32 位常数(如 <con32>) |
数据传送指令
mov 指令
将第二个操作数(源操作数:寄存器、内存地址或常数)复制到第一个操作数(目的操作数:寄存器或内存地址)。注意:两个操作数不能同时为内存地址。
其语法如下:
mov <reg>,<reg>
mov <reg>,<mem>
mov <mem>,<reg>
mov <reg>,<con>
mov <mem>,<con>举例:
mov eax, ebx ; 将寄存器 ebx 的值复制到寄存器 eax
mov eax, [var] ; 将内存地址 var 处 4 字节的数据复制到 eaxmovsx 和 movzx 指令
用于处理不同大小整数间的转换。
- movsx (Move with Sign-Extend): 带符号扩展传送。将较小编码的源操作数复制到较大的目的操作数,并用源操作数的符号位填充目的操作数的高位。
; 假设 bl = 10000001b (-127), ax 是 16 位寄存器 movsx ax, bl ; 执行后 ax = 11111111 10000001b (-127) - movzx (Move with Zero-Extend): 零扩展传送。将较小编码的源操作数复制到较大的目的操作数,并用0填充目的操作数的高位。常用于无符号数。
; 假设 bl = 10000001b (129), ax 是 16 位寄存器 movzx ax, bl ; 执行后 ax = 00000000 10000001b (129)
push 和 pop 指令
用于栈操作。
- push
<src>: 压栈。首先将栈指针esp减去 4(32 位模式),然后将源操作数(寄存器、内存或立即数)存入esp指向的内存位置。push eax ; esp = esp - 4; mov [esp], eax - pop
<dest>: 出栈。首先将esp指向的内存位置的数据读入目的操作数(寄存器或内存),然后将栈指针esp加上 4。pop eax ; mov eax, [esp]; esp = esp + 4
lea 指令
lea (Load Effective Address): 加载有效地址。将源操作数(必须是内存寻址表达式)计算出的地址本身存入目的操作数(必须是寄存器),而不访问该内存地址的内容。lea 指令常用于快速进行无进位的加法和乘法运算。
lea eax, [ebx+ecx*4+100] ; 计算地址值 ebx+ecx*4+100,并将结果存入 eax
; 这条指令等价于 C 语言的: eax = ebx + ecx*4 + 100;算数和逻辑运算指令
这类指令通常会影响 EFLAGS 寄存器 中的标志位,如 CF(进位)、ZF(零)、SF(符号)、OF(溢出)。
| 指令 | 格式 | 含义 |
|---|---|---|
| inc | inc <op> | 加 1 (<op> = <op> + 1) |
| dec | dec <op> | 减 1 (<op> = <op> - 1) |
| neg | neg <op> | 取负 (求补) (<op> = -<op>) |
| not | not <op> | 按位取反 (<op> = ~<op>) |
| add | add <dest>,<src> | 加法 (<dest> = <dest> + <src>) |
| sub | sub <dest>,<src> | 减法 (<dest> = <dest> - <src>) |
| imul | imul <reg>,<op> | 有符号乘法 (<reg> = <reg> * <op>) |
| idiv | idiv <op> | 有符号除法 (商在 EAX, 余数在 EDX) |
| and | and <dest>,<src> | 按位与 (<dest> = <dest> & <src>) |
| or | or <dest>,<src> | 按位或 (<dest> = <dest> | <src>) |
| xor | xor <dest>,<src> | 按位异或 (<dest> = <dest> ^ <src>) |
| shl/sal | shl <op>,<cnt> | 逻辑/算术左移 (<op> << <cnt>) |
| shr | shr <op>,<cnt> | 逻辑右移 (高位补 0) |
| sar | sar <op>,<cnt> | 算术右移 (高位补符号位) |
特殊说明:
- idiv
<op>:被除数是 64 位数,高 32 位在EDX,低 32 位在EAX。<op>是 32 位除数。结果的商存放在EAX,余数存放在EDX。除法前通常需要用cdq指令将EAX的符号位扩展到EDX。 - xor eax, eax:常用于将寄存器
eax清零,比mov eax, 0效率更高。
控制流指令
无条件跳转
- jmp
<label>: 无条件跳转到指定的代码标签label处。
条件跳转
条件跳转指令根据 EFLAGS 寄存器 的状态来决定是否跳转。它们通常跟在 cmp 或 test 指令之后。
- cmp
<op1>, <op2>: 比较操作数。计算<op1> - <op2>,并根据结果设置标志位,但不保存结果。溢出检测的硬件实现op1 == op2→ ZF=1 (零标志)op1 < op2(有符号) → SF != OF (符号标志与溢出标志不同)op1 < op2(无符号) → CF=1 (进位标志)
- test
<op1>, <op2>: 测试操作数。计算<op1> & <op2>,并根据结果设置标志位,但不保存结果。常用于测试某位是否为 1。test eax, eax: 如果eax为 0,则 ZF=1。
| 指令 | 标志位条件 | 描述 (跟在 cmp op1, op2 后) |
|---|---|---|
| je | ZF=1 | op1 == op2 (相等则跳转) |
| jne | ZF=0 | op1 != op2 (不等则跳转) |
| js | SF=1 | 结果为负则跳转 |
| jns | SF=0 | 结果为非负则跳转 |
| jg | ZF=0 and SF=OF | op1 > op2 (有符号大于) |
| jge | SF=OF | op1 >= op2 (有符号大于等于) |
| jl | SF!=OF | op1 < op2 (有符号小于) |
| jle | ZF=1 or SF!=OF | op1 ⇐ op2 (有符号小于等于) |
| ja | CF=0 and ZF=0 | op1 > op2 (无符号大于) |
| jb | CF=1 | op1 < op2 (无符号小于) |
函数调用与返回
- call
<label>: 调用函数。首先将call指令的下一条指令地址压入栈中(作为返回地址),然后跳转到label处。 - ret: 函数返回。从栈顶弹出一个地址,并无条件跳转到该地址。通常在
ret之前会有一系列pop指令恢复调用者保存的寄存器,以及一条leave指令。 - leave: 恢复栈帧。等价于
mov esp, ebp; pop ebp;,用于函数返回前释放当前函数的栈帧。
选择语句的机器级表示
C 语言中的 if-else 语句通常通过条件测试和条件跳转指令实现。
一般形式:if (test_expr) then_statement else else_statement
汇编转换模式:
- 对
test_expr进行求值,通常使用cmp或test指令设置条件码。 - 使用一个条件跳转指令,如果
test_expr为假,则跳转到else部分的代码。 - 执行
then部分的代码。 - 在
then部分代码末尾,使用一个无条件跳转指令jmp跳过else部分。 - 执行
else部分的代码。
示例: C 语言代码
if (x > y) {
z = 1;
} else {
z = 0;
}对应的汇编逻辑
mov eax, [x]
cmp eax, [y]
jle L1 ; 若 x <= y (不满足大于),则跳转到 L1
mov dword ptr [z], 1 ; then 部分
jmp L2 ; 跳过 else 部分
L1:
mov dword ptr [z], 0 ; else 部分
L2:循环语句的机器级表示
循环结构的核心是条件判断和跳转。编译器会将 do-while、while、for 循环都转换为基于条件测试和跳转的汇编代码。
do-while 循环
C 语言形式: do { body-statement; } while (test-expr);
特点: 循环体 body-statement 至少执行一次,因为条件判断在循环体之后。
汇编转换模式:
- 执行循环体代码。
- 对
test-expr求值。 - 使用条件跳转指令,如果
test-expr为真,则跳回循环体的开头。
示例: C 语言代码
do {
x++;
} while (x < 10);对应的汇编逻辑
L1:
inc dword ptr [x] ; 循环体
mov eax, [x]
cmp eax, 10 ; 条件判断
jl L1 ; 如果 x < 10,则跳回 L1while 循环
C 语言形式: while (test-expr) { body-statement; }
特点: 先进行条件判断,循环体可能一次都不执行。
汇编转换模式 (jump-to-middle):
- 使用无条件跳转
jmp跳到循环末尾的条件测试处。 - 循环体代码块。
- 条件测试代码块,如果
test-expr为真,则用条件跳转跳回循环体开头。
示例: C 语言代码
while (x < 10) {
x++;
}对应的汇编逻辑
jmp L2 ; 先跳转到条件测试
L1:
inc dword ptr [x] ; 循环体
L2:
mov eax, [x]
cmp eax, 10 ; 条件判断
jl L1 ; 如果 x < 10,则跳回 L1for 循环
C 语言形式: for (init-expr; test-expr; update-expr) { body-statement; }
特点: for 循环可以等价地转换为一个带有初始化部分的 while 循环。
等价的 while 形式:
init-expr;
while (test-expr) {
body-statement;
update-expr;
}汇编转换模式:
- 执行初始化
init-expr部分的代码。 - 进入一个类似于
while循环的结构,通常使用jump-to-middle转换。
示例: C 语言代码
for (i=0; i<10; i++) {
sum += i;
}对应的汇编逻辑
mov dword ptr [i], 0 ; init-expr
jmp L2 ; 跳转到条件测试
L1:
mov eax, [i]
add [sum], eax ; body-statement
inc dword ptr [i] ; update-expr
L2:
cmp dword ptr [i], 10; test-expr
jl L1 ; 如果 i < 10,则跳回 L1过程调用的机器级表示
过程调用(函数调用)是程序执行中的一个关键机制,它依赖于栈来管理信息。
栈帧结构
每次函数调用都会在栈上创建一个栈帧 (Stack Frame),用于存储该函数的局部信息。一个典型的栈帧包含:
- 参数:为被调用函数传递的参数(由调用者压栈)。
- 返回地址:
call指令自动压栈,指明函数返回后应从何处继续执行。 - 旧 EBP:调用者函数的栈底指针,用于在函数返回时恢复调用者的栈帧。
- 保存的寄存器:根据调用约定,需要保存的寄存器值。
- 局部变量:函数内部定义的非
static局部变量。
栈帧布局 (栈向低地址增长):
+-----------------+
高地址 -> | 参数 N |
+-----------------+
| ... |
+-----------------+
| 参数 1 | <-- 调用者栈帧
+-----------------+
| 返回地址 | <-- call 指令压入
+-----------------+
| 旧 EBP | <-- 被调用者压入 (EBP 指向此处)
+-----------------+
| 保存的寄存器 |
+-----------------+
| 局部变量 | <-- ESP 指向此处 (栈顶)
低地址 -> +-----------------+调用过程
1. 调用者 (Caller) 的任务:
- 保存调用者保存寄存器:如果
EAX,ECX,EDX等寄存器的值在函数调用后仍需使用,则调用者负责将其保存。 - 传递参数:按照调用约定(如 cdecl),将参数从右到左依次压入栈中。
- 发起调用:使用
call指令。该指令会将返回地址压栈,并跳转到被调用函数的起始地址。 - 栈清理:函数返回后,调用者负责将压入栈的参数清理掉,通常通过
add esp, N来实现(N 为参数总字节数)。 - 获取返回值:从
EAX寄存器中获取函数返回值。
2. 被调用者 (Callee) 的任务:
- 建立栈帧:
push ebp: 保存旧的ebp值(指向调用者的栈帧底部)。mov ebp, esp: 设置自己的栈帧底部,ebp现在指向被调用者栈帧的底部。
- 保存被调用者保存寄存器:如果函数会修改
EBX,ESI,EDI等寄存器,则必须先将它们的原始值压栈保存。 - 为局部变量分配空间:
sub esp, N,将栈顶指针向下移动 N 字节。 - 执行函数体:执行函数的核心逻辑。
- 存储返回值:将返回值(如果有)存入
EAX寄存器。 - 恢复栈帧和寄存器:
- 恢复被调用者保存的寄存器(
pop)。 - 释放局部变量空间并恢复
ebp:使用leave指令(等价于mov esp, ebp; pop ebp;)。
- 恢复被调用者保存的寄存器(
- 返回:使用
ret指令。该指令会从栈顶弹出返回地址,并跳转到该地址。