我们已经知道,一个子程序也可以作为调用程序去调用另一个子程序,这种情况称为子程序的嵌套。嵌套的层次不限,其层数称为嵌套深度。动画表示了嵌套深度为2时的子程序嵌套情况。
嵌套子程序的设计并没有什么特殊要求,除子程序的调用和返回应正确使用CALL和RET指令外,要注意寄存器的保存和恢复,以避免各层次子程序之间因寄存器冲突而出错的情况发生。如果程序中使用了堆栈,例如使用堆栈来传送参数等,则对堆栈的操作要格外小心,避免发生因堆栈使用中的问题而造成子程序不能正确返回的错误。
2递归子程序
在子程序嵌套的情况下,如果一个子程序调用的子程序就是它自身,就称为递归调用,这样的子程序称为递归子程序。
递归子程序对应于数学上对函数的递归定义,往往能设计出效率较高的程序,可完成相当复杂的计算,所以是很有用的。这里以阶乘函数为例,说明递归子程序的设计方法。
例
N!= N* (N-1) * (N-2) * … * 1
其递归定义如下:
求N!本身是一个子程序,由于N!是N和(N-1)!的乘积,所以为求(N-1)!必须递归调用求N!的子程序,但每次调用所使用的参数都不相同。
递归子程序的设计必须保证每次调用都不破坏以前调用时所用的参数和中间结果,所以一般把每次调用的参数、寄存器内容及所有的中间结果都存放在堆栈中。
我们把一次调用所保存的信息称为帧(frame),一般一帧包括所保存的寄存器内容、参数或参数地址和中间结果等。每次调用把一帧信息存入堆栈。
递归子程序中还必须包括基数的设置,当调用参数达到基数时,还必须有一条条件转移指令实现嵌套退出,以保证能按反向次序退出并返回主程序。
stack_seg segment
dw 128 dup(0)
tos label word
stack_seg ends
;定义代码段code1
code1 segment
main proc far
assume cs:code1,ds:data_seg,ss:stack_seg
start:
mov ax,stack_seg
mov ss,ax
mov sp,offset tos
push ds
sub ax,ax
push ax
mov ax,data_seg
mov ds,ax
; 程序的主要部分
mov bx,offset result
push bx ;result单元的地址入栈
mov bx,n_v
push bx ; n_v单元的内容入栈
call far ptr fact ;远调用子程序fact
ret
main endp
code1 ends
; 定义代码段code
code segment
; 定义frame帧结构数据
frame struc
save_bp dw ?
save_cs_ip dw 2 dup(?)
n dw ?
result_addr dw ?
frame ends
assume cs:code
fact proc far ;定义子程序fact
push bp
mov bp,sp ;bp用来指向帧结构
push bx
push ax
mov bx,[bp].result_addr ;每帧中result_addr送bx
mov ax,[bp].n ; 每帧中n送ax
cmp ax,0
je done
push bx ;为下一次调用result_addr入栈
dec ax
push ax ;为下一次调用n-1入栈
call far ptr fact
mov bx,[bp].result_addr
mov ax,[bx]
mul [bp].n ; (ax)=n*result
jmp short return
done: mov ax,1 ; (ax)=1
return: mov [bx],ax ; result=(ax)
pop ax
pop bx
pop bp
ret 4
fact endp
code ends
end start
从上述N!的递归子程序中可以看出,由于使用了STRUC伪操作,程序的结构更加清晰,也避免了计算参量地址时可能出现的错误。
左面给出了不用STRUC伪操作编制的求N!的递归子程序。
在编制子程序时,特别是在编制嵌套或递归子程序时,堆栈的使用是十分频繁的。在这里顺便说明一下,在堆栈使用过程中,应该注意有关堆栈溢出的问题。
由于堆栈区域是在堆栈定义时就确定了的,因而堆栈工作过程中有可能产生溢出。堆栈溢出有两种情况可能发生:如堆栈已满,但还想再存入信息,这种情况称为堆栈上溢;另一种情况是,如堆栈已空,但还想再取出信息,这种情况称为堆栈下溢。不论上溢或下溢,都是不允许的。因此在编制程序时,如果可能发生堆栈溢出,则应在程序中采取保护措施。这可以通过给SP规定上、下限,在进栈或出栈操作前先做SP和边界值的比较,如溢出则作溢出处理,以避免破坏其他存储区或使程序出错的情况发生。
datarea segment
n dw ?
result dw ?
datarea ends
stack_seg segment
dw 128 dup(0)
tos label word
stack_seg ends
prognam segment
main proc far
assume cs:prognam,ds:datarea,ss:stack_seg
start:
mov ax,stack_seg
mov ss,ax
mov sp,offset tos
push ds
sub ax,ax
push ax
mov ax,datarea
mov ds,ax
; 程序的主要部分
mov bx,n
push bx
call fact ;调用子程序fact
pop result
ret
main endp
fact procnear ; 定义子程序fact
push ax
push bp
mov bp,sp
mov ax,[bp+6]
cmp ax,0
jne fact1
inc ax
jmp exit
fact1:
dec ax
push ax
call fact
pop ax
mul word ptr[bp+6]
exit: mov [bp+6],ax
pop bp
pop ax
ret
fact endp
prognam ends
end start
例
这一例子的功能是和例6.3相反的。它由HEXIBIN和BINIDEC两个主要的子程序组成,由于主程序和子程序在同一个程序模块中,因而省略了对寄存器的保护和恢复工作,子程序之间的参数传送则采用寄存器传送的方式进行。程序可用CtrlBreak退出。
display equ 2h ; 显示单个字符的功能号是2
key_in equ 1h ;键盘输入单个字符的功能号是1
doscall equ 21h ; DOS中断号
hexidec segment
main proc far
assume cs:hexidec
start:
push ds
sub ax,ax
push ax
call hexibin ; 十六进制转换成二进制
call crlf ; 显示回车和换行
call binidec ; 二进制转换成十进制
call crlf
……
ret
main endp
; 定义子程序hexibin(十六进制转换成二进制,结果在bx中)
hexibin proc near
mov bx,0
newchar:
mov ah,key_in
int doscall ; 键盘输入单个字符
sub al,30h
jl exit
cmp al,10d
jl add_to ; 0~9之间转add_to
; 判断是否a~f之间('a'的ASCII码为61h)
sub al,27h
cmp al,0ah
jl exit
cmp al,10h
jge exit
; 0~9或a~f
add_to:
mov cl,4
shl bx,cl ; (bx)*16
mov ah,0
add bx,ax
jmp newchar
exit:
ret
hexibin endp
;定义子程序binidec(二进制转换成十进制)
binidec proc near
mov cx,10000d
call dec_div ; bx被10000除
mov cx,1000d
call dec_div ; bx被1000除
mov cx,100d
call dec_div ; bx被100除
mov cx,10d
call dec_div ; bx被10除
mov cx,1d
call dec_div ; bx被1除
ret
; 定义子程序dec_div(十进制除)
dec_div proc near
mov ax,bx
mov dx,0 ; 被除数在dx:ax中
div cx
mov bx,dx ; 余数送bx
mov dl,al ; 商送dl
add dl,30h
mov ah,display
int doscall ; 显示单个字符
ret
dec_div endp
binidec endp
crlf proc near
mov dl,0ah
mov ah,display
int doscall
mov dl,0dh
mov ah,display
int doscall
ret
crlf endp
hexidec ends
end start
例本例为一个简单的信息检索系统。在数据区里,有10个不同的信息,编号为0~9,每个信息包括30个字符。现在要求编制一个程序:从键盘接收0~9之间的一个编号,然后在屏幕上显示出相应编号的信息内容。
在这个程序里,10个信息组成一个信息表,对信息表的查找是根据从键盘接收的编号来确定的。请注意从接收编号后到找到表格中所需区域为止的程序段,这也是查找表格的一种常用方法。此外,程序把显示一个信息编成一个独立功能的子程序DISPLAY,并把其中常用的显示一个字符的功能也编成一个子程序DISPCHAR,这样就使程序的结构更加清晰了。
datarea segment
thirty db 30
; 信息表
msg0 db 'I like myIBM-PC---------------------------------------'
msg1 db '8088 programming isfun--------------------------------'
msg2 db 'Time to buy morediskettes-----------------------------'
msg3 db 'This programworks-------------------------------------'
msg4 db 'Turn off thatprinter----------------------------------'
msg5 db 'I have more memory thanyou----------------------------'
msg6 db 'The PSP can beuseful----------------------------------'
msg7 db 'BASIC was easier thanthis-----------------------------'
msg8 db 'DOS isindispensable-----------------------------------'
msg9 db 'Last massage of theday--------------------------------'
; 错误信息
errmsg db 'error!!! invalid parameter!! '
datarea ends
stack segment
db 256 dup(0)
tos label word
stack ends
prognam segment
main proc far
assume cs:prognam,ds:datarea,ss:stack
start:
mov ax,stack
mov ss,ax
mov sp,offset tos
push ds
sub ax,ax
push ax
mov ax,datarea
mov ds,ax
begin:mov ah,1
int 21h ; 从键盘输入一个字符
; 错误输入转error
sub al,'0'
jc error
cmp al,9
ja error
; 正确输入
mov bx,offset msg0 ; 信息表首地址送bx
mul thirty ; (ax)=(al)*30
add bx,ax ; bx指向相应位置
call display ; 显示相应编号信息
jmp begin
error:mov bx,offset errmsg
call display ; 显示错误信息
ret
display proc near
mov cx,30
disp1:mov dl,[bx]
call dispchar ; 显示一个字符
inc bx
loop disp1 ; 循环
mov dl,0dh
call dispchar ; 显示回车
mov dl,0ah
call dispchar ; 显示换行
ret
display endp
dispchar proc near
mov ah,2
int 21h
ret
dispchar endp
main endp
prognam ends
end start
联系客服