主要基于王克义的《微机原理》第二版 ,和其他8086 汇编教材应该没区别。
MOV AX, FF00H
MOV AX, BX
MOV AX, DS:[0010H]
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
指向一个字。因为是小端序,低字节在低地址,所以如果存储的数据是FFAAH
,0010H
就指向AAH
。其他类型属性还有BYTE 和DWORD,表示8 位和32 位。
在数据传送指令中,汇编程序的数据或代码标号默认被视为指针,所以如果有一个数组区域的标号是TAB,代表首地址0010H
,那么:
MOV BX, TAB
就相当于:
MOV BX, [0010H]
也就是把标号指向的数据读出来了。想读取地址,则要加上OFFSET 伪指令,或者用LEA:
MOV BX, OFFSET TAB
LEA BX, LAB
JMP SHORT LAB
JMP NEAR PTR LAB
JMP WORD PTR [BX + SI]
JMP FAR PTR LAB
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
。
DST 是目标,SRC 是源。MOV 指令的数据传送方向存在一些规则:
可见,
此外,CS 寄存器不能直接修改。当一个操作数为立即数,另一个是指针,一般要给指针添加类型:
MOV BYTE PTR DS:[0010H], 22H
以上为把一个立即数送往0010H
,数据类型为字节,另可参见寻址方式。如果使用MASM 5 编译器,用指针作为目标操作数时,段前缀DS: 不一定能省略。
SRC_R 为16 位源寄存器。先将SP 减2,再把数据送入SP 指向的存储单元,SP 指向数据的低字节地址。
DST_R 为CS 以外的16 位目标寄存器。将SP 指向的16 位数据送入目标寄存器,SP 加2。
OPR1 和OPR2 是两个8 位或16 位的操作数,可以是寄存器或存储单元,至少一个是寄存器,所以XCHG 指令可以交换两个寄存器的指令,也可以交换一个寄存器和一个存储单元的值。用XCHG 可以把两个存储器单元值交换的代码优化到三步,就是先给寄存器装入单元A 的数据,让寄存器和另一个单元B 互换,然后再把寄存器值送入A:
MOV AL, [0010H]
XCHG AL, [0020H]
MOV [0010H], AL
这部分指令的操作都围绕累加器AL 或AX,合称AC。
先在BX 中存入一个数组的偏移地址,当AL 中是数组元素的索引值时,可用XLAT 指令按索引值读取一个字节元素到AL 中。示例如下:
MOV BX, OFFSET TABLE ;读取TABLE 地址到BX
MOV AL, 4
XLAT ;将TABLE[4] 读入AL
AL 中存储的是上一步运算的结果,如果这个结果对应的是索引值,用XLAT 就可以一步得到索引对应的数据,但是用处不大。
从外设端口(PORT)读入数据到AX 或AL 中,即可传送8 位或16 位数据。当PORT 地址小于256 时,可用直接寻址,用立即数指示端口地址:
IN AL, 80H
地址大于等于256 则只能用间接寻址。
将AX 或AL 的数据送入外设端口PORT。
把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()
的区别。
将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 是指向指针的指针,即二极指针。
与LDS 相同,只是段地址装入ES 中。
将标志寄存器的低8 位送入AH 寄存器。
将AH 的值送入标志寄存器低8 位。
标志寄存器的值入栈,SP 操作和PUSH 相同。
将栈顶出栈并送入标志寄存器,SP 操作和POP 相同。
令DST = DST + SRC。
令DST = DST + SRC + CF,也就是把前一次运算的进位加到这次的结果中,能用在大数加法里。
令DST = DST + 1。不影响CF 进位/借位标志。
用在单字节组合BCD 码加法后,将AL 中的结果调整成正确的BCD 形式,如:
MOV AL, 19H
ADD AL, 28H ;BCD 加法,19H 应该看作十进制的19,28H 就是28,所以结果应该是47H。
DAA ;在调整后才能保证结果是正确的BCD 形式,否则直接计算结果是41H。
略。
参考上面加法指令下的高亮说明:[算数类指令的特性](#★ 算数类指令的特性)。
令DST = DST - SRC。
令DST = DST - SRC - CF,参考带借位加法的说明。
令DST = DST - 1。不影响CF 进位/借位标志。
执行DST - SRC 运算,但不保存结果,只修改标志位。
令DST = -DST,DST 的值视为补码,结果也是补码形式。
和DAA 相似,用在组合BCD 码减法后,将AL 中的差调整成正确形式。
略。
SRC_R_M 为寄存器或存储单元。
与MUL 相同,乘数和乘积均视为带符号数,满足一般代数运算规则,如负负得正。
SRC_R_M 为寄存器或存储单元。
与DIV 相同,参与运算的数据均被视为带符号数,满足一般代数运算规则,余数的符号和被除数相同。
略。
略。
用于处理带符号数运算时的类型匹配问题,比如,需要把一个8 位带符号数变成16 位用在乘除法中,负数不能直接在高8 位补零。
将AL 中的8 位数高位拼上AH 扩展成16 位,AH 中的所有位都设置成AL 的符号位。如:
MOV AL, 35H
CBW ;AX 变成0035H
MOV AL, 82H ;82H 的最高位是1,当作带符号数时是负数
CBW ;AX 变成FF82H
将AX 中的16 位数高位和DX 拼接扩展成32 位,DX 中的所有位都设置成AX 中的符号位。如:
MOV AX, 3500H
CWD ;{DX, AX} 变为0000 3500H
MOV AX, 8200H
CWD ;{DX, AX} 变为FFFF 8200H
令DST_R_M = ~DST_R_M,DST_R_M 不能是立即数。
令DST = DST & SRC。
令DST = DST | SRC。
令DST = 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 << CNT`。高位移入CF,低位补零。
和SHL 实际是同一条指令,低位补零,符号位会变化。
等效于``DST_R_M >> CNT`。低位移入CF,高位补零。如,80H 无符号右移一次得40H(0b01000000)。
等效于``DST_R_M >>> CNT`。低位移入CF,高位补符号位:正数补0,负数补1。如,80H 带符号右移一次得C0H(0b11000000)。
高位溢出位的返回最低位,同时也送到CF。如,80H 循环左移一位得01H,CF = 1。
低位溢出的位返回最高位,同时也送到CF。如,01H 循环右移一位得80H,CF = 1。
相当于把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
和RCL 相似,CF 先移入最高位,最低位再移入CF。
MOVSB指令(Move String Byte)用于将一个字节从源地址复制到目标地址。
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
重复执行其后的串操作指令,每次让CX - 1,直到CX == 0。如:
MOV CX, 5
REP MOVSB
就是重复执行五次,传送五个字节。每次执行都对CX 先减一,再判断是否为0,所以CX 的初始值至少为1。
两个前缀含义相同,仅当ZF == 1 且CX != 0 时重复执行串操作,也就说存在两个结束条件,如果ZF 在中间不等于1,也就是CMP 出来不相等,那么循环会在CX 计数结束前终止,循环过程中CX 自动增减不会影响标志位,所以之后可以用JZ / JE 和JNZ / JNE 判断ZF 的值,分别跳转到处理两种情况的分支,参考后面CMPSB 串比较指令的例程。
与上一种前缀类似,在CMP 结果相等时中途终止循环。
CMPSB指令(Compare String Byte)用于比较源地址和目标地址处的一个字节。
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
与上面的串操作指令类似,但只通过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相等的字符时执行的代码
与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
与LODSB 相反,用ES: DI 索引一个字符串,每次将AL 或AX 中的一个字节或一个字存入字符串。如果使用无条件重复前缀,则可以用STOSB 把字符串全部设为相同值,也可以和IN 指令配合,把每次从外部端口读入的数据存入串中。
CLD
MOV DI, OFFSET DST_STR
MOV AL, FFH
MOV CX, 100
REP STOSB ;重复100 次,用FFH 覆盖串中的数据
参考转移方式,共有5 种用法,分别是短转移、段内直接转移、段内间接转移、段间直接转移、段间间接转移。其中,直接转移时DST 都是标号,即偏移量立即数,间接转移则用寄存器或存储单元种的16 位或32 值作为转移目的地,更新CS 和IP 寄存器。
先将CALL 指令后的指令地址压栈,作为返回地址,然后转移到DST。转移方式和JMP 类似,只是没有短转移。
将栈中的返回地址弹出,修改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
JS 判断结果最高位是否1,JNS 则判断是否为0。
B 是Below,AE 是Above or Equal,下同。
L 是Less,GE 是Greater or Equal,下同。
循环指令和重复前缀类似,都用CX 作为循环计数器,又可以根据标志位在CX 减到0 之前终止循环。差别是重复前缀只能和串操作指令配合,而循环控制指令是单独的转移指令,用来时循环结构更清晰,便于代码编写和阅读。此外,循环控制指令和条件转移指令一样,只接受一个标号操作数,执行短转移,范围是-128 ~ +127。
循环指令每次执行都对CX 先减一,再判断是否为0,所以CX 的初始值至少为1。
与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 ; 跳转到循环开始处并递减计数器
ZF == 1 且CX != 0 时循环,也就是当遇到不相等或不为0 时,ZF == 0, 循环终止。可用于比较两串数据是否相等。
ZF == 0 且CX != 0 时循环。可用于比较串中的数据。
不操作CX 的值,可用于从无限循环中跳出。
MOV CX, 5 ; 设置计数器初始值为5,循环5 次跳出
MY_LOOP:
DEC CX ; 递减计数器
JCXZ EXIT_LOOP ; 如果计数器为0,则跳出循环
; 循环体中的代码
JMP MY_LOOP ; 跳转到循环开始处
EXIT_LOOP:
NUM_I 是8 位立即数,表示中断类型码。INT 指令根据中断类型码在中断向量表中找到对应的中断服务程序并转移,与段间过程调用类似,需要把CS 和IP 作为返回地址压栈,还会另外自动把标志寄存器FR 压栈保存。
用来测试溢出标志OF,若OF == 1,则调用溢出中断。一般跟在带符号数的算术运算指令后,处理溢出。
放在中断服务函数末尾,从栈中弹出返回地址和标志寄存器的值,和段间返回类似。
ST 表示SET,CL 表示CLEAR,C 是CF,下面的缩写规则相同。
执行LOCK 前缀后的指令时,CPU 将发送**#LOCK** 信号封锁总线。
让CPU 进入暂停状态。可用RESET 或不可屏蔽中断NMI 唤醒CPU,启用中断时,也可用INTR 线上的可屏蔽中断唤醒。
使CPU 进入等待状态,每隔5 个时钟周期测试一次**#TEST** 引脚,直到出现有效信号未知。
用于向协处理器发出请求,协处理器接到ESC 信号后就按需求开始处理,CPU 可以用WAIT 指令进入等待模式,处理完成后协处理器发出#TEST 信号,让CPU 结束等待。
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 语言不同。
除了显而易见的分段结构,汇编程序中还包含三类语句:
数字默认使用十进制,可用伪指令RADIX
用B、D、H、Q 后缀分别表示二进制、十进制、十六进制、八进制;
数字不能以字母开头,所以FFH 要写成0FFH。注意C 语言不能随便加一个没用的0,因为前缀0 用来表示八进制;
在有协处理器的情况下,可用±3.14E±8 的形式定义实数;
用单引号标记一个ASCII 字符串,相邻的字符串间的逗号用来连接。和DB 配合,可用字符串定义并初始化一块内存区域;
用伪指令运算符能够实现编译期运算的常量表达式,分为数值表达式和地址表达式两种。表达式中运算符的操作数只能是编译期常量,或者说是字面量,比如硬编码的数字、代码标号、定义内存区域的变量名。
功能 | |
---|---|
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
MOD | 取余 |
功能 | 用法 | |
---|---|---|
SHL | 左移 | 0110B SHL 1 |
SHR | 右移 | 0110B SHR 1 |
NOT | 非 | NOT 22H |
AND | 与 | 11H AND 22H |
OR | 或 | 11H 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 的作用参考寻址方式。
与C 语言的#define 宏类似,用EQU 可以给表达式和其他符号定义一个别名。
A EQU 8 ; 数字常量
B EQU A + 2 ; 表达式
C EQU AX ; 寄存器别名
D EQU ADD ; 指令别名
E EQU ; 和#define 类似,在尖括号内定义一个任意的字符串
MOV AX, E [0010H] ; E 会被展开成字符串,即:
MOV AX, WORD PTR [0010H]
EQU 定义的符号发挥作用的方式也和宏类似,就是把内容粘贴过去。另外,EQU 定义的符号不允许重定义。
等于号和EQU 类似,但是用等于号定义的符号可以重定义。
A = 9
B = A + 3
和#undef 一样,允许用EQU 重新定义符号。
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 处
和THIS 相似,区别只是不需要和EQU 或= 配合使用。
A LABEL BYTE
DB 'HELLO'
HERE LABEL FAR
INT 43H
分别用于定义字节、字、双字、四字节、十字节类型的数组区域,变量名是可选的。
STR DB 'ABCDE'
BYTES DB 30H, 31H, 32H, 33H, 34H ;定义数组和字符串本质是相同的
DW 0, 0, 0, 0, 0, 0
DB ?, ?, ? ;? 用作占位符,表示没有初始化值
定义数据时按次数重复括号内的模式:
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
与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] ; 也可以用指针直接访问
同样,和C 的union 是同样的概念,用来当作类型可变的单一变量使用,所有成员在内存中相互重叠,位于相同的地址,所以数据会相互覆盖,共用体变量的尺寸等于最大的成员的尺寸。字段名称和结构体一样,也都暴露在全局作用域中,不能重复使用,参考结构体定义。
DWordU UNION
low8 DB ? ; 占用full 的低8 位
low16 DW ? ; 占用full 的低16 位
full DD 0FFFFAABBH ; 最大的成员是32 位,所以整体是32 位
DWordU ENDS
NUM DWordU <0EEH> ; 共用体变量定义时只能设置第一个字段的值
; 所以想设置整体的值,就要让full 作为第一个字段
记录类似用了位域的结构体,只是用法蹩脚的多。记录的整体尺寸小于等于机器的字长,最小一个字节。在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 定义后最好留一行空白。
参数FIELD 为记录字段,也可以是记录名,返回其宽度。
MOV AL, WIDTH a2 ; 给AL 赋值4,因为字段名是暴露在外的,所以WIDTH 可以直接引用
MOV AL, WIDTH Shit2.a2 ; 不可以添加前缀,这样写编译不一定出错,可能导致比较隐蔽的BUG
MOV AL, WIDTH SHIT1 ; Shit1 的宽度是所有字段加起来的14 位,而不是包括空缺位的实际占用16 位
参数只能是记录字段,返回用于操作对应字段的掩码。
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
表示对段基址(20 位)的要求,可为以下值:
描述 | |
---|---|
PAGE | 基址低8 位为0,对齐页边界 |
PARA | 低4 位为0,对齐节边界 |
WORD | 低1 位为0,对齐字边界 |
BYTE | 任意 |
指定本段和其他段的关系,以及链接器对段的处理方式,可为以下值:
PRIVATE | 默认值,与其他段没有关系,段基址独立分配 |
COMMON | 与同名同类别段重叠,段长度为最长值,类似union |
STACK | 指定为栈,自动装入SS,同名同类别段连接为一个段 |
PUBLIC | 同名同类别段连接为一个段 |
MEMORY | 放在其他段后面,地址最高,只能定义一个MEMORY 段 |
AT |
将段基址设为指定的16 位值 |
如果存在多个标注为MEMORY 的段,除第一个,其他段都被视为COMMON。组合类型为STACK 的段会被系统使用,需要保留的变量等数据不能放在栈段中,否则会被覆盖。
类别是一个长度不超过40 的字符串,链接器只允许相同类别的段发生关联。
告诉汇编器各段寄存器的分配情况,辅助生成代码。不体现在实际的程序中,所以CS,SS 以外的段寄存器必须手动设定。
MASM 5 以上的版本可以用简化段定义取代SEGMENT,段的属性根据默认的约定自动设置。
描述 | |
---|---|
.CODE [name] | 代码段 |
.DATA | 初始化的NEAR 数据段 |
.DATA? | 未初始化的NEAR 数据段 |
.STACK [size] | 堆栈段,大小为size 字节,默认1kB |
.FARDATA [name] | 初始化的FAR 数据段 |
.FARDATA? [name] | 未初始化的FAR 数据段 |
.CONST | 常量数据段,在内存中,但运行时无需修改 |
Small 模式下段的默认属性如下:
段 | 默认段名 | 对齐类型 | 组合类型 | 类别 |
---|---|---|---|---|
.CODE | _TEXT | WORD | PUBLIC | ‘CODE’ |
.DATA | DATA | WORD | PUBLIC | ‘DATA’ |
.DATA? | BSS | WORD | PUBLIC | ‘BSS’ |
.STACK | STACK | PARA | STACK | ‘STACK’ |
.FARDATA | FAR_DATA | PARA | NONE | ‘FAR_DATA’ |
.FARDATA? | FAR_BSS | PARA | NONE | ‘FAR_BSS’ |
.CONST | CONST | WORD | PUBLIC | ‘CONST’ |
在需要获取段基址的场合,可以用类似@DATA
的写法获取段名,免得需要记忆默认段名,然后就可以用通常的方法给寄存器赋值:
MOV AX, @DATA ; 和原来的MOV AX, DATA 等效
MOV DS, AX
; 其他段也是相同的格式
MOV AX, @DATA?
MOV AX, @FARDATA
...
设置处理简化定义的段的默认值,MODE 的取值如下:
描述 | |
---|---|
Tiny | 所有的代码、数据和堆栈数据在同一个64kB 段 |
Small | 代码和数据分别用一个64kB 段 |
Medium | 代码段可以都多个64kB 段,数据段只有一个64kB |
Compact | 代码段只有一个64kB 段,数据段可以有多个64kB 段 |
Large | 代码和数据都可以由多个64kB 段,但单个数据项不能超过64kB |
Huge | 在Large 的基础上,一个数据项可超过64kB |
Flat | 代码和数据段使用同一个4GB 段,Win32 程序使用这种模式 |
一般可以默认用Small 模式,Win32 程序只用Flat 模式。
用在简化段的代码段开头,初始化CS, SS 等寄存器。不用再写一个START:
标号指示代码入口,所以结尾处END 不加标号参数。
简化了程序结束时调用DOS 中断的代码,返回值N 可选,默认为0。
包含可选属性的完整使用格式如下:
PROC [NEAR / FAR]
...
ENDP
用NEAR 或FAR 指定过程的距离属性,默认为NEAR。NEAR 为段内调用,FAR 为段间段间调用,CALL 和RET 的行为要适应距离属性,参考无条件转移指令。
指定其后的数据或代码存放的起始偏移地址,默认在段起始位置。
指定其后的数据放置在偶数地址上。8086 可以用一个总线周期读取偶地址上的字数据,若地址为奇数则需要两个周期,所以数据放在偶地址可以提高效率,和段的对齐类型WORD 类似,这也可以被称为两字节对齐。相应的,对于32 位CPU,数据四字节对齐时访存效率最高。
EVEN
DW 10H, 11H, 12H
N 是2 的幂,如2, 4, 8,指定其后的变量N 字节对齐。N == 2 时和EVEN 效果相同。
指示模块开始并命名,不是必要的语句。
指示模块结束,LABEL 是程序入口点的代码标号,有多个模块的情况下,主模块以外的END 语句不需要重复写出标号。
参考字面量的表示。
COMMNET ;
ABC
ABC
;
紧接着COMMENT 指令的分号是定界符,也可以使用其他非空字符。
给程序指定不超过60 字符的标题 / 小标题,显示在LIST 文件每一页的开头。
LIST 文件中每页的默认行列数为66 x 80,行数LIN 的范围是10 ~ 255,列数COL 为60 ~ 132。
PUBLIC 指定的变量或标号可被其他模块引用,如:
PUBLIC A, B, C, D
PUBLIC CODE_POS
与C 中的extern 类似。用EXTERN 引用的变量必须先经过PUBLIC 指定,且注明类型属性。
EXTERN A: BYTE
EXTERN CODE_POS: FAR
引用其他模块并一同汇编。
将多个段合并到一个最大64k 的段中,并赋予其新的段名,如:
GROUP SEG_A, SEG_B, SEG_C, ...
与C 语言的宏函数相似,也是再调用宏的地方把宏的代码复制粘贴进去,或者说展开,因为宏的参数也都是文本替换进去的,所以比PROC 更自由,不需要考虑传参方式。
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
宏是直接替换到代码中的,所以如果宏里定义了代码标号或其他符号,多次调用宏展开后就会发生符号重名,比如:
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 伪指令后的第一行,中间不能有空行或注释。
声明为局部的符号在宏展开后会自动被重命名为不会重复的随机符号。宏里也可以定义分支和循环伪指令,不会有标号冲突的问题,因为伪指令生成的代码中含有的标号本来就是随机的。
重复宏就是类似循环展开的概念,按照给定的参数将宏体的代码重复展开若干次,主要用于生成一些具有固定模式的变量定义或代码。重复宏本身都没有名称,在定义的地方原地展开,不能直接在其他地方调用,但可以嵌套到普通宏中被调用。
主要用于定义具有固定规律的数组,宏会在定义的地方立即按次数展开。
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 RG,
PUSH RG
ENDM
尖括号内的实参会依次送进形参SG 里,把整个宏体重复展开多次:
; 展开后
PUSH AX
PUSH BX
...
接着用宏嵌套的方法,实现一个方便的将多个寄存器压栈保存的宏:
M_STOREG MACRO REGS
IRP RG, ; 用形参SEGS 取代原来的实参列表
PUSH RG
ENDM
ENDM
M_STOSREG ; 宏的实参用尖括号包围时,尖括号内的文本被当作一个实参传入形参SEGS
与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
...
.ELSEIF
...
.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
...
.ENDW
与C 语言while 循环类似,每次进入循环体时计算条件表达式condition,condition 的写法参考分支伪指令。
.UNTILCXZ 也可写成.UNTIL。
.REPEAT
...
.UNTILCXZ [condition]
与do-while 类似,离开循环体时计算条件表达式,所以循环体至少执行一次。condition 可选,如果没有condition,则.UNTILCXZ 与LOOP 指令相同,每次先对CX 减一,再判断结果是否为0,为0 则终止循环。condition 只能写成EXP1 == EXP2 或EXP1 != EXP2 这两种形式,分别等效于LOOPZ 和LOOPNZ,在条件不为真时中途结束循环。
与C 语言的break 类似,跳出循环后紧接着循环体后的代码执行。.BREAK .IF condition
给跳出附加了条件,和把break 放在if 分支里没有区别,条件表达式和.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
MASM 默认编译产生EXE 格式的程序文件,COM 程序文件相对而言结构更简单:
CODE SEGMENT
ORG 100H ; 设置代码入口处偏移地址为100H,所以前面就有0~FFH 共256 个字节
START:
...
MSG DB 'HELLO' ; 数据可以放在段中靠后的位置
CODE ENDS
END STRART
DOS 中断中含有多种功能,通过AH 中的入口参数选择,功能相关的其他参数和返回值通过其他寄存器传递,比如AL,DL,DX,CX
01H 过滤控制字符并回显,08H 不回显,07H 不过滤,不回显。AL 返回ASCII 码。
02H 是在调用中断时在AH 中存放的入口参数,下同。DL 中存放ASCII 码,相当于putch。
输出以$ 结尾的字符串,用DS:DX 指示地址。
等待输入并读取一个字符串,回车终止。[DS: DX] 第一个字节设置为缓冲区大小,第二个字节是实际读入的字符数量,第三个字节开始是读入的字符串,没有结束标记。
相当于kbhit,避免无输入时调用getch 被阻塞,返回值在AL 中,00H 表示false,FFH 表示true。
结束程序,用AL 设置返回值,运行无错误时为0。
总之,参考了两本微机原理书,作者不同,内容组织也是各有各的烂,陈年的破东西,有些坑还得自己去趟。