← 返回学习笔记

Lecture 16:知识 - 高级语言程序与机器级代码之间的对应

408 学习笔记:Lecture 16:知识 - 高级语言程序与机器级代码之间的对应。保留原有知识内容、公式与图示;有对应原件时提供查看和下载入口。

本页目录

描述:408 只要求结合 C 看懂关键 x86 汇编和相应机器码,不要求手工把 C 完整翻译成汇编。关键抓手是:Intel 语法“目的在左、源在右”、条件码、典型 32 位栈帧与 call/ret;具体寄存器保存、传参与对齐属于调用约定/ABI,不能当成所有 x86 程序都固定不变的 ISA 规则。

4.1 考查边界与 x86 基础

  • 题目会给出简单 C、汇编或机器码,要求识别常见指令、选择结构、循环和函数调用;可结合给定编码分析汇编与机器码的对应关系,也会考查指令格式/寻址。这里不主张“同一功能只有唯一机器码”:不同编码或助记写法可能表达相同语义。
  • 通常不要求人工完成 C -> 汇编/机器码的逐行翻译;若出现非 x86 汇编,题目一般会提供充分注释。
  • Intel 格式通常写作 op destination, source,第一个是目的操作数,第二个是源操作数,数据从右向左流动。操作数可在寄存器、主存或指令立即数中。

4.1.1 Intel 与 AT&T 语法边界

同一条 x86 机器指令可用不同汇编语法书写,语法差异不等于 ISA 不同:

项目 Intel 语法 AT&T 语法
操作数顺序 目的, 源 源, 目的
寄存器 eax %eax
立即数 10 或 0x0A $10 或 $0x0A
内存 [ebx+4] 4(%ebx)
宽度 常用 byte/word/dword ptr 常在助记符后加 b/w/l 等后缀

本讲后文统一采用 Intel 语法;读题时先确认语法,不能只凭逗号两侧位置判断数据方向。

4.2 寄存器与标志

4.2.1 常用寄存器

类别 寄存器 典型作用
通用 EAX、EBX、ECX、EDX 计算、暂存、参数/返回值等;32 位寄存器名常以 E 开头
AX 分割 AH/AL,BH/BL,CH/CL,DH/DL 16 位 AX/BX/CX/DX 的高/低 8 位可独立使用
指针/变址 EBP、ESP、ESI、EDI 典型帧指针代码中 EBP 作栈帧基址,ESP 指向栈顶,ESI/EDI 可作源/目的变址
指令 EIP/IP 下一条指令位置
段 CS、DS、SS、ES 代码、数据、栈和附加段
  • EBP 与 ESP 也可作一般算术逻辑寄存器,但其常见约定用途是访问栈。ESP 动态指向栈顶;建立帧指针的函数中 EBP 通常在本次调用期间保持不变。优化编译器可以省略帧指针并把 EBP 当通用寄存器,此时改由 ESP 或其他方式定位栈内对象。
  • 以 EAX 为例:EAX 为 32 bit,AX 为低 16 bit,AH/AL 为 AX 的高/低 8 bit。

4.2.2 重要标志位

标志 含义
OF 有符号溢出
SF 结果符号,负为 1
ZF 结果为 0 时为 1
CF 最高位进位/无符号借位相关
AF 第 3 位向第 4 位的辅助进位
PF 结果低字节中 1 的数目为偶数时为 1
DF 串操作方向,1 时 SI/DI 递减,0 时递增
IF 1 允许可屏蔽中断
TF 调试单步陷阱

4.3 常用指令与寻址

4.3.1 传送与算术逻辑

指令 作用/约束
mov d,s 复制 s 到 d;dword/word/byte ptr 指定内存访问宽度 32/16/8 bit
add/sub d,s 加/减,结果回写 d
mul s 32 位形式将无符号 EAX 与 s 相乘,64 位积放入 EDX:EAX;只有一个显式源操作数
imul s / imul d,s / imul d,s,imm 有符号乘法有一、二、三操作数形式;单操作数 32 位形式的完整积也放入 EDX:EAX
div/idiv s 32 位形式用 EDX:EAX 除以 s,商入 EAX,余数入 EDX;分别按无符号/有符号解释
neg/inc/dec 取负、自增、自减
and/or/not/xor 逐位逻辑运算
shl/shr/sar 左移、逻辑右移、算术右移;不发生有效位丢失时,左移 $k$ 位可对应乘 $2^k$
  • 目的操作数可为寄存器或主存,不能是常量;源操作数可为寄存器、主存或立即数。对本讲常见的 mov 和双操作数算术逻辑指令,通常不允许两个显式操作数同时来自主存;字符串指令等专用形式另有隐含内存操作数,不能把这条简化规则推广到全部 x86 指令。
  • mov eax, ebx 是寄存器寻址;mov eax, dword ptr [0xaf996] 是直接内存寻址;mov eax, dword ptr [ebx] 是寄存器间接寻址。
  • 复杂地址 [base + index*scale + displacement] 体现基址/变址/位移组合,硬件比例因子 scale 只能取 1、2、4、8。例如 mov eax,[ebx+ecx*4+4] 取以 EBX 为基址、ECX 按 4 B 元素缩放后的内存位置;*32 不能直接编码成 x86 的 SIB 比例因子。

