第六章子程序结构.docx
- 文档编号:25543846
- 上传时间:2023-06-09
- 格式:DOCX
- 页数:34
- 大小:146.87KB
第六章子程序结构.docx
《第六章子程序结构.docx》由会员分享,可在线阅读,更多相关《第六章子程序结构.docx(34页珍藏版)》请在冰豆网上搜索。
第六章子程序结构
第六章子程序结构
一、主程序与子程序的概念
子程序又称为过程,它相当于高级语言中的过程和函数。
通常,凡是具有相对独立功能的程序段或需要多次应用的程序段都可以编制成子程序。
程序中当需要这段程序时,用调用指令CALL转到这段程序中去,执行完毕,通过RET指令返回原来的程序。
如下图
(1)所示。
子程序返回后,从CALL的下一条指令X1继续执行主程序。
使用子程序优点:
节省编程时间和存储空间,有利于设计一个大而复杂的程序。
即可以把一个大而复杂的程序设计成由一个主程序和若干个子程序组成,使用模块化程序设计,减少程序设计复杂性。
使用子程序不足之处:
在调用和返回时要进行堆栈操作,增加CPU开销,使程序速度减慢。
主程序与子程序的概念是相对的。
图
(2)是一个子程序嵌套调用的例子。
图中,程序1在A、B两处调用程序2,在C处调用程序3,则称程序2和3是程序1的子程序,程序1是主程序;
而程序2和3又分别调用程序4,则称程序4是程序2和3的子程序,程序2和3是主程序。
这就是子程序中嵌套子程序。
子程序还可以直接或间接调用自己,称为递归子程序和递归调用。
二、调用指令CALL和返回指令RET
(参见指令系统p.98)
CALL调用指令
RET返回指令
由于子程序和调用程序可以在同一个段中,也可以不在同一段中,因此这两条指令的格式如下:
1、调用指令CALLDST
CALL指令语句实现子程序(即过程)的调用。
调用结束后,要由所调用的子程序RET指令返回到CALL指令的下一条指令(即返回调用断点),因此执行CALL指令时要进行进栈操作。
具体分为直接调用和间接调用。
直接调用:
目标地址直接出现在CALL指令语句中,DST就是过程名(即子程序名)。
间接调用:
由16位通用寄存器或者存储单元提供调用地址。
(1)段内直接调用
格式:
CALLDST
执行的操作:
Push(IP);当前IP值进栈
(IP)←(IP)+D16
;目标地址装入IP,程序转移
指令操作分二步:
第一步,是把子程序的返回地址(即调用程序中CALL指令的下一条指令的地址)存入堆栈中,以供子程序返回主程序时使用。
第二步操作则是转移到子程序的入口地址去继续执行。
指令中DST给出转向地址(即子程序入口地址,也即子程序的第一条指令的地址)。
D16是机器指令中的位移量,是转向地址和返回地址之间的差值。
例:
CALLSB1
子程序名SB1是NEAR型标号
(2)段间直接远调用
格式:
CALLDST或CALLFARPTRDST
执行的操作:
Push(CS);返回地址的段地址入栈
Push(IP);返回地址的偏移地址进栈
(IP)←DST指定的偏移地址
(CS)←DST指定的段地址
段内调用和段间调用的堆栈操作如下图所示。
图(a)段内CALL调用后,由于CS保持不变,只修改IP的值,所以返回地址只有IP的值压入堆栈中。
图(b)在段间CALL调用后,由于调用程序和子程序不在同一个段中,调用后CS、IP值都发生了变化,所以返回地址的保存以及转向地址的设置都必须把段地址CS考虑进去。
2、返回指令RET
(1)段内近返回RET
执行的操作:
(IP)←POP()
就是把SP所指的栈顶一个字的内容弹回到指令指针IP中,且SP加上2。
(2)段间远返回RET
执行的操作:
(IP)←POP()
(CS)←POP()
这就是把SP所指的栈顶的一个双字的内容弹回到指令指针IP和代码段寄存器CS中,第一个字弹回到IP,SP加上2,第二个字弹回到CS,SP再加2。
(3)带参数的返回指令RET(P.100)
格式:
RETEXP
EXP——表达式,计算结果是常数N
执行的操作:
完成RET指令操作,再执行(SP)←(SP)+D16
指令在返回地址出栈后再修改堆栈指针,等于把CALL指令调用以前入栈的D16个字节数弹出堆栈作废,故一般D16是一个偶数。
例如:
RET8和RET6等。
6.1子程序的设计方法
6.1.1过程定义后操作
格式:
procedure_namePROCAttribute
RET
procedure_nameENDP
procedure_name:
过程名,任起。
Attribute:
类型属性,FAR或者NEAR。
过程定义中,PROC与ENDP必须成对使用。
过程中,原则上至少有一条RET指令,但也可以多于一条,不一定放在过程最末一条指令。
通过RET返回调用者。
过程名:
被CALL指令调用,是子程序入口的符号地址,是标号。
它具有三个属性:
段属性、偏移地址和类型属性。
指出过程类型十分重要,它是汇编程序生成CALL和RET指令目标代码的依据。
•过程类型为NEAR型:
供段内调用的子程序。
此时调用它的CALL指令生成三字节目标代码,过程中的RET是近返回的目标代码。
•过程类型为FAR型:
供段间调用的子程序。
此时调用它的CALL指令生成五字节目标代码,过程中的RET是远返回的目标代码。
所以,过程的类型决定了CALL和RET指令类型。
过程类型确定原则:
(1)调用程序和过程在同一个代码段中,则使用NEAR属性
例6.1调用程序和子程序在同一个代码段
CODESEGMENT
MAINPROCFAR
ASSUMECS:
CODE
┇
CALLSUBR1
┇
RET
MAINENDP
SUBR1PROCNEAR
┇
RET
SUBR1ENDP
CODEENDS
END
例中,MAIN和SUBR1都属于CODE段,所以SUBR1定义为NEAR型属性。
这样主程序中CALLSUBR1语句及SUBR1中的RET都是NEAR型。
对MAIN主程序,定义为FAR属性,是因为将MAIN当作DOSFAR调用的子程序。
例6.2调用程序和过程不在同一个代码段中,则使用FAR属性。
SEGXSEGMENT
ASSUMECS:
SEGX
┇
SUBTPROCFAR
┇
RET
SUBTENDP
┇
CALLSUBT
┇
SEGXENDS
;
SEGYSEGMENT
ASSUMECS:
SEGY
┇
CALLFARPTRSUBT
┇
SEGYENDS
例中,子程序SUBT在SEGX段中定义,它有两处被调用:
一处是与它在同一段的SEGX段内,另一处是在另一段SEGY内。
当被SEGY段调用时,子程序SUBT和CALLSUBT语句分属二个不同的段,则SUBT属性为FAR以适用SEGY段调用的需要。
这样不论在SEGX段和SEGY段对SUBT的调用,都具有FAR属性了,不会发生错误。
如果使用NEAR属性,在SEGY段内对它的调用将出错。
6.1.2子程序的调用和返回
在执行CALL和RET指令时,CPU自动执行堆栈操作。
CALL指令:
将返回地址压入堆栈,RET指令,将返回地址从栈中弹出到IP(远返回则弹出到IP和CS)。
例:
主程序MAIN在一个代码段中,(CS)=0500H
子程序PRO_A,PRO_B,在另外一个代码段中,调用关系如下:
设SP初值为0100H,调用过程堆栈变化情况如图:
(1)MAIN调用PRO_A前
(2)MAIN调用PRO_A后
(3)PRO_A调用PRO_B后
(4)PRO_B返回PRO_A后
(5)PRO_A返回MAIN后
使用注意点:
要保证子程序正确返回调用程序,要求在执行RET指令前,SP指向返回地址,所以在子程序中,PUSH和POP操作必须是成对的。
调用结束返回后,SP必须恢复到调用前的值。
例:
习题6.1(p.240)下面的程序有错吗?
若有,请指出错误。
CRAYPROC
PUSHAX
ADDAX,BX
RET
ENDPCRAY
解:
该子程序有二个错误:
(1)末行ENDP与子程序名CARY写颠到了。
(2)子程序中,有PUSHAX,但是没有
POPAX,该子程序不能正确返回调用程序。
6.1.3保存与恢复寄存器
由于主程序与子程序是分开编写的,所以使用的寄存器往往会发生冲突。
保存与恢复寄存器又称保护现场和恢复现场。
具体要求是:
在子程序返回主程序后,CPU中工作寄存器的状态必须恢复到调用前的值,包括程序状态字PSW、各数据寄存器、地址寄存器等。
一般方法:
进入子程序执行PUSH操作保护,返回前执行POP操作恢复。
例如:
例如:
SUBTPROCNEAR
PUSHAX
PUSHBX
PUSHCX
┇
POPCX
POPBX
POPAX
RET
SUBTENDP
基本考虑方法:
在主程序和子程序之间传递参数的寄存器不必保存,特别是子程序向主程序回送结果的寄存器不能保存,否则会产生错误。
其他的在子程序中用到的寄存器应该保存。
6.1.4子程序的参数传送
参数传送是调用程序和子程序之间的信息传递,分调用参数和返回参数。
调用参数(又称入口参数):
由调用程序提供给
子程序工作的初始数据.
返回参数(又称出口参数):
由子程序将执行以后的结果数据回送给调用程序。
参数传递有以下几种方法:
1、通过寄存器传递参数——主程序和子程序之间通过约定的寄存器传递参数。
调用前,主程序将调用参数放入约定的寄存器中;进入子程序后,从约定的寄存器中取出参数执行子程序;执行完毕,将结果存入约定的寄存器,返回主程序后就可获得结果。
这是最常用的一种方法。
特点:
简单、直观、使用方便,但参数个数受寄存器限制,不宜过多。
例6.3十进制数到十六进制数转换程序。
要求:
从健盘输入十进制数转换成二进制,然后把该数以十六进制形式在屏幕上显示出来。
采用子程序结构,各子程序功能如下:
DECIBIN:
从键盘取得十进制数,转换成二进制后保存在BX中。
BINIHEX:
用十六进制形式显示BX中的数。
CRLF:
回车、换行,调用该子程序后,屏幕上滚一行,光标回到起始位置,以避免屏幕上显示重叠。
必须指出:
在各个子程序之间用BX寄存器传递信息。
源程序如下:
;主程序
DECIHEXSEGMENT
ASSUMECS:
DECIHEX
MAINPROCFAR
REPEAT:
CALLDECIBIN
CALLCRLF
CALLBINIHEX
CALLCRLF
JMPREPEAT
MAINENDP
·DECIBIN子程序:
(1)十进制数转换成二进制算法:
将十进制数按权展开相加。
例:
有十进制数:
9587
9587=9×103+5×102+8×101+7
=(((0×10+9)×10+5)×10+8)×10+7
式中:
0就是结果寄存器的初值,式中的基本运算就是乘法和加法。
采用二进制乘法指令和加法指令,就得到了二进制的结果。
(2)程序流程图
先将输入一位字符的ASCII码转换成BCD数,然后判断其有效性。
若为0-9之间数字,表示输入有效,进行转换运算,并可继续输入;否则子程序返回。
最后转换结果在BX中,所以输入的最大数是65535,转换后的结果为0FFFFH,超出此范围,结果错误。
程序流程图如下:
源程序:
DECIBINPROCNEAR
MOVBX,0
NEWCHAR:
MOVAH,1
INT21H;(AL)=输入字符ASCII码
SUBAL,30H;转换成二进制
JLEXIT;(AL)<0,转结束
CMPAL,9;(AL)>9?
JGEXIT;是,转结束
CBW;否,进行转换运算
XCHGAX,BX
MOVCX,10
MULCX
XCHGAX,BX
ADDBX,AX
JMPNEWCHAR;继续输入字符
EXIT:
RET
DECIBINENDP
。
子程序BINIHEX:
将BX中二进制数转换成十六进制在控制台屏幕显示。
;
BINIHEXPROCNEAR
MOVCH,4
ROTATE:
MOVCL,4
ROLBX,CL
MOVAL,BL
ANDAL,0FH
ADDAL,30H
CMPAL,3AH
JLPRINTIT
ADDAL,07H
PRINTIT:
MOVDL,AL
MOVAH,2
INT21H
DECCH
JNZROTATE
RET
BINIHEXENDP
;——————————————————————————————————
。
子程序CRLF:
回车,换行
CRLFPROCNEAR
MOVDL,0DH
MOVAH,2
INT21H
MOVDL,0AH
MOVAH,2
INT21H
RET
CRLFENDP
;————————————————————————————————————
DECIHEXENDS
ENDMAIN
2、如过程和调用程序在同一源文件(同一程序模块)中,则过程可以直接访问模块中的变量。
例:
6.4主程序MAIN和过程PROADD在同一源文件中,要求用过程PROADD累加数组中的所有元素,并把和(不考虑溢出的可能性)送到指定的存储单元中去。
源程序如下:
DATASEGMENT;定义数据段
ARYDW100DUP(?
)
COUNTDW100
SUMDW?
DATAENDS
;
CODESEGMENT;定义代码段
MAINPROCFAR
ASSUMECS:
CODE,DS:
DATA
START:
PUSHDS
SUBAX,AX
PUSHAX
MOVAX,DATA
MOVDS,AX
┇
CALLPROADD
┇
RET
MAINENDP
;
PROADDPROCNEAR;定义子程序
PUSHAX
PUSHCX
PUSHSI
LEASI,ARY
MOVCX,COUNT
XORAX,AX
NEXT:
ADDAX,[SI]
ADDSI,2
LOOPNEXT
MOVSUM,AX
POPSI
POPCX
POPAX
RET
PROADDENDP
CODEENDS
ENDSTART
;
这是一个通过约定存储器缓冲区传递参数的方法。
子程序中对固定的数组ARY进行处理,使程序结构清晰,但子程序缺少通用性。
例如数据段中有二个数组,用PROADD对NUM数组累加,处理方法是在调用PROADD累加NUM数组前,先把NUM数组及N的内容全部传递到ARY数组和COUNT单元中去,PROADD运行完毕后,再把SUM的结果送到TOTAL中。
这显然不是一种好方法。
改写PROADD用寄存器传递参数,则需要三个寄存器。
程序如下:
调用参数:
DS:
SI—数组起始地址
CX—数组长度
DS:
DI—和存放地址
返回参数:
无
DATASEGMENT;定义数据段
ARYDW100DUP(?
)
COUNTDW100
SUMDW?
NUMDW200DUP(?
)
NDW200
TOTALDW?
DATAENDS
CODESEGMENT;定义代码段
START:
┇
LEASI,ARY;统计ARY数组和
MOVCX,COUNT
LEADI,SUM
CALLPROADD
┇
LEASI,NUM;统计NUM数组和
MOVCX,N
LEADI,TOTAL
CALLPROADD
┇
PROADDPROCNEAR;求累加和子程序
PUSHAX
XORAX,AX
NEXT:
ADDAX,[SI]
ADDSI,2
LOOPNEXT
MOV[DI],AX
POPAX
RET
PROADDENDP
CODEENDS
ENDSTART
3、通过地址表传送变量地址
基本方法:
设置专用存储器缓冲区作为地址表区,在主程序中把调用参数放入地址表中,然后把地址表的首地址通过寄存器BX传送子程序,子程序通过地址表取得所需参数,运算后,将结果参数(返回参数)存入指定的存储单元。
程序如下:
PROG_SEGSEGMENT;
ORG100H
ASSUMECS:
PROG_SEG,DS:
PROG_SEG,SS:
PROG_SEG,
MAINPROCNEAR;主程序
MOVAX,PROG_SEG
MOVDS,AX
┇
MOVTABLE,OFFSETARY
;填写数组首地址,参数1
MOVTABLE+2,OFFSETCOUNT
;填写数组长度存放地址,参数2
MOVTABLE+4,OFFSETSUM
;填写和存放地址,参数3
MOVBX,OFFSETTABLE
;BX指向参数表首地址
CALLPROADD
;调用PROADD子程序
┇
MOVAX,4C00H
INT21H;返回操作系统
MAINENDP
PROADDPROCNEAR;PROADD子程序
PUSHAX;保护寄存器
PUSHCX
PUSHSI
PUSHDI
MOVSI,[BX];取ARY首地址到SI
MOVDI,[BX+2];取COUNT地址到DI
MOVCX,[DI];取计数值到CXMOVDI,[BX+4];取和存放地址
XORAX,AX;清AX
NEXT:
ADDAX,[SI];求累加和
ADDSI,2
LOOPNEXT
MOV[DI],AX;存和
POPDI;恢复寄存器
POPSI
POPCX
POPAX
RET
PROADDENDP
;
ARYDW100DUP(?
);定义数据区
COUNTDW100
SUMDW?
TABLEDW3DUP(?
)
PROG_SEGENDS
ENDMAIN
本程序采用COM文件形式,代码、数据和堆栈都设在同一个段中,总长度不超过64KB。
与EXE文件相比,COM文件占有的存储空间小,装入速度快,适用于编制较小的程序.教材p.1534.4.6介绍COM文件。
4、通过堆栈传递参数或参数地址
实现方法:
主程序在调用子程序前,将参数保存到堆栈中,进入子程序,从堆栈中取出参数进行计算。
优点:
可以实现子程序递归调用。
例6.4采用通过堆栈传送参数地址的程序
PARM_SEGSEGMENT;定义数据段
ARYDW100DUP(?
)
COUNTDW100
SUMDW?
PARM_SEGENDS
;
STACK_SEGSEGMENT;定义堆栈段
DW100DUP(?
)
TOSLABELWORD
STACK_SEGENDS
;
CODE1SEGMENT;定义代码段
MAINPROCFAR;主程序
ASSUMECS:
CODE1,DS:
PARA_SEG,SS:
STACK_SEG
START:
MOVAX,STACK_SEG
MOVSS,AX
MOVSP,OFFSETTOS
PUSHDS
SUBAX,AX
PUSHAX
MOVAX,PARM_SEG
MOVDS,AX
MOVBX,OFFSETARY
PUSHBX
MOVBX,OFFSETCOUNT
PUSHBX
MOVBX,OFFSETSUM
PUSHBX
CALLFARPTRPROADD
┇
RET
MAINENDP
CODE1ENDS
;
CODE2SEGMENT
ASSUMECS:
CODE2
PROADDPROCFAR
PUSHBP
MOVBP,SP
PUSHAX
PUSHCX
PUSHSI
PUSHDI
MOVSI,[BP+0AH]
MOVDI,[BP+8]
MOVCX,[DI]
MOVDI,[BP+6]
XORAX,AX
NEXT:
ADDAX,[SI]
ADDSI,2
LOOPNEXT
MOV[DI],AX
POPDI
POPSI
POPCX
POPAX
POPBP
RET6
PROADDENDP
CODE2ENDS
ENDSTART
程序说明:
(1)程序中设置了堆栈段,段长100个字。
代码段开始,对SS、SP置初值。
(2)子程序PROADD和主程序MAIN分属二个不同的代码段,用ASSUME语句指示CS寄存器与代码段的关系。
因此主程序中用CALLFARPTRPROADD,指明是一个远调用过程。
(3)在调用子程序之前,将三个参压入堆栈,在子程序PROADD中,用BP作为基址寄存器,采用寄存器相对寻址方式取出椎栈中的参数进行运算。
结果存入存储器。
最后用带参数的返回指令RET6返回调用程序,并且废除调用前压入堆栈中的三个参数,使堆栈能恢复原始状态不变。
6.2子程序的嵌套
子程序的嵌套:
一个子程序可以作为调用程度去调用另一个子程序,这种情况就称为子程序的嵌套。
嵌套层次不限。
嵌套层数称为嵌套深度。
下图:
嵌套深度为2。
嵌套子程序设计注意点:
(1)寄存器的保存和恢复,避免发生寄存器冲突。
(2)若子程序中使用了堆栈操作,在执行RET指令前,SP必须指向返回地址,以实现正确返回。
递归子程序
在子程序嵌套的情况下,子程序包含调用自身的操作(也即子程序自己调用自己),该子程序称为递归子程序。
根据调用自身的方式的不同,分为直接递归和间接递归,如下图所示。
注意:
在编制嵌套或递归子程序时,堆栈使用频繁,应该防止堆栈溢出.
6.3子程序举例
例6.9:
HEXIDEC是一个把十六进制数转换成十进制数的程序。
要求把从键盘输入的0-FFFF的十六进制正数转换为十进制数并从屏幕上显示出来。
本程序的功能和例6.3相反,由HEXIBIN和BINIDEC两个主要的子程序组成。
由于主程序和子程序在同一个模块中,因而省略了对寄存器的保护和恢复工作,子程序之间的变量传送到采用寄存器传送的方式进行,程序可用CtrlBREAK退出。
源程序如下:
DISPLAYEQU2H
KEY_INEQU1H
DOSCALLEQU21H
HEXIDECSEGMENT
MAINPROCFAR
ASSUMECS:
HEXIDEC
START:
PUSHDS
SUBAX,AX
PUSHA
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第六章 子程序结构 第六 子程序 结构