第6章子程序.ppt
《第6章子程序.ppt》由会员分享,可在线阅读,更多相关《第6章子程序.ppt(130页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、第6章 子 程 序 第第6章章 子程序子程序6.1 堆栈堆栈 6.2 子程序的基本格式和有关指令子程序的基本格式和有关指令 6.3 应用子程序进行编程应用子程序进行编程 6.4 整数输入与输出整数输入与输出 6.5 子程序共享的方法子程序共享的方法 *6.6 递归递归 本章要点本章要点 习题六习题六 第6章 子 程 序 子程序是程序设计的重要方法与技术之一。程序设计中经常会遇到重复出现的程序段,如果把这种程序段每次出现时都抄写一遍,一方面会使程序冗长,不易于阅读,另一方面则会给程序的调试和维护带来很多不便。通常,对于有规律重复的程序段可以编制成循环程序,而无规律的重复就无法用循环实现。比如,实
2、现回车换行功能虽然是一个很短小的程序段,却在很多程序中经常使用,并且在程序中的位置没有什么规律可寻,这时使用子程序就是一个很好的方法。6.1 堆栈堆栈第6章 子 程 序 6.1.1 堆栈段堆栈段 图6.1是堆栈的物理结构示意图,图中标出的SS和SP是与堆栈密切相关的寄存器,SS存放堆栈所占用内存区域的段地址,SP所指向的位置称为栈顶。一个程序如果要使用堆栈,必须先留出一片连续内存区域,方法是在程序中定义一个堆栈段。第6章 子 程 序 图6.1 堆栈的逻辑结构 第6章 子 程 序 一个程序如果要使用堆栈,必须先留出一片连续内存区域,方法是在程序中定义一个堆栈段。【格式】段名SEGMENTSTAC
3、KDWn DUP(?)段名ENDS 第6章 子 程 序 (3)按基本格式定义的栈是一个空栈,栈中没有存放有效数据。(4)为了使SS和SP在程序执行时取得正确的值,必须在源程序中写一条伪指令:ASSUME SS:堆栈段段名 但不需要像DS和ES一样在程序中用指令进行赋值。对SS和SP的赋值是由操作系统在把执行程序调入内存时由DOS本身完成的,DOS将把SS赋值为堆栈段的段地址,把SP赋值为2n。第6章 子 程 序 6.1.2 进栈与出栈指令进栈与出栈指令 栈操作指令以它特有的方式存取数据,属于数据传递类指令,但又与MOV等指令有很大的区别。第6章 子 程 序 6.1.2.1 PUSH指令指令 【
4、指令格式】PUSH d 【功能】先把SP的值减去2,然后把操作数d指明的字型数据放入以SS为段地址、SP为偏移地址所对应的内存单元中。【说明】(1)这是单操作数指令,操作数d可以是包括段寄存器在内的任何字型寄存器,或者内存型寻址方式,但不能是立即寻址,当使用内存型寻址方式时可以使用段跨越。(2)PUSH指令的功能包括移动栈顶和存入数据两部分,两部分连续完成,密不可分。第6章 子 程 序 (3)操作数d进栈是以减2以后的SP的值作为偏移地址,但程序中不允许出现SP的写法。不要与基地址寄存器或变址寄存器用作偏地址时的写法相混淆,也就是说,把PUSH指令理解成下面两条指令的组合是不正确的:SUB S
5、P,2 MOV SP,d因为指令“MOV SP,d”存在语法错误。第6章 子 程 序(4)PUSH指令会导致栈顶指针的移动,如果用PUSH指令把很多数据进栈,使SP不断减2,就有可能超出栈的有效范围。在一些高级语言中这种现象会导致堆栈溢出错误,但8088对此并不做任何检测和警告。因此要求编程人员自己注意控制堆栈的大小,估计可能进栈的数据量,以免由于栈溢出导致一些不可预测的后果。第6章 子 程 序 6.1.2.2 POP指令指令 【指令格式】POP d 【功能】从SS为段地址、SP为偏移地址对应的内存中取出一个字型数据,送到操作数d指定的位置,然后把SP的值加2。对操作数d的寻址方式要求与PUS
6、H指令相同。堆栈通常用于临时保存数据。一般做法是先用PUSH指令把需要保存的数据入栈,然后完成一定的指令序列,再用POP指令把原先保存的数据出栈。用堆栈保存数据的特点是不用定义变量,不必关心被保存的数据到底在栈的什么位置,只要保证出栈和进栈的对应关系即可。当CPU中的寄存器不够使用时经常用堆栈临时保存数据。第6章 子 程 序 栈顶所指位置以上的部分是堆栈的空闲区,以下部分是已入栈的数据存放区(见图6.1),例6.1用来说明PUSH指令和POP指令对堆栈的影响。【例6.1】设AX4F8AH,BX307CH,SP1000H,分别逐条执行下列指令,用内存图的形式画出堆栈的变化情况,并分析程序段执行完
7、后AX和BX寄存器的值。PUSH AX PUSH BX POP AX POP BX第6章 子 程 序 【解】堆栈变化见图6.2,程序段执行完后AX307CH,BX4F8AH。图6.2 执行PUSH和POP指令对堆栈的影响第6章 子 程 序 6.1.2.3 PUSHF和和POPF指令指令 【指令格式】PUSHF 【功能】把SP的值减2,并把16位的标志寄存器送入SS:SP所指向的内存,即把标志寄存器入栈。【指令格式】POPF 【功能】把栈顶的一个16位的字型数据送入标志寄存器,并把SP的值加2。第6章 子 程 序 这两条指令除了用于临时保存标志寄存器的值之外,还可以与PUSH、POP指令配合用于
8、设置标志寄存器中的任意一个标志位。一般的做法是先用两条指令PUSHFPOPAX把标志寄存器的值复制到AX中,然后按标志位的分布情况(见图2.3)和实际需要,用AND、OR、XOR等指令修改AX的相应位,再用两条指令PUSH AXPOPF把修改后的值送到标志寄存器中。第6章 子 程 序 6.2 子程序的基本格式和有关指令子程序的基本格式和有关指令 6.2.1 汇编语言子程序格式汇编语言子程序格式 子程序是具有固定功能的程序段,并且有规定的格式。不同的计算机语言对子程序格式的规定不同,汇编语言的子程序基本格式如下:子程序名 PROC 类型 指令序列 子程序名 ENDP 第6章 子 程 序 格式中的
9、首尾两行表示一个子程序的开始和结束,都属于伪指令。“子程序名”是一个标识符,是编程者给子程序起的名字。子程序名同时还代表子程序第一条指令所在的逻辑地址,称为子程序的入口地址。“类型”只有NEAR和FAR两种,它将影响汇编程序对子程序调用指令CALL和返回指令RET的翻译方式。被夹在子程序起止伪指令之间的指令序列是完成子程序固定功能的程序段,通常指令序列的最后一条指令是返回指令RET。第6章 子 程 序 6.2.2 子程序相关指令子程序相关指令6.2.2.1 CALL指令指令 【指令格式】CALL 子程序名 【功能】这是调用子程序的指令。根据被调用的子程序的类型不同,CALL指令的功能分为两种情
10、况:(1)如果子程序是NEAR类型,则先把当前指令指针IP的值入栈,这会使SP的值减2,然后把IP改成子程序的第一条指令所在的偏移地址。这种只修改IP不修改CS的子程序调用称为段内调用。第6章 子 程 序(2)如果子程序是FAR类型,则先把当前CS的值入栈,再把IP的值入栈,结果会使SP的值减4,然后把CS和IP改为子程序第一条指令的逻辑地址。这种同时修改CS和IP的子程序调用称为段间调用。CALL也是一种跳转指令,与无条件跳转及条件跳转指令不同的是,CALL在跳转之前先预留了回来的方法,把IP的当前值或CS与IP的当前值入栈保存。从CS与IP 的作用可以知道,它们存放的是正在执行的指令的下一
11、条指令的逻辑地址,现在这一地址被保存在堆栈中。于是回来的方法就显而易见了,只要从栈中取出逻辑地址值,送回IP或者CS与IP即可。这种返回操作就是由RET指令实现的。第6章 子 程 序 6.2.2.2 RET指令指令 【指令格式】RET 【功能】这是子程序返回指令,必须写在子程序的指令序列之中。根据所在的子程序的类型不同,RET指令的功能也分为两种情况:(1)如果RET所在子程序是NEAR类型,则从堆栈中出栈一个字(当然,SP会加2),送给IP。(2)如果RET所在子程序是FAR类型,则先从堆栈中出栈一个字送到IP,再出栈一个字送到CS,栈顶指SP的值加4。第6章 子 程 序 CALL指令和RE
12、T指令都具有跳转的能力,与条件跳转及无条件跳转一样,都是通过修改IP或者CS与IP来实现的。不论跳转是由哪一条指令造成的,对于只改变IP 的跳转,跳转的目的地与跳转指令必然在同一个代码段内,这种跳转称为段内跳转。相应地,CALL指令功能的第一种情况称为段内调用,RET指令功能的第一种情况称为段内返回。另一种跳转是同时改变了CS和IP的值,这就允许跳转指令与跳转目的地不在同一个段中,使得跳转的目的地可以在整个内存空间的任何位置,这一类跳转称为段间跳转。CALL指令功能的第二种情况称为段间调用,RET指令功能的第二种情况称为段间返回。第6章 子 程 序 6.2.3 子程序的调用与返回子程序的调用与
13、返回 子程序具有固定的功能,这种功能在一个程序或多个程序中经常反复使用。使用子程序的目的就在于编程时不愿意把相同的程序段在每个需要使用的地方抄写一遍。在汇编语言程序中,子程序分为定义和调用两种情况:子程序定义是指按6.2.1节的格式编写程序段;而子程序调用是指用“CALL 子程序名”告诉CPU在执行到此处时转到相应的子程序去执行。在较短的程序中,可以把子程序定义与其余指令写在同一个代码段内。一个代码段中可以定义多个子程序,并且都定义成NEAR类型。对于代码较长的程序,可以把子程序与主程序分别在不同的段中编写,并把允许段间调用的子程序定义成FAR类型。下面是含有子程序的一种可能的程序结构:第6章
14、 子 程 序 段名段名 SEGMENTASSUMECS:段名段名A 子程序1PROCFAR子程序1ENDP段名A ENDS段名B SEGMENTASSUMECS:段名B子程序2PROCNEAR子程序2ENDP入口标号:段名B ENDSEND入口标号 第6章 子 程 序 从“入口标号”起编写主程序部分,整个程序从“入口标号”所在的那条指令开始执行。主程序可以调用子程序1,也可以调用子程序2。在语法规则上,一个子程序可以调用另一个子程序,还可以调用它自身,并且在书写次序上没有“先定义后调用”的限制。子程序1是FAR类型,不管“CALL 子程序1”出现在哪个段内,所有对它的调用都是段间调用。子程序2
15、是NEAR类型,对它的调用都是段内调用,调用指令“CALL 子程序2”必须与子程序2在同一段内,否则无法正确地实现转向及返回。在上面的例子中,从子程序1中调用子程序2就是错误的。第6章 子 程 序 【例6.2】分析下面的程序段的执行过程,以及在执行过程中堆栈及指令指针IP的变化情况,并假设在执行CALL指令前,SP的值是0FEH。subp PROC NEAR INC AL ;假设本指令所在的偏移地址是1234H RETsubp ENDP CALL subp MOV AX,BX ;假设本指令所在的偏移地址是5678H第6章 子 程 序 【解】【解】(1)当计算机把CALL subp对应的机器指令
16、取到CPU中时,IP的值已经是CALL的下一行的MOV指令所在的偏移地址5678H,此时还未进栈,栈的情况如图6.3(a)所示。(2)由于子程序subp是NEAR类型,按照CALL指令功能的第一种情况执行CALL指令,把IP的值入栈,并把IP的值改为subp子程序的入口地址1234H,此时堆栈的情况如图6.3(b)所示。(3)执行完CALL指令 IP的值已经变成1234H,CS没变,CPU按新的IP值,在CS段下取出一条指令,即INC AL指令。第6章 子 程 序 (4)执行INC指令时,CPU自动把IP变成INC的下一行指令的偏移地址,如此逐条执行子程序中的各指令,直至遇到subp子程序的最
17、后一条指令RET。(5)执行RET指令时,堆栈中的情况仍然是图6.3(b),因此执行RET就是取出栈顶所指的一个字,是5678H,并把它送给IP,执行完RET指令后堆栈的情况如图6.3(c)所示。(6)执行完RET指令后,IP的值已经变成5678H,CPU按新的IP值,在CS段下取出一条指令,即MOV AX,BX指令,并继续执行下去。第6章 子 程 序 图6.3 例6.2的程序执行过程中堆栈的变化情况 例6.2描述了段内调用与返回的过程,对于段间调用与返回,仅仅在CALL指令和RET指令的执行效果上不同,这个问题留给读者:把例6.2中的子程序类型改成FAR,执行过程中栈的变化情况又如何?第6章
18、 子 程 序 例6.2中隐藏有一个非常严重的问题,就是如何保证执行完CALL指令后堆栈的情况与执行RET指令前堆栈的情况是相同的。这个问题确实存在,并且是程序员不可回避的。因为完成子程序需要执行多条指令,这些指令中难免会有改变栈顶指针或者改动栈中数据的情况。但是,无论是汇编程序还是计算机硬件本身都对此无能为力,需要程序员自己在编制程序时非常小心。如果不能保证堆栈的情况相同,执行到RET时,计算机仍然按照RET指令本身的功能正常处理,出栈一个字给IP或者连续出栈两个字分别给CS及IP,这时就不会回到调用指令CALL的下一行,而不知跳转到什么地方去了。第6章 子 程 序 【注意】为了避免出现这种情
19、况,编制子程序时应该注意以下几点:(1)子程序中的PUSH指令与POP指令数量应该相同,并且存在一一对应关系。(2)不要把SP用作MOV、ADD等指令的目的操作数,不要使用INC SP、DEC SP等指令,不要使用类似指令改变SP的值。(3)不要使用POP SP指令,该指令会用出栈的一个字型数据修改SP,而不像正常的POP指令一样把SP加2。(4)如果子程序中再次用CALL指令去调用子程序,只要被调用的子程序正确,则不会导致出现上述问题。第6章 子 程 序 6.3.1 子程序实例子程序实例 回车换行是汇编语言程序经常要用到的功能。完成这一功能需要56条指令,如果能把它设计成一个子程序,则源程序
20、中任何需要回车换行的地方只要写上一条 CALL指令就行了。这不仅会使程序简短,也能减少编制程序时出错的可能性。下面的例6.3中就应用了回车换行子程序。6.3 应用子程序进行编程应用子程序进行编程第6章 子 程 序 【例6.3】分析下列程序,描述它的功能。dseg SEGMENT buf DB 80,81 DUP(0)dseg ENDS sseg SEGMENT STACK DW 64 DUP(0)sseg ENDS cseg SEGMENT ASSUME CS:cseg,DS:dseg,SS:sseg第6章 子 程 序 cr PROC NEAR MOV AH,2 MOV DL,13 INT 2
21、1H MOV DL,10 INT 21H RET第6章 子 程 序 cr ENDP main:MOV AX,dseg MOV DS,AX LEA DX,buf MOV AH,10 INT 21H ;输入一个符号串 CALL cr MOV AH,1 INT 21H ;输入一个字符 MOV BL,AL ;用BL保存读入的字符第6章 子 程 序 lab2:MOV DL,SI CMP DL,BL JZ lab1 ;等于第2次输入的符号则转 MOV AH,2 INT 21H INC SI LOOP lab2 lab1:MOV AH,4CH INT 21H cseg ENDS END main第6章 子
22、程 序 6.3.2 对子程序中用到的寄存器进行保护对子程序中用到的寄存器进行保护 【例6.4】设子程序cr的定义如例6.3所示,比较下面两个程序段,分析各自执行完后寄存器AX中的值是多少。(a)MOV AX,102H MOV BX,304H ADD AX,BX(b)MOV AX,102H MOV BX,304H CALL cr ADD AX,BX第6章 子 程 序 【解】程序段(a)中,先把AX赋值为102H,再把BX赋值为304H,然后用ADD指令把两数相加,和为406H,结果放在ADD指令的目的操作数AX中。程序段(b)的前两行与(a)完全相同,AX取值102H,BX取值304H,但在相加
23、之前调用了子程序cr。从例6.3中cr的具体实现方法可以知道,调用过程中寄存器AH的值被改为2,因为INT 21H输出功能,使AL的值也被修改,变成0AH,并且这个值一直保持到调用结束,于是“CALL cr”指令调用子程序后,AX的值不再是调用前的102H,而变成了20AH,当ADD指令进行两个寄存器相加时,结果是50EH,并放到目的操作数AX中。第6章 子 程 序 从例6.4可以看到,两个程序段仅仅相差一个子程序调用,而且子程序cr也只不过完成回车换行的操作,但两个程序段执行的结果却不一样,原因就在于调用子程序前,寄存器AX中放了一个有用的数据102H,但子程序中对AX重新赋了值,破坏了原来
24、的数据。子程序中修改寄存器的值会给程序编制带来很大的麻烦,就如例6.4(b)的情况,想要找出错误的原因是不太容易的。为此,做法之一是在调用前把有用的数据存放到适当的地方保护起来,比如在例6.4(b)的CALL指令之前可以把 AX的值先找一个寄存器(比如SI)临时存放,调用后再取回到AX中;另一个比较好的做法是在子程序中对所有使用到的寄存器进行保护,等到子程序的功能完成后,再恢复这些寄存器的原值,最后以RET指令返回。按照这个原则,把例6.3的子程序cr 改写成如下形式:第6章 子 程 序 cr PROC NEAR PUSH AX PUSH DX MOV AH,2 MOV DL,13 INT 2
25、1H MOV DL,10 INT 21H POP DX POP AX RET cr ENDP第6章 子 程 序 修改后的子程序cr先把AX和DX的值入栈保护,完成回车换行操作后,再从栈中取出原来保存的数据恢复AX和DX的原值。用堆栈临时保存数据是子程序中普遍使用的一种方法。经过这样的修改后,例6.4的两个程序段各自执行后,AX中的值就会是一样的,调用子程序cr进行回车换行操作就不会影响程序的正常执行。入栈指令PUSH和出栈POP指令必须一一对应。从栈操作的“先进后出”方式可以知道,入栈次序与出栈次序是相反的,所以PUSH指令序列中操作数的次序与 POP指令序列中操作数的次序相反,就如同上面的子
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 章子 程序
限制150内