子程序存储在存储器中,可供一个或多个调用程序(主程序)反复调用。主程序调用子程序时使用CALL指令,由子程序返回主程序时使用RET指令。由于调用程序和子程序可以在同一个代码段中,也可以在不同的代码段中,因此,CALL指令和RET指令也有近调用、近返回及远调用、远返回两类格式。
⑴CALL NEAR PTR <子程序名> 近调用(near call)
近调用是CALL指令的缺省格式,可以写为"CALL <子程序名>rotine"。它调用同一个代码段内的子程序(子过程),因此,在调用过程中不用改变CS的值,只需将子程序的地址存入IP寄存器。CALL指令中的调用地址可以用直接和间接两种寻址方式表示。
⑵CALL FAR PTR <子程序名> 远调用(far call)
远调用适用于调用程序(也称为主程序)和子程序不在同一段中的情况,所以也叫做段间调用。和近调用指令一样,远调用指令中的寻址方式也可用直接方式和间接方式。
⑶RET 返回指令(return)
RET指令执行的操作是把保存在堆栈中的返回地址出栈,以完成从子程序返回到调用程序的功能。
● CALL <子程序名> 段内直接调用
执行操作:①(SP) ← (SP)-2,((SP)) ← (IP)当前
②(IP) ← (IP)当前+16位位移量(在指令的第2、3个字节中)
● CALL DESTIN 段内间接调用
执行操作:①(SP) ← (SP)-2,((SP)) ← (IP)当前
②(IP) ← (EA) ; (EA)为指令寻址方式所确定的有效地址
● CALL FAR PTR <子程序名> 段间直接调用
执行操作:①(SP) ← (SP)-2,((SP)) ← (CS)当前
(SP) ← (SP)-2,((SP)) ← (IP)当前
②(IP) ← 偏移地址(在指令的第2、3个字节中)
(CS) ← 段地址(在指令的第4、5个字节中)
● CALL WORD PTR DESTIN 段间间接调用
执行操作:①(SP) ← (SP)-2,((SP)) ← (CS)当前
(SP) ← (SP)-2,((SP)) ← (IP)当前
②(IP) ← (EA) ; (EA)为指令寻址方式所确定的有效地址
(CS) ← (EA+2)
从CALL指令执行的操作可以看出,第一步是把子程序返回调用程序的地址保存在堆栈中。对段内调用,只需将IP的当前值,即CALL指令的下一条指令的地址存入SP所指示的堆栈字单元中。对段间调用,保存返回地址则意味着要将CS和IP的当前值分别存入堆栈的两个字单元中。
CALL指令的第二步操作是转子程序,即把子程序的入口地址交给IP(段内调用)或CS:IP(段间调用)。对段内直接方式,调转的位移量,即子程序的入口地址和返回地址之间
的差值就在机器指令的2、3字节中。对段间直接方式,子程序的偏移地址和段地址就在操作码之后的两个字中。对间接方式,子程序的入口地址就从寻址方式所确定的有效地址中获得。
● RET 段内返回(近返回)
执行操作:(IP) ← ((SP)),(SP) ← (SP)+2
● RET 段间返回(远返回)
执行操作:(IP) ← ((SP)),(SP) ← (SP)+2
(CS) ←((SP)),(SP) ← (SP)+2
● RET N 带立即数返回
执行操作:①返回地址出栈(操作同段内或段间返回)
②修改堆栈指针:(SP) ← (SP)+N
子程序的最后一条指令必须是RET指令,以返回到主程序。如果是段内返回,只需把保存在堆栈中的偏移地址出栈存入IP即可,如果是段间返回,则要把偏移地址和段地址都从堆栈中取出送到IP和CS寄存器中。
带立即数返回指令,除完成偏移地址出栈或偏移地址和段地址出栈的操作外,还要再使SP的内容加上一个立即数N,使堆栈指针SP移动到新的位置。指令中的N可以是一个常数,也可以是一个表达式。带立即数返回指令适用于C或PASCAL的调用规则,这些规则在调用过程(子程序)前先把参数压入堆栈,子程序使用这些参数后,如果在返回时丢弃这些已无用的参数,就在RET指令中包含一个数字,它表示压入到堆栈中参数的字节数,这样堆栈指针就恢复到参数入栈前的值。
CALL指令和RET指令都不影响条件码。
例3.43 根据下面调用程序和子程序的程序清单,画出RET指令执行前和执行后的堆栈情况。假设初始的SS:SP=A000:1000。
0000 B8 001E MOV AX,30
0003 BB 0028 MOV BX,40
0006 50 PUSH AX ; push data1 into stack
0007 53 PUSH BX ; push data2 into stack
0008 E8 0066 CALL ADDM ; call <子程序名>
000B B4 02 MOV AH,2
… … …
0071 ADDM PROC NEAR; entry point (IP)←0071=000b+0066
0071 55 PUSH BP ; save BP
0072 8B E4 MOV BP,SP ; addressing the stack with BP
0074 8B 46 04MOV AX,[BP+4]; get data2 from stack
0077 03 46 06 ADD AX,[BP+6]; add data1
007A CD POP BP ; get back BP
007B C2 0004RET 4; return and revert SP
007E ADDM ENDP
图3.12 CALL指令和RET指令对堆栈的影响
如图3.12所示,主程序中的两条PUSH指令将数据30和40压入堆栈,CALL指令执行后,返回地址000B又压入堆栈,紧接着程序控制转移到子程序ADDM。子程序中的PUSH 指令又使BP的值进栈,此时SP指向栈顶0FFA。MOV指令将0FFA传送给BP,使BP作为寻址堆栈数据的指针。(BP+4)指向的是40,(BP+6)指向的是30,取出数据后用POP 指令恢复了BP原先的值,此时,(SP)=0FFC,这是RET 4指令执行前的堆栈状态。
执行RET 4指令时,先使返回地址出栈:(IP)←000B,(SP)←0FFC+2=0FFD,然后,(SP)+4=0FFD+4=1000,结果使SP跳过了堆栈数据而回到了原始位置。
6.4.4 子程序应用举例
【例】将一个给定的二进制数按位转换成相应的ASCII码字符串,送到指定的存储单元并显示。如二进制数10010011转换成字符串为‘10010011’。要求将转换过程写成子程序,且子程序应具有较好的通用性,而必须能实现对8倍和16倍二进制数的转换。
入口参数:DX存放待转换的二进制数
CX存放待转换数的位数(8位或16位)
DI存放ASCII码首地址
出口参数:转换后的字符串存放在以DI作指针的字节存贮区中
程序如下:
DA TA SEGMENT
NUM8 DB 93H
NUM16 DW 0ABCDH
ASCBUF DB 20 DUP(0)
DA TA ENDS
CODE SEGMENT
ASSUME DS:DA TA,CS:CODE,SS:STACK
START:MOV AX,DA TA
MOV DS,AX
MOV DX,0
MOV DL,NUM8 ;转换二进制数送DX
MOV CX,8 ;置位数8
LEA D I,ASCBUF ;字符串首址→DI
CALL BTASC ;调用子程序BTASC
MOV [DI],BYTE PTR 0DH
MOV [DI+1],BYTE PTR 0AH
MOV [DI+2],BYTE PTR ‘$’
LEA DX,ASCBUF
MOV AH,9
INT 21H
MOV DX,NUM16
MOV CX,16 ;置位数16
LEA D I,ASCBUF
CALL BTASC
MOV [DL],BYTE PTR 0DH
MOV [DL+1],BYTE PTR 0AH
MOV [DL+2],BYTE PTR ‘$’;显示转换后的字符串LEA DX,ASCBUF
MOV AH,9
INT 21H
BTASC PROC
PUSH AX ;保存AX
MOV AL,0
CMP CX,8 ;比较8位数
JNE L1 ;直接转换16位数
MOV DH,DL ;8位数转换送DH
L1:ROL DX,,1 ;DX最高位移入CF
RCL A L,1 ;CF移入AL最低位
ADD AL,30H
MOV [DI],AL
INC DI
LOOP L1
POP AX
RET
BTASC ENDP
CODE ENDS
END START
(1)通过寄存器传送参数
这是最常用的一种方式,使用方便,但参数很多时不能使用这种方法。
十进制到十六进制数转换程序。程序要求从键盘取得一个十进制数,然后把该数以十六进制形式在屏幕上显示出来。
采用子程序结构,用一个子程序DECIBIN实现从键盘取得十进制数并把它转换为二进制数;另一个子程序BINIHEX把此二进制数以十六进制数的形式在屏幕上显示出来。为避免屏幕上的重叠,另外用CRLF 子程序取得回车和换行的效果。整个程序结构如动画所示,在这里,各个子程序之间用BX寄存器来传送信息。
decihex segment
assume cs:decihex
; 程序的主要部分
main proc far
repeat: call decibin ; 调用子程序decibin
call crlf ; 调用子程序crlf
call binihex ; 调用子程序binihex
call crlf
jmp repeat
main endp
; 子程序decibin
decibin proc near
mov bx,0
newchar:
mov ah,1
int 21h ; 从键盘接收单个字符
; 非0~9之间的数退出
sub al,30h
jl exit
cmp al,9d
jg exit
cbw ; al扩展到ax ; BX中的数乘以10
xchg ax,bx
mov cx,10d
mul cx
xchg ax,bx
; 把ax加到bx中
add bx,ax
jmp newchar ; 接收下一个字符exit:
ret
decibin endp
; 子程序
binihex proc near
mov ch,4
rotate: mov cl,4
rol bx,cl
mov al,bl
and al,0fh
add al,30h
cmp al,3ah
jl printit
add al,7h
printit:
mov dl,al
mov ah,2
int 21h
dec ch
jnz rotate
ret
binihex endp
; 子程序crlf
crlf proc near
; 显示回车
mov dl,0dh
mov ah,2
int 21h
; 显示换行
mov dl,0ah
mov ah,2
int 21h
ret
crlf endp
decihex ends
end main
我们已经知道,一个子程序也可以作为调用程序去调用另一个子程序,这种情况称为子程序的嵌套。嵌套的层次不限,其层数称为嵌套深度。动画表示了嵌套深度为2时的子程序嵌套情况。
嵌套子程序的设计并没有什么特殊要求,除子程序的调用和返回应正确使用CALL和RET指令外,要注意寄存器的保存和恢复,以避免各层次子程序之间因寄存器冲突而出错的情况发生。如果程序中使用了堆栈,例如使用堆栈来传送参数等,则对堆栈的操作要格外小心,避免发生因堆栈使用中的问题而造成子程序不能正确返回的错误。