微机原理笔记 - X86 汇编指令系统和语法

主要基于王克义的《微机原理》第二版 ,和其他8086 汇编教材应该没区别。


※ 注

  • <SRC_R_M_I>:尖括号表示指令操作数,SRC 表示操作数名称,后缀R 表示操作数是寄存器,M 为存储单元,I 为立即数。如果没有后缀,除非另行指出,否则默认都可以用作操作数;
  • [ARG]:标题和说明文本中的方括号表示可选的参数或属性 ,和书里一致;
  • {DX, AX}:表示将花括号内的DX 和AX 组合成一个32 位存储器使用。其中右侧AX 存储低16 位,左侧DX 存储高16 位;
  • 用C 语言运算符表示位运算操作,比如按位与 &、按位或 |、按位异或 ^、按位取反 ~,而原书中这些运算的符号分别是: ∧ , ∨ , ∀ \wedge, \vee, \forall ,,,取反没有单独的符号;
  • 用C 语言的**%** 运算符表示取余数运算;
  • 用# 前缀表示低电平有效的信号,如**#LOCK**;
  • 如果指令或语法附带了代码示例,最好默认只有示例中的写法是正确的;

汇编指令

寻址方式

  1. 立即寻址:MOV AX, FF00H
  2. 寄存器寻址:MOV AX, BX
  3. 直接寻址:MOV AX, DS:[0010H]
  4. 间接寻址:MOV AX, DS:0010H[BX][SI]

上面的间接寻址也可以表示为DS:[0010H + BX + SI],类似于访问二维数组,两个寄存器在基地址的基础上索引元素:

AX = ARR[BX][SI]

BX 为基址寄存器,SI 为变址寄存器。在8086 中,只有BX 和BP 能用作基址,SI 和DI 用作变址。32 位CPU 中没有限制,四个通用数据寄存器和基址、变址寄存器都能用作基址或变址。如果用BX 作为基址,默认的段前缀为DS,BP 作为基址时则默认为SS。

如果把直接和间接寻址合并归类为“指针”寻址,表示指针的操作数前可以添加类型属性,如:

MOV AX, WORD PTR DS:[0010H]

表示0010H 指向一个字。因为是小端序,低字节在低地址,所以如果存储的数据是FFAAH0010H 就指向AAH。其他类型属性还有BYTE 和DWORD,表示8 位和32 位。

在数据传送指令中,汇编程序的数据或代码标号默认被视为指针,所以如果有一个数组区域的标号是TAB,代表首地址0010H,那么:

MOV BX, TAB

就相当于:

MOV BX, [0010H]

也就是把标号指向的数据读出来了。想读取地址,则要加上OFFSET 伪指令,或者用LEA

MOV BX, OFFSET TAB
LEA BX, LAB

转移方式

  1. 短转移:JMP SHORT LAB
  2. 段内直接转移:JMP NEAR PTR LAB
  3. 段内间接转移:JMP WORD PTR [BX + SI]
  4. 段间直接转移:JMP FAR PTR LAB
  5. 段间间接转移:JMP DWORD PTR [BX + SI]

因为操作数LAB 是一个标号,也就是汇编器自动算出的偏移量立即数,直接用立即数指示转移地址,所以叫直接转移。间接转移的操作数是一个指针,指向存放有效地址的存储单元,取出其中的数据用于转移,也就是修改CS 和IP 寄存器的值。所以段内间接转移和以下指令等效:

MOV IP, WORD PTR [BX + SI]

实际程序中不能这样直接操作CS 和IP 寄存器。短转移的操作数实际是8 位位移量,所以只能移动-128 ~ +127 的范围;段内转移的操作数是16 位,所以间接转移要用WORD PTR 修饰指针,而段间转移要同时修改CS 和IP 两个16 位寄存器,所以指针前是DWORD PTR

通用数据传送指令

MOV <DST>, <SRC> 传送指令

DST 是目标,SRC 是源。MOV 指令的数据传送方向存在一些规则:

图片来自:blog.csdn.net/xiang_521/article/details/8884348

可见,

  1. 只有通用寄存器之间可以相互传送,存储器单元和段寄存器之间都不能;
  2. 立即数不能直接传入段寄存器,只能送往存储器或通用寄存器;

此外,CS 寄存器不能直接修改。当一个操作数为立即数,另一个是指针,一般要给指针添加类型:

MOV BYTE PTR DS:[0010H], 22H

以上为把一个立即数送往0010H,数据类型为字节,另可参见寻址方式如果使用MASM 5 编译器,用指针作为目标操作数时,段前缀DS: 不一定能省略

PUSH <SRC_R> 入栈指令

SRC_R 为16 位源寄存器。先将SP 减2,再把数据送入SP 指向的存储单元,SP 指向数据的低字节地址。

POP <DST_R>

DST_R 为CS 以外的16 位目标寄存器。将SP 指向的16 位数据送入目标寄存器,SP 加2。

XCHG <OPR1>, <OPR2> 交换指令

OPR1 和OPR2 是两个8 位或16 位的操作数,可以是寄存器或存储单元,至少一个是寄存器,所以XCHG 指令可以交换两个寄存器的指令,也可以交换一个寄存器和一个存储单元的值。用XCHG 可以把两个存储器单元值交换的代码优化到三步,就是先给寄存器装入单元A 的数据,让寄存器和另一个单元B 互换,然后再把寄存器值送入A:

MOV AL, [0010H]
XCHG AL, [0020H]
MOV [0010H], AL

累加器专用传送指令

这部分指令的操作都围绕累加器AL 或AX,合称AC。

XLAT 查表指令

先在BX 中存入一个数组的偏移地址,当AL 中是数组元素的索引值时,可用XLAT 指令按索引值读取一个字节元素到AL 中。示例如下:

MOV BX, OFFSET TABLE    ;读取TABLE 地址到BX
MOV AL, 4
XLAT    ;将TABLE[4] 读入AL

AL 中存储的是上一步运算的结果,如果这个结果对应的是索引值,用XLAT 就可以一步得到索引对应的数据,但是用处不大。

IN <AC>, <PORT> 输入指令

从外设端口(PORT)读入数据到AX 或AL 中,即可传送8 位或16 位数据。当PORT 地址小于256 时,可用直接寻址,用立即数指示端口地址:

IN AL, 80H

地址大于等于256 则只能用间接寻址。

OUT <PORT>, <AC> 输出指令

将AX 或AL 的数据送入外设端口PORT。

地址传送指令

LEA <DST_R>, <SRC_M> 装入有效地址指令

把SRC_M 指向的地址装入16 位目标寄存器,如

LEA BX, DS:[BX + DI + 6]

把指针[BX + DI + 6] 指向的地址装入BX。要注意LEA 和OFFSET 伪指令的区别,OFFSET 是编译期计算的伪指令,对于:

MOV BX, OFFSET TAB

OFFSET TAB 实际是一个在程序中写死的立即数,相应的,TAB 也必须是一个编译期常量,从而可以在编译期完成运算;而LEA 等指令传送的是动态的指针指向的地址,以上面的例子来说,如果DI 的值不同,那么相同的代码往BX 里装入的地址也是不同的。其实就相当于sizeof()strlen() 的区别。

LDS <DST_R>, <SRC_M> 加载数据段指针指令

将SRC_M 指向的32 位指针的值拆分传入16 位寄存器DST_R 和DS。如果在[10H] 处有32 位数据FFFFAAAAH,则:

LDS SI, DS:[10H]

把低16 位AAAAH 传入SI,高16 位FFFFH 传入DS。一般的段内指针是16 位,32 位指针即FAR 类型的指针,其高16 位含有目标段的地址。LDS 用SRC_M 找到32 位指针,再读取32 位指针的值,所以可以说SRC_M 是指向指针的指针,即二极指针

