ITEEDU

5.2 特殊的过程调用

5.2节导航:第一页 第二页

在前面我们讨论过子过程的嵌套调用,在这一节里,我们将讨论一些更为特殊的过程调用情况。学过这一节后,我们将对汇编语言独特的灵活性有更深的认识,同时也会更好地理解某些高级语言中特殊的程序设计技巧。先来看这样一个示例程序:

TESTKEY2.ASM
        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:",这并不影响程序的编译和执行。