精简版 · 小杯2026-08 冻结,已停止更新(发布前修订了 4 处已知错误)。后续勘误与新增内容只在正式版。看正式版(中杯)→
Skip to content

程序的机器级表示

考情分析

这一块以综合题形式考查:给一段 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(按 ab)置位含义
ZFZero Flag结果为 0,即 a=b
SFSign Flag结果的符号位,为 1 表示结果为负
OFOverflow Flag有符号溢出
CFCarry Flag无符号借位,即 a<b

条件转移指令查的就是这四位的组合:有符号比较看 SFOF 是否一致,无符号比较看 CF。例如 jle,有符号)的条件是 ZF=1SFOF。完整对照表见《x86汇编语言基础》。

示例

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

两处差别要认得出来:

  1. 循环变量在 [ebp-8] 这样的栈单元里,不在寄存器里。每次自增要"读出→加→写回"三条指令。找循环变量时看 cmp dword ptr [ebp-X], ... 里的那个栈单元。
  2. 多了一条 jmp 跳到底部——这是 for/while 的第三种编译形态。

第三种编译形态:jmp-to-test

前面给的两种模板(前置判断 + 底部回跳)会把判断代码复制两份。实际编译器常用第三种,判断代码只有一份:

    初始化
    jmp  test          ← 先跳到判断处
body:
    循环体
    更新
test:
    条件判断
    条件成立 → jmp body

统考的反汇编里这一形态很常见:看到初始化之后紧跟一条无条件 jmp 跳到后面某个地址,就要认出这是循环的入口跳转,而不是什么分支。

循环里访问二维数组

真题的循环体里常出现 [ecx+edx*4+00422000H] 这样的地址表达式。对位规则:

addr(a[i][j])=+i××+j××

反推动作:先看比例因子*4 说明元素 4 字节),再由数组维度算行宽。若数组是 int a[64][64],行宽 =64×4=256,那么基址寄存器 ecx 里装的就是 i×256

编译器不会在内层循环里每次都做乘法——它把 i×256 预先算好存进寄存器,外层每轮加 256。所以看到"某寄存器每轮外层循环 +256",那就是行偏移。

过程调用的机器级表示

调用过程

假设过程 P(调用者)调用过程 Q(被调用者):

  1. P 将参数放到 Q 能访问的位置(压入栈中)
  2. P 执行 call Q:将返回地址(call 的下一条指令地址)压栈,跳转到 Q
  3. Q 建立栈帧:push ebp; mov ebp, esp; sub esp, N
  4. Q 执行函数体
  5. Q 将返回值放入 eax,执行 leave(或等价操作)恢复栈帧
  6. 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 的栈帧

图 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 过程调用过程中栈和栈帧的变化

图 3.11 过程调用过程中栈和栈帧的变化

三态对照,注意 ESP/EBP 的位置变化:

瞬间栈上有什么ESP 在哪
a) 过程 Q 被调用前调用者保存寄存器 + 参数 n..1 + 返回地址返回地址处
b) 过程 Q 执行中再加上 EBP 旧值 + 被调用者保存寄存器 + 非静态局部变量当前帧最低处
c) 返回到过程 P 时恢复成 a) 的样子退回返回地址处

"当前栈帧"的边界是 EBP 到 ESP 之间;EBP 之上(高地址侧)是调用者的地盘。

ESP 每一步动了多少

统计栈深度或反推某个地址时,这些 ±4 一个都不能漏:

动作ESP 变化
push 一个参数4
call(压入返回地址)4
push ebp4
sub esp, N(为局部变量开空间)N
pop / leave 回收反向
ret(弹出返回地址)+4

所以进入被调用者、执行完 push ebp; mov ebp, esp 之后,从 EBP 往回数:[ebp] 是旧 EBP、[ebp+4] 是返回地址、[ebp+8] 是第一个参数——每格 4 字节,这就是 +8 这个数的来历

寄存器保存约定

类别寄存器谁负责保存
调用者保存EAX, ECX, EDXP 在 call 前保存(如果之后还要用)
被调用者保存EBX, ESI, EDIQ 使用前先 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        ret

add 函数用 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] 起的栈单元,再在汇编里找哪条指令读写了它。

若问"某寄存器里装的是什么":看它参与的地址表达式,与 +i×+j× 逐项对位。

考点清单

  • 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 四位;有符号跳转看 SFOF 是否一致,无符号看 CF
  • 统考反汇编多为未优化形态:局部变量在 [ebp-X] 栈单元里,不在寄存器里
  • 循环有第三种编译形态:初始化 → jmp 到底部判断 → 循环体 → 判断 → 条件回跳
  • 二维数组地址 = 首地址 + i× 行宽 + j× 元素宽;比例因子就是元素字节数
  • 调用者帧的参数区 = 被调用者的 [ebp+8],同一块内存两个视角
  • 机器码字节数 = 指令长度;下一条地址 = 本条地址 + 字节数;函数长度 = 末 − 首 + 末指令长度

教材出处

  • 袁春风《计算机系统基础(第 2 版)》§3.3 过程调用的机器级表示:图 3.11(p121)、图 3.12(p123)

真题练习