概述
条件汇编是汇编器在汇编阶段,依据所设定的条件,使汇编器汇编某一段程序,或不汇编某一段程序。一般而言,条件汇编常配合宏使用,使得撰写汇编语言源文件能有初步的结构化 (注一 )。底下小木偶使用条件汇编与宏配合而写成的一个简单程序:EXAM05.ASM。
这个例子是先在屏幕上显示『计算:2593 8888 = 』字串,然后再显示结果来。这是一个是很简单的例子,但是小木偶要用一个宏来解决显示字串及数字这两种不同的数据形态。方法就是在宏中加入能自动判断输入的引数是字串或是数字的伪指令。
这两者的分别在高级语言是泾渭分明的,在汇编语言中,这两者都是以二进制数字表示,分别并不是那么明显。EXAM05.ASM 在处理文字与数字的程序码当然也是不一样的。假如数据是文字的话,只需打印即可,如果是数字的话,就得换成十进制再显示出来。但是在汇编语言里很难真的判断数据是文字抑或数字。
TYPE 运算子
小木偶的想法很简单,用 TYPE 运算子这个伪指令来判断该数据是字串或是数字。TYPE 伪指令的用法是:
TYPE 变量
TYPE 会根据变量的定义决定运算结果,假如变量是以『DB』定义,则结果为 1;以『DW』定义,结果为 2;以『DD』定义,结果为 4;以『DQ』定义,结果为 8;以『DT』定义,结果为 10。在一般高级语言里,很少变量只占一个字节,而汇编语言则不一定,但是小木偶在想不出其他更好的方法,只有用 TYPE 来判断数据形态。汇编语言里,定义一个字串,常用『DB』,定义变量,常用『DW』或『DQ』,因此如果 TYPE 传回来的值为 1,则引数应该是字串,反之则否。
IF - ELSE - ENDIF 条件汇编伪指令
解决了判断数据的形态之后,接下来就是依据数据形态决定汇编那一段源程序,也就是这一章的主角,IF - ELSE - ENDIF 伪指令,它的格式是
IF 判断式 叙述一 [ELSE 叙述二] ENDIF
假如判断式为真,汇编器就会汇编『叙述或指令一』内的指令;假如判断式为伪,汇编器会汇编『叙述二』内的指令。假如判断式为伪时,并没有指令需要执行,那也可以省略叙述二,省略时,必须由 ELSE 到叙述二为止的部分省略,ENDIF 是用来表示 IF 叙述结束的,是不可省略的。叙述一或叙述二可以是由很多指令或是叙述组成。
一般而言,判断式大部分是两个数值之比较,比较结果为真,则传回 0FFFFH,比较结果为否,则传回 0,汇编器依据 0FFFFH 或 0 来汇编那一个程序片段。而比较的两个数值必须是在汇编阶段就能够确定大小的数值,因此不可以使用寄存器或变量,而像数据长度,或是地址都是可以拿来作为比较的数值。下表表示能用在判断式的关系运算子:
运算子 | 实例 | 说明 |
EQ | var1 EQ var2 | 若 var1 等于 var2 时,为真 |
NE | var1 NE var2 | 若 var1 不等于 var2 时,为真 |
LT | var1 LT var2 | 若 var1 小于 var2 时,为真 |
LE | var1 LE var2 | 若 var1 小于或等于 var2 时,为真 |
GT | var1 GT var2 | 若 var1 大于 var2 时,为真 |
GE | var1 GE var2 | 若 var1 大于或等于 var2 时,为真 |
NOT | NOT var | 若 var 为伪时,为真 |
AND | var1 AND var2 | 若 var1、var2 皆为真时,为真 |
OR | var1 OR var2 | 若 var1、var2 中有一为真时,为真 |
XOR | var1 XOR var2 | 若 var1 为真且 var2 为伪,或 var1 为伪且 var2 为真时,为真 |
数值 | var | 若 var 不为零时,为真 |
源程序
底下就是 EXAM05.ASM 程序列表:
include mymac.inc ;01 载入 MYMAC.INC 宏程序库 purge display ;02 除去 DISPLAY 宏 .286 ;03 使用 80286 指令集 print_number macro ;;05 此宏用来把 DL 里的数值以 ASCII 字符 add dl,'0' ;;06 方式显示于萤光幕,显示前 DL 应该在 0 mov ah,2 ;;07 到 9 之间。 int 21h endm ;;09 结束 print_number 宏 print macro var ;;11 print 宏开始 local tmp_var,nxt if (type var) eq 1 ;;13 code segment para public 'code' mov dx,offset var mov ah,9 int 21h code ends exitm endif ;;20 if (type var) eq 2 ;;21 data segment para public 'data' tmp_var dt ? data ends code segment para public 'code' fild var fbstp tmp_var mov si,offset tmp_var 2 mov dl,[si] print_number ;;30 调用 print_number 宏 mov cx,2 nxt: dec si mov dl,[si] shr dl,4 print_number mov dl,[si] and dl,0fh print_number loop nxt code ends exitm endif ;;42 endm ;;43 print 宏结束 ;*************************************** data segment para public 'data' string db '计算: 2593 8888 =
print 宏在逻辑上可分成两部分,判断数据形态及依据数据形态如何处理这两部分。就前者而言,第 13 行和第 21 行这两行就是判断引数之数据形态是字串抑或字整数;就后者而言,假如是字串的话,汇编器将汇编第 14 行到第 20 行,假如是字整数的话,汇编器将汇编第 22 到第 41 行。
在 MASM 5.0 及其以后的版本,一个宏里面是可以再使用另一个宏,像这种,宏里面又有宏的情形称为『巢状』,像第 30 行、第 35 行及第 38 行都是在 print 宏里再使用一个宏,这是可以被允许的。MASM 并没有限制巢状宏的层数,只要存储器及堆栈不被使用完即可。
其他条件汇编指令
IFE 伪指令
MASM 所提供的条件汇编叙述,除了 IF - ELSE - ENDIF 之外,还有好几个,它们都可以配合 ELSE 使用,并且似乎『成双成对』。小木偶的意思是,IF 是当条件为真时,汇编 IF 之后的叙述或指令,而还有一个 IFE 伪指令与之配对,IFE 是指当条件为伪时,汇编 IFE 之后的叙述或指令。
IF1 与 IF2 伪指令
IF1、IF2 是测试目前的汇编步骤。MASM 是两阶段 ( 注二 )的汇编器,IF1 与 IF2 就是分别只在第一阶段汇编或第二阶段汇编才汇编的条件汇编伪指令。一般而言,宏只需汇编一次,所以可以用 IF1 来增快汇编速度。这两个伪指令的语法是:
IF1 叙述1 [ELSE 叙述2] ENDIF
IF2 也是一样,都不需要测试条件,因为都已经写在 IF 之后了。
IFDEF 与 IFNDEF 伪指令
IFDEF 伪指令是用来测试其后的变量或标号等符号是否经过定义,如果是的话才汇编;而 IFNDEF 则是未定义才汇编。其语法是:
IFDEF 符号名 叙述1 [ELSE 叙述2] ENDIF
但这个指令却有令人不解的地方。假如某个符号在 IFDEF 之后才定义,在第一阶段汇编 ( 注二 ) 时,当然是还未定义,但第二阶段汇编时就是已定义了;又如果该符号在 IFDEF 之前就已经定义了,不管第一阶段或第二阶段汇编都是已定义,所以照这样看来,似乎都得汇编 IFDEF 之后的叙述了,这样 IFDEF、IFNDEF 岂不是根本就毫无用处?
原来要使用 IFDEF 或 IFNDEF 有两个方法可供使用,一是配合前面的 IF1 或 IF2 使用,另一种方法是根本就不要在源程序中定义该符号,等到要使用时,再于 DOS 命令提示下输入 MASM 的参数『/D』来定义该变量,例如底下这个程序,SUM02.ASM,是计算由一开始,公差为一的等差数列之和,至于最末一项是什么,则是由『/D』参数后面的定义来决定,如果没以『/D』参数定义最末项,则设为 100。
last_number macro ;;01 是否定义最后一数 ifdef number n dw number ;;02 是,则以选项定义为准 else n dw 100 ;;03 否,则加到 100 endif endm ;*************************************** code segment assume cs:code,ds:code org 100h ;--------------------------------------- start: jmp short begin last_number ;15 定义最后一项之值 counter dw 1 ;16 计数器 string db "1 2 ... ___FCKpd___5quot; ;17 显示的字串 begin: sub bx,bx ;18 BX 为和 mov cx,n ;19 CX 为项数 next: add bx,counter ;20 相加循环 inc counter loop next mov dx,offset string mov ah,9 int 21h push bx ;27 保存和 mov ax,n ;28 显示最后一项 call display_ax mov dl,'=' ;30 显示等号 mov ah,2 int 21h pop ax ;34 显示和 call display_ax mov ax,4c00h ;36 结束 int 21h ;--------------------------------------- ;39 AL 之数值为十进制之个位数,此子程序将加上 30h ;40 使之成为 ASCII 字符,显示屏幕上 display_decimal proc near cmp n_zero,0 ;42 检查 jnz dply or al,al ;44 检查最高位数是否为零 jz exit ;45 若是,则不显示出来 or n_zero,1 ;46 若否,则显示出来并且使 n_zero 设为一 dply: push ax push dx mov dl,al add dl,'0' mov ah,2 int 21h pop dx pop ax exit: ret n_zero db 0 ;56 n_zero 为一个标志,若是最高位数为 display_decimal endp ;57 零则为一,依次递减直到最高位数不为 ;-------------------------------;58 零时,n_zero 才设为一 ;59 把 AX 内的十六进制数值,以十进制方式显示屏幕上 display_ax proc near sub dx,dx mov bx,10000 mov n_zero,dl div bx call display_decimal mov ax,dx mov bx,1000 sub dx,dx div bx call display_decimal mov ax,dx mov bl,100 div bl mov dl,al call display_decimal mov al,ah cbw mov bl,10 div bl mov dl,al call display_decimal mov dl,ah add dl,'0' mov ah,2 int 21h ret display_ax endp ;--------------------------------------- code ends ;*************************************** end start
汇编 SUM02.ASM 可以像以前一样直接于 DOS 提示号下『MASM SUM02;』即可,这样的话执行结果会是『1 2 ... 100=5050』。但您也可以指定末项为其他数,执行结果会不同喔。例如:
H:/HomePage/SOURCE>masm /Dnumber=200 sum02; [Enter] Microsoft (R) Macro Assembler Version 5.00 Copyright (C) Microsoft Corp 1981-1985, 1987. All rights reserved. 51502 418690 Bytes symbol space free 0 Warning Errors 0 Severe Errors H:/HomePage/SOURCE>link sum02; [Enter] Microsoft (R) Personal Computer Linker Version 2.40 Copyright (C) Microsoft Corp 1983, 1984, 1985. All rights reserved. Warning: no stack segment H:/HomePage/SOURCE>exe2bin sum02 sum02.com [Enter] H:/HomePage/SOURCE>sum02 [Enter] 1 2 ... 200=20100 H:/HomePage/SOURCE>
注意到 MASM 汇编器白色部分的参数选项,改变其值就会造出不同的执行文件来。
IFB 与 IFNB 伪指令
这两个伪指令的语法是:
IFB <引数> IFNB <引数>
IFB 是用来测试是否有引数传到宏中,如果没有引数的话 ( B 是指空格,blank,的意思,即没有引数 ),则汇编。而 IFNB 则是有引数 ( NB 是指不空格,即有引数 ),则汇编。这样说,您可能还是不懂,待小木偶举个例子吧,底下这个宏,push_reg,可以把好几个寄存器推入堆栈,直到没有指定的寄存器可推入,而推入堆栈的寄存器数目可以不固定且可以是任意十六位的寄存器。
push_reg MACRO reg_string IRP reg,<reg_string> IFNB <reg> push reg ENDIF ENDM ENDM
使用这个宏时,输入之参数必须以角括号包围起来,例如在程序中用
push_reg <ax,bx,si>
来使用此宏,因为角括号的关系,MASM 会把输入的 ax,bx,si 当做一个字串传入 push_reg 宏,宏的主要内容是一个不定重复块,该不定重复块的引数就是刚刚传入宏的字串,而后依次取出一个寄存器推入堆栈,直到寄存器都被提出为止。如何检查寄存器全都被提出了呢?就是用 IFNB 来检查,当还有寄存器未被提出时,IFNB 为真,汇编 push reg 这一行,若为伪时,则汇编 exitm,就跳出宏了。
IFIDN 和 IFDIF 伪指令
其语法是
IFIDN <引数1>,<引数2> IFDIF <引数1>,<引数2>
IFIDN 是用来比较引数1 和引数2 是否相同,IDN 是 identical 之意,假如相同则汇编。IFDIF 则是用来比较引数1 和引数2 是否不同,DIF 是 different 之意,假如不同则汇编。这些引数都必须用角括号包住,并以『,』隔开。
IFIDN 和 IFDIF 比较时,会考虑英文字母的大小写,意思是,AX 和 ax 被视为不同的字串;如果要忽略大小写,则可以用 IFIDNI 和 IFDIDI,这最后的 I 字母表示忽略之意。
范例:通用的推入堆栈宏
8086 指令的 PUSH 只能把十六位的寄存器或十六位变量推入堆栈,不能把十六位立即值 (常数) 或八位的寄存器推入堆栈,而底下这个宏范例,push_op,也能使立即值或八位的寄存器推入堆栈。底下是 push_op 源代码:
page ,132 ;01 push_op MACRO arg ;;03 reg16 = 0 ;;05 reg08 = 0 ;;06 addr = 0 ;;07 ;;09 检查输入参数是否为 16 位的寄存器 IRP reg,<AX,BX,CX,DX,CS,DS,ES,SS,SI,DI,BP,SP,ax,bx,cx,dx,cs,ds,es,ss,si,di,bp,sp> IFIDN <reg>,<arg> push arg ;;12 如果相等的话,推入堆栈 reg16 = 0ffffh ;;13 数定虚拟变量为真 exitm ;;14 跳出 IRP 块 ENDIF ENDM IF reg16 ;;17 若 reg16 为真 exitm ;;18 则跳出 push_op 宏 ENDIF ;;21 检查输入参数是否为 16 位的寄存器 IRP reg,<aX,bX,cX,dX,cS,dS,eS,sS,sI,dI,bP,sP,Ax,Bx,Cx,Dx,Cs,Ds,Es,Ss,Si,Di,Bp,Sp> IFIDN <reg>,<arg> push arg reg16 = 0ffffh exitm ENDIF ENDM IF reg16 exitm ENDIF ;;33 检查输入参数是否为 8 位的寄存器 IRP reg,<al,bl,cl,dl,ah,bh,ch,dh,AH,BH,CH,DH,AL,BL,CL,DL> IFIDN <reg>,<arg> reg08 = 0ffffh exitm ENDIF ENDM IF reg08 IRPC char,arg ;;41 取得寄存器名的第一个字母 push char&&x ;;42 推入堆栈 exitm ;;43 跳出 IRPC 块 ENDM exitm ;;45 跳出 push_op 宏 ENDIF ;;48 检查输入参数是否为 8 位的寄存器 IRP reg,<Al,Bl,Cl,Dl,Ah,Bh,Ch,Dh,aL,bL,cL,dL,aH,bH,cH,dH> IFIDN <reg>,<arg> reg08 = 0ffffh ENDIF ENDM IF reg08 IRPC char,arg push char&&x exitm ENDM exitm ENDIF ;;62 检查输入参数是否为含有寄存器间接寻址模式,即 [BX]、[SI]……等等 IRPC char,arg IF ('&char' eq '[') addr=0ffffh exitm ENDIF ENDM IF addr push arg exitm ENDIF arg_size=((type arg) 1)/2 ;;74 输入参数之长度 arg_type=(.type arg) and 3 ;;75 输入参数之类型 ;;77 检查输入参数是否为变量 IF arg_type eq 2 arg_offset =0 REPT arg_size arg_address=word ptr arg arg_offset push arg_address arg_offset=arg_offset 2 ENDM exitm ENDIF ;;88 检查输入参数是否为标号 IF arg_type eq 1 push bp mov bp,sp push ax mov ax,offset arg xchg ax,[bp] mov bp,ax pop ax exitm ;;98 若不是寄存器、寻址模式、变量、标号的话,应为立即值 ELSE push bp mov bp,sp push ax mov ax,arg xchg ax,[bp] mov bp,ax pop ax ENDIF ENDM
这个宏结构很明显,先检查要推入堆栈的参数是否为 16 位寄存器( 第 9 行到第 31 行 ),如果不是再检查是否为 8 位寄存器 ( 第 33 行到第 60 行 ),如果不是寄存器的话,再检查是否为寄存器间接寻址 ( 第 62 行到第 72 行 ),如果不是以上这几种的话,再检查是否推入变量到堆栈 ( 第 77 行到第 87 行 ),接下来检查是否推入标号到堆栈 ( 第 88 行到第 97 行 ),假如都不是上述情形的话,就是推入立即值到堆栈了 ( 第 98 行到第 107 行 )。
检查是否为寄存器的方法是用不定重复块 ( IRP ) 来指定要比较的范围,故引数列 ( 即第 10 行角括号内的引数 ) 包含所有 16 位寄存器名称,但是因为参数与引数都被视为字串,所以大小写是有差别的,必须在引数列里包含不同的大小写排列方式。指定好比较范围后,再用 IFIDN 比较输入参数是否为引数列中的一个,假如是 16 位寄存器的话,则直接把该参数推入堆栈即可,并设定一个虚拟变量,reg16,为 0ffffh,0ffffh 表示真的意思 ( 第 12、13 行 )。然后再跳出 push_op 堆栈。
小木偶再把 IRP 重复块的执行方式说明一遍。第 10 行到第 16 行程序码为:
IRP reg,<AX,BX,CX,DX,CS,DS,ES,SS,SI,DI,BP,SP,ax,bx,cx,dx,cs,ds,es,ss,si,di,bp,sp> IFIDN <reg>,<arg> push arg ;;12 如果相等的话,推入堆栈 reg16 = 0ffffh ;;13 数定虚拟变量为真 exitm ;;14 跳出 IRP 块 ENDIF ENDM IF reg16 ;;17 若 reg16 为真 exitm ;;18 则跳出 push_op 宏 ENDIF
表示在第 10 行到第 16 行程序码会重复汇编。第一次时,reg 会以 AX 代入汇编,第 11 行是比较 arg 与 reg 这两字串是否相等,如果相等则汇编第 12 行到第 14 行之间的程序码,不相等则结束 IFIDN,然后遇到 ENDM,故重复第二次,使 reg 以 BX 代入汇编……一直到 sp 所有引数结束。
第 14 行,是因为假如已经找到相符合的寄存器,就没必要再比较了,这样可以加快汇编速度。( 虽然也没快多少。) 第 17 行到第 19 行,也是这样的道理,既已找到是把寄存器推入堆栈,也就没必要汇编以下的程序了,故直接跳出 push_op 堆栈。
您可能会问,第 14 行就已有了 exitm,为何第 18 行还要有个 exitm 呢?这是因为 RPT、IRP、IRPC 这三个重复块,类似宏结构,若要在中间停止汇编都可以用 exitm 来跳出宏或块,所以第 14 行是跳出 IRP 块,第 17 行是跳出 push_op 宏。
第 33 行到第 61 行,是检查参数是否为 8 位寄存器,方法和上述几乎相同,差别在于 8 位寄存器 ( 例如 ah ) 无法推入堆栈必须改成 16 位寄存器 ( 例如 ax )。所以第 41 行到第 44 行多了个 IRPC 重复块,此重复块是为了取得寄存器的第一个字母,当取得第一个字母就把该字母加上『x』再推入堆栈,然后跳出 IRPC 块及 push_op 宏。至于该 IRPC 重复块的运作方式如下:该 IRPC 块重复次数只有两次,分别以 8 位寄存器名称的两个字母代入汇编,当第一次时即以 8 位寄存器名称的第一个字母代入,然后加上『x』再推入堆栈,然后立刻跳出 IRPC 块,故事实上这个重复块只汇编一次而已。
第 42 行的『&&x』为何要有两个『&』呢?这是因为根据 MASM 手册上说每层块要使用『&』,故第二层要用两个『&』。
第 62 行到第 73 行是用来检查推入堆栈的参数是否为寄存器间接寻址,寄存器间接寻址模式是像底下的样子:
mov ax,[bx] push [si] sub ax,[bx 200h]
观察以上几个例子,您会发现,这种寻址模式含有两个中括号,因此检验方法就是以 IRPC 检查参数中是否有『[』( 第 64 行到第 67 行的 IF 条件汇编 ),假如有的话,会使虚拟变量,addr,设为真。然后接下来的就直接使该参数推入堆栈,因为 PUSH 指令就可以直接推入寄存器间接寻址模式。
接下来就只剩下变量、标号与立即值未处理,要区别前两者可用 MASM 所提供的 .TYPE 运算子。
.TYPE 运算子
.TYPE 和 TYPE 不同,TYPE 已在稍前说明过了,这儿小木偶只说明 .TYPE 的用法:( .TYPE 前有个小数点,不可省略 )
.TYPE 运算式
.TYPE 运算会根据运算式传回一个字节大小的数据,假如运算式不合法,则传回零;如果合法,所传回的字节只有第 0、1、5、7 位有意义,其他位均为零,这四个位所代表的意义如下表:
位 该位为零 该位为一 说明 ------------------------------------------------------------ 0 与程序无关 与程序有关 与程序有关是指标号……等 1 与数据无关 与数据有关 与数据有关是指变量……等 5 未定义 已定义 7 区域性或公共性 外部的
当第 75 行的虚拟变量,arg_type,为 2 时,表示为变量,由第 79 行到第 85 行的程序处理;若 arg_type 为 1 时,表示为标号,由第 90 到第 97 行的程序处理;若不为 1 也不为 2,表示为立即值,由第 100 行到第 106 行的程序处理。
处理变量时,不只要能处理字变量,也为了要能处理双字、四字等类型的变量,帘狴H先取得变量长度,再除以 2,就能求出推入堆栈的次数,而每次推入堆栈时地址都得增加 2,这些细节都可在第 79 行到第 85 行 IF-ENDIF 之间的程序处理。
处理立即值的方式很特别,小木偶为了只把立即值推入堆栈,并且使所有寄存器都不改变,,当然只有 SP 寄存器会因为推入了一个立即值而减少二。为了达到上面的目的,写了第 100 行到第 106 行的程序,虽然有点儿复杂,但应该不太难懂。处理堆栈其实就是这样。而处理标号的方法和立即值相似,因为标号其实就是一个立即值,他表示程序地址。
注一:古早以前,写程序,尤其是利用 BASIC 撰写的程序,常常因为条件跳转(IF-ELSE-THEN)即无条件跳转(GOTO)使得源程序被切割成支离破碎,很不易维护。因此后来有许多程序设计师,不再滥用 GOTO 指令,遇到条件跳转时,使条件为真儿要执行的指令包含在一个块中,不用执行的指令包含在另一个块中,并大量用子程序,这样就使得源程序较易维护。PASCAL、C、C 这些语言就是属于结构化的语言。
注二:MASM 汇编源文件时,是分两次汇编的 ( two pass ),要这么做的原因是这样的,请看以下说明。当 MASM 开始汇编时先读入源文件,由上而下汇编,如果遇到尚未定义的标号、变量等,MASM 会先假设,而预留下一些空间给这些未定义的数据,当读到源程序后面时,MASM 发现这些未定义的标号、变量在后面定义,于是当第二次汇编时,再把这些先前假设的地址或长度修改成正确的数值。
当然如果先前假设的正确就没有问题,如果假设错误的话,可以分为两种情形。第一种情形是 MASM 所假设的空间或长度比所定义的来得大或多,那第二阶段汇编时,MASM 会把多余的空间以 NOP 指令取代。假如所假设的空间或长度比所定义的来得小或少的话,那就会产生错误,这就是所谓的『相位错误』(Phase error between passes)。
NOP 指令
这是一个 8086 指令集的其中一个指令,它的功用只是让 CPU 空转一个时脉,并不做任何事。
sum dw 2593 8888 data ends ;*************************************** initial print string print sum exit 0 end startprint 宏在逻辑上可分成两部分,判断数据形态及依据数据形态如何处理这两部分。就前者而言,第 13 行和第 21 行这两行就是判断引数之数据形态是字串抑或字整数;就后者而言,假如是字串的话,汇编器将汇编第 14 行到第 20 行,假如是字整数的话,汇编器将汇编第 22 到第 41 行。
在 MASM 5.0 及其以后的版本,一个宏里面是可以再使用另一个宏,像这种,宏里面又有宏的情形称为『巢状』,像第 30 行、第 35 行及第 38 行都是在 print 宏里再使用一个宏,这是可以被允许的。MASM 并没有限制巢状宏的层数,只要存储器及堆栈不被使用完即可。
其他条件汇编指令
IFE 伪指令
MASM 所提供的条件汇编叙述,除了 IF - ELSE - ENDIF 之外,还有好几个,它们都可以配合 ELSE 使用,并且似乎『成双成对』。小木偶的意思是,IF 是当条件为真时,汇编 IF 之后的叙述或指令,而还有一个 IFE 伪指令与之配对,IFE 是指当条件为伪时,汇编 IFE 之后的叙述或指令。
IF1 与 IF2 伪指令
IF1、IF2 是测试目前的汇编步骤。MASM 是两阶段 ( 注二 )的汇编器,IF1 与 IF2 就是分别只在第一阶段汇编或第二阶段汇编才汇编的条件汇编伪指令。一般而言,宏只需汇编一次,所以可以用 IF1 来增快汇编速度。这两个伪指令的语法是:
___FCKpd___3
IF2 也是一样,都不需要测试条件,因为都已经写在 IF 之后了。
IFDEF 与 IFNDEF 伪指令
IFDEF 伪指令是用来测试其后的变量或标号等符号是否经过定义,如果是的话才汇编;而 IFNDEF 则是未定义才汇编。其语法是:
___FCKpd___4
但这个指令却有令人不解的地方。假如某个符号在 IFDEF 之后才定义,在第一阶段汇编 ( 注二 ) 时,当然是还未定义,但第二阶段汇编时就是已定义了;又如果该符号在 IFDEF 之前就已经定义了,不管第一阶段或第二阶段汇编都是已定义,所以照这样看来,似乎都得汇编 IFDEF 之后的叙述了,这样 IFDEF、IFNDEF 岂不是根本就毫无用处?
原来要使用 IFDEF 或 IFNDEF 有两个方法可供使用,一是配合前面的 IF1 或 IF2 使用,另一种方法是根本就不要在源程序中定义该符号,等到要使用时,再于 DOS 命令提示下输入 MASM 的参数『/D』来定义该变量,例如底下这个程序,SUM02.ASM,是计算由一开始,公差为一的等差数列之和,至于最末一项是什么,则是由『/D』参数后面的定义来决定,如果没以『/D』参数定义最末项,则设为 100。
___FCKpd___5
汇编 SUM02.ASM 可以像以前一样直接于 DOS 提示号下『MASM SUM02;』即可,这样的话执行结果会是『1 2 ... 100=5050』。但您也可以指定末项为其他数,执行结果会不同喔。例如:
___FCKpd___6
注意到 MASM 汇编器白色部分的参数选项,改变其值就会造出不同的执行文件来。
IFB 与 IFNB 伪指令
这两个伪指令的语法是:
___FCKpd___7
IFB 是用来测试是否有引数传到宏中,如果没有引数的话 ( B 是指空格,blank,的意思,即没有引数 ),则汇编。而 IFNB 则是有引数 ( NB 是指不空格,即有引数 ),则汇编。这样说,您可能还是不懂,待小木偶举个例子吧,底下这个宏,push_reg,可以把好几个寄存器推入堆栈,直到没有指定的寄存器可推入,而推入堆栈的寄存器数目可以不固定且可以是任意十六位的寄存器。
___FCKpd___8
使用这个宏时,输入之参数必须以角括号包围起来,例如在程序中用
___FCKpd___9
来使用此宏,因为角括号的关系,MASM 会把输入的 ax,bx,si 当做一个字串传入 push_reg 宏,宏的主要内容是一个不定重复块,该不定重复块的引数就是刚刚传入宏的字串,而后依次取出一个寄存器推入堆栈,直到寄存器都被提出为止。如何检查寄存器全都被提出了呢?就是用 IFNB 来检查,当还有寄存器未被提出时,IFNB 为真,汇编 push reg 这一行,若为伪时,则汇编 exitm,就跳出宏了。
IFIDN 和 IFDIF 伪指令
其语法是
___FCKpd___10
IFIDN 是用来比较引数1 和引数2 是否相同,IDN 是 identical 之意,假如相同则汇编。IFDIF 则是用来比较引数1 和引数2 是否不同,DIF 是 different 之意,假如不同则汇编。这些引数都必须用角括号包住,并以『,』隔开。
IFIDN 和 IFDIF 比较时,会考虑英文字母的大小写,意思是,AX 和 ax 被视为不同的字串;如果要忽略大小写,则可以用 IFIDNI 和 IFDIDI,这最后的 I 字母表示忽略之意。
范例:通用的推入堆栈宏
8086 指令的 PUSH 只能把十六位的寄存器或十六位变量推入堆栈,不能把十六位立即值 (常数) 或八位的寄存器推入堆栈,而底下这个宏范例,push_op,也能使立即值或八位的寄存器推入堆栈。底下是 push_op 源代码:
___FCKpd___11
这个宏结构很明显,先检查要推入堆栈的参数是否为 16 位寄存器( 第 9 行到第 31 行 ),如果不是再检查是否为 8 位寄存器 ( 第 33 行到第 60 行 ),如果不是寄存器的话,再检查是否为寄存器间接寻址 ( 第 62 行到第 72 行 ),如果不是以上这几种的话,再检查是否推入变量到堆栈 ( 第 77 行到第 87 行 ),接下来检查是否推入标号到堆栈 ( 第 88 行到第 97 行 ),假如都不是上述情形的话,就是推入立即值到堆栈了 ( 第 98 行到第 107 行 )。
检查是否为寄存器的方法是用不定重复块 ( IRP ) 来指定要比较的范围,故引数列 ( 即第 10 行角括号内的引数 ) 包含所有 16 位寄存器名称,但是因为参数与引数都被视为字串,所以大小写是有差别的,必须在引数列里包含不同的大小写排列方式。指定好比较范围后,再用 IFIDN 比较输入参数是否为引数列中的一个,假如是 16 位寄存器的话,则直接把该参数推入堆栈即可,并设定一个虚拟变量,reg16,为 0ffffh,0ffffh 表示真的意思 ( 第 12、13 行 )。然后再跳出 push_op 堆栈。
小木偶再把 IRP 重复块的执行方式说明一遍。第 10 行到第 16 行程序码为:
___FCKpd___12
表示在第 10 行到第 16 行程序码会重复汇编。第一次时,reg 会以 AX 代入汇编,第 11 行是比较 arg 与 reg 这两字串是否相等,如果相等则汇编第 12 行到第 14 行之间的程序码,不相等则结束 IFIDN,然后遇到 ENDM,故重复第二次,使 reg 以 BX 代入汇编……一直到 sp 所有引数结束。
第 14 行,是因为假如已经找到相符合的寄存器,就没必要再比较了,这样可以加快汇编速度。( 虽然也没快多少。) 第 17 行到第 19 行,也是这样的道理,既已找到是把寄存器推入堆栈,也就没必要汇编以下的程序了,故直接跳出 push_op 堆栈。
您可能会问,第 14 行就已有了 exitm,为何第 18 行还要有个 exitm 呢?这是因为 RPT、IRP、IRPC 这三个重复块,类似宏结构,若要在中间停止汇编都可以用 exitm 来跳出宏或块,所以第 14 行是跳出 IRP 块,第 17 行是跳出 push_op 宏。
第 33 行到第 61 行,是检查参数是否为 8 位寄存器,方法和上述几乎相同,差别在于 8 位寄存器 ( 例如 ah ) 无法推入堆栈必须改成 16 位寄存器 ( 例如 ax )。所以第 41 行到第 44 行多了个 IRPC 重复块,此重复块是为了取得寄存器的第一个字母,当取得第一个字母就把该字母加上『x』再推入堆栈,然后跳出 IRPC 块及 push_op 宏。至于该 IRPC 重复块的运作方式如下:该 IRPC 块重复次数只有两次,分别以 8 位寄存器名称的两个字母代入汇编,当第一次时即以 8 位寄存器名称的第一个字母代入,然后加上『x』再推入堆栈,然后立刻跳出 IRPC 块,故事实上这个重复块只汇编一次而已。
第 42 行的『&&x』为何要有两个『&』呢?这是因为根据 MASM 手册上说每层块要使用『&』,故第二层要用两个『&』。
第 62 行到第 73 行是用来检查推入堆栈的参数是否为寄存器间接寻址,寄存器间接寻址模式是像底下的样子:
___FCKpd___13
观察以上几个例子,您会发现,这种寻址模式含有两个中括号,因此检验方法就是以 IRPC 检查参数中是否有『[』( 第 64 行到第 67 行的 IF 条件汇编 ),假如有的话,会使虚拟变量,addr,设为真。然后接下来的就直接使该参数推入堆栈,因为 PUSH 指令就可以直接推入寄存器间接寻址模式。
接下来就只剩下变量、标号与立即值未处理,要区别前两者可用 MASM 所提供的 .TYPE 运算子。
.TYPE 运算子
.TYPE 和 TYPE 不同,TYPE 已在稍前说明过了,这儿小木偶只说明 .TYPE 的用法:( .TYPE 前有个小数点,不可省略 )
___FCKpd___14
.TYPE 运算会根据运算式传回一个字节大小的数据,假如运算式不合法,则传回零;如果合法,所传回的字节只有第 0、1、5、7 位有意义,其他位均为零,这四个位所代表的意义如下表:
___FCKpd___15
当第 75 行的虚拟变量,arg_type,为 2 时,表示为变量,由第 79 行到第 85 行的程序处理;若 arg_type 为 1 时,表示为标号,由第 90 到第 97 行的程序处理;若不为 1 也不为 2,表示为立即值,由第 100 行到第 106 行的程序处理。
处理变量时,不只要能处理字变量,也为了要能处理双字、四字等类型的变量,帘狴H先取得变量长度,再除以 2,就能求出推入堆栈的次数,而每次推入堆栈时地址都得增加 2,这些细节都可在第 79 行到第 85 行 IF-ENDIF 之间的程序处理。
处理立即值的方式很特别,小木偶为了只把立即值推入堆栈,并且使所有寄存器都不改变,,当然只有 SP 寄存器会因为推入了一个立即值而减少二。为了达到上面的目的,写了第 100 行到第 106 行的程序,虽然有点儿复杂,但应该不太难懂。处理堆栈其实就是这样。而处理标号的方法和立即值相似,因为标号其实就是一个立即值,他表示程序地址。
注一:古早以前,写程序,尤其是利用 BASIC 撰写的程序,常常因为条件跳转(IF-ELSE-THEN)即无条件跳转(GOTO)使得源程序被切割成支离破碎,很不易维护。因此后来有许多程序设计师,不再滥用 GOTO 指令,遇到条件跳转时,使条件为真儿要执行的指令包含在一个块中,不用执行的指令包含在另一个块中,并大量用子程序,这样就使得源程序较易维护。PASCAL、C、C 这些语言就是属于结构化的语言。
注二:MASM 汇编源文件时,是分两次汇编的 ( two pass ),要这么做的原因是这样的,请看以下说明。当 MASM 开始汇编时先读入源文件,由上而下汇编,如果遇到尚未定义的标号、变量等,MASM 会先假设,而预留下一些空间给这些未定义的数据,当读到源程序后面时,MASM 发现这些未定义的标号、变量在后面定义,于是当第二次汇编时,再把这些先前假设的地址或长度修改成正确的数值。
当然如果先前假设的正确就没有问题,如果假设错误的话,可以分为两种情形。第一种情形是 MASM 所假设的空间或长度比所定义的来得大或多,那第二阶段汇编时,MASM 会把多余的空间以 NOP 指令取代。假如所假设的空间或长度比所定义的来得小或少的话,那就会产生错误,这就是所谓的『相位错误』(Phase error between passes)。
NOP 指令
这是一个 8086 指令集的其中一个指令,它的功用只是让 CPU 空转一个时脉,并不做任何事。
最后
以上就是明理康乃馨为你收集整理的条件汇编的全部内容,希望文章能够帮你解决条件汇编所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复