Intel系列微处理器不断发展并保持向上兼容,不同版本的MSAM对应不同代的微处理器的指令集和汇编伪指令。因此方式选择伪指令既是选择微处理器,也是选择相应的指令集。
MASM宏汇编程序提供的方式选择伪指令如下。
· .8086:只汇编8086/8088的指令,此为默认方式。
· .286或.286C:同.8086并只汇编80286非特权(实模式)指令。
· .286P:同.8086并汇编80286全部指令(包括保护方式)。
· .287:用于汇编80287协处理器指令。
· .386或.386C:同.286并汇编80386非特权(实模式)指令。
· .386P:同.286P并汇编80386全部指令(包括保护方式)。
· .387:用于汇编80387协处理器指令。
· .486或.486C:同.386并汇编80486非特权(实模式)指令。
· .486P:同.386P并汇编80486全部指令(包括保护方式)。
· .586或.586C:用于汇编Pentium微处理器非特权(实模式)指令。
· .586P:用于汇编Pentium微处理器保护方式在内的所有指令,是MASM 6.12引入的。
· .686:用于汇编Pentium Pro到Pentium 4微处理器非特权(实模式)指令。
· .686P:用于汇编Pentium Pro到Pentium 4微处理器保护方式在内的所有指令。
· .MMX:用于汇编MMX指令。
· .K3D:用于汇编AMD微处理器3D Now!指令,是MASM 6.13引入的。
· .XMM:用于汇编SSE和SSE2指令,是MASM 6.15引入的。
对80386及其后的微处理器,方式选择伪指令放在.MODEL伪指令前面,那么段将定义成32位;如果要处理器使用16位的段,则应在.MODEL伪指令之后设置方式选择伪指令。
逻辑段的定义和说明伪指令以及其他伪指令和指令语句的格式和参数因汇编程序的类型和版本不同而有所差异。本节参照MASM 5.0和6.x介绍MASM常用的语句格式和参数。
使用完整的段定义伪指令来定义一个段,可具体控制汇编程序MASM和链接程序LINK在内存中组织代码和数据的方式。整个逻辑段必须用段定义语句括起来。
段定义格式如下:
<段名> SEGMENT[定位类型][组合方式][段长度][分类名]
……
<段名>ENDS
每个逻辑段都以SEGMENT语句开头,以ENDS语句结束,两语句前的段名作为逻辑段的标识符,它必须是唯一的,确定该段在存储器中的位置。
段开始和结束语句之间部分是段的内容,对数据段、堆栈段和附加段而言,一般由存储单元定义、分配等伪指令所组成,对代码段,则主要是由可执行指令以及某些伪指令组成。方括号中是定位类型、组合方式、段长度和分类名等的说明,视情况可以省略。若对本逻辑段在内存中的定位加以限定或须与其他段相链接,则须按规定选择这些项。
为叙述方便,本节约定:语句定义中<…>括起来的是必选项,[…]括起来的是可选项(根据需要选择),{…}内的内容为注释,最外层括号本身不是语句的一部分。
(1)定位类型(align type)规定该段的起始位置,有以下几种选择。
· BYTE:段的起始地址可以从任何单元开始,段间不留空隙,存储器利用率最高。
· WORD:段的起始地址为偶数,即从字的边界开始;适合字类型的数据段定位。
· DWORD:段的起始地址是4的整数倍,适合双字类型的数据段定位。
· PARA:段的起始位置是16的整数倍,即从节(24=16为一节)的边界开始。
· PAGE:段的起始地址必须是256的整数倍,即从页(28=256为一页)的边界开始。
· INPAGE:该段长度不超过256个字节(一页之内),不允许跨页存放。
定位类型省略时默认为PARA方式。
(2)组合方式(combine type)又称链接方式,规定链接程序LINK处理本段与其他逻辑段时如何进行相互链接,可以有以下几种链接方式。
· NONE:本段与其他段无组合关系,这是默认方式,也可用等价的关键字PRIVATE。
· PUBLIC:在满足定位的条件下,将本段与其他具有PUBLIC属性的同名段链接成一个连续的大逻辑段,共用一个段基址;链接次序由链接命令指定。
· COMMON:本段和其他具有COMMON属性的同名段重叠,对各段指定相同的起始地址(即段基址),因而产生覆盖;COMMON链接后段的长度是各链接段中最大段的长度。
· STACK:把不同模块中具有STACK属性的同名段链接成一个连续的堆栈段,链接后的堆栈空间是被链接的各个堆栈空间之和。LINK自动将新段的段基址送SS,将新段的长度送SP;定义堆栈段时没有将其说明为STACK类型,就要在程序中用指令给SS和SP赋值;链接程序LINK在链接时会给出一个警告信息。
· MEMORY:早期版本的选项,LINK不再单独区分MEMORY类型,将其视为PUBLIC类型,MASM允许它继续使用,主要是为了与支持Intel MEMORY类型的链接程序兼容。
· AT<表达式>:定位本段在指定节的整数边界上,节号由表达式给出。AT方式不能用在代码段中。使用AT方式的逻辑段中,用DB、DW预置数据将失败。
(3)段长度对32位微处理器新增了USE属性说明,又称段字说明。16位CPU默认的段长是16位,不使用USE选项。32位CPU有两种方式(默认选项是USE32)。
· USE16:表示逻辑段最大长度为64KB,访问该逻辑段采用16位寻址方式。
· USE32:表示逻辑段最大长度为4GB,访问该逻辑段采用32位寻址方式。
(4)分类名是用单引号括起来的长度不超过40个字符的字符串。链接程序把不同模块中分类名相同的段组织成一类,存放在连续的存储区域。习惯上数据段/代码段/堆栈段的分类名分别用‘DATA’/‘CODE’/‘STACK’表示。
逻辑段允许嵌套定义,即一个逻辑段内可以再定义一个或多个其他逻辑段,嵌套的深度通常不受限制,但不允许交叉设置。
段分配伪指令ASSUME用来通知汇编程序:CS、DS、SS、ES将被设置成哪些段的段基址寄存器,这样在汇编时就能知道语句中引用的变量和标号是通过哪些寄存器进行访问的。
ASSUME语句的格式如下:
ASSUME<段寄存器名>:<逻辑段名>[, <逻辑段名>…]; [<段寄存器名>:<逻辑段名>
[, <逻辑段名>…]; …; [<段寄存器名>:NOTHING]]
一个源程序至少有一个代码段。ASSUME语句放在代码段的可执行指令之前,它将冒号右边由段名(或段基址表达式)标识的段分配给冒号左边的段寄存器,若有多个段分配给同一个段寄存器,则段名之间用逗号隔开;不同段寄存器的分配用分号隔开。如果冒号右边的段名部分的关键字为NOTHING,则废除对冒号左边段寄存器以前的段指配。
若没有另外的ASSUME语句重新进行段分配,则原ASSUME语句的设置一直有效。伪指令语句ASSUME NOTHING将删除所有已设置的段寄存器与段名之间的关联。ASSUME伪指令只是建立段名和段寄存器之间的关联,并未把各个段的段基址装入相应的段寄存器中。段基址的装入通常采用程序方法。
(1)DS和ES装入:在程序中用指令经通用寄存器间接地把段基址传送给DS和ES。
(2)SS装入:有两种方法,一种是在SEGMENT语句的组合类型中选用STACK,在ASSUME语句中建立堆栈段与SS的关联,则链接时LINK自动给SS和SP赋予正确的初值;另一种SEGMENT语句中组合类型未选用STACK参数,或者程序中使用了另一个堆栈段,可用传送指令间接地给SS和SP赋初值(组合类型未选用STACK参数,链接时LINK会发出警告信息)。
(3)CS和IP装入:对CS和IP,通常是按照结束伪指令END[<起始地址>]指定的地址自动完成段基址和偏移地址的装入。
当用段定义语句SEGMENT/ENDS定义了若干个逻辑段而总容量不超过64KB时,就可把这些段用GROUP语句定义为一个“组”,格式如下:
<组名>GROUP<段名1>[,段名2……]
GROUP把程序模块中若干个不同名字的段组合成一个组,并赋予一个组名,组内各段都装在一个物理段中,使组内各段间的跳转都视作段内跳转。
语句中的段名可以是表达式SEG<变量名>或SEG<标号>,表达式的值是定义该变量或标号的段号。
组名是标识组的起始地址的标号,表示该组的段基址。组中各个段内的偏移地址都以这个基址为起点,而不以本段原来的段基址为起点。组名的使用方法与段名相同。
汇编程序编译该语句时并不能确定这些段合并成一个组后的大小是否超过64KB的范围,如果超过64KB,在链接时由链接程序LINK指出该错误。
(1)ORG语句。
汇编源程序时,汇编程序将段名填入段表,并为该段配置一个位置计数器(初值为0),该计数器依次累计段内语句被汇编后所生成的目标代码的字节数,跟踪汇编过程。伪指令ORG能改变这个位置计数器的值,并从指定位置分配代码或数据。
格式: ORG<数值表达式>
数值表达式中可用“$”表示位置计数器(该语句执行前)的当前值,表达式的值为正整数,ORG语句把表达式的值赋给位置计数器。这样,在代码段,ORG语句后的指令语句就从指定位置生成目标代码;在数据段,ORG语句后的数据定义伪指令就从指定位置分配所定义的数据。
(2)EVEN语句。
格式: EVEN
EVEN语句使段内位置计数器当前值$与偶地址对齐。遇到EVEN语句时,若地址计数器值$为奇数,在代码段汇编程序自动插入一条单字节的空操作指令NOP,使$变为偶数;在数据段自动将$值加1,使下面定义的数据从偶地址单元开始存放。
(1)模块开始语句。
模块开始语句NAME表示源程序的开始并为模块取名字。
格式:NAME<模块名>
汇编程序将以语句中的模块名作为模块的名字,也可使用TITLE语句。
格式:TITLE<标题>
TITLE伪指令指定了在每一页上打印的标题,最多为60个字符。如果程序中没有使用NAME伪指令,则汇编程序MASM将用TITLE语句中标题的前6个字符作为模块名。如果程序中既无NAME语句,又无TITLE语句,则将用源文件名作为模块名。
NAME语句和TITLE语句不是必需的,一般常使用TITLE语句,以使在文件列表中能打印出标题来。
(2)模块结束语句。
END表示源程序到此结束,并可指出程序的启动地址。
格式:END[标号/过程名]
该语句一般放在源程序最后一行,程序中有且只能有一个END语句。其中标号或过程名给出了程序开始执行的启动单元地址,只有主模块才可有此选项。
当连接的多个模块中都没有给出启动地址或者有多个模块都指定了启动地址,前者将从代码段的第一个字节处开始执行,后者将从最后一条带标号的END语句指定的启动地址单元开始执行。它们都可能导致程序的错误执行,编程时应予以避免。
汇编语言源程序经过汇编、链接后生成可执行程序。程序一般会在DOS等平台上运行,程序运行完毕应返回操作系统。返回操作系统的方式有下面几种。
(1)使用功能号为4CH的DOS功能调用。
调用方式:
MOV AH,4CH ; 功能号4CH送AH寄存器
INT 21H ; DOS系统功能调用,这是最有效且兼容性最好的返回DOS方式
(2)将主程序定义为远过程。
在代码段中将主程序定义为远过程由RET语句返回。
远过程定义如下:
<主过程名>PROC FAR
…… {主程序}
RET
<主过程名>ENDP
(3)使用20H功能号的BIOS中断调用。这种方式只能用于扩展名为COM的可执行文件。
调用方式:
INT 20H
(4)使用0号DOS系统功能调用。
该方式与INT 20H一样,只有在COM类型的文件中才可使用。
调用方式:
MOV A11,0 ; 功能号送AH
INT 21H ; DOS系统功能调用
在MASM 5.0及以上版本中,段的定义可以非常简单,极大地方便了程序设计。使用简化段定义伪指令前必须先说明用户程序使用的内存模式。
内存模式指用户程序的代码和数据的存放格式,以及它们占用内存的大小。定义内存模式伪指令语句格式如下:
.MODEL<存储模式>[,语言类型][,操作系统类型][,堆栈类型]
(1)存储模式。存储模式共有以下7种。
· TINY:微型模式。程序中的代码和数据放在同一个64KB段内,程序为COM型,该模式是MASM 6.0才引入的。
· SMALL:小型模式。这是一般程序的默认模式。程序中的代码和数据各放入一个64KB的段内,数据段、堆栈段和附加段一起共用一个段。对代码和数据的访问通过近程调用(NEAR)来实现。
· MEDIUM:中型模式。程序中代码长度大于64KB,可安排在不同的段内;数据存放在一个64KB的段内;因此代码访问是远程(FAR)的,数据访问是近程的。
· COMPACT:压缩模式。程序中所有代码安排在一个64KB的段内,数据区可大于64KB,安排在不同的段中(但任一数据段存储空间不得超过64KB)。对代码访问是近程的,对数据访问是远程的。
· LARGE:大型模式。程序中的代码和数据都超过64KB,但静态(常数)数据限制在64KB之内。对代码和数据的访问默认都是远程的。
· HUGE:巨型模式。程序中的代码和数据均大于64KB,静态数据也可以超过64KB。对代码、数据和数组的访问都是远程的。
· FLAT:平展模式。DOS下不能使用此模式。该模式用于创建32位的Windows程序,只能运行在32位的80x86微处理器上。
(2)语言类型。子程序语言类型用来告诉汇编程序使用什么样的标识符命名风格、子程序调用和返回约定等。语言类型说明可使汇编程序和其他语言程序共享。有效的语言类型为C、SYSCALL(系统调用)、STDCALL(标准调用)、BASIC、FORTRAN、PASCAL等。
(3)操作系统类型。OS DOS是当前唯一支持的选项,也是该选项的默认值。
(4)堆栈类型。堆栈类型有如下两个选项,不同选值主要影响伪指令STARTUP所生成的指令序列。
· NEARSTACK:堆栈段和数据段是同一个段,这也是该选项的默认值。
· FARSTACK:堆栈段和数据段是不同的段,且堆栈段不在段组DGROUP中。
(1)简化段定义伪指令。
简化段定义伪指令在.MODEL<存储模式>语句之后,如下所示,每个简化段伪指令都可用来表示一个段的开始,同时也说明前一个段的结束。
.STACK[长度] ; 定义堆栈段,长度默认值为1KB
.CODE[名字] ; 定义代码段
.DATA ; 定义数据段
.DATA? ; 定义数据段,初值不确定
.FARDATA[名字] ; 定义远调用数据段
.FARDATA?[名字] ; 定义远调用数据段,初值不确定
.CONST ; 定义只读常数数据段
.STARTUP ; 程序起始点,并初始化DS、SS
.EXIT 0 ; 程序结束点,返回操作系统
上述简化段定义伪指令中带方括号的项可省略。伪指令.EXIT 0相当于MOV AX,4C00H和INT 21H两条指令。
例如,用简化段定义编程,计算两个32位无符号数相乘:Z=X×Y。
.MODEL SMALL ; 设置小型内存模式
.386 ; 可汇编80386指令
.STACK 100 ; 定义堆栈段,长度为100字节
.DATA ; 数据段开始,堆栈段结束
X DD 11223344H
Y DD 55667788H
Z DD64(?) ; 预留64位乘积的存储空间
.CODE ; 代码段开始,数据段结束
.STARTUP ; 程序开始
MOV EAX, X
MUL Y
MOV Z, EAX ; 保留乘积的低32位
MOV Z+4, EDX ; 保存乘积的高32位
.EXIT 0 ; 程序结束,返回操作系统
END ; 汇编结束
(2)默认段名。
使用简化段定义伪指令,每个段都有一个默认的段名,如表10-2所示。
表10-2 不同内存模式下的默认段名及其属性
内存模式 |
段定义伪指令 |
段 名 |
定 位 |
组 合 |
类 别 |
组 名 |
TINY |
.CODE |
_TEXT |
WORD |
PUBLIC |
‘CODE’ |
DGROUP |
.DATA |
_DATA |
WORD |
PUBLIC |
‘DATA’ |
DGROUP | |
.CONST |
CONST |
WORD |
PUBLIC |
‘CONST’ |
DGROUP | |
.DATA? |
_BSS |
WORD |
PUBLIC |
‘BSS’ |
DGROUP |
续上表
内存模式 |
段定义伪指令 |
段 名 |
定 位 |
组 合 |
类 别 |
组 名 |
SMALL/ME-DIUM |
.CODE |
NALTLC_TEXT① |
WORD |
PUBLIC |
‘CODE’ |
|
.DATA |
_DATA |
WORD |
PUBLIC |
‘DATA’ |
DGROUP | |
SMALL/ME-DIUM |
.CONST |
CONST |
WORD |
PUBLIC |
‘CONST’ |
DGROUP |
.DATA? |
_BSS |
WORD |
PUBLIC |
‘BSS’ |
DGROUP | |
.STACK |
STACK |
PARA |
STACK |
‘STACK’ |
DGROUP | |
COMPACT/ LARGE/HUGE |
.CODE |
NAME_TEXT② |
WORD |
PUBLIC |
‘CODE’ |
|
.FARDATA |
FAR_DATA |
PARA |
PRⅣATE |
‘FAR_DATA’ |
| |
.FARDATA? |
FAR_BSS |
PARA |
PRⅣATE |
‘FAR_BSS’ |
| |
.DATA |
_DATA |
WORD |
PUBLIC |
‘DATA’ |
DGROUP | |
.CONST |
CONST |
WORD |
PUBLIC |
‘CONST’ |
DGROUP | |
.DATA? |
_BSS |
WORD |
PUBLIC |
‘BSS’ |
DGROUP | |
.STACK |
STACK |
PARA |
STACK |
‘STACK’ |
DGROUP | |
FLAT |
.CODE |
_TEXT |
DWORD |
PUBLIC |
‘CODE’ |
|
.FARDATA |
DATA |
DWORD |
PUBUC |
‘DATA’ |
| |
.FARDATA? |
_BSS |
DWORD |
PUBLIC |
‘BSS’ |
| |
.DATA |
_DATA |
DWORD |
PUBLIC |
‘DATA’ |
| |
.CONST |
CONST |
DWORD |
PUBLIC |
‘CONST’ |
| |
.DATA? |
_BSS |
DWORD |
PUBLIC |
‘BSS’ |
| |
.STACK |
STACK |
DWORD |
PUBLIC |
‘STACK’ |
|
注:①表示SMALL模式的代码段段名没有可变部分name;②表示COMPACT模式的代码段段名没有可变部分name。
在中型模式、大型模式和巨型模式下,伪指令.CODE表示的默认段名为name TEXT,其中name是该段名的可变部分,表示程序模块的一个具体名字。
.DATA、.CONST、.DATA?和.STACK定义的段内数据存放在一个叫DGROUP的段组中。
.FARDATA或.FARDATA?使用的默认段名在各种模式下可以替换,它们定义的数据属于远程数据。
上述伪指令.DATA、.DATA?、.CONST、.FARDATA和.FARDATA?实际上定义的都是数据段,各自用于存放不同的数据。
(3)等价段名。
MASM 5.0宏汇编语言中规定了几个代替真实段名的等价段名,允许用@代替简化段定义伪指令前面的小数点。如@CODE代表.CODE定义的段名,@DATA代表.DATA、.DATA?、
.CONST和.STACK共享的组名,@FARDATA代表.FARDATA定义的段名等。
(4)定义段序伪指令。
MASM可以对汇编结果的目标文件中各段的位置进行排序。定义段序的伪指令有3种。
①.SEG:按照段在程序中出现的顺序对段排序,这是完整段定义格式中的默认顺序。
②.ALPHA:按照字母顺序对段排序。
③.DOSSEG:按照DOS定义的标准段序,即代码段、数据段、堆栈段的次序对段排序,这是用.MODEL伪指令的简化段定义格式中默认的顺序。
(1)EXE文件编程格式。
该格式下源程序允许使用多个逻辑段,适合编写大型程序,汇编、连接后生成扩展名为EXE的可执行文件。EXE文件编程格式的源程序最少有一个代码段。代码段的开始用一条ASSUME语句设置段约定。
在实模式下,每个逻辑段的目标模块最大64KB;在保护模式下,80286不超过16MB,32位的80x86不超过4GB。
ASM源程序汇编、链接后生成可执行文件,EXE文件有重定位信息。DOS装载.EXE文件,再根据重定位信息完成对目标模块的重定位,然后在目标程序的上方(低地址处)自动生成256个字节的程序段前缀(Program Segment Prefix,PSP),DOS通过PSP管理用户程序(如程序结束时返回DOS的途径等)。
DOS自动使DS=ES=PSP的段基址,FS=GS=0,CS:IP指向用户程序的启动地址,SS:SP指向用户堆栈的栈顶。然后DOS把控制权交给用户程序。由于DOS给DS、ES、FS、GS所赋的初值不等于用户程序数据段、附加段的段基址,因此用户程序在开始处必须对所用的段寄存器DS(及ES、FS、GS)等进行初始赋值。
(2)COM文件编程格式。
该格式只允许源程序设置一个代码段(堆栈在代码段内),代码段的目标模块要小于64KB,适合编写中小程序。在.MODEL TINY伪指令定义的内存模式下即可生成.COM结构的源程序。程序中使用的数据可集中设置在代码段的开始或末尾,程序的启动指令需要设置在代码段内偏移量为100H的单元处。
符合上述规定的编程格式,源程序汇编、链接后生成扩展名为COM的可执行文件。
COM文件没有重定位信息,比EXE文件要小得多。DOS装载COM文件时也在用户程序上方生成256个字节单元的程序段前缀PSP,PSP开始处存放一条中断指令INT 20H(2个字节)。DOS自动赋值使CS=DS=ES=SS-PSP的段基址、FS=GS=0,使IP=100H、SP=FFFEH,然后把控制权交给用户程序,CPU从CS:100H处开始执行用户程序。DOS在装载COM文件时自动在用户区高端(高地址处)设置堆栈,COM文件编程不需要设置堆栈。
常见可执行文件的执行级别从高到低依次是COM、EXE、BAT。可用EXE2BIN.EXE软件将汇编、链接生成的小于64KB的EXE文件转换成COM文件。
EXE2BIN<文件名[.EXE]><文件名.COM>
前一个文件名后缀.EXE可省略,但后一个文件名的后缀.COM必须写上,否则将生成.BIN文件(它不能装入执行)。
用高版本汇编TASM.EXE汇编ASM文件后,只需在链接(TLINK.EXE)时使用小写的“t”做连接参数就可直接生成COM文件。
宏指令使用之前应进行宏指令的定义,用来向汇编程序声明宏指令对应的一组指令。汇编程序对每一条宏指令汇编时,用它对应的一组指令代替,称为宏展开。
宠宏指令的格式如下:
宏指令名 MACRO[形式参数表]
…; 宏体(指令组)
ENDM
宏指令名是用户为这组指令起的一个名字,应满足标识符命名的一般规定。MACRO和ENDM是一对伪指令,表示宏定义的开始和结束。形式参数表中的参数可以为空(没有),也可以有多个,用逗号分隔。宏体则由指令、伪指令和前面已经定义的宏指令组成。
例如,定义一个宏,参数为0时执行输入的DOS功能调用,否则执行输出的DOS功能调用。
INOUT MACRO P
IF P
MOV AH,2
INT 21H ; 输出DI中的字符
ELSE
MOV AH,1
INT 21H ; 输入的字符送AL
ENDIF
ENDM
格式:<宏名>[实际参数表]
宏名必须是已定义的,实际参数表中参数的类型和顺序应与宏定义时的形式参数一一对应;实参个数多于形参时,多余的实参被忽略;实参个数少于形参的,多余的形参用空串取代。
汇编源程序时,汇编程序要对源程序中所有的宏调用进行展开,即将宏名所定义的指令序列插入到宏调用处。
宏与子程序(即过程)有所不同。宏调用是把宏体展开,程序中有几处宏调用就展开几次,源程序代码长,会浪费存储空间,但展开后执行速度快,节省运行时间;子程序调用不展开子程序代码(子程序仍存储在原处),但改变程序流程,由主程序调用处转入子程序去执行,执行完毕再返回主程序调用处继续执行,子程序调用前后要保护和恢复现场,因此执行时间长,但节省存储空间。
例如,对已定义的宏INOUT P,宏调用INOUT 0展开后为:
MOV AH,1 ; 输入字符到AL
INT 21H
宏调用INOUT 1 展开后为:
MOV AH,2 ; 输出DI中字符
INT 21H
当宏定义中出现了标号或变量的定义时,若该宏指令被程序多次调用,那么在宏展开后程序中会出现多个相同的标号或变量,这在汇编过程中会出现重复定义的错误。为解决这类问题,宏定义中应采用局部标号或变量。局部标号或变量由LOCAL伪指令定义。
格式:LOCAL标号1/变量1,标号2/变量2,……
伪指令LOCAL必须是伪指令MACRO后的第一条语句,并且在MACRO和LOCAL之间也不允许有注释和分号标志。
汇编程序在每次进行宏扩展时,总是把由LOCAL说明的标号用一个唯一的符号(从??0000~??FFFF)来代替,从而避免标号重定义的错误。
例如,定义一个求绝对值的宏。
Abs MACRO Num
CMP Num,0
JGE next
NEG Num
next:
ENDM
假设对宏Abs有以下两次调用:
Abs BX
Abs AL
则它们将会显示汇编程序对它们进行宏展开后所得到程序片段。
CMP BX,0
JGE next
NEG BX
next:
CMP AL,0
JGE next
NEG AL
next:
在上述片段中,显然标号next定义了两次,所以汇编程序将显示“标号重复定义”错误信息。为了避免这种情况发生,需要用下面的方法定义宏:
Abs MACRO Num
LOCAL next
CMP Num,0
JGE next
NEG Num
next:
ENDM
此时,汇编程序对它们进行宏展开时,将得到下列程序片段:
CMP BX,0
JGE next
NEG BX
?? 0000:
……
CMP AL,0
JGE next
NEG AL
?? 0001:
这样,汇编程序不会再显示“标号重复定义”错误信息。
在编写源程序时,有时会出现连续相同或相似的语句(组)。当出现这种情况时,可利用重复宏伪指令来重复语句,从而达到简化程序的目的。重复汇编伪指令所定义的重复块是宏的一种特殊形式,也是由伪指令ENDM来结束重复块。用重复汇编伪指令定义的重复块也可带有参数,在汇编过程中参数被实参代替,但重复块不会被命名,所以不能在程序的其他地方引用。重复汇编伪指令有如下3种。
(1)伪指令REPT。
其一般使用格式如下:
REPT数值表达式
; 宏体
ENDM
功能:重复地执行宏体中的语句,重复次数由数值表达式的值确定。
例如,定义100个初值分别为1,2,…,100的字节单元,该存储单元的起始符号地址为Table。这样的定义要求,按照前面所学的语句是难以实现的。有了重复宏伪指令,则可实现如下:
Table EQU THIS BYTE
COUNT=1
REPT 100
DB COUNT
COUNT=COUNT+1
ENDM
(2)伪指令IRP。
格式:
IRP形参,<实参1,实参2,……>
; 宏体
ENDM
功能:将宏体重复汇编,重复汇编次数由尖括号括起来的实参个数决定。每次重复汇编语句序列时,用一个实参替代形参。第一次用第一个实参,第二次用第二个实参,直到实参用完为止。
注意:实参必须用尖括号括起来,各个实参之间用逗号分开。如果整个实参为空,那么宏汇编程序便跳过IRP与ENDM之间的所有语句。
例如,用IRP重复汇编伪指令定义一个保存寄存器信息的宏定义。
PushReg MACRO text
IRP Re9, <text>
PUSH Reg
ENDM
ENDM
利用上述宏定义的宏调用:
PushReg<AX,BX,CX,DX>
在汇编源程序时,宏展开可得:
PUSH AX
PUSH BX
PUSH CX
PUSH DX
(3)伪指令IRPC。
格式:
IRPC形参, 字符串
; 宏体
ENDM
功能:将宏体重复汇编,重复次数由字符串中的字符个数确定。每次汇编宏体时,依次用字符串中的一个字符替代形参,直到字符串的字符替代完毕。例如前面PushReg宏定义是保留寄存器信息于堆栈中,现在我们编制一个用IRPC伪指令从堆栈中恢复寄存器内容的宏定义。
PopReg MACRO String
IRPC Reg,String
POP Reg&X
ENDM
ENDM
对上述宏定义的宏调用:
PopReg DCBA
在汇编期间,宏展开可得:
POP DX
POP CX
POP BX
POP AX
宏和子程序都是为了简化源程序的编写,提高程序的可维护性,但是两者之间存在着以下本质的区别:
(1)汇编程序对宏通过宏展开来加入其定义体,宏调用多少次,就相应展开多少次,所以,调用宏不会缩短目标程序;而子程序代码不论调用多少次,其在目标程序中只出现一次,因此,可简化相应的目标程序,从而节省存储空间。
(2)宏引用时,参数是通过“实参”替换“形参”的方式来实现传递的,参数形式灵活多样;而子程序调用时,参数是通过寄存器、堆栈或约定存储单元进行传递的。
(3)利用宏调用语句执行重复语句不会有额外的时间开销;而用子程序执行重复语句,子程序的调用和返回均需要时间。
总之,当程序片段不长,速度是关键因素时,可采用宏来简化源程序,但当程序片段较长,存储空间是关键因素时,可采用子程序的方法来简化源程序和目标程序。
宏汇编程序MASM提供了相应的宏操作符,用于宏参数的传递连接,如表10-3所示。
表10-3 宏操作符
格 式 |
功 能 |
&<形式参数> |
替换操作符。形参在其他字符前后或在带引号的字符串中时,用&放在参数之前(与其他字符分开),用实际参数替换&操作符后的形式参数。替换后移去& |
<<文本>> |
串传递。宏调用时,传递的文本中含有逗号、空格等间隔符时必须用尖括号括起来 |
!<字符> |
转义操作符。将!操作符后的字符以其原有的意义进行处理,而不含特殊意义 |
%<表达式> |
表达式操作符。宏调用时,把表达式的值作为实参传递,而不是表达式本身 |
;;<文本> |
宏注释符。将文本内容作为注释,它不被宏展开 |
当宏在程序中调用完之后而再也用不到时,可用PURGE伪指令删除之,以释放它所占用的存储空间。
格式:PURGE<宏名>[,宏名…]
宏定义没有全局的概念,对于许多程序都要用到的通用的宏定义,也都要在相应的程序中进行定义,这增加了编程与操作人员的负担。为避免重复定义的麻烦,可将通用的宏定义集中放在一个单独的文件中,即放在后缀为.MAC的宏库文件中,宏库中可以有多个宏定义。
可用任一个编辑程序像建立源程序那样来建立宏库。宏库中的宏必须是通用的例行程序或程序段,对宏中使用的寄存器及状态标志要入栈保存,宏定义中的标号要先用LOCAL伪指令定义(限定只在宏内使用)。
在程序中可用INCLUDE伪指令将宏库中的宏定义复制到该指令所在的位置。
INCLUDE伪指令格式: INCLUDE[盘符:\路径\]<文件名>[.扩展名]
功能是通知汇编程序把指定的文件复制一份,插入到该语句下方,供汇编使用。如INCLUDE MLIB.MAC语句将宏库中MLIB.MAC的全部宏定义复制出来。一般INCLUDE伪指令也放在源程序的开始部分。
为避免汇编程序两次扫描都去重复读宏库文件,可用条件汇编IF1使宏库文件只在第一遍扫描时读入。
IF1
INCLUDE MLIB.MAC
MASM 6.11系统定义了大量的标准宏,程序员能很方便地使用它们。主要的系统宏库文件有DOS.inc和BIOS.inc,它们存放在系统的include子目录中。
条件汇编指汇编程序根据条件的不同汇编不同的程序段。在汇编语言源程序有条件汇编语句的位置上,宏汇编程序首先测试语句指定的条件。如果条件成立,便汇编指定的程序段,产生目标代码;如果条件不成立,便舍去指定的程序段。
格式:
IFnnnn条件表达式
语句组1
[ELSE语句组2]
ENDIF
其中:IFnnnn是表10-4中的伪指令。
功能:若条件汇编伪指令后面的“条件表达式”为真,那么,语句组1将被汇编;否则,语句组2将被汇编(如果含有ELSE伪指令)。
语句组1或语句组2内还可以包含有条件汇编伪指令,这时就形成了嵌套的条件汇编伪指令。一个嵌套的ELSE伪指令总是与最近的IFnnnn伪指令相匹配。
每条条件汇编伪指令的具体含义如表10-4所示。
表10-4 条件汇编伪指令
伪指令 |
汇编条件 |
伪指令 |
汇编条件 |
IF 1 IF 2 IF表达式 IFE表达式 IFDEF符号 |
在第一遍扫描时 在第二遍扫描时 表达式不等于0 表达式等于0 符号已定义 |
IFNDEF符号 IF变量 IFNB变量 IFIDN变量1,变量2 IFNIDN变量1,变量2 |
符号未定义 变量是空格 变量不是空格 变量1和变量2相同 变量1和变量2不相同 |
例如,有如下条件汇编时使用:
incre MACRO sour,dest
IFE 1_TypeSour
INC sour
MOV AL,sour
MOV dest,AL
ENDIF
IFE 2_TypeSour
ADD sour,2
MOV AX,sour
MOV dest,AX
ENDIF
IFE4_TypeSour
ADD WORD PTR sour,4
ADC WORD PTR sour+2,0
MOV AX,WORD PTR sour
MOV WORD PTR dest,AX
MOV AX,WORD PTR sour+2
MOV WORD PTR dest+2,AX
ENDIF
ENDM
上面的宏定义根据源操作数sour是由DB,DW,DD定义数据的情况,分别对它加1、加2或加4,然后把相加后的结果再传送给目的操作数dest。如何判断定义数据的大小呢?因为若用DB定义数据,则1_TypeSour=0,若用DW定义数据,则2_TypeSour=0,若用DD定义数据,则4_TypeSour=0。当表达式等于0时,条件汇编伪指令IFE就会汇编指定的语句序列,否则就不汇编。