Data Movement Instructions⚓︎
约 10458 个字 225 行代码 预计阅读时间 55 分钟
MOV Revisited⚓︎
Machine Language⚓︎
- 机器语言(machine language) 是被微处理器用作控制运算的指令(长度 1-15 字节不等)的原生二进制码
- 有超过 10 万种机器语言指令的变体,但目前没有关于所有指令变体的完整列表
- 一些机器语言指令的位是提供的,剩余的位由指令的具体变体决定
Operation Modes⚓︎
有三类操作模式
- 16 位模式(实模式、vm86、保护模式
) :默认地址和操作数大小为 16 位 - 32 位保护模式:默认地址和操作数大小为 32 位
- 64 位模式:默认地址为 64 位,操作数大小仍为 32 位
Processor Directive in MASM⚓︎
MASM 中的处理器指示符(processor directive) 是一个伪指令 (pseudo-instruction),告诉汇编器在汇编过程中启用哪些指令集、处理器类型以及相应的功能。它决定了:
- 什么 CPU 指令是合法的
- 默认的操作数 / 地址大小(16 位 / 32 位)
- 可用的寄存器和寻址模式
- 允许的语法
MASM 的处理器指示符包括:
- 16 位:
.8086、.186、.286 - 32 位:
386、486、586、686 - 浮点:
.8087、.287、.387 - 特殊目的:
.MMX、.XMM
例子
处理器指示符的功能:
-
验证指令合法性,比如:
-
基于默认操作数 / 地址大小生成正确的指令编码
在代码段描述符中,D/B-bit 和 L-bit 表示操作模式:
- L = 0,D/B = 0:16 位
- L = 0,D/B = 1:32 位
- L = 1:64 位
操作数大小前缀 66H 选择了非默认的操作数大小,而地址大小前缀 67H 改变了内存操作数的默认地址大小。
Instructions Stored in Memory⚓︎
- 指令以小端序(little-endian) 存储在内存中,即指令的第一个字节存储在最低的内存地址上
- 因为指令本质上是字节串,所以它们能在任何内存地址上起始
- 总的指令长度不超过 15 个字节,超出限制的话就会触发通用保护异常(general-protection exception)
Opcode-Byte 1⚓︎
第一个操作码字节负责选择由微处理器执行的运算(比如加法、减法等
- 第一个字节的前 6 位是二进制操作码
-
剩余 2 位分别表示数据流的方向(D)和数据大小(W
) (字节或字)- 方向位 D
- D = 1:R/M 区域 -> REG 区域
- D = 0:REG 区域 -> R/M 区域
- 宽度位 W
- W = 1:字或双字
- W = 0:只能是字节
其中 W 位出现在大多数指令中,而 D 位主要出现在
MOV指令和其他一些指令中 - 方向位 D
为何要设置方向位?
MOD Field-Byte 2⚓︎
MOD 字段指定所选指令的寻址模式,并选择寻址类型以及所选类型是否带有偏移。
- 如果 MOD 字段包含 11,则选择的是寄存器寻址模式,此时会使用 R/M 字段来指定一个寄存器
- 如果 MOD 字段包含 00、01 或 10,则 R/M 字段选择数据内存寻址模式之一
REG/Opcode Field-Byte 2⚓︎
REG/Opcode 字段指明了寄存器号或有关操作码信息的 3 个额外位。
R/M Memory Addressing⚓︎
R/M 字段指明一个作为操作数的寄存器,或者和 MOD 字段结合来编码一种寻址模式。
- 若 MOD 字段为 00、01、10,那 R/M 字段就有了新的含义
例子
下图展示了 16 位指令 MOV DL,[DI](8A15H)的机器码:
- Opcode = 100010,D = 1,W = 0,MOD = 00,REG/Opcode = 010(
DL) ,R/M = 101([DI]) -
- 前面一个字节保持不变
- MOD 字段变为
01,用于 8 位偏移 - 于是指令变为
MOV DL,[DI+1]
若指令变成
8A5501H(多了01H的偏移量)
32-Bit Addressing Modes⚓︎
当 R/M = 100 时,一个叫做比例变址字节(scaled-index byte) 的额外字节出现在指令中,表示比例变址寻址的额外形式。
- 在 80386 到 Core 2 处理器中,有超过 32,000 中
MOV指令的变体 -
下图展示了当 80386 及以上版本使用 32 位地址时,指令中 R/M = 100 时比例变址字节的格式:
- 最左边两位表示比例因子(乘数)1x、2x、4x、8x
- 比例变址寻址还可以使用一个乘以比例因子的单个寄存器
- 变址和基址字段同时包含寄存器号
64-Bit Mode for the Pentium 4 and Core2⚓︎
- 在 64 位模式下,添加了一个名为 ** REX(寄存器扩展)** 的前缀,用于启用操作数大小扩展和寄存器
R8-R15 - REX 不是一个单个的唯一值,而是占据一个范围(40h 到 4Fh
) ,并位于其它前缀之后,操作码之前 - 目的是修改指令第二字节中的 REG 和 R/M 字段
- REX 用于引用寄存器
R8-R15
- REX 用于引用寄存器
- REX 包含五个字段:高四位是 REX 前缀独有的,作为标识;低四位分为四个 1 位字段(W、R、X、B)
下图说明了 REX 在操作码第二个字节的结构和应用:
- REG 字段只能包含寄存器赋值,就像在其他操作模式中
- R/M 字段包含寄存器或内存赋值
下图展示了带有 REX 前缀的比例变址字节,用于更复杂的寻址模式:
Legacy Prefixes⚓︎
指令前缀分为四组。每条指令只能使用每个组中的一个前缀:
- 组 1
- 0xF0: LOCK
- 0xF2: REPNE/REPNZ
- 0xF3: REP or REPE/REPZ
- 组 2
- 0x26: ES 段重写
- 0x2E: CS 段重写
- 0x36: SS 段重写
- 0x3E: DS 段重写
- 0x64: FS 段重写
- 0x65: GS 段重写
- 组 3:0x66: 操作数大小重写前缀
- 组 4:0x67: 地址大小重写前缀
Lock Prefix⚓︎
- LOCK 前缀会让某些类型的内存读 / 改 / 写指令执行原子操作
- 该前缀旨在使处理器在多处理器系统中独占 (exclusive) 使用共享内存
-
LOCK 前缀只能与以下写入内存操作数的指令一起使用:
-
如果将 LOCK 前缀与上面没提到的指令一起使用,将发生未定义的操作码异常(undefined opcode exception)(#UD)
Segment Override Prefix⚓︎
-
处理器可以自动根据以下规则选择默认段:
- 指令:CS
- 局部数据:DS
- 栈:SS
- 目标字符串:ES
-
程序员可以使用段重写前缀来重写默认段,该前缀要放置在指令开头的字节上
例子
在 32 位模式下:
Operand-Size Override Prefix⚓︎
- 在 64 位模式下,指令默认使用 32 位操作数大小
- 前缀允许混合 16 位、32 位和 64 位数据:
- REX(REX.W)前缀可以指定 64 位操作数大小
- 66H 前缀指定 16 位操作数大小
- REX 前缀的优先级高于 66H 前缀
默认操作数大小由当前操作模式定义,但操作数大小重写前缀(REX 或 66H)可以改变默认操作数大小。
操作数大小总结
| Prefix / Bit | Effect on Operand Size | Notes |
|---|---|---|
| Opcode W-bit | 8-bit \(\leftrightarrow\) full-size (16, 32, 64-bit) | Works in all modes |
| 0x66 prefix | 16-bit override (16-bit \(\leftrightarrow\) 32-bit) | Works in all modes |
| REX.W bit | 32-bit \(\leftrightarrow\) 64-bit | Only in 64-bit mode |
Address-Size Override Prefix⚓︎
内存操作数的默认地址大小由当前操作模式确定,但可以通过地址大小重写前缀(67H)来重写。比如在 64 位模式下,地址默认为 64 位,但可以通过地址大小前缀重写为 32 位。
Quiz
VEX Prefix (Vector Extensions)⚓︎
- VEX 前缀以字节
C4H或C5H开始 - 可用于编码操作
YMM或XMM寄存器的指令 - 支持超过三个操作数
- 支持非破坏性源操作数的指令语法编码
例子
指令 VADDPS XMM0, XMM1, XMM2 编码为 C5 F0 58 C2
-
功能:添加打包的单精度浮点值:
-
语法:标准的三操作数 AVX 语法
- 2 字节 VEX 前缀:
C5 F0
Escape Sequence / Opcode⚓︎
- 由于架构定义的指令超过 256 条,因此必须定义多个不同的操作码映射
- 通过使用一个转义操作码字节(0FH)或两字节的转义(0F38H,0F3AH),转义序列用于扩展编码空间,以提供给其他操作码
例子
0F AF是IMUL r16, r/m16的操作码0F B6是MOVZX r16, r/m8的操作码- 这些常见指令是在 8086 之后引入的,并且没有剩余的编码空间来为它们提供单字节操作码
Load Effective Address⚓︎
- 加载有效地址指令集用来支持像 C 这样的高级语言
- 两种加载有效地址指令类型:
LEA:加载近指针(偏移量)LDS、LES、LFS、LGS、LSS加载远指针(段选择符和偏移量)
例子
考虑以下表示 (x, y) 坐标的结构体:
假如有以下函数:
-
MOV EDX, [EBX + 8*EAX + 4] -
LEA ESI, [EBX + 8*EAX + 4]
注:EBX 是数组(点)的基址,EAX 表示变量 i,8 是每个点的比例因子,4 是 y 的偏移量
LEA⚓︎
- 将操作数指定的数据的偏移地址加载到 16 位或 32 位寄存器中
- 通过比较
LEA与MOV,我们观察到:LEA BX, [DI]将[DI](DI的内容)指定的偏移地址加载到BX寄存器中MOV BX, [DI]将[DI]地址指向的内存位置存储的数据加载到寄存器BX中
SEG和OFFSET指令返回内存位置的段和偏移值- 如果操作数是一个偏移量,那么
OFFSET执行和LEA相同的功能 -
OFFSET指令比LEA指令更高效MOV BX, OFFSET LIST需要 1 个时钟周期- 80486 微处理器中,
LEA BX, LIST需要 2 个时钟周期
-
实际上,
MOV BX, OFFSET LIST指令被汇编为立即数移动指令,效率更高(比如MOV BX, 0x9) - 既然
OFFSET已经能完成同样的任务,为什么还要有LEA指令?OFFSET只能与像LIST这样简单的操作数一起使用,不能用于像[DI]、[SI]、[BX+DI]这样的操作数,例如:LEA BX, [DI]->MOV BX, DILEA SI, [BX+DI]:这条指令将BX加到DI上,并将和存储在SI寄存器中;由这条指令生成的和是一个模 64K 的和
LDS, LES, LFS, LGS, and LSS⚓︎
LDS和LES从内存加载远地址- 使用检索自内存中的偏移地址来加载一个 16 或 32 位寄存器
- 然后将
DS或ES加载为从内存中检索到的段地址或段选择符
- 指令使用任何内存寻址模式来访问包含段和偏移地址的 32 位或 48 位内存段
- 32 位远指针:16 位段 + 16 位偏移
- 48 位远指针:16 位选择符 + 32 位偏移
- 在 80386 及以上版本中,增加了
LFS、LGS和LSS指令 - 使用一个偏移地址加载任何 16 位或 32 位寄存器,并且使用一个段地址或段选择子来加载
DS、ES、FS、GS或SS段寄存器 -
LDS、LES、LFS、LGS、LSS指令从内存中获取一个新的远地址。- 偏移地址首先出现,随后是段地址或段选择符
-
此格式用于存储所有 32 位内存地址
-
远地址可以通过汇编器存储在内存中,其中最有用的加载指令是
LSS指令
CLI(禁用中断)和STI(启用中断)指令必须包含禁用中断的内容
String Data Transfers⚓︎
- 五个字符串数据传输指令(string data transfer instructions):
LODS,STOS,MOVS,INS,OUTS -
两个字符串比较指令(string comparison instructions):
SCAS,CMPS- 指令的前 2-3 个字母表明了指令的功能,
- 所有指令中的 "S" 代表字符串
-
每个指令都允许以单字节、字或双字的形式进行数据传输或比较,并隐式使用
DI、SI或同时使用两个寄存器来寻址内存 - 字符串指令执行效率高,因为它们会自动重复并增加数组索引
-
DI/EDI,SI/ESIDI/EDI带有额外段ES,不能被覆盖SI/ESI带有数据段DS,可以被覆盖
-
方向标志(direction flag)
- D = 0:自动递增
- D = 1:自动递减
-
REP和CX/ECX- 重复前缀(
REP)使得指令可以重复 n 次,其中 n 是存储在CX/ECX中的值
- 重复前缀(
-
允许的带后缀的形式
B:字节W:字D:双字- 例如:
–
MOVSB:字节大小的MOVS–LODSW:字大小的LODS
DI and SI⚓︎
-
在执行字符串指令期间,内存访问通过
DI和SI寄存器进行DI偏移地址访问使用附加段(ES)的所有字符串指令中的数据SI偏移地址默认访问数据段(DS)中的数据
-
在 32 位模式下操作时,使用
EDI和ESI寄存器代替DI和SI- 此时字符串能使用整个 4G 字节保护模式地址空间中的任何内存位置
Direction Flag⚓︎
-
方向标志(direction flag)(D,位于标志寄存器中)在字符串操作期间选择
DI和SI寄存器的自动增减操作- 仅与字符串指令一起使用
-
CLD指令清除 D 标志,STD指令设置它CLD指令选择自动增模式STD指令选择自动减模式
Using a Repeat Prefix⚓︎
- 字符串原语指令仅处理单个内存值或一对值;若添加了一个重复前缀(repeat prefix),则指令会重复执行,并使用
CX或ECX作为计数器 - 重复前缀使用单个指令来处理整个数组
-
下面列举了可用的重复前缀:
示例:拷贝一个字符串
- 在以下示例中,
MOVSB将 10 个字节从string1移动到string2 - 当
MOVSB重复执行时,ESI和EDI会自动增加,此行为受方向标志控制
LODS⚓︎
-
LODS从源位置DS:SI传输一个字节、字或双字到AL、AX或EAX;后缀表示操作数的大小LODSB:DS:SI/ESI\(\pm\) 1LODSW:DS:SI/ESI\(\pm\) 2LODSD:DS:SI/ESI\(\pm\) 4
-
LODS使用隐式操作数(implicit operands)(AL、AX、 EAX) ,即在操作数或操作码中未明确提及的操作数
下图展示了 LODSW 指令的操作细节:
STOS⚓︎
-
STOS将一个字节、字或双字从AL、AX或EAX存储到目标位置ES:DI上STOSB:ES:DI/EDI\(\pm\) 1STOSW:ES:DI/EDI\(\pm\) 2STOSD:ES:DI/EDI\(\pm\) 4
-
STOS使用隐式操作数(implicit operands)(AL、AX、 EAX) - 重复前缀(
REP)可被添加到除LODS型指令外(防止寄存器中的数据被覆盖)的任何字符串数据传输指令中 - 如果
CX值为 0,字符串指令终止,程序继续执行 - 如果
CX的值为 100,并执行REP STOSB指令,微处理器将自动重复STOSB100 次 - 当与
REP前缀一起使用时,STOS指令可用于将单个值填充到字符串或数组的所有元素中
例子
以下代码将 string1 中的每个字节初始化为 0FFh
MOVS⚓︎
-
MOVS从DS:SI传输一个字节、字或双字到ES:DI,并且更新SI和DIMOVSB: (DS:SI和ES:DI)\(\pm\) 1MOVSW(DS:SI和ES:DI)\(\pm\) 2MOVSD(DS:SI和ES:DI)\(\pm\) 4
-
用于移动一个内存块
- 该指令是唯一一个在 8086 到 Pentium 4 微处理器中可以实现内存到内存传输(memory-to-memory transfer) 的合法指令
例子
传输两个双字内存块:
- C++ 版本:
void TransferBlocks(int blockSize, int* blockA, int* blockB) {
for (int a = 0; a < blockSize; a++) {
*blockB = *blockA++;
blockB++;
}
}
-
内联汇编:
void TransferBlocks(int blocksize, int* blockA, int* blockB) { _asm{ push es ; save registers push edi push esi push ds ; copy DS into ES pop es mov esi, blockA ; address blockA mov edi, blockB ; address blockB mov ecx, blocksize ; load count rep movsd ; move data pop esi pop edi pop es ; restore registers } }
INS⚓︎
INS 指令将一个字节、字或双字数据从 I/O 设备传输到附加段内存位置。
INS使用DX或EDX作为源操作数来指定 I/O 地址或 I/O 端口- 目标操作数是由
ES:DI或ES:EDI寻址的内存位置 - 适用于将外部 I/O 设备的一块数据直接输入到内存中
- 一种应用将数据从硬盘驱动器(通常被视为计算机系统中的 I/O 设备)传输到内存中
- 存在两种形式的
INS指令:显式操作数形式(explicit-operands form) 和无操作数形式(no-operands form)- 显式操作数形式允许显式指定源操作数和目的操作数,例如,
INS WORD PTR [DI], DX- 源操作数必须是
DX,目的操作数应该是ES:DI或ES:EDI
- 源操作数必须是
- 无操作数形式提供了
INS指令的字、字和双字版本的“简短形式”INSB:从 8 位 I/O 设备输入数据,并将其存储在由DI索引的内存位置INSW:输入 16 位 I/O 数据,并将其存储在字大小的内存位置INSD:输入 32 位 I/O 数据,并将其存储在双字大小的内存位置- 这些指令可以使用
REP前缀重复使用,允许将整个输入数据块从 I/O 设备存储到内存中
- 显式操作数形式允许显式指定源操作数和目的操作数,例如,
例子
以下指令序列从 I/O 设备(其 I/O 端口为 03ACH)输入 50 字节的数据,并将数据存储在额外段内存数组 LISTS 中:
OUTS⚓︎
-
OUTS指令将一个字节、字或双字数据从数据段内存地址传输到 I/O 设备中- 源操作数是一个由
DS:SI或DS:ESI寻址的内存位置 - 目标操作数(I/O 地址或 I/O 端口)包含在
DX寄存器中,与INS指令相同
- 源操作数是一个由
-
存在两种形式的
OUTS指令:显式操作数形式和无操作数形式-
显式操作数形式允许显式指定源操作数和目的操作数,例如
OUTS DX, WORD PTR [SI]- 源操作数应为
DS:SI或DS:ESI,而目的操作数必须是DX
- 源操作数应为
-
无操作数形式提供了
OUTS指令的字节、字和双字版本的“简短形式”OUTSB:将SI索引的字节数据输出到 8 位 I/O 设备OUTSW:将字大小的内存数据输出到 16 位 I/O 设备OUTSD:将双字大小的内存数据输出到 32 位 I/O 设备
-
-
注意:在 Pentium 4 和 Core 2 的 64 位模式下,没有 64 位的输出,并且在
OUTS指令中,64 位的内存地址位于RDI中
例子
以下指令序列将数据从数据段内存数组(ARRAY)传输到 I/O 地址为 3ACH 的 I/O 设备中:
Miscellaneous Data Transfer Instructions⚓︎
XCHG⚓︎
XCHG指令将寄存器的内容与任何其他寄存器或内存位置交换,但不能交换段寄存器或内存到内存的数据- 是一种具有双输出的罕见指令类型
- 交换的数据可以是字节、字、双字或四字,并使用除了立即寻址以外的任何寻址模式
- 使用 16 位
AX寄存器与另一个 16 位寄存器进行XCHG操作,是最有效的交换;此指令占用 1 个字节的内存 -
下表展示了各种
XCHG指令的形式:汇编语言 操作 XCHG AL, CL交换 AL和CL的内容XCHG CX, BP交换 CX和BP的内容XCHG EDX, ESI交换 EDX和ESI的内容XCHG AL, DATA2交换 AL和数据段内存位置DATA2的内容XCHG RBX, RCX交换 RBX和RCX的内容(64 位模式) -
当使用内存寻址模式和汇编器时,哪个操作数寻址内存无关紧要,例如
XCHG AL, [DI]与XCHG [DI], AL相同 XCHG对于实现进程同步中的信号量(semaphore) 很有用
LAHF and SAHF⚓︎
-
LAHF指令将EFLAGS寄存器的低 8 位传输到AH寄存器-
AH := EFLAGS(SF:ZF:0:AF:0:PF:1:CF)- 包括加载符号标志(
SF) 、零标志(ZF) 、辅助进位标志(AF) 、奇偶标志(PF)和进位标志(CF) EFLAGS的保留位(第 1、3 和 5 位)被分别设置为 1、0 和 0
- 包括加载符号标志(
-
如果
CPUID.80000001H:ECX.LAHF-SAHF[bit 0] = 1,则LAHF指令在 64 位模式下可用
-
-
SAHF指令将AH寄存器对应位(分别对应位 7、6、4、2 和 0)的值传输给EFLAGS寄存器的SF、ZF、AF、PF标志- 忽略
AH寄存器的第 1、3 和 5 位,直接将EFLAGS寄存器中的这些位分别设置为 1、0 和 0 - 如果
CPUID.80000001H:ECX.LAHF-SAHF[bit 0] = 1,则SAHF指令在 64 位模式下可用
- 忽略
XLAT⚓︎
-
XLAT(表查找转换 (table look-up translation))指令使用隐式操作数(AL, BX)AL寄存器中的无符号整数作为偏移量到表([BX])中,并将该位置的表内容([BX + AL])复制到AL寄存器中XLAT的作用类似于MOV AL, [seg:BX + AL]seg:[BX]是表的基址;DS是默认段,它可能被段前缀重写
-
注意
[seg:BX + AL]不是一个合法的内存操作数,只有XLAT会接受它 -
XLAT常用于将一种格式的数据翻译成另一种格式;下面以“把菜单中食物的索引转换为食物的价格”为例:- 首先,为包含价格的表预留 256 字节
- 然后,使用该表的地址加载
DS:BX,并将食物的索引放入AL上 - 接着
XLAT把表中的索引转换为价格
-
XLAT写入AL而不改变EAX[31:8] XLAT指令的工作流程:- 首先将
AL的内容加到BX上,形成数据段内的内存地址 - 然后将该地址的内容复制到
AL中
- 首先将
例子
假设一个 7 段 LED 显示器查找表存储在地址 TABLE 的内存中。XLAT 指令使用查找表将 AL 中的 BCD 数字转换为 AL 中的 7 段编码。
TABLE DB 3FН, 06H, 5BH, 4FH ; lookup table (7-segment code)
DB 66H, 6DH, 7DH, 27H
DB 7FH, 6FH
LOOK: MOV AL, 5 ; load AL (BCD number) with 5 (a test number)
MOV BX, OFFSET TABLE ; address lookup table
XLAT ; convert
下图展示了上述示例程序在 TABLE = 1000H,DS = 1000H,以及 AL 的初始值为 05H(BCD)时的操作。转换后,AL = 6DH。
Input Ports and Output Ports⚓︎
- 外部设备如屏幕、显示器、键盘、鼠标、硬盘和网络通过输入端口(input ports) 和输出端口(output ports) 连接到数据总线
- 每个输入或输出端口都有一个唯一的地址,就像内存中的每个字节单元都有一个唯一的地址一样
-
输出端口
- 输出端口有一个比较器(comparator),该比较器比较固定地址与地址总线上的值
- 如果地址等于端口地址,且控制总线上有写信号,锁存器 (latch) 将存储数据总线上的值
-
输入端口
- 每个来自外部设备的输入都经过一个三态缓冲器(three-state buffer) 到达数据总线
- 当地址总线等于输入端口的固定地址,且控制总线上有读信号时,三态缓冲器被启用
IN and OUT⚓︎
IN和OUT指令执行 I/O 操作-
仅允许
AL、AX或EAX的内容在 I/O 设备和微处理器之间传输IN:将外部 I/O 设备的数据传输到AL、AX或EAX,例如IN AL, 19HOUT:将AL、AX或EAX的数据传输到外部 I/O 设备,例如,OUT 32H, AX
-
指令通常存储在 ROM 中
- 存储在 ROM 中的固定端口指令,其端口号由于只读存储器的特性而永久固定
-
而存储在 RAM 中的固定端口地址可以被修改,但这种修改不是良好的编程实践
- 在 I/O 操作期间,端口地址出现在地址总线(address bus) 上
- I/O 设备(端口)的寻址方式分为以下两类:
- 固定端口寻址(fixed-port addressing):使用 8 位 I/O 端口地址在
AL、AX或EAX之间进行数据传输,例如IN AL, 12H,OUT 25H, AX- 端口号是紧随指令操作码之后的字节立即值(
00h至FFh)
- 端口号是紧随指令操作码之后的字节立即值(
- 可变端口寻址(variable-port addressing):在
AL、AX或EAX与 16 位端口地址之间进行数据传输,例如IN AL, DX,OUT DX, AX- I/O 端口号存储在寄存器
DX(0000h至FFFFh)中,可以在程序执行过程中改变
- I/O 端口号存储在寄存器
- 固定端口寻址(fixed-port addressing):使用 8 位 I/O 端口地址在
OUT指令中使用的源寄存器决定了端口的大小(8 位、16 位或 32 位)-
下表展示了各种形式下的
IN和OUT指令:汇编语言 操作 IN AL, p88 位数据从 I/O 端口 p8输入到ALIN AX, p816 位数据从 I/O 端口 p8输入到AXIN EAX, p832 位数据从 I/O 端口 p8输入到EAXIN AL, DX8 位数据从 I/O 端口 DX输入到ALIN AX, DX16 位数据从 I/O 端口 DX输入到AXIN EAX, DX32 位数据从 I/O 端口 DX输入到EAXOUT p8, AL8 位数据从 AL输出到 I/O 端口p8OUT p8, AX16 位数据从 AX输出到 I/O 端口p8OUT p8, EAX32 位数据从 EAX输出到 I/O 端口p8OUT DX, AL8 位数据从 AL输出到 I/O 端口DXOUT DX, AX16 位数据从 AX输出到 I/O 端口DXOUT DX, EAX32 位数据从 EAX输出到 I/O 端口DX注:
p8= 8 位 I/O 端口号(0000H到00FFH) ,DX= 存储在寄存器DX中的 16 位 I/O 端口号(0000H到FFFFH) 。
例子
下面是一个点击电脑中扬声器的程序。扬声器(仅在 DOS 中)通过访问 I/O 端口 61H 进行控制。如果先设置该端口最低两位(11
MOVSX and MOVZX⚓︎
MOVZX(移动和零扩展(move and zero-extend))和 MOVSX(移动和符号扩展(move and sign-extend))指令存在于 80386 至 Pentium 4 指令集中。例如:
- 如果一个 8 位的
8FH进行零扩展到 16 位数字,它变为008FH - 如果一个 8 位的
8FH进行符号扩展到 16 位数字,它变为FF8FH
下图展示了这两条指令的操作细节:
下表展示了各种形式下的 MOVZX 和 MOVSX 指令:
| 汇编语言 | 操作 |
|---|---|
MOVSX CX, BL |
将 BL 进行符号扩展到 CX 中 |
MOVSX ECX, AX |
将 AX 进行符号扩展到 ECX 中 |
MOVSX BX, DATA1 |
将 DATA1 处的字节数据进行符号扩展到 BX 中 |
MOVSX EAX, [EDI] |
将由 EDI 地址的数据段内存位置处的字数据进行符号扩展到 EAX 中 |
MOVSX RAX, [RDI] |
将地址 RDI 处的双字数据进行符号扩展到 RAX 中(64 位模式) |
MOVZX DX, AL |
将 AL 进行零扩展到 DX 中 |
MOVZX EBP, DI |
将 DI 进行零扩展到 EBP 中 |
MOVZX DX, DATA2 |
将 DATA2 处的字节数据进行零扩展到 DX 中 |
MOVZX EAX, DATA3 |
将 DATA3 处的字数据进行零扩展到 EAX 中 |
MOVZX RBX, ECX |
将 ECX 进行零扩展到 RBX 中 |
BSWAP⚓︎
BSWAP(字节交换)指令反转 32 位或 64 位寄存器操作数的字节顺序BSWAP指令用于在大小端形式之间转换数据- 将任何 32 位寄存器的内容取出,将第一个字节与第四个字节交换,第二个字节与第三个字节交换
例子
对于 BSWAP EAX 指令,当 EAX = 12345678H 时,会交换 EAX 中的字节,结果为 EAX = 78563412H
不使用 BSWAP 的情况下,等价的字节交换指令序列如下:
- 在 64 位操作中,对于四字长,位 7:0 与位 63:56 交换,位 15:8 与位 55:48 交换,位 23:16 与位 47:40 交换,位 31:24 与位 39:32 交换
- 在 64 位模式下,指令的默认操作大小为 32 位;使用
REX前缀可以访问额外的寄存器(R8-R15)
例子
| 指令 | 操作码 | 描述 |
|---|---|---|
BSWAP reg32 |
0F C8 + rd |
反转一个 32 位寄存器中的字节顺序 |
BSWAP reg64 |
REX.W + 0F C8 + rd |
反转一个 64 位寄存器中的字节顺序 |
在“操作码”列中,+ rd 表示操作码字节低 3 位用于编码无 modR/M 字节的寄存器操作数,例如:
- 对 16 位寄存器应用
BSWAP指令的结果是未定义的 - 所以要交换 16 位寄存器的字节,请使用
XCHG指令 - 例如,要交换
AX寄存器的字节,请使用XCHG AL, AH指令
CMOV⚓︎
- 每条
CMOVcc指令在EFLAGS寄存器(CF、OF、PF、SF和ZF)的状态标志处于指定状态(或条件)时执行移动操作 -
每条指令都与一个条件码(condition code, cc) 相关联,以指示正在测试的条件
- 只有在条件为真时才移动数据
- 如果条件不满足,则不执行移动,并且继续执行
CMOVcc指令之后的指令
-
例如,
CMOVZ指令仅在先前指令的结果为零时移动数据 - 目标仅限于 16 位、32 位或 64 位寄存器,但源可以是 16 位、32 位或 64 位寄存器或内存位置
- 由于这是一条新指令,除非在程序中添加了
.686开关,否则汇编器无法使用它
下表罗列了各种条件移动指令:
| 汇编语言 | 测试的标志位 | 操作 |
|---|---|---|
CMOVB |
C = 1 |
如果低于则移动 |
CMOVAE |
C = 0 |
如果高于或等于则移动 |
CMOVBE |
Z = 1 or C = 1 |
如果低于或等于则移动 |
CMOVA |
Z = 0 and C = 0 |
如果高于则移动 |
CMOVE 或 CMOVZ |
Z = 1 |
如果相等则移动,或如果为零则移动 |
CMOVNE 或 CMOVNZ |
Z = 0 |
如果不相等则移动,或如果不为零则移动 |
CMOVL |
S != O |
如果小于则移动 |
CMOVLE |
Z = 1 or S != O |
如果小于或等于则移动 |
CMOVG |
Z = 0 and S = O |
如果大于则移动 |
CMOVGE |
S = O |
如果大于或等于则移动 |
CMOVS |
S = 1 |
如果有符号 ( 负数 ) 则移动 |
CMOVNS |
S = 0 |
如果无符号 ( 正数 ) 则移动 |
CMOVC |
C = 1 |
如果进位则移动 |
CMOVNC |
C = 0 |
如果无进位则移动 |
CMOVO |
O = 1 |
如果溢出则移动 |
CMOVNO |
O = 0 |
如果无溢出则移动 |
CMOVPE 或 CMOVPE |
P = 1 |
如果奇偶校验成立则移动,或如果奇偶校验为偶数则移动 |
CMOVNP 或 CMOVPO |
P = 0 |
如果无奇偶校验则移动,或如果奇偶校验为奇数则移动 |
-
CMOV的目的在于避免分支- 当 CPU 看到分支(比如
JNE)时,它将猜测分支是否会被执行,然后开始推测性地执行指令
- 如果猜测错误,会有性能损失,因为 CPU 必须丢弃任何先前通过推测执行的工作,然后开始获取和执行正确的路径
- 而对于条件移动(例如
CMOVE eax, edx) ,CPU 不需要猜测哪段代码将被执行,从而避免了预测错误的分支成本
- 当 CPU 看到分支(比如
-
此外,
CMOVcc指令将控制依赖转换为数据依赖,并将多个路径中的指令合并到基本块中,这使得基本块包含更多指令,并扩展了指令调度空间
Segment Override Prefix⚓︎
段重写前缀(segment override prefix) 可以添加到任何内存寻址模式中的任何指令
- 允许程序员偏离默认段
- 唯一不能加前缀的指令是将代码段寄存器用于(存储)生成地址的跳转和调用指令
- 在指令前面附加额外的字节,以选择备用段寄存器
下表列举了一些包含段的指令:
| 汇编语言 | 访问的段 | 默认段 |
|---|---|---|
MOV AX, DS: [BP] |
Data | Stack |
MOV AX, ES: [BP] |
Extra | Stack |
MOV AX, SS: [DI] |
Stack | Data |
MOV AX, CS: LIST |
Code | Data |
MOV ES: [SI], AX |
Extra | Data |
LODS ES: DATA1 |
Extra | Data |
MOV EAX, FS: DATA2 |
FS | Data |
MOV GS: [ECX], BL |
GS | Data |
Assembler Details⚓︎
汇编器(assembler) 可以通过两种方式使用:
- 使用特定汇编器独有的模型
- 使用全段定义,允许对汇编过程进行完全控制,并且对所有汇编器通用
本节将介绍这两种方法,并解释如何通过使用汇编器来组织程序的内存空间,以及一些与此汇编器一起使用的重要指令的目的和用法。
Directives vs Instructions⚓︎
汇编语言语句包括指示符和指令两类:
-
指示符(directive):告诉汇编器如何做
- 生成机器代码,分配存储等
- 仅在汇编时使用,自身不会产生任何代码
-
指令(instruction):告诉 CPU 做什么
- 编译成机器代码,最终链接到最终的可执行代码中
- 在运行时由 CPU 执行
Directives in MASM⚓︎
-
MASM 的指示符能够指示汇编器如何处理操作数或程序的一部分
- 一些结果存储在内存中,另一些则不存储
-
BYTE PTR指示由指针或索引寄存器引用的数据的大小 DB(定义字节)指令将数据字节存储在内存中- 数据分配:
DB,DW,DD,DQ,DT - 过程:
PROC,ENDP - 结构:
STRUCT,RECORD - 宏:
MACRO,ENDM - 代码标签:
ALIGN,ORG - 杂项:
EQU,INCLUDE - 段:
SEGMENT,ENDS,ASSUME - 处理器:
.386,.486,.586 - 简化段:
.CODE,.DATA,.STACK,.MODEL,.EXIT
更多指示符请参考:https://docs.microsoft.com/en-us/cpp/assembler/masm/directives-reference?view=msvc-160
Storing Data in a Memory Segment⚓︎
DB(定义字节 (define byte)) 、 DW(定义字 (define word))和DD(定义双字 (define doubleword))通常与 MASM 一起使用,来定义和存储内存数据- 如果一个数值协处理器 (numeric coprocessor) 在系统中执行软件
, DQ(定义四字)和DT(定义十字节)指示符也常见 - 这些指示符使用符号名称标记内存位置并指示其大小
-
DUP指示符允许对相同值进行多次初始化,例如:DATA1 DB 0, 0, 0, 0, 0等价于DATA2 DB 5 DUP(0) ; reserves 5 bytes of 0
-
使用问号(
?)作为DB、DW或DD指示符的操作数来为内存预留空间;汇编器会预留一个位置,不会将其初始化为任何特定值 -
ALIGN指令将下一个数据元素或指令对齐到参数的倍数地址;参数必须是小于或等于段对齐的 2 的幂- 值得注意的是,字大小的数据应放置在字边界上,而双字大小的数据应放置在双字边界上;否则微处理器将花费额外的时间来访问这些数据类型
例子
LIST_SEG SEGMENT DATA1 DB 1,2,3 ; define bytes DB 45H ; hexadecimal DATA3 DD 300H ; define doubleword DD 2.123 ; real DD 3.34E+12 ; real LISTA DB ? ; reserve 1 byte LISTB DB 10 DUP (?) ; reserve 10 bytes ALIGN 2 ; set word boundary LISIC DW 100H DUP (0) ; reserve 100H words LISTD DD 22 DUP (?) ; reserve 22 doublewords
思考
请确定指令执行后寄存器 AX 的值。
ASSUME, EQU and ORG⚓︎
-
EQU- 相等指示符(
EQU)将数字、ASCII 或标签等价于另一个标签,用于定义常量 - 语法:
CONSTANT_NAME EQU expression -
等价指令使程序更清晰并简化调试,例如:
- 相等指示符(
-
THIS:- 编译器只能将字节、字或双字地址分配给标签
- 要将字节标签分配给字,使用
THIS指示符 THIS指令始终以THIS BYTE、THIS WORD、THIS DWORD或THIS QWORD的形式出现
-
ORGORG(origin)语句可以改变数据段或代码段中数据的起始偏移地址- 有时,数据或代码的源必须使用
ORG语句分配给一个绝对偏移地址,例如引导扇区入口必须分配给07c00h
-
ASSUME告诉汇编器代码、数据、附加和栈段已选择的名字
例子
; Using the THIS and ORG directives
DATA_SEG SEGMENT
ORG 300H ; (1)
DATA1 EQU THIS BYTE ; (2)
DATA2 DW ?
DATA_SEG ENDS
CODE_SEG SEGMENT 'CODE' ; (3)
ASSUME CS: CODE_SEG, DS: DATA_SEG
MOV BL, DATA1
MOV AX, DATA2
MOV BH, DATA1+1
CODE_SEG ENDS
- 使用
ORG指示符设置位置 - 使用
THIS指示符将一个字地址标签赋给一个字节标签 - 使用
SEGMENT指示符定义一个程序段
PROC and ENDP⚓︎
PROC 和 ENDP 指示符表示一个必须赋予名称的过程 (procedure)(子程序 (subroutine))的开始和结束。
PROC指示符必须跟随着一个NEAR或FAR(仅在 32 位系统中有效)NEAR过程是指位于程序相同代码段中的过程,通常被认为是局部的FAR过程可能位于内存系统的任何位置,被认为是全局的
例子
下面有一个名为 SumOf 的过程,它通过传递寄存器参数来计算三个 32 位整数的和。
- 将三个整数赋给
EAX、EBX、ECX - 该过程用
EAX返回和
.data
theSum DWORD ?
.code
main PROC
; Before calling SumOf, values are assigned to EAX, EBX, and ECX
mov eax, 10000h ; argument
mov ebx, 20000h ; argument
mov ecx, 30000h ; argument
call Sumof ; EAX = (EAX + EBX + ECX)
; After the CALL, the sum in EAX are copied to “theSum” variable
mov theSum, eax ; save the sum
MACRO and ENDM⚓︎
MACRO和ENDM指示符表示一个宏(一个命名的汇编语言语句块)- 调用一个宏过程时,它的代码副本将直接插入到程序中被调用的位置
- 这种类型的自动代码插入也称为内联展开(inline expansion)
例子
一个名为 mPutchar 的宏接收一个输入,并通过调用 WriteChar 将其显示在控制台上。
mPutchar MACRO char
push eax
mov al, char ; passing arguments to the procedure
call WriteChar
pop eax
ENDM
- 语句
mPutchar 'A'调用mPutchar并传递字母A - 汇编器的预处理器将该语句扩展为以下代码:
宏也可以在数据段中使用,比如为 GDT 描述符定义一个宏。
INCLUDE⚓︎
INCLUDE 指示符用于在汇编过程中将给定文件名的源代码插入到当前源文件中。
语法:INCLUDE filename
Memory Organization⚓︎
- 内存组织(memory organization) 定义了软件的内存相关属性,包括代码大小、数据指针、指令编码、段组合类型和段加载顺序等
- 汇编器使用两种基本格式来定义内存组织:
- 一种方法使用全段定义(full-segment definitions):提供了对汇编语言任务的更好的控制,建议用于复杂程序
- 另一种方法使用模型(models)
- MASM 汇编器提供了许多内存模型可供选择,从小型到大型不等,这些模型控制着段寄存器的使用方式和指针的默认大小
- 使用
.MODEL指令来确定代码和数据指针的大小
Full-Segment Definitions⚓︎
全段定义使用 SEGMENT、ENDS 和 ASSUME 指令来定义段,并通知汇编器和链接器。
name SEGMENT [readonly] [align] [combine] [use] ['combine-class']
statements
name ENDS
cseg SEGMENT readonly word use32 'code'
mov ax, 10
inc ax
ret
cseg ENDS
Processor Directive VS Segment Attribute⚓︎
- 处理器指示符(例如,
.286、 .386)隐式定义默认操作模式 - 段属性(segment attributes)(
use16/use32)显式指定局部段内的操作模式,允许它重写默认模式 -
逻辑上必须匹配,例如:
.286-> 应与use16一起使用.386-> 可与use16或use32一起使用
-
它们共同工作以实现灵活的模式切换,例如混合 16 位和 32 位操作模式
Controlling Segments with the ASSUME Directive⚓︎
- 段指示符不说明段类型,而
assume指示符为汇编器提供此信息 -
形式如下:
-
有效的
assume指示符使用示例:assume DS:DSEGassume CS:CSEG, DS:DSEG, ES:DSEG, SS:SSEGassume CS:CSEG, DS:NOTHING
-
当汇编器遇到一条指令(例如
mov var, 0)时,它首先做的是确定var的段 - 如果
var没有在当前假设的任何一个段中声明,那么汇编器将生成一个错误,声称程序无法访问该变量 - 将
assume指示符放在程序中所有过程之前是理想的位置,因为程序中声明为段的段指针很少改变(除了过程入口和出口处)
END⚓︎
END指示符用于通知汇编器某个模块的结束- 通常,每个源模块都将有一个
END语句作为模块的最后一行;只有一个模块可以有一个这种形式的END语句 END指令还可以设置程序的入口点(entry point)- 语法:
END label- 比如:
END start,此时它指定标签start是程序的入口点,DOS 将在该地址开始执行程序
- 比如:
Models⚓︎
- 内存模型是 MASM 独有的
.MODEL指示符包括 MASM 实模式下六个内存模型,即tiny、small、compact、medium、large和huge- 对于保护模式,有一个模型
flat可用 - 使用如
@DATA、@STACK、@CODE的特殊指示符来识别各种段 .MODEL不用于 x64 的 MASM
选择可容纳数据和代码的最小内存模型,因为近引用比远引用操作更高效。
评论区





















































