条件宏汇编

条件汇编是汇编器在汇编阶段,依据所设定的条件,使汇编器汇编某一段程序,或不汇编某一段程序。一般而言,条件汇编常配合宏使用,使得撰写汇编语言源文件能有初步的结构化 (注一 )。底下小木偶使用条件汇编与宏配合而写成的一个简单程序: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 来汇编那一个程序片段。而比较的两个数值必须是在汇编阶段就能够确定大小的数值,因此不可以使用寄存器或变量,而像数据长度,或是地址都是可以拿来作为比较的数值。下表表示能用在判断式的关系运算子:

运算子实例说明
EQvar1 EQ var2若 var1 等于 var2 时,为真
NEvar1 NE var2若 var1 不等于 var2 时,为真
LTvar1 LT var2若 var1 小于 var2 时,为真
LEvar1 LE var2若 var1 小于或等于 var2 时,为真
GTvar1 GT var2若 var1 大于 var2 时,为真
GEvar1 GE var2若 var1 大于或等于 var2 时,为真
NOTNOT var若 var 为伪时,为真
ANDvar1 AND var2若 var1、var2 皆为真时,为真
ORvar1 OR var2若 var1、var2 中有一为真时,为真
XORvar1 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><span class="input">masm <span class="em">/Dnumber=200</span> sum02; [Enter]</span>
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><span class="input">link sum02; [Enter]</span>

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><span class="input">exe2bin sum02 sum02.com [Enter]</span>

H:/HomePage/SOURCE><span class="input">sum02 [Enter]</span>
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 start

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 来增快汇编速度。这两个伪指令的语法是:

___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 空转一个时脉,并不做任何事。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值