程序的机器级代码表示

TIP

2022 年之后新增机器代码考点,主要是 x86 汇编相关内容。 参考王道书

常用汇编指令介绍

相关寄存器

x86-32 (IA32) 体系结构包含 8 个 32 位的通用寄存器,它们都有特定的用途,但也可以通用。

32 位寄存器16 位8 位高/低主要用途
EAXAXAH / AL累加器 (Accumulator)。常用于算术运算、逻辑运算和函数返回值。
ECXCXCH / CL计数器 (Counter)。常用于循环控制。
EDXDXDH / DL数据寄存器 (Data)。常与 EAX 配合进行乘除法,或存放 I/O 端口地址。
EBXBXBH / BL基址寄存器 (Base)。常作为内存地址的基址。
ESISI-源变址寄存器 (Source Index)。字符串操作中的源地址。
EDIDI-目的变址寄存器 (Destination Index)。字符串操作中的目的地址。
ESPSP-栈指针 (Stack Pointer)。始终指向栈顶。
EBPBP-基址指针 (Base Pointer)。通常指向当前栈帧的底部。

汇编指令格式

AT&T 和 Intel 格式的区别

统考中要求掌握的是 Intel 格式,它们的区别如下:

  1. 操作数顺序:Intel 格式为 指令 目的操作数, 源操作数;AT&T 格式为 指令 源操作数, 目的操作数
  2. 寄存器名称:Intel 格式直接使用寄存器名(如 eax);AT&T 格式在寄存器名前加前缀 %(如 %eax)。
  3. 立即数:Intel 格式直接使用数值(如 123,十六进制用 0Ah);AT&T 格式在立即数前加前缀 $(如 $123$0xA)。
  4. 内存寻址:Intel 格式为 [基址 + 变址 * 比例因子 + 位移];AT&T 格式为 位移(基址, 变址, 比例因子)
  5. 指令后缀:Intel 格式根据操作数类型推断操作大小;AT&T 格式通常使用后缀 b (byte), w (word), l (long) 明确指出操作大小。
AT&T 格式Intel 格式含义
movl $5, %eaxmov eax, 5将立即数 5 移动到寄存器 eax 中。
movl %ebx, %eaxmov eax, ebx将寄存器 ebx 的值移动到 eax 中。
movl 8(%ebp), %eaxmov eax, [ebp + 8]将内存地址 ebp+8 处的值移动到 eax 中。
leal (%eax, %eax, 4), %eaxlea 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 字节的数据复制到 eax
movsx 和 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(溢出)。

指令格式含义
incinc <op>加 1 (<op> = <op> + 1)
decdec <op>减 1 (<op> = <op> - 1)
negneg <op>取负 (求补) (<op> = -<op>)
notnot <op>按位取反 (<op> = ~<op>)
addadd <dest>,<src>加法 (<dest> = <dest> + <src>)
subsub <dest>,<src>减法 (<dest> = <dest> - <src>)
imulimul <reg>,<op>有符号乘法 (<reg> = <reg> * <op>)
idividiv <op>有符号除法 (商在 EAX, 余数在 EDX)
andand <dest>,<src>按位与 (<dest> = <dest> & <src>)
oror <dest>,<src>按位或 (<dest> = <dest> | <src>)
xorxor <dest>,<src>按位异或 (<dest> = <dest> ^ <src>)
shl/salshl <op>,<cnt>逻辑/算术左移 (<op> << <cnt>)
shrshr <op>,<cnt>逻辑右移 (高位补 0)
sarsar <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 寄存器 的状态来决定是否跳转。它们通常跟在 cmptest 指令之后。

  • 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 后)
jeZF=1op1 == op2 (相等则跳转)
jneZF=0op1 != op2 (不等则跳转)
jsSF=1结果为负则跳转
jnsSF=0结果为非负则跳转
jgZF=0 and SF=OFop1 > op2 (有符号大于)
jgeSF=OFop1 >= op2 (有符号大于等于)
jlSF!=OFop1 < op2 (有符号小于)
jleZF=1 or SF!=OFop1 op2 (有符号小于等于)
jaCF=0 and ZF=0op1 > op2 (无符号大于)
jbCF=1op1 < 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

汇编转换模式

  1. test_expr 进行求值,通常使用 cmptest 指令设置条件码。
  2. 使用一个条件跳转指令,如果 test_expr 为假,则跳转到 else 部分的代码。
  3. 执行 then 部分的代码。
  4. then 部分代码末尾,使用一个无条件跳转指令 jmp 跳过 else 部分。
  5. 执行 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-whilewhilefor 循环都转换为基于条件测试和跳转的汇编代码。

do-while 循环

C 语言形式: do { body-statement; } while (test-expr);

特点: 循环体 body-statement 至少执行一次,因为条件判断在循环体之后。

汇编转换模式:

  1. 执行循环体代码。
  2. test-expr 求值。
  3. 使用条件跳转指令,如果 test-expr 为真,则跳回循环体的开头。

示例: C 语言代码

do {
  x++;
} while (x < 10);

对应的汇编逻辑

L1:
  inc dword ptr [x] ; 循环体
  mov eax, [x]
  cmp eax, 10       ; 条件判断
  jl L1             ; 如果 x < 10,则跳回 L1

while 循环

C 语言形式: while (test-expr) { body-statement; }

特点: 先进行条件判断,循环体可能一次都不执行。

汇编转换模式 (jump-to-middle):

  1. 使用无条件跳转 jmp 跳到循环末尾的条件测试处。
  2. 循环体代码块。
  3. 条件测试代码块,如果 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,则跳回 L1

for 循环

C 语言形式: for (init-expr; test-expr; update-expr) { body-statement; }

特点: for 循环可以等价地转换为一个带有初始化部分的 while 循环。

等价的 while 形式:

init-expr;
while (test-expr) {
    body-statement;
    update-expr;
}

汇编转换模式:

  1. 执行初始化 init-expr 部分的代码。
  2. 进入一个类似于 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 指令。该指令会从栈顶弹出返回地址,并跳转到该地址。