ITEEDU

3.4 细致的咀嚼

上一节给出了CALL指令的一种用法,这是很不全面的。这一节我们将"品尝"几个新程序,同时要"吃"出CALL指令更多的营养。请看下面的这个程序:

PROG4-B
-u100 12a[Enter]
0A3E:0100 B401	        MOV	AH,01			;选择DOS API的01功能
0A3E:0102 CD21		    INT	21				;调用21H中断等待键盘输入
0A3E:0104 9A1F01220A	CALL	0A3D:011F	;调用0A3D:011F处的子程序
0A3E:0109 3C1B			CMP	AL,1B			;输入的字符是"ESC"吗?
0A3E:010B 75F3			JNZ	100				;不是"ESC"则转至0100H
0A3E:010D CD20		 	INT	20			    ;是"ESC"则结束程序
0A3E:010F 50			PUSH	AX			;子程序开始,保存AX寄存器副本
0A3E:0110 88C6			MOV	DH,A			;输入字符的ASCII码置入DH寄存器
0A3E:0112 B780			MOV	BH,80			;BH寄存器置入"掩膜"10000000B
0A3E:0114 B90800		MOV	CX,0008			;处理8个数位
0A3E:0117 88F3			MOV	BL,DH			;BL寄存器置入输入字符的ASCII码
0A3E:0119 B230			MOV	DL,30			;DL寄存器置入字符0的ASCII码30H
0A3E:011B 20FB			AND	BL,BH			;将ASCII码与"掩模"相与
0A3E:011D 7402			JZ	121				;若结果为0,转至0121显示字符0
0A3E:011F B231			MOV	DL,31			;将字符1的ASCII码置入DL寄存器
0A3E:0121 B402 		 	MOV	AH,02			;选择DOS API的02H功能
0A3E:0123 CD21			INT	21				;显示DL寄存器中的字符
0A3E:0125 D0EF	        SHR	BH,1			;"掩模"向右移动一位
0A3E:0127 E2EE      	LOOP	117			;循环至0117处理Bit6位
0A3E:0129 58        	POP	AX				;恢复AX寄存器的原值
0A3E:012A CB		 	RETF 				;返回主程序 

我们以"U"的形式给出这个程序,此程序没有存盘的必要。不过请大家务必注意不要原封不动地打入CALL指令后给出的"段:偏移"逻辑地址,因为在不同的机器上需要输入不同的段地址。段地址计算很简单,只需将CS中的实际段地址减1即可。输入程序后我们用"P"命令跟踪:

-p
AX=0100 BX=0000 CX=002B DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=0102 NV UP EI PL NZ NA PO NC
0A3E:0102 CD21 INT 21

注意跟踪指令"INT 21"时务必要使用"P"命令。

-p
a   输入小写字母"a"   注意SP寄存器的内容
AX=0161 BX=0000 CX=002B DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=0104 NV UP EI PL NZ NA PO NC
0A3E:0104 9A1F013D0A CALL 0A3D:011F

为了观察这个"CALL"指令的执行情况,这里要使用"T"命令跟踪。

-t     请注意SP寄存器
AX=0161 BX=0000 CX=002B DX=0000 SP=FFEA BP=0000 SI=0000 DI=0000
DS=0A3E ES=0A3E SS=0A3E CS=0A3D IP=011F NV UP EI PL NZ NA PO NC
0A3D:011F 50 PUSH AX

注意观察SP寄存器的变化,此时堆栈中压入了两个数据:

-dfffa     返回地址的偏移     返回地址的段
0A3E:FFF0  09 01 3E 0A 00 00  ..>...

0A3E:0109就是返回地址,可以看到CPU先将段地址压栈,然后才压入偏移地址。同时执行CALL指令之后不仅IP寄存器有变化,且CS寄存器也发生了变化。对于CALL指令的这种用法,我们称为"远程调用",因为调用后CS寄存器要改变。

