在前面我们讨论过子过程的嵌套调用,在这一节里,我们将讨论一些更为特殊的过程调用情况。学过这一节后,我们将对汇编语言独特的灵活性有更深的认识,同时也会更好地理解某些高级语言中特殊的程序设计技巧。先来看这样一个示例程序:
code segment assume cs:code,ds:code org 100h main proc near jmp start ;跳过数据区 ascii db '0123456789ABCDEF' ;定义十六进制数对应的ASCII码 start: mov bx,offset ascii ;BX指向ASCII表 mov ah,0 ;等待输入按键 int 16h mov dh,al ;按键的ASCII码送入DH寄存器暂存 mov cl,4 ;准备右循环移位次数 ror al,cl ;AL寄存器向右循环移4位 call hex2asc ;调用HEX2ASC部分 low4b: mov al,dh ;取回ASCII码的副本 hex2asc label near ;定义标号HEX2ASC and al,0fh ;保留AL原值的高4位 xlat ascii ;取得数字对应的ASCII码 mov dl,al ;显示一个数位 mov ah,2 int 21h ret ;① 返回LOW4B处,② 返回操作系统 main endp code ends end main
这个程序由前面的TESTKEY.ASM变化而得,和它的"祖先"相比这个程序显得短了一些,它是一个COM文件。我们先来看看新增的"LABEL"伪指令:
这个伪指令提供了一种在程序中定义标号的方法,它的应用格式为:标号名 LABEL 属性
以前我们一直都是在指令前直接加带冒号的标号,象上面程序中的"START:",和"LOW4B:"等,那么用LABEL伪指令所定义的标号和这些直接给出的标号有什么区别呢?不言而喻,主要的区别就在于LABEL伪指令可以使所定义的标号具有属性。这属性指示出转移到(或调用)此标号时,是段内还是段间转移。
我们在这个程序使用LABEL定义了一个名为HEX2ASC标号,这个标号将MAIN过程切割成两部分,程序中的CALLHEX2ASC指令实际是MAIN过程对它自身的一部分进行调用,这次调用将显示出AL寄存器的高4bit,而显示低4bit时程序直接进入了HEX2ASC部分。不难看出程序最后的RET指令被执行了两次,第一次执行使CPU返回到标号"LOW4B:"处继续执行指令,而第二次执行结束了整个进程。下面是用DEBUG跟踪这个程序的过程:
C:\ASM\>DEBUG TESTKEY2.COM[Enter] -g=100 120[Enter] AX=01B1 BX=0103 CX=0004 DX=1B00 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=0120 OV UP EI PL NZ NA PO CY 0E69:0120 E80200 CALL 0125
利用"G"命令控制程序执行到0E69:0120处停下,以便于观察CALL指令的执行情况。注意当程序等待键盘输入时敲一下"ESC"键。
-t AX=01B1 BX=0103 CX=0004 DX=1B00 SP=FFFC BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=0125 OV UP EI PL NZ NA PO CY 0E69:0125 240F AND AL,0F
用"T"命令跟踪CALL指令,可以看到CPU转至0E69:0125处执行,此时我们可以观察一下存入堆栈中的返回地址:
dss:fffc? 注意返回地址为0123 0E69:FFF0? 23 01 00 00? #...
可以看到返回地址是0123,恰好是LOW4B处。当第一次执行RET指令时,CPU将返回到这个地址处继续执行。
-g=125 12e 1? 程序显示出ESC键的ASCII码高4位对应的十六进制数 AX=0231 BX=0103 CX=0004 DX=1B31 SP=FFFC BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=012E NV UP EI PL NZ NA PO NC 0E69:012E C3 RET
利用"G"命令在012E处打一个断点,以便于观察RET指令的执行。
-t AX=0231 BX=0103 CX=0004 DX=1B31 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=0123 NV UP EI PL NZ NA PO NC 0E69:0123 8AC6 MOV AL,DH
CPU返回0123处继续执行程序
可以看到CPU正确地返回到0123处继续执行程序,再往下CPU将直接进入0125处。
-t AX=021B BX=0103 CX=0004 DX=1B31 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=0125 NV UP EI PL NZ NA PO NC 0E69:0125 240F AND AL,0F
程序直接进入了0125处
我们再次用"G"命令在012E处打上断点,以便于观察RET指令第二次执行的情况。
-g=125 12e B 程序显示出ESC键对应ASCII码的低半个字节 AX=0242 BX=0103 CX=0004 DX=1B42 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=012E NV UP EI PL NZ NA PO NC 0E69:012E C3 RET
再次用"T"命令跟踪RET指令,注意此时堆栈中已经没有CALL指令压入的返回地址,只有DOS存入的一个"0"
-t AX=0242 BX=0103 CX=0004 DX=1B42 SP=0000 BP=0000 SI=0000 DI=0000 DS=0E69 ES=0E69 SS=0E69 CS=0E69 IP=0000 NV UP EI PL NZ NA PO NC 0E69:0000 CD20 INT 20
事实上,在上面这个程序中完全可以不用LABEL伪指令,而直接采用传统的标号"HEX2ASC:",这并不影响程序的编译和执行。