4.3.2 数组寻址例

int a[24][64] 采用行优先、元素 4 B 时,一行大小为 $64\times4=256$ B。指令:

mov [ecx + edx*4 + 0x00422000], 0x0A
  • 0x0A 是立即数源操作数;中括号说明目的为内存单元。
  • 0x00422000 是数组基址;ecx 可预先保存 row*256,edx*4 表示列号乘元素大小。因此有效地址为 base + row*256 + col*4。行偏移 256 B 需由前序移位/乘法得到,不能在该内存操作数中写成硬件不支持的 row*256 比例因子。
  • 原题写法省略了目的内存操作数的宽度;真正汇编 mov [地址], 立即数 时,单凭立即数 0x0A 通常无法判定写 8、16 还是 32 bit,须由题设或 byte/word/dword ptr 等声明确定。本题 int 元素占 4 B,若要写整个元素,应明确为 mov dword ptr [ecx + edx*4 + 0x00422000], 0x0A。

4.4 选择与循环

4.4.1 选择结构

cmp a,b 本质执行比较并设置标志位,不保存算术结果;随后 jz/jne/jbe/... 根据 ZF、SF、OF、CF 等条件跳转。典型 if/else:比较 -> 条件跳转到 else/then -> 必要时无条件跳过另一分支 -> 汇合。

例如原页的函数比较两个指针 p1,p2:先从 [ebp+8]、[ebp+12] 取参数,cmp eax,edx,随后 jbe .L1 在按无符号地址比较得到 p1<=p2 时跳转(原页注释只写 <,遗漏等于)。两条路径分别执行 mov eax,[edx] 或 mov eax,[eax],EAX 承载返回值。若把原 C 代码当作严格 ISO C,p1>p2 的关系比较只在标准规定的相关对象之间有定义(例如同一数组中的元素及尾后位置、同一结构体的成员);任意两个无关对象的指针不能仅凭这段汇编反推为可移植的 C 语义。

4.4.2 循环结构

循环机器级结构为:初始化 -> 首次条件(可跳过)-> 循环体 -> 更新 -> 条件跳回。loop L1 的控制流语义是先将 ECX 减 1,若 ECX 不为 0 就跳到 L1;它本身不修改 EFLAGS。若只关心是否跳转,可近似理解为:

dec ecx
cmp ecx, 0
jne L1
  • 上述三条伪代码会修改标志位,因而与真实 loop 并非完全等价;后续代码若读取标志位,必须按真实指令语义分析。
  • loop 默认以 ECX 为计数器;loopnz 要求递减后的 ECX 非零且原有 ZF=0,loopz 要求递减后的 ECX 非零且原有 ZF=1。这些 loop 指令不会替前面的比较重新计算 ZF。
  • 不要只看 jmp 就断定无限循环,需结合跳转前的计数器/比较更新判断出口。

4.5 函数调用与栈帧

4.5.1 call/ret 与寄存器约定

  • call func 的作用近似“把返回 IP 压栈,再跳转到 func”;ret 从栈顶弹出返回 IP 并跳回调用点之后。
  • 在常见 32 位 cdecl 类约定中,EAX、ECX、EDX 通常由调用者保存,EBX、ESI、EDI、EBP 通常由被调用者保护;内层函数若改动被调用者保护寄存器,返回前须恢复。stdcall、fastcall、System V、Windows x64 等约定的传参、清栈和保存集合可能不同,这些规则属于 ABI/调用约定而非 x86 ISA 本身。

4.5.2 栈帧布局与访问

在本讲采用的典型 32 位、基于帧指针的调用约定中,栈向低地址增长,常以 4 B 为基本压栈宽度。一个过程的栈帧可由稳定的 EBP 和变化的 ESP 定位;每个未被尾调用消除的普通嵌套调用形成自己的调用记录。以下偏移布局以常见的栈上传参方式为例,不是所有 x86 ABI 的唯一布局。

