Appearance
程序的机器级表示
考情分析
这一块以综合题形式考查:给一段 C 代码和对应的反汇编,要求指出某条指令在做什么、某个寄存器或栈单元里装的是哪个变量。三个抓手:选择语句怎么变成 cmp + 条件跳转、循环语句的三种编译形态、过程调用的栈帧布局与 call/ret 对栈的影响。
大纲定位
考纲第四章(六)「高级语言程序与机器级代码之间的对应」第 2~4 条:选择结构语句、循环结构语句、过程(函数)调用的机器级表示。
第 1 条(编译器/汇编器/链接器的基本概念)和汇编指令本身见《x86汇编语言基础》。
选择语句的机器级表示
if-else 语句的通用翻译模式(goto 形式):
t = test_expr;
if (!t) goto false;
then_statement
goto done;
false:
else_statement
done:编译器使用 cmp/test 设置标志位,再用条件转移指令实现分支选择。
是哪几个标志位——这一步不能只说"标志位"三个字:
| 标志 | 全称 | cmp a, b(按 |
|---|---|---|
| ZF | Zero Flag | 结果为 0,即 |
| SF | Sign Flag | 结果的符号位,为 1 表示结果为负 |
| OF | Overflow Flag | 有符号溢出 |
| CF | Carry Flag | 无符号借位,即 |
条件转移指令查的就是这四位的组合:有符号比较看 jle(
示例:
c
int get_cont(int *p1, int *p2) {
if (p1 > p2) return *p2;
else return *p1;
}参数 p1 在 [ebp+8],p2 在 [ebp+12],返回值放 eax。
asm
mov eax, dword ptr [ebp+8] ; 加载 p1
mov edx, dword ptr [ebp+12] ; 加载 p2
cmp eax, edx ; 比较 p1 和 p2
jbe .L1 ; 若 p1 <= p2,跳到 L1(无符号比较)
mov eax, dword ptr [edx] ; 取 *p2 作为返回值
jmp .L2
.L1:
mov eax, dword ptr [eax] ; 取 *p1 作为返回值
.L2:指针比较按无符号整数处理,因此使用 jbe(无符号"小于等于")。
循环语句的机器级表示
三种循环都可以转换为"条件测试 + 条件跳转"的形式。编译器通常统一转换为 do-while 结构。
do-while
loop:
body_statement
t = test_expr;
if (t) goto loop;循环体至少执行一次。
while
先判断再进入循环体,转换为带前置判断的 do-while:
t = test_expr;
if (!t) goto done;
loop:
body_statement
t = test_expr;
if (t) goto loop;
done:for
for (init; test; update) body 等价于:
init;
t = test;
if (!t) goto done;
loop:
body;
update;
t = test;
if (t) goto loop;
done:示例:
c
int nsum_for(int n) {
int i, result = 0;
for (i = 1; i <= n; i++)
result += i;
return result;
}asm
mov ecx, dword ptr [ebp+8] ; 加载参数 n
mov eax, 0 ; result = 0
mov edx, 1 ; i = 1
cmp edx, ecx ; 比较 i 与 n
jg .L2 ; 若 i > n,跳过循环
.L1:
add eax, edx ; result += i
add edx, 1 ; i++
cmp edx, ecx ; 比较 i 与 n
jle .L1 ; 若 i <= n,继续循环
.L2:result 分配到 eax(同时是返回值),i 分配到 edx。int 型比较属于有符号,使用 jg/jle。
但真题给的多半不是这个形态
上面是开了优化的代码——局部变量被分配到寄存器。而统考的反汇编基本都是未优化形态:局部变量落在栈上,从 [ebp-4] 起向下排。同一个循环会长成这样:
asm
mov dword ptr [ebp-8], 0 ; i = 0,i 在栈上不在寄存器
jmp .L_test ; ← 先跳到底部判断
.L_body:
mov eax, [ebp-8] ; 读 i
add eax, 1
mov [ebp-8], eax ; i++(读-改-写三条指令)
.L_test:
cmp dword ptr [ebp-8], 10
jl .L_body两处差别要认得出来:
- 循环变量在
[ebp-8]这样的栈单元里,不在寄存器里。每次自增要"读出→加→写回"三条指令。找循环变量时看cmp dword ptr [ebp-X], ...里的那个栈单元。 - 多了一条
jmp跳到底部——这是 for/while 的第三种编译形态。
第三种编译形态:jmp-to-test
前面给的两种模板(前置判断 + 底部回跳)会把判断代码复制两份。实际编译器常用第三种,判断代码只有一份:
初始化
jmp test ← 先跳到判断处
body:
循环体
更新
test:
条件判断
条件成立 → jmp body统考的反汇编里这一形态很常见:看到初始化之后紧跟一条无条件 jmp 跳到后面某个地址,就要认出这是循环的入口跳转,而不是什么分支。
循环里访问二维数组
真题的循环体里常出现 [ecx+edx*4+00422000H] 这样的地址表达式。对位规则:
反推动作:先看比例因子(*4 说明元素 4 字节),再由数组维度算行宽。若数组是 int a[64][64],行宽 ecx 里装的就是
编译器不会在内层循环里每次都做乘法——它把
过程调用的机器级表示
调用过程
假设过程 P(调用者)调用过程 Q(被调用者):
- P 将参数放到 Q 能访问的位置(压入栈中)
- P 执行
call Q:将返回地址(call 的下一条指令地址)压栈,跳转到 Q - Q 建立栈帧:
push ebp; mov ebp, esp; sub esp, N - Q 执行函数体
- Q 将返回值放入 eax,执行
leave(或等价操作)恢复栈帧 - Q 执行
ret:弹出返回地址,跳回 P
栈帧结构
栈从高地址向低地址增长。EBP 指向当前栈帧底部(保存旧 EBP 的位置),ESP 指向栈顶。
高地址
┌─────────────────┐
│ 参数 n │ [ebp+12]
│ 参数 1 │ [ebp+8]
│ 返回地址 │ [ebp+4] ← call 指令压入
├─────────────────┤
│ 旧 EBP │ [ebp] ← push ebp
├─────────────────┤
│ 局部变量 1 │ [ebp-4]
│ 局部变量 2 │ [ebp-8]
│ ... │
│ 被调用者保存寄存器│
│ 为下次 call 准备 │
│ 的参数区 │
└─────────────────┘ ← ESP(栈顶)
低地址关键:两个帧是拼接在一起的
上面这张图只画了一帧,会掩盖一件要紧事——调用者帧底部的"参数区"和被调用者帧顶部的 [ebp+8],是同一块内存的两个视角。

图 3.12 caller 和 add 的栈帧
图里 caller 把实参 125 和 80 压在自己帧的底部,call 再压入返回地址;进入 add 后,add 用 [ebp+8]、[ebp+12] 读到的正是这两个数。同一块内存,caller 从上往下写、add 从下往上读——这就是参数传递的全部机制。
图中两个 EBP 箭头也标出了 push ebp; mov ebp, esp 这一步的效果:新 EBP 落在"EBP 在 caller 中的值"那一格。
调用全过程的三个瞬间

图 3.11 过程调用过程中栈和栈帧的变化
三态对照,注意 ESP/EBP 的位置变化:
| 瞬间 | 栈上有什么 | ESP 在哪 |
|---|---|---|
| a) 过程 Q 被调用前 | 调用者保存寄存器 + 参数 | 返回地址处 |
| b) 过程 Q 执行中 | 再加上 EBP 旧值 + 被调用者保存寄存器 + 非静态局部变量 | 当前帧最低处 |
| c) 返回到过程 P 时 | 恢复成 a) 的样子 | 退回返回地址处 |
"当前栈帧"的边界是 EBP 到 ESP 之间;EBP 之上(高地址侧)是调用者的地盘。
ESP 每一步动了多少
统计栈深度或反推某个地址时,这些
| 动作 | ESP 变化 |
|---|---|
push 一个参数 | |
call(压入返回地址) | |
push ebp | |
sub esp, N(为局部变量开空间) | |
pop / leave 回收 | 反向 |
ret(弹出返回地址) |
所以进入被调用者、执行完 push ebp; mov ebp, esp 之后,从 EBP 往回数:[ebp] 是旧 EBP、[ebp+4] 是返回地址、[ebp+8] 是第一个参数——每格 4 字节,这就是 +8 这个数的来历。
寄存器保存约定
| 类别 | 寄存器 | 谁负责保存 |
|---|---|---|
| 调用者保存 | EAX, ECX, EDX | P 在 call 前保存(如果之后还要用) |
| 被调用者保存 | EBX, ESI, EDI | Q 使用前先 push,返回前 pop 恢复 |
完整示例
c
int add(int x, int y) { return x + y; }
int caller() {
int temp1 = 125, temp2 = 80;
int sum = add(temp1, temp2);
return sum;
}caller 的汇编代码:
asm
push ebp ; 保存调用者的 EBP
mov ebp, esp ; 建立新栈帧
sub esp, 24 ; 分配局部变量和参数空间
mov dword ptr [ebp-12], 125 ; temp1 = 125
mov dword ptr [ebp-8], 80 ; temp2 = 80
mov eax, dword ptr [ebp-8] ; 加载 temp2
mov [esp+4], eax ; 参数 2 入栈(高地址)
mov eax, dword ptr [ebp-12] ; 加载 temp1
mov [esp], eax ; 参数 1 入栈(低地址)
call add ; 调用 add,返回值在 eax
mov dword ptr [ebp-4], eax ; sum = 返回值
mov eax, dword ptr [ebp-4] ; 将 sum 作为 caller 的返回值
leave ; mov esp,ebp + pop ebp
ret ; 弹出返回地址,返回add 的汇编代码(含机器码):
asm
8048469: 55 push ebp
804846a: 89 e5 mov ebp, esp
804846c: 8b 45 0c mov eax, dword ptr [ebp+12] ; 加载 y
804846f: 8b 55 08 mov edx, dword ptr [ebp+8] ; 加载 x
8048472: 8d 04 02 lea eax, [edx+eax] ; eax = x + y
8048475: 5d pop ebp
8048476: c3 retadd 函数用 lea 指令完成加法运算(lea 计算地址但不访存,这里利用地址计算实现 x+y)。
leave 指令
等价于:
asm
mov esp, ebp ; 释放局部变量空间,ESP 回到保存旧 EBP 的位置
pop ebp ; 恢复调用者的 EBP解题动作清单:拿到一段反汇编先做什么
三道大题的起手式是同一套,按顺序走:
① 切函数边界——找 push ebp; mov ebp, esp(序言,函数开始)和 ret(尾声,函数结束)。
② 认参数和局部变量——以 EBP 为界:
[ebp+8]、[ebp+12]、[ebp+16]…… 是参数(向高地址排)[ebp-4]、[ebp-8]、[ebp-0Ch]…… 是局部变量(向低地址排)
③ 划基本块——圈出所有 cmp / jcc / jmp / call / ret。它们是控制流的接缝,两条跳转之间就是一个顺序执行的块。
④ 认控制结构:
cmp紧跟jcc且跳向后面 → 选择结构- 初始化后一条
jmp跳向后面,那里又有jcc跳回来 → 循环(jmp-to-test 形态) - 底部
cmp + jcc跳回来 → 循环(do-while 形态)
⑤ 逐块翻回 C——把 [ebp-X] 换成变量名,把跳转还原成 if/for/while。
反向题(给 C 代码问某变量在哪)动作相反:先按声明顺序把局部变量映到 [ebp-4] 起的栈单元,再在汇编里找哪条指令读写了它。
若问"某寄存器里装的是什么":看它参与的地址表达式,与
考点清单
- if-else:cmp + 条件跳转(真分支顺序执行,假分支 goto)
- 循环:for/while 都可以转换为 do-while + 前置判断
- call = push 返回地址 + jmp;ret = pop 返回地址 + jmp
- 栈帧:EBP 指向底部(固定),ESP 指向栈顶(动态变化)
- 参数通过
[ebp+8]、[ebp+12]等访问(32 位系统中参数每个占 4 字节) - 返回值放在 EAX 中
- EAX/ECX/EDX 为调用者保存,EBX/ESI/EDI 为被调用者保存
- leave = mov esp,ebp + pop ebp
- cmp 置 ZF/SF/CF/OF 四位;有符号跳转看
与 是否一致,无符号看 - 统考反汇编多为未优化形态:局部变量在
[ebp-X]栈单元里,不在寄存器里 - 循环有第三种编译形态:初始化 → jmp 到底部判断 → 循环体 → 判断 → 条件回跳
- 二维数组地址
首地址 行宽 元素宽;比例因子就是元素字节数 - 调用者帧的参数区 = 被调用者的
[ebp+8]起,同一块内存两个视角 - 机器码字节数 = 指令长度;下一条地址 = 本条地址 + 字节数;函数长度 = 末 − 首 + 末指令长度
教材出处
- 袁春风《计算机系统基础(第 2 版)》§3.3 过程调用的机器级表示:图 3.11(p121)、图 3.12(p123)