第4章子程序设计和DOS功能调用.docx
《第4章子程序设计和DOS功能调用.docx》由会员分享,可在线阅读,更多相关《第4章子程序设计和DOS功能调用.docx(52页珍藏版)》请在冰豆网上搜索。
第4章子程序设计和DOS功能调用
第4章子程序设计和DOS功能调用
4.1子程序设计
子程序——过程——具有特定功能的程序模块
子程序是提高程序设计效率的良好手段,也为模块化设计提供了很好的基础。
4.1.1过程调用和返回指令
1.call指令——过程调用指令
格式:
call目的操作数
具体格式同jmp
注意:
在功能上CALL指令与JMP指令都是要转换到新的位置执行程序,所不同的是,JMP不保护断点,CALL要保护断点。
这里的断点是指CALL指令后继第一条指令的起始地址。
当执行CALL指令时,CS:
IP正指向其后继第一条指令——断点,CALL指令保护之,以便执行完过程后能正确返回调用主程序继续原来程序的执行。
可在DEBUG.EXE下观察到CALL保护断点的情况。
①段内直接调用
call过程名(near型)
操作:
堆栈←ip,ip←ip+相对位移量(汇编结果),ip指向目标
②段内间接调用
子程序的起始地址(偏移地址)事先存放在通用寄存器或内存单元中
callbx
callcx
callwordptr[bx+di+2]
操作:
堆栈←ip(断点偏移地址),ip←目标地址
③段间直接调用
call过程名(far型)
操作:
堆栈←[cs:
ip](断点地址),[cs:
ip]←目标地址
④段间间接调用
子程序的起始地址(逻辑地址:
偏移地址)存放在四字节的地址指针中
calldwordptr[bx]
操作:
堆栈←[cs:
ip](断点地址),[cs:
ip]←目标地址
2.ret指令——过程返回指令
格式:
ret
retn(弹出值为偶数)
功能:
①ret指令将控制从一个过程返回到调用该过程的call指令之后的那条指令(调用程序的断点),段内或段间返回都用RET.
ret完成由堆栈恢复断点的功能:
段内返回:
ip←断点偏移量
段间返回:
ip←断点偏移量
cs←断点段地址
②带弹出值的返回指令,其弹出值只能是偶数,在从堆栈中弹出返回地址后,再次修改堆栈指针sp的值:
sp←sp+n
以废除调用程序装入栈中的参数恢复调用之前的栈顶。
把执行call指令前压入堆栈的参数弹出丢去.
4.1.2过程定义语句
格式:
过程名proc[near/far]
....
ret
....
ret
过程名endp
说明:
①过程名不能省略,proc和endp前的过程名必须相同
②near————近调用过程,即仅能被同一逻辑代码段的程序调用.
far——远调用过程,即能被其他逻辑代码段的程序调用.
如果无类型说明,隐含near.
③过程调用与返回
主程序用call命令调用过程,执行完后,在过程中执行ret返回主程序,所以在过程中,一般最后一条语句均为ret指令
④过程的嵌套与递归——汇编程序也允许过程的嵌套调用和递归调用
过程嵌——在过程中调用过程
过程递归——过程调用自己
这两种调用方式都是依靠CALL指令和RET指令配合实现的。
子程序的编写方法
1.子程序的定义
⑴调用程序与子程序在同一代码段中,子程序的类型为near
codesegment
assume.....
mainprocfar
.....
callsub
.....
ret
mainendp
subprocnear
....
ret
subendp
codeends
⑵调用程序与子程序不在同一代码段中,子程序必须是far型
code1segment
assume.....
mainprocfar
.....
callfarptrsub
.....
ret
mainendp
code1ends
code2segment
subprocfar
....
ret
subendp
.....
callsub
.....
code2ends
endmain
2.编写子程序的要求
⑴保护寄存器与存贮器工作单元
codesegment
assume.....
mainprocfar
.....
callsub;调用延时子程序
.....
ret
mainendp
subprocnear;延时子程序
pushbx;子程序要使用的寄器中数据送堆栈保护
pushcx
movbl,20
delay:
movcx,5600;延时循环
wait:
loopwait
decbl
jnzdelay
popcx;由堆栈恢复保护的数据
popbx
ret
subendp
codeends
⑵正确使用堆栈
①执行CALL指令之间后,转入子程序之前,堆栈顶保存了断点地址.
②在子程序中执行RET指令时,要从堆栈中弹出断点地址,以便正确返回调用程序.
③在子程序中使用了堆栈,必须成对地执行PUSH和POP,确保在执行RET时,栈顶能恢复刚进入子程序时的位置,使RET指令能正确恢复断点.否则程序的执行结果将无法预测.
⑶程序中加入必要的说明和注释,增加程序的可读性和可理解性,一般应说明:
子程序名、功能、入/出口地址、使用的寄存器和存贮单元,子程序中调用的其它子程序名等。
⑷处理好调用程序与子程序之间的参数传递,常用的参数传递方法有:
①借助寄存器②借助内存中建立的参数表③借助堆栈
4.1.3子程序举例
例1(p116-1.asm)写一个把用ASCII码表示的两位十进制数转换为对应二进制数的子程序.
子程序入口参数:
DH=十位ASCII码,DL=个位ASCII码
出口参数:
AL=对应二进制数
转换方法:
高位×10+低位
datasegment
pr1db“Inputtwonumbers:
$”
pr2db0ah,0dh,”Out:
$”
dataends
codesegment
assumecs:
code,ds:
data
start:
movax,data
movds,ax
movah,09h
leadx,pr1
int21h
movah,01h
int21h
movdh,al
movah,01h
int21h
movdl,al
callsubr
movbl,al;显示程序入口bl=显示值
calldisp;调用按二进制显示子程序
movah,4ch
int21h
subrproc;
moval,dh
andal,0fh
movah,10
mulah
movah,dl
andah,0fh
addal,ah
ret
subrendp
dispproc
leadx,pr2
movah,09h
int21h
movcx,08h
lp1:
rolbl,1
movdl,bl
anddl,01h
adddl,30h
movah,02h
int21h
looplp1
ret
dispendp
codeends
endstart
例2(p116-2.asm)写一个把16位二进制数转换为4位十六进制数ASCII码的子程序,并用此子程序显示地址为F000:
0000H的字单元内容.
转换方法:
①把要转换的字数据循环左移4位,把最高4位移到最低4位
②析出最低4位,相当于一位十六进制数
③把析出的一位十六进制数转换为ASCII码
④重复上述操作4次.
入口参数:
DX=待转换的二进制数
DS:
BX=存放转换所得ASCII码串的缓冲区首地址
dsegsegment
buffdb4dup(0),"H",0dh,0ah,"$"
dsegends
csegsegment
assumecs:
cseg,ds:
dseg
start:
movax,dseg
movds,ax
movax,0f000h
moves,ax
movdx,es:
[0000h]
movbx,offsetbuff
callhtascs
movdx,offsetbuff
movah,09h
int21h
movah,4ch
int21h
htascsproc
movcx,0404h
lp:
roldx,cl
moval,dl
andal,0fh
addal,30h
cmpal,39h
jbenext
addal,07h
next:
mov[bx],al
incbx
decch
jnzlp
ret
htascsendp
csegends
endstart
例3(P118-1.ASM)写一个把16位二进制无符号数转换为5位十进制数ASCII码的子程序,并用该子程序转换地址为FFFF:
0000h的一个字
转换方法:
除10取余数,并把余数转换ASCII码,先取得的余数在低位,后取得的余数在高位。
入口参数:
AX=欲转换的二进制数
DS:
BX=存放转换所得ASCII码串的缓冲区首地址
出口参数:
转换得到的十进制ASCII码串按由高位到低位的顺序存放在缓冲区
dsegsegment
buffdb18dup("$")
dsegends
csegsegment
assumecs:
cseg,ds:
dseg
start:
movax,dseg
movds,ax
movax,0ffffh
moves,ax
movax,es:
[0000h]
movbx,offsetbuff
callbtoasc
movdx,offsetbuff
movah,09h
int21h
movah,4ch
int21h
btoascproc
xorcx,cx
movsi,10
lp:
xordx,dx
divsi
adddl,30h
pushdx
inccx
orax,ax
jzlp1
jmplp
lp1:
popdx
mov[bx],dl
incbx
looplp1
ret
btoascendp
csegends
endstart
4.1.4子程序说明信息
为了方便子程序的使用,应给简明确切的说明,上面已经讲述到这方面的要求,下面给出一总结性的说明:
前三点:
即子程序名、功能描述、入口和出口参数是必须给出来的,否则难以使用。
⑴子程序名
⑵功能描述
⑶入口和出口叁数
⑷所用的寄存器和存储单元
⑸使用的算法和重要的性能指标
⑹其它调用注意事项和说明信息
⑺调用实例
4.1.5寄存器的保护与恢复
为了在子程序执行结束后,返回调用程序(主程序),程序能正常继续运行,应该在执行子程序的操作前,保护寄存器中原来的数据,当子程序操作结束后恢复寄存器中原有的数据,以确保程序的顺利运行。
方法一:
在调用程序(主程序)中保护寄存器(压入堆栈)和恢复寄存器(弹出堆栈)中的数据。
即,在调用子程序前,把相关的寄存器的内容用PUSH指令压入堆栈,当由子程序返回调用程序时,又用POP指令把这些寄存器原来的内容由堆栈中取回到对应的寄存器中。
优点:
仅需要保护和恢复程序在主、子程序中都要用到的寄存器,不保护就会影响程序正常运行的寄存器。
缺点:
使主程序中出现大量的PUSH、POP指令,如果子程序调用频繁应就会使主程序编写极为繁琐,且不易理解,甚至会漏掉某些应该保护的寄存器。
方法二:
在调用子程序中保护寄存器(压入堆栈)和恢复寄存器(弹出堆栈)中的数据。
即在子程序的开始,用PUSH指令所在子程序中要使用改变内容的寄存中的数据送堆栈中保护起来,在子程序返回主程序之前,即在RET指令前,用POP指令把保护在堆栈中的数据送回到相应的寄存器中。
优点:
主程序编写方便,不必考虑保护哪些寄存器,且不会造成数据保护的因遗漏。
更为重要的是,这样写出来的子程序相对独立,与主程序没有关系,是模块化程序设计的基础。
btoascproc
pushax
pushbx
pushcx
pushdx
movcx,10
lp:
xordx,dx
divcx
cmpdx,0
jenext
adddl,30h
mov[bx],dl
incbx
jmplp
popdx
popcx
popbx
popax
next:
ret
btoascendp
说明:
⑴在.286、.386等模式下可以使用PUSHA和POPA指令简程序的编写。
格式:
PUSHA
功能:
把通用寄存器AX、BX、CX、DX、BX、SP、BP、SI、DI的内容顺序推入堆栈。
格式:
POPA
功能:
把用PUSHA保存到堆栈中的8个通用寄存器的值弹回到相应的寄存中,并丢去原先SP的值
用这两条指令,上述子程序可以简化为:
btoascproc
pusha
movcx,10
lp:
xordx,dx
divcx
cmpdx,0
jenext
adddl,30h
mov[bx],dl
incbx
jmplp
popa
next:
ret
btoascendp
⑵如果程序需要,可以在主程序中用PUSHF和POPF保护和恢复标志寄存器的标志。
各标志位中,常作为入口、出口参数的是CF
⑶一般来讲,不需要保护含有子程序入口参的寄存器。
⑷当然,若有必要,也可以象保护和恢复寄存器的数据一样,可以保护和恢复某些关键的存储单元中的数据。
4.2主程序与子程序间的参数传递
主程序与子程序间的参数传递有多种方法:
寄存器传递法、约定内存单元传递法、堆栈传递法、CALL后续区传递法。
4.2.1利用寄存器传递参数
利用寄存器传递数据简单方便,但由于寄存器的个数较少,只适用于所要传递的数较少的情况。
上面的大多数例子都是利用寄存器传递的参数。
例1(p121-1.ASM)写一个把大写字母改为小写字母的子程序。
在第三章我们介绍过:
由ASCII码表(P6)可知:
A、B、C……的ASCII码:
01000001B、01000010B、01000011B、……
a、b、c……的ASCII码:
01100001B、01100010B、01100011B、……
所以,只要把大写字母ASCII码中的D5位由0改为1,便实现了由大写转换为小写。
要实现这个操作只需要用一个D5=1,其它各位均为0的字节与大写字母的ASCII码进行或操作(OR)。
这个字节=00100000B=20H
由此可知,程序用不需要字符是大写还是小写进行判断。
datasegment
pr1db"Inputastring:
$"
pr2db0ah,0dh,"Capitalstring:
$"
buffdb100
nudb0
stringdb100dup("$")
dataends
codesegment
assumecs:
code,ds:
data
start:
movax,data
movds,ax
leadx,pr1
movah,09h
int21h
movah,0ah
leadx,buff
int21h
xorch,ch
movcl,nu
movsi,offsetstring
lp:
moval,[si]
calluptolw
mov[si],al
incsi
looplp
leadx,pr2
movah,09h
int21h
leadx,string
movah,09h
int21h
movah,4ch
int21h
uptolwproc
pushf
oral,20h
popf
ret
uptolwendp
codeends
endstart
例2(p122-1.asm)写一个判别字符是否为数字字符的子程序,并利用该子程序把一个字符串中的所有数字字符删除。
方法:
用ASCII码进行比较,其ASCII码值在30H与39H之间是数字字符。
入口参数:
AL=字符的ASCII码
出口参数:
是数字字符,CF=0;否则CF=1
dsegsegment
pr1db"Inputastring:
$"
pr2db0dh,0ah,"Out:
$"
buffdb100
nudb0
stringdb100dup("$")
dsegends
csegsegment
assumecs:
cseg,ds:
dseg
start:
movax,dseg
movds,ax
movdx,offsetpr1
movah,09h
int21h
leadx,buff
movah,0ah
int21h
movsi,offsetstring
movdi,si
movcl,nu
xorch,ch
lp:
moval,[si]
incsi
callisdecm
jncnext
mov[di],al
incdi
next:
looplp
incsi
moval,[si];前移一个”$”
mov[di],al
leadx,pr2
movah,09h
int21h
leadx,string
movah,09h
int21h
movah,4ch
int21h
isdecmproc
cmpal,30h
jbisdecm1;al<30h:
cf=1,非数字
cmpal,3ah;cf=1:
al<3ah,是数字
cmc;CF取反,使用其与要求一致
isdecm1:
ret
isdecmendp
csegends
endstart
4.2.2利用约定的存储单元传递参数
利用数据段或附加段中的变量或参数表来传递参数,程序编写方便,且一般不会因为参数传递问题出错。
存在的问题是子程序的通用性降低。
例3(P123-1.ASM)实现32位(4字节)数相加的子程序。
方法:
用两次字加法实现双字数据的加法操作,第二次加法操作用ADC。
入口参数:
两个加数存放在双字缓冲区DATA1和DATA2
出口参数:
双字缓冲区DATA3,在DATA3后的第5个字节存放进位
注:
下面程序中增加了按十六进制ASCII码显示加数、和的操作。
datasegment
data1dd12345678h
data2dd98765432h
data3dd?
data4dd?
;显示缓冲区
dataends
codesegment
assumecs:
code,ds:
data
start:
movax,data
movds,ax
callmadd
movax,wordptrdata1
movwordptrdata4,ax
movax,wordptrdata1+2
movwordptrdata4+2,ax
calldisp
movax,wordptrdata2
movwordptrdata4,ax
movax,wordptrdata2+2
movwordptrdata4+2,ax
calldisp
movax,wordptrdata3
movwordptrdata4,ax
movax,wordptrdata3+2
movwordptrdata4+2,ax
calldisp
movah,4ch
int21h
maddproc;双字加法子程序
pushax
pushcx
pushsi
movcx,2
xorsi,si
madd1:
movax,wordptrdata1[si]
adcax,wordptrdata2[si]
movwordptrdata3[si],ax
incsi
incsi
loopmadd1
moval,0
adcal,0
movbyteptrdata3+4,al
popsi
popcx
popax
ret
maddendp
dispproc;双字显示子程序
movcx,0804h
lp:
roldata4,cl
movdl,byteptrdata4
anddl,0fh
adddl,30h
cmpdl,3ah
jbnext
adddl,07h
next:
movah,02h
int21h
decch
jnzlp
movdl,0ah
movah,02h
int21h
movdl,0dh
movah,02h
int21h
ret
dispendp
codeends
endstart
说明:
更常的方法是在存储区建立参数表,参数表的首地址通过寄存器传递给子程序,这样可以提高子程序的通用性。
[例4](p124-1.asm)设计一个把以ASCII码表示的十进制数字串转换为二进进制数的子程序。
假设十进制不大于65535。
方法:
高位×10+低位
入口参数:
DS:
BX=缓冲区首地址,首字节为字串字符数
出口参数:
AX=转换得到的二进制数。
datasegment
pr1db"Inputanumberstring:
$"
pr2db0ah,0dh,"Out:
$"
buffdb6
nudb0
stringdb6dup("0")
dataends
codesegment
assumecs:
code,ds:
data
start:
movax,data
movds,ax
leadx,pr1
movah,09h
int21h
movah,0ah
movdx,offsetbuff
int21h
leabx,nu
calldtobin
movbx,ax
leadx,pr2
movah,09h
int21h
movcx,16
lp:
rolbx,1
movdl,bl
anddl,01h
ad