典型区域(相对 EBP) 含义
[ebp] 上一层 EBP(保存的旧帧基址)
[ebp+4] 返回地址
[ebp+8] 起 调用参数,靠前参数通常更靠近栈顶/低正偏移
[ebp-...] 局部变量,通常在负偏移区域
  • push x 可压入立即数、寄存器或内存值;pop d 弹出到寄存器或内存。sub esp,n 可一次为局部变量留空间,add esp,n 回收。
  • 常见恢复序列:mov esp, ebp 后 pop ebp。编译器可能把栈帧大小对齐到 16 B,因此会出现未使用填充区。

原页的 caller() 示例令 temp1=125、temp2=80,调用 add(temp1,temp2),其 return x+y 得到 $125+80=205$。在所示 32 位栈上传参及帧指针代码里,caller 用 [ebp-12]、[ebp-8] 保存两个局部变量,调用前把实参压栈;add 以 [ebp+8]、[ebp+12] 读取两个实参,结果由 EAX 返回。这里的负偏移和正偏移是该实例的布局,不是任意编译器都必须照用的数值。

4.5.3 进程地址空间中的栈

源 PDF 还给出了典型进程虚拟地址空间:只读代码/常量、可写数据、堆和栈属于不同区域,堆常向高地址扩展,栈常向低地址扩展。具体区域顺序、随机化地址、共享库映射和保护权限由操作系统、装载器与 ABI 决定;该图用于理解栈帧所处位置,不属于 x86 ISA 对地址布局的硬性规定。

题型:#函数代码阅读

第一部分:固定流程

  • 若代码建立了帧指针,先找函数入口的保存 EBP/建立帧、sub esp 分配局部空间和结尾恢复序列,确定栈向下增长与参数偏移;若没有这套序言,考虑编译器省略帧指针、直接基于 ESP 寻址或进行了优化。
  • 再把 [ebp+...] 标成参数、[ebp-...] 标成局部变量,追踪 EAX 返回值和条件跳转目标。
  • 遇到 call 记住返回地址压栈;遇到 ret 从栈顶取返回地址。若有嵌套调用,检查哪些寄存器需要保存恢复。

第二部分:易错点

  • Intel 语法目的在左源在右,不能按 AT&T 语法反读。
  • mov 只是复制,cmp 主要改标志,jcc 决定控制流;三者连读才知道 C 条件的方向。
  • 建立帧指针时,EBP 在帧内通常固定而 ESP 会随 push/pop/sub/add 变化;若编译器省略 EBP,仍可在精确跟踪 ESP 变化后用其偏移访问参数和局部变量,不能机械套用 [ebp+8] 模板。

源 PDF 的递归函数与机器码例题(2019 年)

原页给出 f1(n):$n>1$ 时返回 $n\times f1(n-1)$,否则返回 $1$。题图列出部分 32 位 x86 机器码与地址,可把控制流直接读成下表:

地址与机器码 汇编 在该题中的作用
00401018: 83 7D 08 01 cmp dword ptr [ebp+8], 1 将参数 $n$ 与 1 比较,设置标志位
0040101C: 7E 17 jle 00401035 $n\le1$ 时跳到基本情形,属于条件转移
00401021: 83 E8 01、00401024: 50 sub eax,1、push eax 形成并压入递归实参 $n-1$
00401025: E8 D6 FF FF FF call f1 (00401000) 压入返回地址并调用同一函数,属于调用转移
00401033: EB 05 jmp 0040103A 跳过基本情形,属于无条件转移
00401035: B8 01 00 00 00 mov eax,1 基本情形的返回值
0040104A: C3 ret 从栈中取返回地址并返回

相对位移按指令末尾的顺序下一地址计算:jle 长 2 B,0040101E + 0x17 = 00401035;jmp 长 2 B,00401035 + 0x05 = 0040103A。call 长 5 B,下一地址为 0040102A,后四字节 D6 FF FF FF 在小端下是有符号 32 位位移 0xFFFFFFD6=-42,故 0040102A-0x2A=00401000。这些数值既说明控制流,也把前一节的 PC 相对寻址与本节机器码联系起来;未显示的指令不从截图臆测。

相关知识

4.6 本节复盘

  • 408 要求会读 x86 的 C/汇编/机器码对应关系,不要求手译整段程序;Intel 语法为 目的,源。
  • 寄存器、标志、mov、算术逻辑、cmp+jcc 是阅读选择和循环的基础;二维数组地址遵循基址加行列字节偏移。
  • 普通 loop 只递减计数寄存器 ECX/CX 并在其非零时跳转,不检查 ZF;loope/loopz、loopne/loopnz 才把计数条件与 ZF 结合。选择与一般条件循环常通过比较后改变 IP。
  • call 压返回地址并跳转,ret 弹出返回地址;典型 32 位帧指针代码用 EBP/ESP 组织向低地址生长的栈帧,参数常在 EBP 正偏移、局部变量常在负偏移,但最终以题设调用约定和实际指令为准。