PROG5
-A100
0A3E:0100 JMP 111
0A3E:0102 DB'Hello,World!',0D,0A,24
0A3E:0111 MOV DX,0102
0A3E:0114 MOV AH,09
0A3E:0116 INT 21
0A3E:0118 MOV AX,0000
0A3E:011B JMP AX 

有"远程调用"自然就有"远程返回","RETF(F表示FAR)"指令就是远程返回指令,它可以将事先存入堆栈的返回地址弹到CS:IP中。大家可以自己验证这一点。

在此我们必须仔细谈谈"跨段"转移的问题,这个问题在讲JMP指令时遗留了下来。一般情况下程序的转移分成三类:短程、近程和远程。凡是转移后CS寄存器被改变,即转移发生在两个段之间,那么这样的转移就是远程的;反之若转移后CS寄存器没有改变,即转移发生在一个段内,这样的转移就是近程的;若转移发生在-128-+127范围内,则这种转移就是短程的;短程转移是近程转移的一个特例。

为什么会有这样的区别,在本书的后面会有更详细的介绍。

PROG5是由PROG3-B演变而来,去掉了INT 20指令,而用JMP指令转移至CS:0处去执行那里的"INT 20"。

这里采用了JMP指令的新用法,程序先将"0"放入AX,用"JMP AX"转移到CS:0。如果我们把"JMP 00"看作"立即数寻址"方式,那么可以说"JMP AX"应用的就是"寄存器寻址"方式。可见转移指令JMP和MOV、INC一样有不同的寻址方式。现在来跟踪一下JMP指令的执行:

-g=100 11b
Hello,World!
AX=0000 BX=0000 CX=001D DX=0102 SP=FFFE BP=0000 SI=0000 DI=0000
DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=011B NV UP EI PL NZ NA PO NC
0A3E:011B FFE0 JMP AX


-t
AX=0000 BX=0000 CX=001D DX=0102 SP=FFFE BP=0000 SI=0000 DI=0000
DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=0000 NV UP EI PL NZ NA PO NC
0A3E:0000 CD20 INT 20

PROG5-A中应用"直接寻址"方式,PROG5-B中则是应用了"寄存器间接寻址"方式。请大家注意"FFFE"这个数据,它实际应与SP寄存器的初值相同。如果进入DEBUG之后发现SP的值是FFEE或其它的,则应将方括号中的数据修改成与SP寄存器相同的值。
所有这些寻址方式同样适于CALL指令。

PROG5-A
-A100[Enter]
0A3E:0100 JMP 111
0A3E:0102 DB'Hello,World!',0D,0A,24
0A3E:0111 MOV DX,0102
0A3E:0114 MOV AH,09
0A3E:0116 INT 21
0A3E:0118 JMP [FFFE]
0A3E:011C


PROG5-B
-A100[Enter]
;偏移0100-0116与PROG5-A相同 0A3E:0118 MOV BX,FFFE
0A3E:011B JMP BX 

最后来谈一谈编制子程序时一个至关重要的问题。子程序运行时必然也要用到寄存器或内存中的数据,在使用寄存器中的数据时务必要注意一点,那就是主程序在调用这个子程序后还要使用的数据不能被子程序修改。

仔细分析PROG4-A,可以发觉若要保证执行主程序中的CMP指令时AL寄存器中确实是我们所按键的ASCII码,那么就要求子程序不得改变AL寄存器,但事实上系统提供的02H功能要在AL中返回输出字符的ASCII码,所以我们在子程序一开始就写了指令"PUSH AX",在子程序返回前用"POP AX"恢复AL寄存器中的数据,这样才能达到按下"Esc"时程序能返回DOS。这一点在编制子程序时要格外注意。

本章结束语

在这一章里我们对中断和子程序的概念作较深入的介绍,当然,还有一些更深层的内容没有涉及,比如主程序和子程序间应如何传递数据;怎样自己编写中断服务程序;以及子程序的递归调用等。这些问题留在后面解决。