Chapter 3 寻址方式
本笔记依据课件《Chapter 3 – Addressing Modes》(共 143 页)整理。内容包括数据寻址、程序控制转移、栈操作,以及数组、结构体和课件附录例题。前置阅读:第 2 章:微处理器体系结构与内存管理。
除特别注明外,代码使用 Intel 操作数顺序,MASM 风格数字和类型标注;不同模式的片段分别说明。示例用来展示指令语义,不是能不加修改地混合编译的完整程序。PDF 页序用于定位原图和原例。
3.1 地址大小与操作数大小
地址大小(Address Size)决定有效地址计算采用的位宽;操作数大小(Operand Size)决定指令读写或运算的数据宽度。 两者不能混为一谈。(第 5–7 页)
| 典型代码环境 | 默认地址大小 | 一般整数指令默认操作数大小 |
|---|---|---|
| 16 位代码 | 16 位 | 16 位 |
| 32 位代码 | 32 位 | 32 位 |
| 64 位模式代码 | 64 位 | 通常 32 位 |
前缀可以改变部分默认值,某些指令另有规则。例如 64 位模式的普通 PUSH/POP 通常操作 64 位,而不是套用表中的 32 位默认值。
; 64 位模式
MOV EAX, [RBX]
RBX 提供 64 位地址计算的基址,指令从该地址读 4 字节到 EAX。写 EAX 又会清除 RAX 的高 32 位。因此这条指令同时涉及“64 位地址、32 位数据、64 位寄存器别名”的不同层次。
3.2 指令格式与三类操作数
3.2.1 Intel 语法的方向
load_value: MOV AX, BX ; 将 BX 的值复制到 AX
一行通常包含标号、助记符、操作数和注释。Intel 语法写成 指令 目的, 源,MOV 的数据从右向左复制,源值不因复制而消失。(第 8–14 页)
操作码(Opcode)是机器码中指定操作的部分,MOV 是相应的助记符(Mnemonic);完整机器指令还可能有前缀、寄存器/寻址编码、位移和立即数。
3.2.2 三类操作数
| 类型 | 示例 | 含义 |
|---|---|---|
| 立即数(Immediate) | MOV AX,1234H |
数值编码在指令中 |
| 寄存器(Register) | MOV AX,BX |
从寄存器中取值 |
| 内存(Memory) | MOV AX,[BX] |
计算地址后从内存取值 |
在普通 MOV 中,立即数只能作为源;普通两操作数 MOV 也不允许两个显式操作数同时为内存。
MOV AX, BX ; 复制值
MOV AX, [BX] ; 读取 BX 指向的内存中的字
MOV [BX], AX ; 把 AX 中的字写到 BX 指向的内存
MOV EAX,BX 的宽度不匹配。若需要扩展一个较窄的整数,应选择 MOVZX(零扩展)或适用的 MOVSX(符号扩展)形式,例如 MOVZX EAX,BX。
上述源不变的规则是对 MOV 的描述,不能推广为“任何指令的第二操作数都不改变”;例如交换指令有不同语义。
3.2.3 MASM 常量与符号
| 写法 | 含义 |
|---|---|
100 |
默认十进制 100 |
100H |
十六进制 100,即十进制 256 |
100B |
二进制 100,即十进制 4 |
0F2H |
以 A–F 起始的十六进制数加前导 0 |
'A' |
字符常量 |
OFFSET array |
MASM 中取得标号对应的偏移地址 |
课件第 24 页强调 F2H 可能被当作标识符,应写成 0F2H。多字符立即数的打包规则与汇编器有关;再保存到小端内存时还涉及字节序,初学时更宜逐字节定义字符串。
标识符长度和允许字符由汇编器版本规定。课件中的长度上限和特殊字符列表是特定语法背景,不应当作所有 x86 汇编器的通用规范。
3.3 内存寻址的统一公式
课件第 27–28 页把常见内存寻址归纳为:
- Base:基址寄存器的值,常用来保存数组或对象起点。
- Index:变址寄存器的值,常用来保存索引或字节偏移。
- Scale:比例因子,常见编码允许 1、2、4、8。
- Displacement:位移,作为常量编码在指令中。
这些项可按编码规则省略,但不是任意组合都合法。16 位地址形式有更严格的寄存器组合限制;缩放变址适用于 386 及以后相应的 32/64 位地址形式。
得到 EA 后,还要按 第 2 章 的规则形成线性地址并进行分页。只有普通平坦段基址为零时,才可直接把 EA 当作线性地址。
3.4 各类数据寻址方式
3.4.1 寄存器与立即数寻址
MOV AX,BX 是寄存器寻址;MOV EAX,13456H 是立即数寻址。后者把常量本身写入 EAX,并不读取地址 13456H 的内存。(第 17–26 页)
| 指令 | EAX 得到什么 |
|---|---|
MOV EAX,3000H |
数值 3000H |
MOV EAX,[3000H] |
地址表达式为 3000H 的内存中的双字 |
3.4.2 直接数据寻址
仅使用位移时,\(EA=Disp\)。例如实模式下 DS=1000H:
MOV AL, [1234H]
线性地址为 10000H + 1234H = 11234H,读取其中一个字节。(第 29–38 页)
课件将直接数据寻址细分为累加器专用形式与一般位移形式,主要反映机器编码区别:
16 位地址环境:
MOV AL,[1234H] → A0 34 12
MOV CL,[1234H] → 8A 0E 34 12
前者可使用累加器专用的 moffs 编码,后者使用包含 ModR/M 字节的一般形式。它们计算的是同一个内存位置。3 字节、4 字节只是这个例子的长度,换地址大小、寄存器宽度或前缀后会变化;汇编器也可能为等价指令选择另一合法编码。
课件第 34 页的 32 位机器码分类:
| 机器码对应的指令 | 课件分类 |
|---|---|
A1 ...:MOV EAX,[3000H] |
累加器专用直接形式 |
8B 1D ...:MOV EBX,[3000H] |
一般位移形式 |
03 05 ...:ADD EAX,[3000H] |
一般位移形式,不能因为用了 EAX 就判成专用 MOV 形式 |
03 1D ...:ADD EBX,[3000H] |
一般位移形式 |
编译器可用这类地址表达式访问静态对象;符号的最终地址还可能由链接器和装载器确定。64 位代码也常使用 RIP 相对形式。
3.4.3 寄存器间接寻址
寄存器间接寻址(Register Indirect Addressing)把寄存器值当作 EA,而非直接把它当作数据。(第 39–48 页)
; 16 位实模式,DS=0100H,BX=1000H
MOV AX, [BX]
地址为 0100H × 10H + 1000H = 2000H。指令读取 2000H、2001H 两个字节,按小端序组成 AX。
16 位地址形式可使用 [BX]、[BP]、[SI]、[DI],不能写成 [AX] 或 [SP]。[BP] 的编码需要一个零位移,但其寻址语义仍是从 BP 给出的偏移访存。
32 位地址形式可以使用 [EAX]、[EDX]、[ESP] 等;普通 64 位形式可使用 [RAX]、[R8] 等。实模式下使用 32 位地址形式也不会自动解除普通段界限检查。
3.4.4 基址加变址寻址
基址加变址寻址(Base-Plus-Index Addressing)使用 \(EA=Base+Index\)。(第 49–56 页)
; 16 位实模式,DS=0100H,BX=1000H,DI=0010H
MOV DX, [BX+DI]
EA=1010H,线性地址=2010H。如果 BX 指向字节数组开头,DI 表示字节索引,这种形式就自然对应数组访问。
16 位合法双寄存器组合为:
| 基址 | 可搭配变址 | 默认段 |
|---|---|---|
| BX | SI、DI | DS |
| BP | SI、DI | SS |
[BX+BP]、[SI+DI] 都不在合法组合内。索引寄存器中的值是字节偏移;访问字数组的第 \(i\) 个元素时,需先得到 \(2i\),不能把元素编号直接当字节偏移。
3.4.5 寄存器相对寻址
寄存器相对寻址(Register Relative Addressing)使用 \(EA=Register+Disp\),适合访问结构体字段或栈帧局部位置。(第 57–61 页)
; 16 位实模式,DS=0200H,BX=0100H
MOV AX, [BX+1000H] ; EA=1100H,线性地址=3100H
例如 struct { int a; int b; } 在假设 int 为 4 字节且字段按此布局时,若 EAX 保存结构体地址,MOV EAX,[EAX+4] 可读取成员 b。
3.4.6 基址相对加变址寻址
基址相对加变址寻址(Base Relative-Plus-Index Addressing)使用:
课件示例 MOV AX,[BX+SI+100H] 将两个寄存器值和常量位移相加。它可以把对象起点、记录偏移、字段偏移分别放在不同项中。(第 62–65、129–130 页)
例如一个结构体有四个 4 字节整数,大小 16 字节,最后一个字段位于偏移 12:
; 32 位模式,EAX=数组起点,ECX=元素索引
SHL ECX, 4 ; ECX = i * 16
MOV EAX, [EAX+ECX+12] ; 读取第 i 个结构体的最后一个字段
寻址项较多并不意味着一定低效;现代编译器经常用它直接表达对象布局。
3.4.7 缩放变址寻址
缩放变址寻址(Scaled-Index Addressing)支持 \(EA=Base+Index\times Scale+Disp\),Scale 可为 1、2、4、8。(第 66–68 页)
; 32 位模式,EBX 指向 DWORD 数组,ECX 为元素编号
MOV EAX, [EBX+ECX*4]
如果结构体有两个 4 字节字段,总大小 8 字节,第二个字段偏移为 4,则数组成员访问可写作:
MOV EAX, [EBX+ECX*8+4]
机器编码中的 Scale 不能直接写 3、5、16。可以先计算偏移,或用 LEA 组合乘法,例如 [ECX+ECX*2] 可计算 \(3\times ECX\)。
32 位寻址的 ESP 不能作为 SIB 编码中的 index,但可以作为 base。普通 64 位 SIB 形式中 RSP 也不能作为 index,R12 则可通过扩展编码用作 index;不要仅因寄存器名称相似就套用相同限制。
3.5 段选择、宽度与合法性检查
3.5.1 默认段取决于 base
在传统 32 位地址形式中,编码的 base 为 EBP 或 ESP 时,默认段通常是 SS;其他普通 base 默认 DS。关键是编码中的 base 角色,不是只要表达式里出现 EBP 就选 SS。(第 82–84 页)
对于 [EBX+EBP]:
- 如果编码为 base=EBX、index=EBP,默认 DS。
- 如果编码为 base=EBP、index=EBX,默认 SS。
数学上的加法可以交换,但编码中的两种角色不同。实际判断应查看汇编器生成的编码;需要特定段时,可使用适用的显式段覆盖(Segment Override),例如 DS:[EBP]。
64 位模式的普通 DS/SS 基址按零处理,但 FS/GS 覆盖仍可引入相应基址。
3.5.2 PTR 标注内存操作数的宽度
MOV [DI], AL ; 可由 AL 推知写 1 字节
MOV BYTE PTR [DI],10H ; 明确写 1 字节
MOV WORD PTR [DI],10H ; 明确写 2 字节
MOV [DI],10H 无法仅凭立即数确定写多少字节。BYTE/WORD/DWORD/QWORD PTR 是给汇编器的类型说明,不是让 CPU 在运行时进行指针类型转换。(第 44–46 页)
课件的 64 位编码例子:
| 指令 | 编码 | 实际写入 |
|---|---|---|
MOV BYTE PTR [RAX],5 |
C6 00 05 |
1 字节 |
MOV WORD PTR [RAX],5 |
66 C7 00 05 00 |
2 字节 |
MOV DWORD PTR [RAX],5 |
C7 00 05 00 00 00 |
4 字节 |
MOV QWORD PTR [RAX],5 |
48 C7 00 05 00 00 00 |
8 字节 |
最后一条使用符号扩展的 32 位立即数写入 64 位内存操作数,不能据此推断普通 MOV r/m64,imm 可以直接写任意 64 位常量。必要时先把常量加载到 64 位寄存器再存储。
PTR 也可用于 INC BYTE PTR [RAX] 等需要消除宽度歧义的场景,不只限于“内存加立即数”的 MOV。
3.5.3 课件合法性选择题
在第 83 页的 32 位地址形式中:
| 地址表达式 | 是否合法 | 原因 |
|---|---|---|
[ESP+2] |
是 | ESP 为 base |
[ESP+EBX+2] |
是 | 可令 ESP 为 base、EBX 为 index |
[ESP+2*EBX+2] |
是 | EBX 可以缩放 |
[2*ESP+EBX+2] |
否 | ESP 被要求作为缩放 index |
判断任何一条汇编时,都应先明确代码模式、地址大小、操作数宽度,再检查寄存器组合和指令支持。
3.6 RIP 相对寻址与规范地址
3.6.1 以“下一条指令”为基准
64 位模式提供 RIP 相对数据寻址(RIP-relative Addressing):(第 69–73、136、138 页)
基准是当前指令结束后的地址,位移为有符号 32 位数,因此相对范围为 \(-2^{31}\) 到 \(2^{31}-1\) 字节。它适合在代码与数据整体移动时保持相对关系,常用于位置无关代码(Position-Independent Code, PIC)。
课件第 136 页的机器码是:
地址:00007FF778A06490H
机器码:48 8B 05 F9 67 00 00
指令长 7 字节,下一条地址为 00007FF778A06497H。位移的字节按小端排列 F9 67 00 00,数值为 000067F9H:
从目标地址读取 8 字节 88 13 00 00 00 00 00 00,得到 RAX=1388H=5000。计算访存地址与解码内存数值是两步不同的工作。
RIP 相对形式不能直接再叠加一个普通 SIB index;例如“RIP 加数组索引乘 4”通常先取得数组地址,再使用普通基址加缩放变址。相关地址计算定义可核对 Intel 指令集参考手册。
3.6.2 规范形式只是第一道检查
48 位线性地址模型要求高 16 位复制位 47;57 位模型要求位 63–57 复制位 56。详细例子见 第 2 章。
课件中的 1122334455667788H 不满足普通 48 位或 57 位规范地址规则。即使符合规范,还需要有效页表映射与权限,才能完成访问。课件第 138 页旧示意图出现的 “56-bit” 不应替代前面讲解的五级分页 57 位规则。
3.7 Intel 与 AT&T 语法对照
两种语法可以表达同一条机器指令,主要区别如下。(第 74–79 页)
| 项目 | Intel 风格 | AT&T 风格 |
|---|---|---|
| 操作数顺序 | 目的、源 | 源、目的 |
| 寄存器 | eax |
%eax |
| 立即数 | 1 |
$1 |
| 内存表达式 | [ebx+ecx*4+20] |
20(%ebx,%ecx,4) |
| 宽度 | 寄存器或 BYTE/WORD/DWORD/QWORD PTR |
常见后缀 b/w/l/q |
| 32 位复制常量 | mov eax,1 |
movl $1,%eax |
示例为 GNU 工具常见 AT&T 写法与 Intel 风格对照:
; Intel
MOV EAX, [EBX+ECX*4+20]
ADD EAX, 10
# AT&T
movl 20(%ebx,%ecx,4), %eax
addl $10, %eax
GCC/Clang 可使用 -masm=intel 选择输出风格。Intel 风格并不意味着 MASM、NASM、GAS 的伪指令、符号寻址和类型语法完全相同;把示例移到另一汇编器时应同时核对这些规则。
3.8 程序控制转移的寻址
程序寻址决定下一条执行哪条指令。它既有“目标怎样给出”的区别,也有“是否改变代码段”的区别,两条分类轴不能混在一起。(第 85–97、137、142–143 页)
3.8.1 相对、寄存器间接与内存间接
| 形式 | 示例 | 目标地址的来源 |
|---|---|---|
| 相对转移 | JMP label |
下一条指令地址加有符号位移 |
| 寄存器间接 | 16 位 JMP AX |
AX 中的代码段内目标偏移 |
| 内存间接 | 16 位 JMP WORD PTR [BX] |
先读 DS:BX 处保存的目标偏移 |
| 直接远转移 | 概念形式 JMP 1234H:5678H |
指令中编码段值/选择子与偏移 |
JMP BX 与 JMP [BX] 的区别,类似于使用指针值与对指针再解引用:后者首先读取 BX 指向内存中保存的目标地址。跳转表(Jump Table)把多个入口地址放在数组中,再由索引选出目标。
例如 32 位近跳转表的每个条目占 4 字节,可用 JMP DWORD PTR [table+EAX*4] 选取入口;表项格式、索引范围和代码模式必须事先匹配。
3.8.2 短、近、远转移
| 类型 | 主要含义 |
|---|---|
| 短跳转(Short Jump) | 近跳转的一种,使用有符号 8 位位移,范围 -128 到 +127 |
| 近跳转(Near Jump) | 保持代码段,改变 IP/EIP/RIP |
| 远跳转(Far Jump) | 提供段选择子/段值和偏移,重新加载代码段相关状态 |
| 任务切换 | 传统保护模式中的特殊机制,不等同于普通用户程序跳转 |
近转移可以是相对或间接。16 位和 32 位相对转移常见 rel16、rel32;64 位模式常见 rel8、rel32,不能把“64 位处理器”理解为每条分支都携带 64 位立即地址。
远转移必须遵守段类型和权限检查,并非可以任意跳进任何内存或任意特权级。直接立即远跳转的传统编码也不能直接照搬到 64 位代码中。
3.8.3 相对位移例题
若地址 10000H 处是短跳转机器码 EB 03,它长 2 字节,因此目标是:
若要从 10000H 跳回 0FFF0H,位移为 \(-12H=-18\),8 位补码为 EEH,可编码为 EB EE。
课件有时写“当前 IP 加位移”,这里的 IP 应理解为取完当前指令、指向下一条的值,而不是指令起始地址。
3.8.4 直接远地址编码
课件第 137 页的五字节编码:
EA 00 00 00 10
在对应的 16 位环境中,EA 是直接远跳转操作码;接下来两字节为 offset=0000H,最后两字节为 segment=1000H。它表示跳到 1000H:0000H,实模式线性地址为 10000H。
原图用 JMP [10000H] 标注容易被误读成内存间接跳转。这个机器码实际携带的是立即远地址;汇编表示中的逻辑顺序“段:偏移”与机器码中的“先偏移、后段”需要区分。
3.8.5 CALL 与 RET 的联系
近 CALL 在转移控制之前把返回地址压栈,RET 从栈顶取回返回地址。返回地址指向 CALL 后的下一条指令。若不平衡地修改栈指针、覆盖返回地址,返回时就可能跳到错误位置。
远调用和跨特权级转移涉及更多保存内容及检查,不应只按普通近调用的压栈过程推断。
3.9 栈寻址
3.9.1 后进先出与栈指针
栈(Stack)遵循后进先出(Last In, First Out, LIFO)。x86 常规栈向低地址增长,栈指针指向当前栈顶。(第 98–113 页)
对 16 位栈中的普通字操作,可概括为:
PUSH value:SP ← SP - 2;把 value 写入 SS:SP
POP dest :从 SS:SP 读出字到 dest;SP ← SP + 2
这用于理解普通操作数;像 POP SP、PUSH SP 或以栈指针参与寻址的特殊情况,应再查对应处理器和指令的细节。
3.9.2 手算 PUSH 与 POP
假设实模式 SS=2000H、SP=0100H、AX=1234H,执行 PUSH AX:
- SP 变为
00FEH。 - 栈顶地址为
20000H+00FEH=200FEH。 200FEH保存低字节34H,200FFH保存高字节12H。
再执行 POP BX,BX 得到 1234H,SP 回到 0100H。原内存字节不一定清除,只是逻辑上不再属于当前栈内容。
PUSH AX
PUSH BX
POP CX ; CX 得到原 BX
POP DX ; DX 得到原 AX
弹出顺序与压入顺序相反。程序必须保持所需的栈平衡,尤其要避免覆盖 CALL 保存的返回地址。
3.9.3 宽度与模式限制
| 环境 | 常见 PUSH/POP 数据宽度 | 栈指针变化 |
|---|---|---|
| 8086 / 普通 16 位操作 | 16 位 | 2 字节 |
| 32 位代码 | 通常 32 位,可有 16 位形式 | 4 或 2 字节 |
| 64 位模式 | 通常 64 位,也有 16 位形式 | 8 或 2 字节 |
没有普通的 8 位 PUSH/POP 数据形式。PUSH imm8 中“8 位”指编码的立即数宽度,它会按指令规则扩展到栈操作宽度;在 64 位模式中也没有普通 PUSH EAX 这样的 32 位寄存器压栈形式。
立即数可以压栈,不能作为 POP 的目的。传统模式可以 PUSH CS,但没有对应的普通 POP CS;64 位模式又对可用段寄存器压栈形式施加额外限制,不能把“所有段寄存器都能压栈”跨模式使用。
3.9.4 PUSHA、POPA 与 FLAGS
16 位 PUSHA 的顺序是:
AX → CX → DX → BX → 原始 SP → BP → SI → DI
共占 16 字节。PUSHAD 保存对应 32 位寄存器,共占 32 字节。保存的是开始执行指令前的 SP/ESP,不是压入几个寄存器后的中间值。
POPA 按 DI、SI、BP、跳过已保存的 SP 槽位、BX、DX、CX、AX 的顺序恢复;POPAD 同理跳过 ESP 槽位。课件第 112、127 页不能理解为会把槽位中的值直接写回 SP/ESP。
PUSHA/POPA/PUSHAD/POPAD 不适用于 64 位模式。PUSHF/PUSHFD/PUSHFQ 与相应 POP 形式处理标志寄存器,但恢复哪些标志位受到运行模式和权限约束,不是任意修改系统标志的方法。
3.9.5 栈的初始化
课件的 MASM 传统段定义:(第 104–107 页)
STACK_SEG SEGMENT STACK
DW 100H DUP(?)
STACK_SEG ENDS
这里分配 100H 个字,即 200H 字节。传统可执行文件可由汇编、链接与装载信息建立初始栈;裸机代码则需自行准备合法的栈存储、段状态和指针。
课件关于程序段前缀(Program Segment Prefix, PSP)的讨论属于 DOS 环境,不能套用到所有操作系统。有限位宽 SP 的回绕也不代表栈可无限循环使用:越界可能破坏数据,保护模式的界限或分页保护还可能触发异常。
3.10 数组与结构体:把地址公式用起来
3.10.1 同一数组访问可以有不同写法
课件第 133、135 页把字节数组偏移 10H 的元素复制到偏移 20H。假设数组至少有 21H 个字节、DS 已正确初始化:
; 方式一:基址放在 BX,索引放在 DI
MOV BX, OFFSET array
MOV DI, 10H
MOV AL, [BX+DI]
MOV DI, 20H
MOV [BX+DI], AL
; 方式二:数组起点编码在位移中
MOV DI, 10H
MOV AL, array[DI]
MOV DI, 20H
MOV array[DI], AL
两者都经过 AL 中转,因为普通 MOV 不能直接使用两个显式内存操作数。数组若改成字数组,第 10H 个元素的字节偏移就是 20H,不能沿用字节数组的索引解释。
3.10.2 采样循环中的步长
第 131–132 页例题读取传统 PC BIOS 数据区 0000H:046CH,并保存 50 个字。按课件语境整理其核心如下:
; 16 位实模式示意;DS 已指向数据段
; 数据声明:samples WORD 50 DUP(?)
XOR AX, AX
MOV ES, AX
MOV BX, OFFSET samples
MOV CX, 50
again:
MOV AX, ES:[046CH]
MOV [BX], AX
ADD BX, 2
LOOP again
ES: 指定读源所用的段,写入 [BX] 默认使用 DS。每个样本是 2 字节,因此 BX 每次加 2;数组总共需要 100 字节。课件早先关于“50 字节表”的示意与这里的“50 个字”不能混用。
在该传统 PC 环境中,046CH 对应 BIOS 时钟节拍计数的数据区位置;这里读取的是计数的低字,而不是直接执行 RTC 硬件端口读取。紧密循环也不保证获得 50 个不同节拍值。本例主要展示地址、段覆盖和步长,不能当作已经实现固定间隔采样。
3.10.3 二维数组的地址推导
课件第 134 页给出:
struct foo {
int a;
int b;
} tbl[10][10];
假设 int 为 4 字节,结构体大小 8 字节,数组按 C 的行优先(Row-major)顺序布局,则:
其中 10i+j 是线性元素编号,乘 8 得到结构体字节偏移,再加 4 选择成员 b。
; 32 位示意:EAX=i,EDX=j
LEA EAX, [EAX+EAX*4] ; 5*i
LEA EAX, [EDX+EAX*2] ; j+10*i
MOV EAX, tbl[EAX*8+4] ; 读取成员 b
加载有效地址(Load Effective Address, LEA)只计算地址表达式的数值,不读取方括号表示的内存,也不进行该地址的数据访存。这里借它完成整数乘加;最后的 MOV 才真正读取数组内容。
结构体大小可能受到对齐和填充(Padding)影响。实际翻译 C 结构时应确认 sizeof 与字段偏移,不能总假设成员大小相加就等于整个对象大小。
3.10.4 MASM 结构体定义
课件第 139–141 页使用 STRUC/STRUCT 与 ENDS 描述记录模板。简化为只含字节字段、无额外填充的示例:
INFO STRUCT
NAMES BYTE 32 DUP(?)
STREET BYTE 32 DUP(?)
CITY BYTE 16 DUP(?)
STATE BYTE 2 DUP(?)
ZIP BYTE 5 DUP(?)
INFO ENDS
person INFO <>
| 字段 | 相对记录起点的字节偏移 | 长度 |
|---|---|---|
| NAMES | 0 | 32 |
| STREET | 32 | 32 |
| CITY | 64 | 16 |
| STATE | 80 | 2 |
| ZIP | 82 | 5 |
结构模板描述布局,person 才是分配的实例。字段名称对应偏移,最终访问仍归结为“实例起点加字段偏移”。这些固定长度字段也不会自动成为带终止零的 C 字符串。
课件以 REP STOSB 清空字段。若在传统 16 位环境中 ES 已设为与数据段 DS 相同、DI 指向目标字段,则:
CLD ; 地址向高处递增
MOV AX, DS
MOV ES, AX
MOV DI, OFFSET person.NAMES
MOV CX, 32
MOV AL, 0
REP STOSB
每次 STOSB 将 AL 写入 ES:DI,并按 DF 调整 DI;REP 根据计数寄存器重复,最终清零 32 字节。源值 AL、目的 ES:DI、方向 DF 与计数 CX 都是这段代码必须解释的隐含状态。
3.10.5 传统 MASM 内存模型
课件第 117–118 页提到 .MODEL TINY 和 .MODEL SMALL:前者用于传统 DOS 中代码和数据合并的紧凑布局,常见产物为 .COM;后者使用分别的代码段与数据段,常见产物为 .EXE。
.MODEL 属于汇编器与链接布局约定,不是 CPU 从实模式切换到保护模式的指令。现代 64 位工具链也不直接沿用这些 DOS 内存模型。
3.11 课件易错点与练习
| 需要辨清的问题 | 正确理解 |
|---|---|
| 64 位模式的 AH 是否完全不可用 | 没有 REX 前缀时仍可编码;课件第 26 页对应注释过于绝对 |
| 地址里含 EBP 就一定默认 SS | 32 位形式要看编码中的 base |
| ESP 不能参与任何地址计算 | 可作 base,不能作普通 SIB index |
| RIP 相对位移加在指令开头 | 应加在下一条指令地址 |
LEA 读取方括号内存 |
它只计算有效地址数值 |
PUSH imm8 只消耗一个栈字节 |
立即数先扩展到对应的栈操作宽度 |
POPA 把保存的值恢复到 SP |
它跳过 SP 槽位 |
JMP [地址] 等于直接跳到该地址 |
它从内存取目标;与立即远地址形式不同 |
第 128 页的教材作业题号:3-10、23、28–32、35、36、45、48、49、53。完整题干需结合指定教材。
以下例题可用于检查计算流程:
| 条件 | 核对结果 |
|---|---|
实模式 DS=1000H、BX=0200H、SI=0010H,读 [BX+SI+4] |
EA=0214H,线性地址=10214H |
32 位平坦段中 EBX=1000H、ECX=3,读 [EBX+ECX*4+8] |
地址=1014H,数据宽度仍由指令确定 |
10000H 处机器码 EB 03 |
跳转目标=10005H |
16 位 SP=0100H,压入两字再弹出一字 |
SP=00FEH |
课件 RIP 例题:下一条=...6497H,位移=67F9H |
目标=00007FF778A0CC90H |
上述 tbl[2][3].b 相对数组起点的偏移 |
\((2\times10+3)\times8+4=188\) 字节 |
3.12 术语表
| 中文术语 | English | 缩写 | 含义 |
|---|---|---|---|
| 寻址方式 | Addressing Mode | — | 指令指定操作数或控制转移目标的方法 |
| 地址大小 | Address Size | — | 有效地址计算使用的位宽 |
| 操作数大小 | Operand Size | — | 指令处理的数据宽度 |
| 立即数 | Immediate | — | 编码在指令中的常量 |
| 位移 | Displacement | Disp | 地址计算中的编码常量 |
| 基址寄存器 | Base Register | — | 地址计算中的起点项 |
| 变址寄存器 | Index Register | — | 地址计算中的索引项 |
| 缩放变址 | Scaled-Index Addressing | — | 对索引乘以 1、2、4、8 后参与地址计算 |
| 段覆盖 | Segment Override | — | 用显式段选择替换适用的默认段 |
| RIP 相对寻址 | RIP-relative Addressing | — | 用下一条指令地址加有符号位移寻址 |
| 位置无关代码 | Position-Independent Code | PIC | 便于在不同装载地址保持语义的代码 |
| 近跳转 | Near Jump | — | 不改变代码段的控制转移 |
| 远跳转 | Far Jump | — | 提供段和偏移的控制转移 |
| 跳转表 | Jump Table | — | 按索引选择控制转移目标的表 |
| 后进先出 | Last In, First Out | LIFO | 栈中最后压入的数据最先弹出 |
| 栈帧 | Stack Frame | — | 一次函数调用使用的栈区域 |
| 行优先布局 | Row-major Layout | — | 数组同行相邻元素连续存放 |