LES <DST_R>, <SRC_M> 加载附加段指令

与LDS 相同,只是段地址装入ES 中。

标志传送指令

LAHF 标志存储指令

将标志寄存器的低8 位送入AH 寄存器。

SAHF 标志赋值指令

将AH 的值送入标志寄存器低8 位。

PUSHF 标志入栈指令

标志寄存器的值入栈,SP 操作和PUSH 相同。

POPF 标志出栈指令

将栈顶出栈并送入标志寄存器,SP 操作和POP 相同。

算数加法指令

★ 算数类指令的特性

  • 以下各类算数运算指令中,双操作数的指令有和MOV 相同的方向规则,比如目的操作数不能是CS
  • 除了CBW 和CWD,运算指令的结果都会影响标志寄存器
  • 除了特别注明的,其他运算指令都能用于8 位或16 位操作数

ADD <DST>, <SRC> 加法指令

DST = DST + SRC。

ADC <DST> <SRC> 带进位加法指令

DST = DST + SRC + CF,也就是把前一次运算的进位加到这次的结果中,能用在大数加法里。

INC <DST> 自增指令

DST = DST + 1。不影响CF 进位/借位标志。

DAA 组合BCD 码加法调整指令

用在单字节组合BCD 码加法后,将AL 中的结果调整成正确的BCD 形式,如:

MOV AL, 19H
ADD AL, 28H    ;BCD 加法,19H 应该看作十进制的19,28H 就是28,所以结果应该是47H。
DAA            ;在调整后才能保证结果是正确的BCD 形式,否则直接计算结果是41H。

AAA 非组合BCD 或ASCII 码加法调整指令

略。😝

算数减法指令

参考上面加法指令下的高亮说明:[算数类指令的特性](#★ 算数类指令的特性)。

SUB <DST>, <SRC> 减法指令

DST = DST - SRC。

SBB <DST>, <SRC> 带借位减法指令

DST = DST - SRC - CF,参考带借位加法的说明。

DEC <DST> 自减指令

DST = DST - 1。不影响CF 进位/借位标志。

CMP <DST>, <SRC> 比较指令

执行DST - SRC 运算,但不保存结果,只修改标志位。

NEG <DST> 取相反数指令

DST = -DST,DST 的值视为补码,结果也是补码形式。

DAS 组合BCD 码减法调整指令

和DAA 相似,用在组合BCD 码减法后,将AL 中的差调整成正确形式。

AAS 非组合BCD 或ASCII 码减法调整指令

略。

算数乘除法指令

MUL <SRC_R_M> 无符号乘法指令

SRC_R_M 为寄存器或存储单元。

  • 若SRC_R_M 为8 位,则令AX = AL × SRC_R_M。AL 中预先存放另一个8 位乘数,乘积为16 位;
  • 若SRC_R_M 为16 位,则令**{DX, AX}** = AX × SRC_R_M。AX 中存放另一个16 位乘数,将DX 和AX 合并为32 位存储乘积,高16位在DX;

IMUL <SRC_R_M> 带符号乘法指令

与MUL 相同,乘数和乘积均视为带符号数,满足一般代数运算规则,如负负得正。

DIV <SRC_R_M> 无符号除法指令

SRC_R_M 为寄存器或存储单元。

  • 若SRC_R_M 为8 位,则令AL = AX / SRC_R_M;AH = AX % SRC_R_M。AX 中预先存放16 位被除数,商和余数都是8 位,分别放在AL 和AH;
  • 若SRC_R_M 为16 位,则令AX = {DX, AX} / SRC_R_M;DX = {DX, AX} % SRC_R_M。{DX, AX} 中存放32 位被除数,商和余数都是16 位,分别放在AX 和DX;

IDIV <SRC_R_M> 带符号除法指令

与DIV 相同,参与运算的数据均被视为带符号数,满足一般代数运算规则,余数的符号和被除数相同。

AAM 非组合BCD 码的乘法调整指令

略。

AAD 非组合BCD 码的除法调整指令

略。

符号扩展指令

用于处理带符号数运算时的类型匹配问题,比如,需要把一个8 位带符号数变成16 位用在乘除法中,负数不能直接在高8 位补零。

CBW 字节扩展到字指令

将AL 中的8 位数高位拼上AH 扩展成16 位,AH 中的所有位都设置成AL 的符号位。如:

MOV AL, 35H
CBW           ;AX 变成0035H
MOV AL, 82H   ;82H 的最高位是1,当作带符号数时是负数
CBW           ;AX 变成FF82H

CWD 字扩展到双字指令

将AX 中的16 位数高位和DX 拼接扩展成32 位,DX 中的所有位都设置成AX 中的符号位。如:

MOV AX, 3500H
CWD              ;{DX, AX} 变为0000 3500H
MOV AX, 8200H
CWD              ;{DX, AX} 变为FFFF 8200H

二进制位运算指令

  • 除了单操作数的NOT 指令,这类指令的操作数方向规则和MOV 相同;
  • 操作数都可以是8 位或16 位;

NOT <DST_R_M> 按位取反指令

DST_R_M = ~DST_R_M,DST_R_M 不能是立即数。

AND <DST>, <SRC> 按位与指令

DST = DST & SRC。

OR <DST>, <SRC> 按位或指令

DST = DST | SRC。

XOR <DST>, <SRC> 按位异或指令

DST = DST ^ SRC。

TEST <DST>, <SRC> 按位测试指令

执行DST & SRC 运算,不存储结果,只改变标志位。可用于测试特定位的值,比如:

MOV AL, F1H
MOV AH, F0H
TEST AL, 01H    ;结果为01H,表示最低为是1,ZF = 0
TEST AH, 01H    ;结果为00H,表示最低为是0,ZF = 1

二进制移位指令

★移位指令的特性

  • 移位指令都有两个参数,第一参数是移位目标DST_R_M,8位或16位,第二参数是移位次数CNT,可以是立即数1,或CL 寄存器
  • 无论方向,移位过程中溢出的位都送进CF 标志位

SHL <DST_R_M>, <CNT> 无符号左移指令

等效于``DST_R_M << CNT`。高位移入CF,低位补零。

SAL <DST_R_M>, <CNT> 带符号左移指令

和SHL 实际是同一条指令,低位补零,符号位会变化。

SHR <DST_R_M>, <CNT> 无符号右移指令

等效于``DST_R_M >> CNT`。低位移入CF,高位补零。如,80H 无符号右移一次得40H(0b01000000)。

SAR <DST_R_M>, <CNT> 带符号右移指令

等效于``DST_R_M >>> CNT`。低位移入CF,高位补符号位:正数补0,负数补1。如,80H 带符号右移一次得C0H(0b11000000)。

ROL <DST_R_M>, <CNT> 循环左移指令

高位溢出位的返回最低位,同时也送到CF。如,80H 循环左移一位得01H,CF = 1。

ROR <DST_R_M>, <CNT> 循环右移指令

低位溢出的位返回最高位,同时也送到CF。如,01H 循环右移一位得80H,CF = 1。

RCL <DST_R_M>, <CNT> 带进位循环左移指令

相当于把CF 拼接到DST,扩展成9 位或17位循环移位,CF 的值移入最低位,最高位的值再移入CF。如,CF = 1,01H 带进位循环左移一次,得03H。和带进位加法指令ADC 类似,RCL 也可用于大数左移,如:

;若{DX, AX} = 8101 8000H
SHL AX, 1    ;AX 最高位移入CF
RCL DX, 1    ;把CF 移入DX 得最低位,结果是0203 0000H

RCR <DST_R_M>, <CNT> 带进位循环右移指令

和RCL 相似,CF 先移入最高位,最低位再移入CF。

串操作指令

MOVSB / MOVSW 串传送指令

MOVSB指令(Move String Byte)用于将一个字节从源地址复制到目标地址。

  • 源地址为DX: SI中,目标地址为ES: DI中。
  • 执行指令后,SI和DI的值会根据DF标志位的值自动增加或减少1。
  • 如果DF为0,则SI和DI增加1;如果DF为1,则SI和DI减少1。

MOVSW 与之类似,每次传送一个字,所以SI 和DI 分别增加或减少2。下面是一个简单的示例代码,使用MOVSB指令将字符串从源地址复制到目标地址:

; 设置源地址和目标地址
LEA SI, [src_string]
LEA DI, [dst_string]
MOV AX, DS
MOV ES, AX

; 设置方向标志位为0,使得SI和DI自动增加
CLD

; 循环复制字符串中的每个字节
CP_LOOP:
    MOVSB                 ; 复制一个字节
    CMP BYTE PTR [SI], 0  ; 检查是否到达字符串末尾
    JNE CP_LOOP              ; 如果没有到达末尾,则继续循环

src_string db 'Hello, World!', 0
dst_string times 14 db 0

REP 无条件重复前缀

重复执行其后的串操作指令,每次让CX - 1,直到CX == 0。如:

MOV CX, 5
REP MOVSB

就是重复执行五次,传送五个字节。每次执行都对CX 先减一,再判断是否为0,所以CX 的初始值至少为1

REPE / REPZ 相等或为0 重复前缀

两个前缀含义相同,仅当ZF == 1 且CX != 0 时重复执行串操作,也就说存在两个结束条件,如果ZF 在中间不等于1,也就是CMP 出来不相等,那么循环会在CX 计数结束前终止,循环过程中CX 自动增减不会影响标志位,所以之后可以用JZ / JEJNZ / JNE 判断ZF 的值,分别跳转到处理两种情况的分支,参考后面CMPSB 串比较指令的例程。

REPNE / REPNZ 不相等或非0 重复前缀

与上一种前缀类似,在CMP 结果相等时中途终止循环。

CMPSB / CMPSW 串比较指令

CMPSB指令(Compare String Byte)用于比较源地址和目标地址处的一个字节。

  • 源地址为DS: SI,目标地址为ES: DI。
  • 执行指令后,SI和DI的值会根据DF标志位的值自动增加或减少1。
  • 如果DF为0,则SI和DI增加1;如果DF为1,则SI和DI减少1。

CMPSW与之类似,每次比较一个字,所以SI和DI分别增加或减少2。

下面是一个简单的示例代码,它使用CMPSB指令来比较两个字符串是否相等,用REPZ 做循环操作:

; 设置源地址和目标地址
MOV SI, OFFSET SRC_STRING
MOV DI, OFFSET DST_STRING
MOV AX, DS
MOV ES, AX

; 设置方向标志位为0,使得SI和DI自动增加
CLD

; 计算字符串长度并将其存储在BX寄存器中
MOV BX, 0
STRLEN_LOOP:
    INC BX
    CMP BYTE PTR [SI + BX], 0
    JNE STRLEN_LOOP

MOV CX, BX
; 使用REPZ指令重复执行CMPSB指令,比较整个字符串
REPZ CMPSB

; 检查ZF标志位以确定字符串是否相等
JNZ NOT_EQUAL

; 字符串相等
JMP EQUAL

NOT_EQUAL:
; 字符串不相等

EQUAL:
; 字符串相等

SRC_STRING DB 'Hello, World!', 0
DST_STRING DB 'Hello, World!', 0

SCASB / SCASW 串扫描指令

与上面的串操作指令类似,但只通过ES: DI 索引一个目标串,用AL 或AX 中预先存入的值与串中的元素CMP,修改标志位。可用于在串中查找给定的字符的位置,比如:

CLD             ; 清除方向标志位,使得字符串操作指令自增
MOV CX, 100     ; 设置计数器CX为100
MOV DI, OFFSET DST_STR ; 将目标字符串的偏移地址存入DI寄存器

MOV AL, '$'     ; 将字符'$'存入AL寄存器
REPNZ SCASB     ; 重复执行SCASB指令,直到找到与AL相等的字符或者计数器CX减为0

JZ FOUND_LAB    ; 如果找到了与AL相等的字符,则跳转到FOUND_LAB标签处

JMP NOT_FOUND_LAB; 如果没有找到与AL相等的字符,则跳转到NOT_FOUND_LAB标签处

FOUND_LAB:      ; 找到了与AL相等的字符时执行的代码

NOT_FOUND_LAB:  ; 没有找到与AL相等的字符时执行的代码

LODSB / LODSW 取串指令

与SCASB 类似,只通过DS: SI 索引一个目标串,每次向AL 或AX 中转入一个字节或一个字。一般不加重复前缀,因为累加器中只能保留最后一次取到的值。取到的值不影响标志位,所以不能和REPZ 或REPNZ 配合直接查找,只能用在循环中,单独判断取到的值。

CLD             ; 清除方向标志位,使得字符串操作指令自增
MOV DI, OFFSET DST_STR ; 将目标字符串的偏移地址存入DI寄存器
MOV BL, -1      ; 将-1存入BL 作为默认值

LOD_LOOP:       ; 循环开始
    LODSB       ; 从DS:SI指向的内存单元中取一个字节到AL寄存器,并根据DF标志位更新SI
    CMP AL, 0   ; 比较AL与0
    JZ END_LOOP ; 如果AL等于0,表示字符串读到了结尾,没有找到#,直接跳转到END_LOOP标签处
    CMP AL, '#' ; 比较AL与'#'
    JNZ LOD_LOOP; 如果AL不等于'#',则继续向下遍历

LODSB           ; 如果循环中止,说明在串中找到#,则再读一次得到# 后的第一个字节的值
MOV BL, AL      ; 将# 后的第一个字节存入BL
END_LOOP:       ; 循环结束,如果没有找到#,那么BL 会保持默认值-1

STOSB / STOSW 存串指令

与LODSB 相反,用ES: DI 索引一个字符串,每次将AL 或AX 中的一个字节或一个字存入字符串。如果使用无条件重复前缀,则可以用STOSB 把字符串全部设为相同值,也可以和IN 指令配合,把每次从外部端口读入的数据存入串中。

CLD
MOV DI, OFFSET DST_STR
MOV AL, FFH
MOV CX, 100
REP STOSB    ;重复100 次,用FFH 覆盖串中的数据

无条件转移指令

JMP <DST> 无条件转移指令

参考转移方式,共有5 种用法,分别是短转移、段内直接转移、段内间接转移、段间直接转移、段间间接转移。其中,直接转移时DST 都是标号,即偏移量立即数,间接转移则用寄存器或存储单元种的16 位或32 值作为转移目的地,更新CS 和IP 寄存器。

CALL <DST> 过程调用指令

先将CALL 指令后的指令地址压栈,作为返回地址,然后转移到DST。转移方式和JMP 类似,只是没有短转移。

RET [OPR-I] 调用返回指令

将栈中的返回地址弹出,修改CS 和IP 的值。RET 有两种机器码形式,分别对应段内返回和段间返回,段内返回只需要弹出IP 的值,而段间返回需要加上CS,弹回两个字。定义过程的PROC 伪指令可以加上NEAR 或FAR 属性分别说明这个过程是段内还是段间,同时决定RET 的行为。OPR-I 是可选的立即数参数,在弹出返回地址后,令SP = SP + OPR-I,也就是继续弹出OPR-I 个字节,由于栈操作以字为单位,所以OPR-I 只能是偶数。

SUB_ROUTINE_1 PROC FAR    ;定义为段间过程
    ;........
    RET     ;执行段间返回
SUB_ROUTINE_1 ENDP

CALL FAR PTR SUB_ROUTINE_1    ;也要用段间调用与之配合,否则将使错误的值被装入CS

条件转移指令

条件转移的特性

  • 条件转移指令都是根据特定的标志位决定是否执行转移,且都是短转移,只接受标号参数,转移范围是-128 ~ +127
  • 指令的后缀表示关联的标志位和判断条件,如:JZ 表示测试ZF 标志,为1 转移,JNZ 则是为0 转移

JZ / JE <LABEL> 相等 / 为0 转移指令

JNZ / JNE <LABEL> 不相等 / 不为0 转移指令

JC / JNC <LABEL> 有进位 / 无进位转移指令

JO / JNO <LABEL> 溢出 / 无溢出转移指令

JS / JNS <LABEL> 正数 / 负数转移指令

JS 判断结果最高位是否1,JNS 则判断是否为0。

JP / JNP <LABEL> 奇偶位为1 / 0 转移指令

JB / JNAE <LABEL> 无符号数“低于” 转移指令

B 是Below,AE 是Above or Equal,下同。

JNB / JAE <LABEL> 无符号数“高于等于” 转移指令

JA / JNBE <LABEL> 无符号数“高于” 转移指令

JNA / JBE <LABEL> 无符号数“低于等于” 转移指令

JL / JNGE <LABEL> 有符号数“小于” 转移指令

L 是Less,GE 是Greater or Equal,下同。

JNL / JGE <LABEL> 有符号数“大于等于” 转移指令

JG / JNLE <LABEL> 有符号数“大于” 转移指令

JNG / JLE <LABEL> 有符号数“小于等于” 转移指令

循环控制指令

循环指令和重复前缀类似,都用CX 作为循环计数器,又可以根据标志位在CX 减到0 之前终止循环。差别是重复前缀只能和串操作指令配合,而循环控制指令是单独的转移指令,用来时循环结构更清晰,便于代码编写和阅读。此外,循环控制指令和条件转移指令一样,只接受一个标号操作数,执行短转移,范围是-128 ~ +127。

循环指令每次执行都对CX 先减一,再判断是否为0,所以CX 的初始值至少为1

LOOP <LABEL> 无条件循环指令

与REP 类似,只要CX 没有减到0,就跳转到LABEL 处。

;用循环指令实现串操作REP STOSB 的效果,用FFH 覆盖5 个字节长度的存储单元

MOV CX, 5    ; 设置循环计数器为5,执行5次循环
MOV DI, 0    ; 初始化目标索引寄存器DI为0
LOOP_LAB:    ; 循环开始处的标签
    MOV BYTE PTR DS:[DI], FFH ; 将内存地址为DI处的字节设置为FFH
    INC DI     ; 增加目标索引寄存器DI的值
    LOOP LOOP_LAB ; 跳转到循环开始处并递减计数器

LOOPE / LOOPZ <LABEL> 相等 / 为0 循环指令

ZF == 1 且CX != 0 时循环,也就是当遇到不相等或不为0 时,ZF == 0, 循环终止。可用于比较两串数据是否相等。

LOOPNE / LOOPNZ <LABEL> 不相等 / 不为0循环指令

ZF == 0 且CX != 0 时循环。可用于比较串中的数据。

JCXZ <LABEL> CX 为0 跳转指令

不操作CX 的值,可用于从无限循环中跳出。

MOV CX, 5 ; 设置计数器初始值为5,循环5 次跳出
MY_LOOP:
    DEC CX ; 递减计数器
    JCXZ EXIT_LOOP ; 如果计数器为0,则跳出循环
    ; 循环体中的代码
    JMP MY_LOOP ; 跳转到循环开始处

EXIT_LOOP:

中断调用指令

INT <NUM_I> 中断调用指令

NUM_I 是8 位立即数,表示中断类型码。INT 指令根据中断类型码在中断向量表中找到对应的中断服务程序并转移,与段间过程调用类似,需要把CS 和IP 作为返回地址压栈,还会另外自动把标志寄存器FR 压栈保存。

INTO 溢出中断指令

用来测试溢出标志OF,若OF == 1,则调用溢出中断。一般跟在带符号数的算术运算指令后,处理溢出。

IRET 中断返回指令

放在中断服务函数末尾,从栈中弹出返回地址和标志寄存器的值,和段间返回类似。

标志操作指令

STC / CLC 置位 / 清零进位标志CF 指令

ST 表示SET,CL 表示CLEAR,C 是CF,下面的缩写规则相同。

CMC 进位标志CF 取反指令

STD / CLD 置位 / 清零方向标志DF 指令

STI / CLI 置位 / 清零中断标志IF 指令

总线操作指令

LOCK 总线封锁前缀

执行LOCK 前缀后的指令时,CPU 将发送**#LOCK** 信号封锁总线。

HLT 暂停指令

让CPU 进入暂停状态。可用RESET 或不可屏蔽中断NMI 唤醒CPU,启用中断时,也可用INTR 线上的可屏蔽中断唤醒。

WAIT 等待指令

使CPU 进入等待状态,每隔5 个时钟周期测试一次**#TEST** 引脚,直到出现有效信号未知。

ESC 交权指令

用于向协处理器发出请求,协处理器接到ESC 信号后就按需求开始处理,CPU 可以用WAIT 指令进入等待模式,处理完成后协处理器发出#TEST 信号,让CPU 结束等待。

其他指令

NOP 空指令

CPU 不做任何事,等待3 个时钟周期。


汇编语法

程序基本结构

ASSUME DS:DATA,SS:STACK,CS:CODE ; 假设数据段为 DATA,堆栈段为 STACK,代码段为 CODE

DATA SEGMENT ; 定义数据段
    MSG_TEXT DB 'HELLO WORLD', '$' ; 定义字符串变量 MSG_TEXT,其值为 "HELLO WORLD"
    EXTRA_SIZE EQU 64 ; 定义常量 EXTRA_SIZE,其值为 64
    DB EXTRA_SIZE DUP(0) ; 分配 EXTRA_SIZE 个字节的空间,并将其初始化为 0
    MIX_DATA DB 10 DUP(0) 10 DUP(0FFH)  ; 前10 个字节初始化为0,后10 个为FFH

DATA ENDS ; 数据段结束

STACK SEGMENT STACK ; 定义堆栈段
    STACK_SPACE DW 64 DUP(0) ; 分配 64 字的空间作为堆栈,并将其初始化为 0
    STACK_LEN EQU LENGTH STACK_SPACE ; 定义常量 STACK_LEN,其值等于堆栈空间的长度
    STACK_SIZE EQU SIZE STACK_SAPCE  ; 定义常量 STACK_SIZE,其值等于堆栈占用的字节数

STACK ENDS ; 堆栈段结束
    
CODE SEGMENT ; 定义代码段
    START: 
    MOV AX,DATA ; 将数据段地址加载到寄存器 AX 中
    MOV DS,AX   ; 将寄存器 AX 的值传送到数据段寄存器 DS 中
    MOV DX, OFFSET MSG_TEXT; 将字符串变量 MSG_TEXT 的地址加载到寄存器 DX 中
    MOV AH,09H; 设置 AH 寄存器的值为 09H(DOS 中断功能号:显示字符串)
    MOV AL,01H; 设置 AL 寄存器的值为 01H(显示方式:光标跟随)
    INT 21H; 调用 DOS 中断,执行显示字符串操作

    MOV AH,4CH; 设置 AH 寄存器的值为 4CH(DOS 中断功能号:结束程序)
    MOV AL,0; 设置 AL 寄存器的值为 0(返回码)
    INT 21H; 调用 DOS 中断,执行结束程序操作
    
CODE ENDS; 代码段结束
END START; 程序从 START 标签处开始执行

这段程序调用了DOS 中断的字符串显示功能,把MSG_TEXT 的内容“HELLO WORLD ” 显示在屏幕上,字符串的结尾标志是$ ,这是DOS 中断的要求,和C 语言不同。

  • 开头的段名假设只对汇编器有效,辅助汇编器判断各个段的位置;
  • CS 寄存器自动根据START 标签所在段设置;
  • STACK 段加了STACK 后缀,SS 寄存器也会自动设置;
  • DS 等其他寄存器必须在代码中手动设置,因为立即数不能直接给段寄存器赋值,所以要先把DATA 赋值给AX;
  • DOS 中断中包含了给所有程序提供的系统操作工具,所以使用了中断调用的形式;

除了显而易见的分段结构,汇编程序中还包含三类语句:

  1. 指令语句:对应具体机器码的指令;
  2. 伪指令语句:也称为指示性指令,如EQU、DB 等,用来指导汇编器生成程序,或实现编译期的运算和操作,不体现为机器码;
  3. 宏指令:与C 语言的宏类似,也可以用来实现代码自动复制粘贴等效果;

字面量的表示

  • 数字默认使用十进制,可用伪指令RADIX <N> 修改默认基数;

  • 用B、D、H、Q 后缀分别表示二进制、十进制、十六进制、八进制;

  • 数字不能以字母开头,所以FFH 要写成0FFH。注意C 语言不能随便加一个没用的0,因为前缀0 用来表示八进制;

  • 在有协处理器的情况下,可用±3.14E±8 的形式定义实数;

  • 用单引号标记一个ASCII 字符串,相邻的字符串间的逗号用来连接。和DB 配合,可用字符串定义并初始化一块内存区域;

表达式

用伪指令运算符能够实现编译期运算的常量表达式,分为数值表达式和地址表达式两种。表达式中运算符的操作数只能是编译期常量,或者说是字面量,比如硬编码的数字、代码标号、定义内存区域的变量名。

运算符

算数运算符

功能
+
-
*
/
MOD取余

位运算符

功能用法
SHL左移0110B SHL 1
SHR右移0110B SHR 1
NOTNOT 22H
AND11H AND 22H
OR11H OR 22H
XOR异或11H XOR 22H
HIGH分离16 位的高字节
LOW分离低字节

要注意,这些位运算符是伪指令,如AND 运算符,两个操作数一左一右,和同名的AND 指令不用。不过NOT 运算符和NOT 指令的用法是相同的,汇编器可以根据代码上下文区分开,比如:

MOV AX, NOT 22H

指令不会出现在操作数的位置上,所以这里的NOT 是运算符。另外要注意区分位运算和逻辑运算,位取反是~ ,逻辑非是!,~0110B = 1001B,!0110B = 0000B。

关系运算符

功能用法
EQ相等2 EQ 10B,下同
NE不等
LT小于
LE小于等于
GT大于
GE大于等于

True == 0FFH,False == 0。

分析运算符

功能用法
SEG返回标号所在段的基址SEG START
OFFSET返回标号的偏移地址OFFSET STR
LENGTH返回变量中的元素数量LENGTH STR
TYPE返回变量或标号的类型属性TYPE SUB_PROC
SIZE返回变量占用的字节数SIZE STR
$返回当前指令在段内的偏移量JMP $

LENGTH 根据定义变量时的类型决定元素的类型,如:

MSG_TEXT DB 'HELLO','$'
DATA DB 64 DUP(0) 10 DUP(0FFH)
DATA2 DW 64 DUP(0)

LENGTH MSG_TEXT    ; 返回MSG_TEXT 的字节数
LENGTH DATA    ; 返回64,即第一个DUP 重复的次数
LENGTH DATA2   ; 返回64,即定义的字的数量,而不是字节数

SIZE 与LENGTH 的区别就是不论类型,只返回字节数。

TYPE 对返回变量类型的尺寸,BYTE, WORD, DWORD, QWORD, TBYTE 分别返回1, 2, 4, 8, 10。所以SIZE 的值就等于 LENGTH × TYPE。而对于代码标号,段内的NEAR 标号返回-1,段间FAR 标号返回-2。

类型属性运算符

功能
PTR指定指针类型属性

PTR 的作用参考寻址方式

符号定义伪指令

EQU 符号定义

与C 语言的#define 宏类似,用EQU 可以给表达式和其他符号定义一个别名。

A EQU 8       ; 数字常量
B EQU A + 2   ; 表达式
C EQU AX      ; 寄存器别名
D EQU ADD     ; 指令别名

E EQU <WORD PTR>  ; 和#define 类似,在尖括号内定义一个任意的字符串
MOV AX, E [0010H] ; E 会被展开成字符串,即:
MOV AX, WORD PTR [0010H]

EQU 定义的符号发挥作用的方式也和宏类似,就是把内容粘贴过去。另外,EQU 定义的符号不允许重定义。

= 符号定义

等于号和EQU 类似,但是用等于号定义的符号可以重定义。

A = 9
B = A + 3

PURGE 取消符号

和#undef 一样,允许用EQU 重新定义符号。

THIS 定义别名

THIS 用于给存储区域设置别名和类型,如:

A EQU THIS BYTE    ;定义一个BYTE 类型的变量标号A
B DB 'HELLO'

MOV AL, [A]    ;给AL 赋值‘H’

标号A 会关联到区域‘HELLO ’ 的第一个字节,所以用A 能读到字符H。

THIS 还可以指定代码标号的类型属性:

HERE EQU THIS FAR    ;定义一个FAR 类型的标号HERE
    INT 43H
    
JMP FAR PTR HERE    ;将通过FAR 转移到INT 43H 处

LABEL 定义标签

和THIS 相似,区别只是不需要和EQU 或= 配合使用。

A LABEL BYTE
DB 'HELLO'

HERE LABEL FAR
	INT 43H

数据定义伪指令

DB / DW / DD / DQ / DT 数据定义

分别用于定义字节、字、双字、四字节、十字节类型的数组区域,变量名是可选的。

STR   DB 'ABCDE'
BYTES DB 30H, 31H, 32H, 33H, 34H  ;定义数组和字符串本质是相同的

DW 0, 0, 0, 0, 0, 0
DB ?, ?, ?    ;? 用作占位符,表示没有初始化值

DUP 复制操作符

定义数据时按次数重复括号内的模式:

DB 3 DUP(1)
DB 1, 1, 1    ;复制操作符展开后就是重复3 次(1)

DB 3 DUP(1, 2)
DB 1, 2, 1, 2, 1, 2  ;重复3 次(1, 2)

DB 3 DUP(?)  ;也可以使用?

DUP 指令也可以嵌套使用:

DB 2 DUP(1, 2 DUP(2))
;重复两次(1, 2 DUP(2))
;也就是重复两次(1, 2, 2)
DB 1, 2, 2, 1, 2, 2    

DB 2 DUP(2 DUP(1, 2), 3)
;重复两次(2 DUP(1, 2), 3),也就是
;2 DUP(1, 2), 3, 2 DUP(1, 2), 3,最后等于
DB 1, 2, 1, 2, 3, 1, 2, 1, 2, 3

STRUC / ENDS 结构体

与C 的结构体类似,先用结构体把其他类型组合起来,形成一个新的类型,然后在内存中定义对应结构体类型的变量。虽说,熟悉C 语言的人看见这个缩写估计会有点难受,一字之差,就像看到creat 一样。

结构体定义
NewType STRUC
    num DW 0             ;定义了四个类型不同的成员变量,前三个定义了初始值,第四个noinit 不初始化
    str DB 'HELLO', 0
    dnum DD 0FFH
    noinit DQ 10 DUP(?)
NewType ENDS

结构体中的成员也叫字段,有名称的字段可以用名称引用,没有的则只能用指针直接访问存储空间,和C 的结构体类似。要注意字段名称是暴露在全局作用域中的,即,如果一个结构体类型里存在字段名称num,那么其他地方都不能再定义一个num 符号,字段名称并没有被包裹在结构体内部。比如:

NewType2 STRUC
    num DB 9    ; num 符号已经被上面的NewType 使用了,所以再用一次就会冲突
NewType2 ENDS

num DB 10   ; 也不允许作为其他用途

这和C 里的枚举enum 是类似的,成员字段会污染全局作用域,必须加上冗长的前缀以避免重名。

结构体变量

结构体变量的定义和初始化也和C 类似:

VAR1 NewType <10, , 0AAH>    ; 覆盖了第一个成员num 和第三个成员dnum 的初始值,中间的str 跳过,noinit 省略
VAR2 NewType <>    ; 全部用默认值,尖括号内留空,不能把尖括号也省略

MOV AX, VAR1.num    ; 用名称引用字段

LEA BX, VAR1
MOV AX, WORD PTR [BX + 0]    ; 也可以用指针直接访问

UNION / ENDS 共用体

同样,和C 的union 是同样的概念,用来当作类型可变的单一变量使用,所有成员在内存中相互重叠,位于相同的地址,所以数据会相互覆盖,共用体变量的尺寸等于最大的成员的尺寸。字段名称和结构体一样,也都暴露在全局作用域中,不能重复使用,参考结构体定义

共用体定义
DWordU UNION
    low8 DB ?              ; 占用full 的低8 位
    low16 DW ?             ; 占用full 的低16 位
    full  DD 0FFFFAABBH    ; 最大的成员是32 位,所以整体是32 位
DWordU ENDS
共用体变量
NUM DWordU <0EEH>   ; 共用体变量定义时只能设置第一个字段的值
                    ; 所以想设置整体的值,就要让full 作为第一个字段

RECORD 记录

记录类似用了位域的结构体,只是用法蹩脚的多。记录的整体尺寸小于等于机器的字长,最小一个字节。在8086 上,一个记录变量整体的尺寸最大16 位。同样,内部的字段名称暴露在全局作用域,参考结构体定义

记录定义
Shit1 RECORD a:4, b:4 = 0FH, c: 6 = 0   ; 最多十六位,字段名后是字段的宽度,依次是4位、4位、6位,一共 14位,占用两字节空间。字段的默认值可选
;各个字段在16位内的分布依次是 MSB - 0 0 aaaa bbbb cccccc,即按次序从左到右排列,右对齐,左侧空缺的位补零

Shit2 RECORD a2:4, b2:4 = 0FH   ; 两个字段加起来8 位,所以整体只占用一个字节,没有空缺位,注意字段不能重名
;字段分布是 MSG - aaaa bbbb

S2 Shit2 <1, 2>   ;记录类型变量的定义方式和结构体一样

如果在MASM 6.11 下编译,RECORD 定义后最好留一行空白。

WIDTH <FIELD> 字段宽度操作符

参数FIELD 为记录字段,也可以是记录名,返回其宽度。

MOV AL, WIDTH a2       ; 给AL 赋值4,因为字段名是暴露在外的,所以WIDTH 可以直接引用
MOV AL, WIDTH Shit2.a2 ; 不可以添加前缀,这样写编译不一定出错,可能导致比较隐蔽的BUG

MOV AL, WIDTH SHIT1  ; Shit1 的宽度是所有字段加起来的14 位,而不是包括空缺位的实际占用16 位
MASK <FIELD> 字段掩码操作符

参数只能是记录字段,返回用于操作对应字段的掩码。

MOV AL, MASK a2    ; 对应a2 的掩码为:11110000B, 因为Shit2 只有一个字节,所以掩码也是一个字节
MOV AX, MASK a     ; 对应a 的掩码为:0011110000000000B,掩码对应Shit1 的总尺寸,也是16 位,两个字节
引用字段值

直接读取字段的名称,值为将该字段右移对齐最低位的次数,比如:

MOV CL, a2    ; 值为4, 因为Shit2 的布局是aaaa bbbb,所以右移4 次就变成了0000 aaaa,也就是a2 对齐了最低位

要注意,任何时候想将字段作为操作数时都必须直接使用字段名,不能添加前缀,如:

MOV CL, Shit2.a2

这样写用MASM 5 编译不会报错,但运行会出错。想读取任意字段值,方法是先用掩码置零其他字段值,然后右移,让字段的值等于整体的值,比如:

S1 Shit1 <>      ; 定义Shit1 类型变量
MOV AX, S1       ; 将S1 整体16 位数据装入AX
AND AX, MASK b   ; 用b 的掩码清零b 以外的字段
MOV CL, b        ; 给移位计数器CL 赋值
SHR AX, CL       ; 右移,使AX 内的布局变成 0000 0000 0000 bbbb,此时AX 或AL 的值就是字段b 的值

; 字段c 在最右侧,已经对齐最低位,所以只需用掩码清零其他字段
MOV AX, S1
AND AX, MASK c

;字段a 在最左侧,直接右移就可以清零其他字段,不用使用掩码
MOV AX, S1
MOV CL, a
SHR AX, CL

先掩码再右移的方法适用于任何字段,不用考虑字段的位置。下面两种方法省去了无用的步骤,提高效率,但不够通用。

段定义伪指令

SEGMENT / ENDS 段开始 / 结束

包含可选属性的完整使用格式如下:

<NAME> SEGMENT [定位类型] [组合类型] '类别'
    ...
<NAME> ENDS
对齐类型

表示对段基址(20 位)的要求,可为以下值:

描述
PAGE基址低8 位为0,对齐页边界
PARA低4 位为0,对齐节边界
WORD低1 位为0,对齐字边界
BYTE任意
组合类型

指定本段和其他段的关系,以及链接器对段的处理方式,可为以下值:

PRIVATE默认值,与其他段没有关系,段基址独立分配
COMMON与同名同类别段重叠,段长度为最长值,类似union
STACK指定为栈,自动装入SS,同名同类别段连接为一个段
PUBLIC同名同类别段连接为一个段
MEMORY放在其他段后面,地址最高,只能定义一个MEMORY 段
AT <ADDR>将段基址设为指定的16 位值

如果存在多个标注为MEMORY 的段,除第一个,其他段都被视为COMMON。组合类型为STACK 的段会被系统使用,需要保留的变量等数据不能放在栈段中,否则会被覆盖。

类别

类别是一个长度不超过40 的字符串,链接器只允许相同类别的段发生关联。

ASSUME 段分配

告诉汇编器各段寄存器的分配情况,辅助生成代码。不体现在实际的程序中,所以CS,SS 以外的段寄存器必须手动设定。

.CODE / .DATA 简化段定义

MASM 5 以上的版本可以用简化段定义取代SEGMENT,段的属性根据默认的约定自动设置。

描述
.CODE [name]代码段
.DATA初始化的NEAR 数据段
.DATA?未初始化的NEAR 数据段
.STACK [size]堆栈段,大小为size 字节,默认1kB
.FARDATA [name]初始化的FAR 数据段
.FARDATA? [name]未初始化的FAR 数据段
.CONST常量数据段,在内存中,但运行时无需修改

Small 模式下段的默认属性如下:

默认段名对齐类型组合类型类别
.CODE_TEXTWORDPUBLIC‘CODE’
.DATADATAWORDPUBLIC‘DATA’
.DATA?BSSWORDPUBLIC‘BSS’
.STACKSTACKPARASTACK‘STACK’
.FARDATAFAR_DATAPARANONE‘FAR_DATA’
.FARDATA?FAR_BSSPARANONE‘FAR_BSS’
.CONSTCONSTWORDPUBLIC‘CONST’

@DATA

在需要获取段基址的场合,可以用类似@DATA 的写法获取段名,免得需要记忆默认段名,然后就可以用通常的方法给寄存器赋值:

MOV AX, @DATA    ; 和原来的MOV AX, DATA 等效
MOV DS, AX

; 其他段也是相同的格式
MOV AX, @DATA?
MOV AX, @FARDATA
...

.MODEL <MODE> 存储模式

设置处理简化定义的段的默认值,MODE 的取值如下:

描述
Tiny所有的代码、数据和堆栈数据在同一个64kB 段
Small代码和数据分别用一个64kB 段
Medium代码段可以都多个64kB 段,数据段只有一个64kB
Compact代码段只有一个64kB 段,数据段可以有多个64kB 段
Large代码和数据都可以由多个64kB 段,但单个数据项不能超过64kB
Huge在Large 的基础上,一个数据项可超过64kB
Flat代码和数据段使用同一个4GB 段,Win32 程序使用这种模式

一般可以默认用Small 模式,Win32 程序只用Flat 模式。

.STARTUP 程序开始

用在简化段的代码段开头,初始化CS, SS 等寄存器。不用再写一个START: 标号指示代码入口,所以结尾处END 不加标号参数。

.EXIT [N] 程序结束

简化了程序结束时调用DOS 中断的代码,返回值N 可选,默认为0。

过程定义伪指令

PROC / ENDP 过程开始 / 结束

包含可选属性的完整使用格式如下:

<NAME> PROC [NEAR / FAR]
    ...
<NAME> ENDP

用NEAR 或FAR 指定过程的距离属性,默认为NEAR。NEAR 为段内调用,FAR 为段间段间调用,CALL 和RET 的行为要适应距离属性,参考无条件转移指令

地址定位伪指令

ORG <ADDR> 偏移地址定位

指定其后的数据或代码存放的起始偏移地址,默认在段起始位置。

EVEN 偶对齐

指定其后的数据放置在偶数地址上。8086 可以用一个总线周期读取偶地址上的字数据,若地址为奇数则需要两个周期,所以数据放在偶地址可以提高效率,和段的对齐类型WORD 类似,这也可以被称为两字节对齐。相应的,对于32 位CPU,数据四字节对齐时访存效率最高。

EVEN
DW 10H, 11H, 12H

ALIGN <N> 地址对齐

N 是2 的幂,如2, 4, 8,指定其后的变量N 字节对齐。N == 2 时和EVEN 效果相同。

其他伪指令

NAME <NAME> 模块开始

指示模块开始并命名,不是必要的语句。

END [LABEL] 模块结束

指示模块结束,LABEL 是程序入口点的代码标号,有多个模块的情况下,主模块以外的END 语句不需要重复写出标号。

RADIX <N> 默认基数

参考字面量的表示

COMMENT 多行注释

COMMNET ;
ABC
ABC
;

紧接着COMMENT 指令的分号是定界符,也可以使用其他非空字符。

TITLE / SUBTITLE <NAME> 标题 / 小标题

给程序指定不超过60 字符的标题 / 小标题,显示在LIST 文件每一页的开头。

PAGE <LIN>, <COL> 设置每页行列数

LIST 文件中每页的默认行列数为66 x 80,行数LIN 的范围是10 ~ 255,列数COL 为60 ~ 132。

模块连接伪指令

PUBLIC 公开变量

PUBLIC 指定的变量或标号可被其他模块引用,如:

PUBLIC A, B, C, D
PUBLIC CODE_POS

EXTERN 引用其他模块中的变量

与C 中的extern 类似。用EXTERN 引用的变量必须先经过PUBLIC 指定,且注明类型属性。

EXTERN A: BYTE
EXTERN CODE_POS: FAR

INCLUDE 模块引用

引用其他模块并一同汇编。

GROUP 段合并

将多个段合并到一个最大64k 的段中,并赋予其新的段名,如:

<NAME> GROUP SEG_A, SEG_B, SEG_C, ...

宏定义伪指令

与C 语言的宏函数相似,也是再调用宏的地方把宏的代码复制粘贴进去,或者说展开,因为宏的参数也都是文本替换进去的,所以比PROC 更自由,不需要考虑传参方式。

MACRO / ENDM 宏定义

<NAME> MACRO [ARG1] [, ARG2] ...
    ...
ENDM    ; 注意ENDM 前不重复写名称,和其他地方都不同

注意 ENDM 前不能重复宏名称,可能是为了让人写宏的时候思维更紧张一点。

宏函数对形参ARGx 的处理方式和C 宏函数没有区别,都是直接把实参按文本替换进去。宏里的内容也没有具体的限制,一般就是一段可以包含各种成分的代码片段。宏的形参是局部符号,不会污染全局作用域。

宏展开

先定义一个和**.EXIT** 功能差不多的宏,方便调用DOS 系统中断功能:

M_EXIT MACRO N     ; 宏名为M_EXIT,有一个参数N
    MOV AH, 4CH
    MOV AL, N    ; 参数N 用作程序结束时的返回值
    INT 21H      ; 调用DOS 系统中断,结束程序
ENDM

然后可以“正常”的调用宏函数:

.CODE
.STARTUP
...
M_EXIT 0  ; 0 对应形参N

END

宏展开后就变成:

.CODE
.STARTUP
...
MOV AH, 4CH
MOV AL, 0   ; 形参N 展开后被0 替换
INT 21H

END

LOCAL <LAB…> 宏局部符号

宏是直接替换到代码中的,所以如果宏里定义了代码标号或其他符号,多次调用宏展开后就会发生符号重名,比如:

M_LOOP MACRO
    MOV DI, 0
    COPY:
        MOV DS:[DI], 0AH
        INC DI
        CMP DI, 5
        JE END_LOOP
    JMP COPY
    END_LOOP:
ENDM

其中定义了标号COPY 和END_LOOP,多次调用后展开就会发生符号重名,解决方法是在宏的开头用LOCAL 将其声明为局部符号:

M_LOOP MACRO
    LOCAL COPY, END_LOOP
    COPY:
        ...
ENDM

LOCAL 声明必须放在MACRO 伪指令后的第一行,中间不能有空行或注释

声明为局部的符号在宏展开后会自动被重命名为不会重复的随机符号。宏里也可以定义分支和循环伪指令,不会有标号冲突的问题,因为伪指令生成的代码中含有的标号本来就是随机的。

重复宏伪指令

重复宏就是类似循环展开的概念,按照给定的参数将宏体的代码重复展开若干次,主要用于生成一些具有固定模式的变量定义或代码。重复宏本身都没有名称,在定义的地方原地展开,不能直接在其他地方调用,但可以嵌套到普通宏中被调用。

REPT / ENDM 按次数重复

主要用于定义具有固定规律的数组,宏会在定义的地方立即按次数展开。

TAB LABEL BYTE    ; TAB 将作为数组的名称使用
COUNT = 1   ; 指示数组长度和初始化值得字面量,不占用存储
REPT    100    ; 定义重复宏,重复次数100,没有名称,将在定义处立即展开
    DB COUNT           ; 用COUNT 作为初始值定义一个字节
    COUNT = COUNT + 1  ; 重定义COUNT 的值
ENDM

这个重复宏用于定义一个有100 个元素的数组,元素的初始值具有1, 2, 3, 4,… 的模式,原地展开后的形式如下:

TAB LABEL BYTE
COUNT = 1
DB COUNT    ; COUNT == 1
COUNT = COUNT + 1
DB COUNT    ; COUNT == 2
COUNT = COUNT + 1
DB COUNT    ; COUNT == 3
...

将重复宏放在普通宏内就可以在其他位置调用,外层的宏先展开,把重复宏的定义释放出来,然后重复宏再原地展开:

M_LOOP_ARR MACRO N
    COUNT = 1    ; 每次宏展开都将COUNT 重定义成1
    REPT    N    ; 用形参N 替换重复次数
        DB COUNT
        COUNT = COUNT + 1
    ENDM
ENDM

TAB200 LABEL BYTE    ; 同样可以给生成的数组定个名字
M_LOOP 200   ; 重复200 次。展开后的形式和上面的相同

IRP / ENDM 按参数重复

相当于只有一个形参的宏,但是依次把多个传入的实参代入形参并重复展开。

IRP RG, <AX, BX, CX, DX, BP>
    PUSH RG
ENDM

尖括号内的实参会依次送进形参SG 里,把整个宏体重复展开多次:

; 展开后
PUSH AX
PUSH BX
...

接着用宏嵌套的方法,实现一个方便的将多个寄存器压栈保存的宏:

M_STOREG MACRO REGS    
    IRP RG, <REGS>   ; 用形参SEGS 取代原来的实参列表
        PUSH RG
    ENDM
ENDM

M_STOSREG <AX, BX, CX>    ; 宏的实参用尖括号包围时,尖括号内的文本被当作一个实参传入形参SEGS

IRPC / ENDM 按字符重复

与IRP 类似,只是实参的形式是一个字符串,其中的每个字符依次送入形参。

IRPC RG, ABCD
    PUSH RG&X    ; 此处& 用来把传入的实参和X 连接起来,类似C 宏里的##
ENDM

每个实参传入后和X 连接起来构成寄存器名,所以展开后就变成:

PUSH AX
PUSH BX
...

同样用宏嵌套:

M_STOREG MACRO STR
    IRPC RG, STR
        PUSH RG&X
    ENDM
ENDM

M_STOREG ABCD

分支伪指令

是从MASM 6.11 开始支持的写法。

.IF / .ELSE / .ELSEIF /.ENDIF 分支

.IF <condition1>
    ...
.ELSEIF <condition2>
    ...
.ELSE
    ...
.ENDIF

编译后,伪指令会自动转换成对应的条件运算和跳转指令。其中condition 是条件表达式,可以使用新的操作符和标志位检测语法。

条件表达式操作符

描述
==等于
!=不等于
>大于
>=大于等于
<小于
<=小于等于
&位与
!逻辑非
&&逻辑与
||逻辑或

条件表达式检测标志

等效于
ZERO?ZF == 1
PARITY?PF == 1
CARRY?CF == 1
OVERFLOW?OF == 1
SIGN?SF == 1

没有半进位标志AF。

使用

条件表达式的计算结果为0 表示false,非0 表示true,与C 相同。可以混合使用以上的标志检测和操作符实现比较复杂的条件,分支结构也可以嵌套。

MOV AX, DS:[0010H]
MOV CL, BYTE PTR DS:[0020H]
.IF AX < 10
    MOV AX, 0
.ELSEIF AX > 10 && AX < 20
    MOV AX, 2
.ELSE
    .IF CL > 0           ; 嵌套分支
        SHR AX, CL
    .ENDIF    
.ENDIF

循环伪指令

也是从MASM 6.11 开始支持的写法。

.WHILE / .ENDWILE 循环

.WHILE <condition>
    ...
.ENDW

与C 语言while 循环类似,每次进入循环体时计算条件表达式condition,condition 的写法参考分支伪指令

.REPEAT / .UNTILCXZ 循环

.UNTILCXZ 也可写成.UNTIL。

.REPEAT
    ...
.UNTILCXZ [condition]

与do-while 类似,离开循环体时计算条件表达式,所以循环体至少执行一次。condition 可选,如果没有condition,则.UNTILCXZ 与LOOP 指令相同,每次先对CX 减一,再判断结果是否为0,为0 则终止循环。condition 只能写成EXP1 == EXP2 或EXP1 != EXP2 这两种形式,分别等效于LOOPZ 和LOOPNZ,在条件不为真时中途结束循环。

.BREAK / .RREAK .IF 终止循环

与C 语言的break 类似,跳出循环后紧接着循环体后的代码执行。.BREAK .IF condition 给跳出附加了条件,和把break 放在if 分支里没有区别,条件表达式和.IF 的规则相同。

.CONTINUE / .CONTINUE .IF 继续循环

同样,与C 语言的continue 功能相同,直接跳到循环条件检查处。如果是WHILE ,就跳回开头做判断,REPEAT 循环则跳过循环体剩下的部分,来到.UNTIL 处,如:

MOV CX, 1
.REPEAT
    INC AX
    .CONTINUE .IF CX > 0    ; CX == 1 > 0,所以INC BX 被跳过,直接转移到.UNTILCXZ 做条件判断
    INC BX
.UNTILCXZ    ; CX == 1,减一后等于0,所以循环终止

条件编译伪指令

与C 的类似,不赘述,直接抄书,只是要注意容易和分支伪指令混淆

条件
IF 表达式表达式不等于0
IFE 表达式表达式等于0
IFDEF 符号符号已存在
IFNDEF 符号符号未定义
IF 变量变量是空格
IFNB 变量变量不是空格
IFIDN 变量1, 变量2变量相同
IFNIDN 变量1, 变量2变量不同
IFDEF CONDITION
...
ELSE
...
ENDIF

COM 程序格式

MASM 默认编译产生EXE 格式的程序文件,COM 程序文件相对而言结构更简单:

  • 只有一个段(code),最大64kB;
  • 在段起始地址到代码入口地址间要用ORG 100H 留出256 字节的空白
CODE SEGMENT

   ORG 100H   ; 设置代码入口处偏移地址为100H,所以前面就有0~FFH 共256 个字节
   START:
   ...
   
   MSG DB 'HELLO'   ; 数据可以放在段中靠后的位置
   
CODE ENDS
END STRART

附件

INT 21H - DOS 中断功能

DOS 中断中含有多种功能,通过AH 中的入口参数选择,功能相关的其他参数和返回值通过其他寄存器传递,比如AL,DL,DX,CX

01H / 07H / 08H -> <AL> 读取一个字符

01H 过滤控制字符并回显,08H 不回显,07H 不过滤,不回显。AL 返回ASCII 码。

02H <DL> 输出字符

02H 是在调用中断时在AH 中存放的入口参数,下同。DL 中存放ASCII 码,相当于putch。

09H <DS> <DX> 输出字符串

输出以$ 结尾的字符串,用DS:DX 指示地址。

0AH <DS> <DX> 读取字符串

等待输入并读取一个字符串,回车终止。[DS: DX] 第一个字节设置为缓冲区大小,第二个字节是实际读入的字符数量,第三个字节开始是读入的字符串,没有结束标记。

0BH -> <AL> 检查是否有字符可读

相当于kbhit,避免无输入时调用getch 被阻塞,返回值在AL 中,00H 表示false,FFH 表示true。

4CH <AL> 程序结束

结束程序,用AL 设置返回值,运行无错误时为0。

P.S.

总之,参考了两本微机原理书,作者不同,内容组织也是各有各的烂,陈年的破东西,有些坑还得自己去趟。

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值