1、 第第6章章 子程序子程序 6.1 堆栈堆栈 6.2 子程序的基本格式和有关指令子程序的基本格式和有关指令 6.3 应用子程序进行编程应用子程序进行编程 6.4 整数输入与输出整数输入与输出 6.5 子程序共享的方法子程序共享的方法 *6.6 递归递归 本章要点本章要点 习题六习题六 返回章目录6.1 堆栈堆栈 在汇编语言和机器语言中,堆栈在物理结构上是一段存放数据的连续的内存区域,以及一个称为栈顶指针的专用存储单元。堆栈中只能存入16位的字型数据,存入数据的操作称为“进栈”或“压栈”,已存入的数据也可以取出,称为“出栈”或“弹出”,数据的存取操作由专用指令完成。从逻辑上说,堆栈是一种按“先进
2、后出”原则进行操作的数据结构,栈顶指针用于指出入栈操作和出栈操作的位置。6.1.1 堆栈段堆栈段 图6.1是堆栈的物理结构示意图,图中标出的SS和SP是与堆栈密切相关的寄存器,SS存放堆栈所占用内存区域的段地址,SP所指向的位置称为栈顶。已入栈的数据存放区 栈的空闲区 SS:SP栈顶指针 一个程序如果要使用堆栈,必须先留出一片连续内存区域,方法是在程序中定义一个堆栈段。【基本格式】段名 SEGMENT STACK DW n DUP(?)段名 ENDS【说明】(1)保留字STACK是堆栈段的专用符号,SEGMENT后面的保留字STACK表明这个段专供堆栈使用。(2)段定义中用“DW n DUP(
3、?)”说明堆栈所用内存区的大小为2n字节,其中n是一个常量。可根据程序需要,调节堆栈段的大小。因为堆栈只能存放字型数据,所以习惯上都是用DW伪指令来定义栈的大小。这不并是说用其它伪指令不行。(3)按基本格式定义的栈是一个空栈,栈中没有存放有效数据。(4)为了使SS和SP在程序执行时取得正确的值,必须在源程序中写一条伪指令:ASSUME SS:堆栈段段名 但不需要像DS和ES一样在程序中用指令进行赋值。对SS和SP的赋值是由操作系统在把执行程序调入内存时由DOS本身完成的,DOS将把SS赋值为堆栈段的段地址,把SP赋值为2n。6.1.2 进栈与出栈指令进栈与出栈指令 栈操作指令以它特有的方式存取
4、数据,属于数据传递类指令,但又与MOV等指令有很大的区别。6.1.2.1 PUSH指令指令 【指令格式】PUSH d 【功能】先把SP的值减去2,然后把操作数d指明的字型数据放入以SS为段地址、SP为偏移地址所对应的内存单元中。【说明】(1)这是单操作数指令,操作数d可以是包括段寄存器在内的任何字型寄存器,或者内存型寻址方式,但不能是立即寻址,当使用内存型寻址方式时可以使用段跨越。(2)PUSH指令的功能包括移动栈顶和存入数据两部分,两部分连续完成,密不可分。(3)操作数d进栈是以减2以后的SP的值作为偏移地址,但程序中不允许出现SP的写法。不要与基地址寄存器或变址寄存器用作偏地址时的写法相混
5、淆,也就是说,把PUSH指令理解成下面两条指令的组合是不正确的:SUB SP,2 MOV SP,d 因为指令“MOV SP,d”存在语法错误。(4)PUSH指令会导致栈顶指针的移动,如果用PUSH指令把很多数据进栈,使SP不断减2,就有可能超出栈的有效范围。在一些高级语言中这种现象会导致堆栈溢出错误,但8088对此并不做任何检测和警告。因此要求编程人员自己注意控制堆栈的大小,估计可能进栈的数据量,以免由于栈溢出导致一些不可预测的后果。6.1.2.2 POP指令指令 【指令格式】POP d 【功能】从SS为段地址、SP为偏移地址对应的内存中取出一个字型数据,送到操作数d指定的位置,然后把SP的值
6、加2。对操作数d的寻址方式要求与PUSH指令相同。堆栈通常用于临时保存数据。一般做法是先用PUSH指令把需要保存的数据入栈,然后完成一定的指令序列,再用POP指令把原先保存的数据出栈。用堆栈保存数据的特点是不用定义变量,不必关心被保存的数据到底在栈的什么位置,只要保证出栈和进栈的对应关系即可。当CPU中的寄存器不够使用时经常用堆栈临时保存数据。栈顶所指位置以上的部分是堆栈的空闲区,以下部分是已入栈的数据存放区(见图6.1),例6.1用来说明PUSH指令和POP指令对堆栈的影响。【例6.1】设AX4F8AH,BX307CH,SP1000H,分别逐条执行下列指令,用内存图的形式画出堆栈的变化情况,
7、并分析程序段执行完后AX和BX寄存器的值。PUSH AX PUSH BX POP AX POP BX 【解】堆栈变化见图6.2,程序段执行完后AX307CH,BX4F8AH。XX 0FFC XX 0FFD XX 0FFE XX 0FFF YY 1000 SP SP XX 0FFC XX 0FFD 8A0FFE 4F 0FFF YY 1000 7C 0FFC 30 0FFD8A 0FFE 4F 0FFF YY 1000 SP(a)执行前 (b)PUSH AX后 (c)PUSH BX后 XX 0FFC XX 0FFC XX 0FFD XX 0FFDSP 8A0FFE XX 0FFE 4F0FFF
8、XX 0FFF YY 1000 SP YY 1000 (d)POP BX后 (e)POP AX后 注:XX表示栈空闲区填充的无用数据,YY表示栈中已存放的有效数据图6.2 执行PUSH和POP指令对堆栈的影响 6.1.2.3 PUSHF和和POPF指令指令 【指令格式】PUSHF 【功能】把SP的值减2,并把16位的标志寄存器送入SS:SP所指向的内存,即把标志寄存器入栈。【指令格式】POPF 【功能】把栈顶的一个16位的字型数据送入标志寄存器,并把SP的值加2。这两条指令相互配合可以设置标志寄存器中的任意一个标志位,一般的做法是:PUSHF POPAX ;按标志位的分布情况和实际需要,修改A
9、X中的值 PUSHAX POPF6.2 子程序的基本格式和有关指令子程序的基本格式和有关指令 6.2.1 汇编语言子程序格式汇编语言子程序格式 子程序是具有固定功能的程序段,并且有规定的格式。不同的计算机语言对子程序格式的规定不同,汇编语言的子程序基本格式如下:子程序名 PROC 类型 指令序列 子程序名 ENDP 格式中的首尾两行表示一个子程序的开始和结束,都属于伪指令。“子程序名”是一个标识符,是编程者给子程序起的名字。子程序名同时还代表子程序第一条指令所在的逻辑地址,称为子程序的入口地址。“类型”只有NEAR和FAR两种,它将影响汇编程序对子程序调用指令CALL和返回指令RET的翻译方式
10、。被夹在子程序起止伪指令之间的指令序列是完成子程序固定功能的程序段,通常指令序列的最后一条指令是返回指令RET。6.2.2 子程序相关指令子程序相关指令 6.2.2.1 CALL指令指令 【指令格式】CALL 子程序名 【功能】这是调用子程序的指令。根据被调用的子程序的类型不同,CALL指令的功能分为两种情况:(1)如果被调用的子程序是NEAR类型,则先把当前指令指针IP的值入栈,这会使SP的值减2,然后把IP改成子程序的第1条指令的偏移地址。(2)如果被调用的子程序是FAR类型,则先把当前CS寄存器的值入栈,再把IP入栈,结果会使SP的值减4,然后把CS和IP改为子程序第1条指令的逻辑地址。
11、CALL也是一种跳转指令,与无条件跳转及条件跳转指令不同的是,CALL在跳转之前先预留了回来的方法,把IP的当前值或CS与IP的当前值入栈保存。从CS与IP 的作用可以知道,它们存放的是正在执行的指令的下一条指令的逻辑地址,现在这一地址被保存在堆栈中。于是回来的方法就显而易见了,只要从栈中取出逻辑地址值,送回IP或者CS与IP即可。这种返回操作就是由RET指令实现的。6.2.2.2 RET指令指令 【指令格式】RET 【功能】这是子程序返回指令,必须写在子程序的指令序列之中。根据所在的子程序的类型不同,RET指令的功能也分为两种情况:(1)如果RET所在子程序是NEAR类型,则从堆栈中出栈一个
12、字(当然,SP会加2),送给IP。(2)如果RET所在子程序是FAR类型,则先从堆栈中出栈一个字送到IP,再出栈一个字送到CS,栈顶指SP的值加4。CALL指令和RET指令都具有跳转的能力,与条件跳转及无条件跳转一样,都是通过修改IP或者CS与IP来实现的。不论跳转是由哪一条指令造成的,对于只改变IP 的跳转,跳转的目的地与跳转指令必然在同一个代码段内,这种跳转称为段内跳转。相应地,CALL指令功能的第一种情况称为段内调用,RET指令功能的第一种情况称为段内返回。另一种跳转是同时改变了CS和IP的值,这就允许跳转指令与跳转目的地不在同一个段中,使得跳转的目的地可以在整个内存空间的任何位置,这一
13、类跳转称为段间跳转。CALL指令功能的第二种情况称为段间调用,RET指令功能的第二种情况称为段间返回。6.2.3 子程序的调用与返回子程序的调用与返回 在汇编语言程序中,子程序分为定义和使用两部分。在较短的程序中,通常把子程序与其余指令写在同一个代码段内,一个代码段中可以定义多个子程序,并且都定义成NEAR类型。这样编写的代码段的基本结构如下:段名段名 SEGMENT 子程序1 PROC NEAR 子程序1 ENDP 子程序2 PROC NEAR 子程序2 ENDP 子程序n PROC NEAR 子程序n ENDP 入口标号:段名 ENDS 从入口标号起的程序段是主程序。RET指令必须出现在子
14、程序中,而CALL指令可以出现在代码段的任何地方。主程序可以调用子程序,一个子程序可以调用另一个子程序,还可以调用它自身,并且在书写次序上没有“先定义后调用”的限制。源程序中的指令段在经过汇编程序的翻译后,所有伪指令都不存在了。作为CALL指令的操作数,“子程序名”部分会翻译成子程序第一条指令的逻辑地址。当计算机在执行CALL 指令时,CS和IP已经是下一条指令的逻辑地址。CALL指令具有保存当前IP或者CS和IP并修改它们的值的能力,因此CALL执行完后,会按照新的CS及IP,转去执行子程序的第一条指令,并依次执行后续指令,完成子程序的功能,直至遇到RET指令。RET指令的执行效果是从栈中取
15、出由CALL保存的数据,恢复在执行CALL指令时的IP或者CS与IP值,从而回到CALL指令的下一行继续执行。【例6.2】分析下面的程序段的执行过程,以及在执行过程中堆栈及指令指针IP的变化情况,并假设在执行CALL指令前,SP的值是0FEH。subp PROC NEAR INC AL ;假设本指令所在的偏移地址是1234H RET subp ENDP CALL subp MOV AX,BX ;假设本指令所在的偏移地址是5678H 【解】【解】(1)当计算机把CALL subp对应的机器指令取到CPU中时,IP的值已经是CALL的下一行的MOV指令所在的偏移地址5678H,此时还未进栈,栈的情
16、况如图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指令。(4)执行INC指令时,CPU自动把IP变成INC的下一行指令的偏移地址,如此逐条执行子程序中的各指令,直至遇到subp子程序的最后一条指令RET。(5)执行RET指令时,堆栈中的情况仍然是图6.3(b),因此执行RET就是取出栈顶所指的一个字,是567
17、8H,并把它送给IP,执行完RET指令后堆栈的情况如图6.3(c)所示。(6)执行完RET指令后,IP的值已经变成5678H,CPU按新的IP值,在CS段下取出一条指令,即MOV AX,BX指令,并继续执行下去。XX 00FA XX 00FA XX 00FA XX 00FB XX 00FB XX 00FB XX 00FC SP 78 00FC XX00FC XX 00FD 56 00FD XX00FD SP YY 00FE YY 00FE SP YY00FE(a)CALL指令执行前 (b)CALL指令执行后 (c)RET指令执行后 图6.3 例6.2的程序执行过程中堆栈的变化情况 例6.2描述
18、了段内调用与返回的过程,对于段间调用与返回,仅仅在CALL指令和RET指令的执行效果上不同,这个问题留给读者:把例6.2中的子程序类型改成FAR,执行过程中栈的变化情况又如何?例6.2中隐藏有一个非常严重的问题,就是如何保证执行完CALL指令后堆栈的情况与执行RET指令前堆栈的情况是相同的。这个问题确实存在,并且是程序员不可回避的。因为完成子程序需要执行多条指令,这些指令中难免会有改变栈顶指针或者改动栈中数据的情况。但是,无论是汇编程序还是计算机硬件本身都对此无能为力,需要程序员自己在编制程序时非常小心。如果不能保证堆栈的情况相同,执行到RET时,计算机仍然按照RET指令本身的功能正常处理,出
19、栈一个字给IP或者连续出栈两个字分别给CS及IP,这时就不会回到调用指令CALL的下一行,而不知跳转到什么地方去了。【注意】为了避免出现这种情况,编制子程序时应该注意以下几点:(1)子程序中的PUSH指令与POP指令数量应该相同,并且存在一一对应关系。(2)不要把SP用作MOV、ADD等指令的目的操作数,不要使用INC SP、DEC SP等指令,不要使用类似指令改变SP的值。(3)不要使用POP SP指令,该指令会用出栈的一个字型数据修改SP,而不像正常的POP指令一样把SP加2。(4)如果子程序中再次用CALL指令去调用子程序,只要被调用的子程序正确,则不会导致出现上述问题。6.3 应用子程
20、序进行编程应用子程序进行编程 6.3.1 子程序实例子程序实例 【例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 cr PROC NEAR MOV AH,2 MOV DL,13 INT 21H MOV DL,10 INT 21H RET cr ENDP main:MOV AX,dseg MOV DS,AX LEA DX,buf MOV AH,10 INT
21、 21H ;输入一个符号串 CALL cr MOV AH,1 INT 21H ;输入一个字符 MOV BL,AL ;用BL保存读入的字符 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 main6.3.2 对子程序中用到的寄存器进行保护对子程序中用到的寄存器进行保护 【例6.4】设子程序cr的定义如例6.3所示,比较下面两个程序段,分析各自执行完后寄存器AX中的值是多少。(a)MOV AX,102H MOV
22、 BX,304H ADD AX,BX(b)MOV AX,102H MOV BX,304H CALL cr ADD AX,BX 【解】程序段(a)中,先把AX赋值为102H,再把BX赋值为304H,然后用ADD指令把两数相加,和为406H,结果放在ADD指令的目的操作数AX中。程序段(b)的前两行与(a)完全相同,AX取值102H,BX取值304H,但在相加之前调用了子程序cr。从例6.3中cr的具体实现方法可以知道,调用过程中寄存器AH的值被改为2,因为INT 21H输出功能,使AL的值也被修改,变成0AH,并且这个值一直保持到调用结束,于是“CALL cr”指令调用子程序后,AX的值不再是调
23、用前的102H,而变成了20AH,当ADD指令进行两个寄存器相加时,结果是50EH,并放到目的操作数AX中。从例6.4可以看到,两个程序段仅仅相差一个子程序调用,而且子程序cr也只不过完成回车换行的操作,但两个程序段执行的结果却不一样,原因就在于调用子程序前,寄存器AX中放了一个有用的数据102H,但子程序中对AX重新赋了值,破坏了原来的数据。子程序中修改寄存器的值会给程序编制带来很大的麻烦,就如例6.4(b)的情况,想要找出错误的原因是不太容易的。为此,做法之一是在调用前把有用的数据存放到适当的地方保护起来,比如在例6.4(b)的CALL指令之前可以把 AX的值先找一个寄存器(比如SI)临时
24、存放,调用后再取回到AX中;另一个比较好的做法是在子程序中对所有使用到的寄存器进行保护,等到子程序的功能完成后,再恢复这些寄存器的原值,最后以RET指令返回。按照这个原则,把例6.3的子程序cr 改写成如下形式:cr PROC NEAR PUSH AX PUSH DX MOV AH,2 MOV DL,13 INT 21H MOV DL,10 INT 21H POP DX POP AX RET cr ENDP 修改后的子程序cr先把AX和DX的值入栈保护,完成回车换行操作后,再从栈中取出原来保存的数据恢复AX和DX的原值。用堆栈临时保存数据是子程序中普遍使用的一种方法。经过这样的修改,例6.4的
25、两个程序段各自执行后,AX中的值就会是一样的,调用子程序cr进行回车换行操作就不会影响程序的正常执行。【注意】入栈指令PUSH和出栈的POP指令必须一一对应。从栈操作的“先进后出”方式可以知道,入栈次序与出栈次序是相反的,所以PUSH指令序列中操作数的次序与 POP指令序列中操作数的次序相反,就如同上面的子程序cr中两条PUSH指令是先AX 再DX,而两条POP则是先DX再AX。6.3.3 带参数的子程序带参数的子程序 【例6.5】编写一个子程序,对一个无符号的字型数组的各元素求和。在调用子程序之前,已把数组的段地址放在DS中,起始偏移地址放在寄存器SI中,数组元素个数(0)放在CX中。要求子
26、程序把计算结果以双字的形式存放,高位放在DX中,低位放在AX中。【解】sum PROC NEAR PUSH BX ;保护用到的寄存器BX XOR AX,AX MOV DX,AX ;求和前先把存放结果的DX,AX清0 MOV BX,AX s1:ADD AX,BX+SI ;把一个元素加到AX中 ADC DX,0 ;若有进位,DX加1 INC BX INC BX ;BX加2,指向数组的下一元素 LOOP s1 POP BX ;恢复寄存器BX的值 RETsum ENDP 子程序说明应该包含如下4个部分:(1)子程序的功能。用来指明该子程序完成什么样的操作。(2)入口参数。说明调用子程序前应该把什么样的
27、数据放在什么地方。(3)出口参数。说明调用后从什么地方取得处理结果。(4)破坏的寄存器。指出子程序中哪些寄存器没有被保护。6.3.4 参数传递的方法参数传递的方法 1通用寄存器传值 2通用寄存器传地址 3标志寄存器传递逻辑型数据 【例6.6】编写一个子程序,以放在AX中的公元年份为入口参数,判断该年是否为闰年。另有一个应用程序,它已定义了一个字节型数组t,依次存放着12个月的每月天数,其中2月份的天数是28。应用程序已经在DX中存放了年份值,利用前面编写的子程序,编写程序段调整数组t中2月份的天数。【解】子程序清单如下:;功能:判断一个年份是否为闰年 ;入口:AX 公元年份 ;出口:CF,1表
28、示是闰年,0表示非闰年 ;破坏寄存器:AX jud PROC NEAR PUSH BX PUSH CX PUSH DX MOV CX,AX ;临时保存年份值 MOV DX,0 MOV BX,4 DIV BX ;除以4,为预防溢出,用双字除以字 CMP DX,0 JNZ lab1 ;不能整除4则不是闰年,转 MOV AX,CX ;取回年份值 MOV BX,100 DIV BX ;除以100 CMP DX,0 JNZ lab2 ;不能整除100则是闰年,转 MOV AX,CX MOV BX,400 DIV BX ;除以400 CMP DX,0 JZ lab2 lab1:CLC ;把CF清0表示非闰
29、年,设置出口参数 JMP lab3 lab2:STC ;把CF置1表示是闰年,设置出口参数 lab3:POP DX POP CX POP BX RET jud ENDP 对于DX中存放的年份值,需要先放到AX中,才能调用子程序jud,然后以调用返回后的CF值决定是否把t数组中表示2月份天数的t+1加1。程序段如下:MOV AX,DX CALL jud ADC BYTE PTR t+1,0 ;原值0CF 4用数据段中已定义的变量存放参数用数据段中已定义的变量存放参数 用数据段中定义的变量作为参数传递的载体也是一种常用方法。这种方法要求子程序与调用者之间约定好以哪个变量或哪几个变量进行参数传递。具
30、体做法是:对于用作入口参数的变量,调用者在调用子程序的CALL指令之前,先把变量赋以一定的值,然后以CALL指令转到子程序执行,子程序则取出该变量中的数据进行处理;对用作出口参数的变量,也有赋值与取值两个阶段,子程序进行数据处理后,把处理结果放到约定好的变量中,然后以RET指令返回调用者,调用者可以从变量中取出处理结果使用。【例6.7】用变量传递参数,编写例6.6要求的子程序。【解】;功能:根据一个年份是否为闰年,设置该年2月份的天数 ;入口:DS段中的字型变量year 公元年份 ;出口:DS段中的字节型变量day 该年2月份天数 ;破坏寄存器:无 jud1 PROC NEAR PUSH AX
31、 PUSH BX PUSH CX PUSH DX MOV BYTE PTR day,28 MOV AX,year MOV DX,0 MOV BX,4 DIV BX ;除以4 CMP DX,0 JNZ lab1 ;不能整除4则不是闰年,转 MOV AX,year ;取回年份值 MOV BX,100 DIV BX ;除以100 CMP DX,0 JNZ lab2 ;不能整除100则是闰年,转 MOV AX,year MOV BX,400 DIV BX ;除以400 DIV BX ;除以400 CMP DX,0 JZ lab2 lab2:INC BYTE PTR day;是闰年,把天数加1,设置出口
32、参数 lab1:POP DX POP CX POP BX POP AX RET Jud1:ENDP 5用堆栈传递用堆栈传递 参数传递不仅要在传递者之间约定数据的类型,还要约定参数存放地。如果约定用通用寄存器放参数,有可能会出现寄存器不够使用的情况。而约定用变量存放参数又要求在子程序和调用程序之外再写出变量定义,灵活性较差。用堆栈传递参数就可以克服这些缺点。对于调用者来说,传递给子程序的数据可以按字型(如果不是字型,先要转换成字型)用PUSH指令压入堆栈中;对于子程序来说,如何准确地取到栈中数据就是关键性问题。下面的例6.8用一个实际例子说明在子程序中取得参数值的具体方法。【例6.8】用堆栈传递
33、入口参数,编写子程序,把接收的两个带符号整数中大的一个作为结果,出口参数放在AX中。【解】;功能:求两个带符号整数中大的一个 ;入口参数:调用前把两个带符号整数入栈 ;出口参数:AX ;破坏寄存器:无 _max PROC NEAR PUSH BP ;暂时保存寄存器BP的值 MOV BP,SP MOV AX,WORD PTR BP+6 ;取第1个参数到AX CMP AX,WORD PTR BP+4 ;与第2个参数比较 JGE lab MOV AX,WORD PTR BP+4 ;取第2个参数到AX lab:POP BP ;恢复寄存器BP的原值 RET _max ENDP 6.3.5 子程序的嵌套调
34、用子程序的嵌套调用 一个子程序往往有其固定的功能,子程序中的指令序列是实现这种功能的具体方法和步骤,这个指令序列中可能、也允许再出现CALL指令。这种由一个子程序中的CALL指令去调用另一个子程序的形式就是子程序的嵌套调用。汇编语言对于子程序嵌套调用几乎没有什么限制,主程序可以调用子程序,子程序可以再调用其它子程序,也可以调用其自身(即后面所说的递归)。可以说,汇编语言源程序中,任何可以写指令的地方都可以写一条CALL指令,这一点与Pascal语言有很大的不同。在Pascal语言中,每个子程序(过程或函数)都有其归属问题,主程序下辖若干个子程序,一个子程序可以再管辖它的若干子程序。整个归属关系
35、构成树形结构,主程序是树根,每个子程序是树中的一个结点,没有下属子程序的结点是树叶。对于树形结构中的任何一个子程序(记作SUBP),如果把从根结点到这个子程序所经过的各个子程序称为一条路径,则任何子程序只有唯一的路径,这条路径上的任何结点都是SUBP的祖先。在Pascal中规定,SUBP可以调用:(1)其直接管辖的子程序,即它的子结点。(2)它自身。(3)它的任何祖先,主程序除外。(4)它的祖先所管辖的直接子结点。另外,任何调用都必须遵守“先定义后调用”的规则。6.4 整数输入与输出整数输入与输出 对于高级语言来说,整数的输入输出是标准的输入输出命令(语句)必备的功能。把数值型数据按正确写法写
36、在输出命令中,就可以在屏幕上得到输出结果;在输入语句中写上正确的数值型变量,就可以把键盘上按键情况变成数值放到指定变量中。但是在汇编语言中,没有这类指令或功能供直接调用,而只有输入字符型数据(即ASCII值)的方法,所以需要程序员自己编写整数输入输出的程序段,不过这样的程序段功能固定、使用频繁,适合于编写成子程序的形式,在各个需要它的程序中共享。【例6.10】编写子程序write,把整型数据以十进制形式显示到屏幕上。【分析】参照高级语言中输出语句的功能,write子程序应具备这样一些特点:被显示的整数可以是无符号的,也可以是带符号的,但需要明确指出是哪一种情况;整数在计算机内部是字型数据,范围
37、为32768+65535;被输出的数据是带符号数时,负号“”必须输出,而正号“”总是省略;输出数据的最大位数是十进制的5位,当计算出5位中的某一位是0时,需要判断这个0是否应该输出,输出条件是前面已经输出过非0数字或者这个0是个位数。write子程序的流程图见图6.5。流程中的SI用于记载是否已输出过非0数字。【解】下面是按子程序格式编写的write的清单,并附有简单注释。;功能:在屏幕上输出整数值 ;入口:AX 待输出的整数 ;CF 为0表示输出无符号数,为1则输出带符号数 ;出口:无 ;破坏寄存器:无 write PROC NEAR PUSH AX PUSH BX PUSH CX PUSH
38、 DX PUSH SI PUSH DI MOV SI,0 ;SI清0表示还没有输出过非0数字 MOV DI,AX;保存待输出的数值到DI中 JNC w1;作为无符号数输出转 CMP AX,0 JGE w1 ;AX是正数转 MOV DL,-MOV AH,2 INT 21H;输出负号 NEG DI;取绝对值放在DI中 w1:MOV BX,10000 ;第一次的被除数 MOV CX,5 ;重复次数 w2:MOV AX,DI ;取回待输出数值 MOV DX,0 ;被除数高位清0 DIV BX ;做双字除以字的除法 MOV DI,DX ;余数保存在DI中 CMP AL,0 JNE w3 ;商非0转 CM
39、P SI,0 ;商是0,判断前面是否输出过数字 JNE w3 ;前面已输出过数字,则当前的0应 该输出,转 CMP CX,1 ;判断是否是个位 JNE w4 ;不是个位则不输出当前的0,转 w3:MOV DL,AL ADD DL,30H MOV AH,2 INT 21H ;输出当前这一位数字 MOV SI,1 ;用SI记载已输出过数字 w4:MOV AX,BX MOV DX,0 MOV BX,10 DIV BX MOV BX,AX ;bx/10=bx,计算下一次的除数 LOOP w2 POP DI POP SI POP DX POP CX POP BX POP AX RET write END
40、P 相对于整数输出而言,整数的输入问题就更加复杂,因为它不仅是提供给操作人员输入整数的方法,而且要处理操作员可能的操作错误。不妨考察一下高级语言中整数输入语句对键盘输入的要求:(1)等待键盘输入,直到操作员按回车键,按回车键前如果发现输入有误,可以用退格键删去错误部分并重新输入。(2)输入串可以是一串数字。(3)输入串可以是一个正(或负)号,再紧接着一串数字。(4)输入串可以是若干个空格之后,再出现(2)或(3)的情况。(5)当输入串是(2)至(4)的某一种情况,但后面有多余符号时,则当前一次输入只到正确的输入串为止,后续多余的符号留作下一次输入的符号串,也可以废弃多余的符号,如同Pascal
41、语言中READLN的处理方式。(6)当输入串是数字但超出正确范围时,多数高级语言的处理方法是忽略掉超范围部分,即整数的内部表示共16位,对超过16位的部分自动忽略。(7)当输入串不正确时,不同的高级语言处理方法不同,但一个总的原则是要指出输入有错误。【例6.11】编写子程序read,从键盘上读入一个整数。【分析】为了尽可能与高级语言中整数输入的情况一致,子程序不仅要能读入正确输入时的数据,还要能对不正确的输入做出适当的反应,因此设计上要注意几个问题:首先是要用字符串输入方式(DOS的10号子功能),因为这种方式支持退格键修改功能,因而需要准备相应的输入缓冲区;出口参数需要两个,以CF的设置表示
42、输入是否正确,当输入正确时把整数值放在AX中作为输入结果;要能够跳过若干个连续的空格符;要能够处理正负号。【解】;功能:从键盘读入整数值 ;入口:CF 为0表示废弃多余符号,相当于READLN;;为1则把多余符号留作下一次输入,相当于READ ;出口:CF 0表示正常读入,1表示输入有错 ;破坏寄存器:无 read PROC NEAR PUSH BX PUSH CX PUSH DX PUSH SI PUSH DS ;以上为寄存器保护 PUSHF PUSH CS POP DS ;令DS取CS的值 rd1:MOV BX,CS:point ;取上次输入后已读取到输入串的位置 rd2:INC BX C
43、MP CS:bufin+BX+1,JE rd2 ;跳过空格 CMP CS:bufin+BX+1,13 JNZ rd4 ;不是回车键,转读入数值处理 rd3:LEA DX,CS:bufin MOV AH,10 INT 21H ;遇回车键要求再次输入 MOV AH,2 MOV DL,10 INT 21H ;换行 MOV CS:point,0 JMP rd1 ;对新的输入再转去跳过前导空格 rd4:MOV SI,BX DEC SI ;令SI指向输入串的第一个有效字符 MOV AX,0 MOV BX,10 MOV CX,0 rd5:CMP CS:bufin+SI+2,+JNZ rd6 ;不是正号转 C
44、MP CL,1 JE rd10 ;已读到正确数值后,遇正号转 CMP CL,0 JE rd8 ;正号是第一个有效字符转 STC ;输入有错 JMP rd13 rd6:CMP CS:bufin+SI+2,-JNZ rd9 CMP CL,1 ;已读到正确数值后,遇负号转 JE rd10 CMP CL,0 JE rd7 ;负号是第一个有效字符转 STC ;输入有错 JMP rd13 rd7:MOV CH,1 ;记下读入的是负数 rd8:MOV CL,2 ;记下已读入正/负号 INC SI ;指向下一字符 JMP rd5 rd9:CMP CS:bufin+SI+2,0 JB rd10 ;不是数字转 C
45、MP CS:bufin+SI+2,9 JA rd10 ;不是数字转 MUL BX ;已读入的数值10 MOV DL,CS:bufin+SI+2 SUB DL,30h MOV DH,0 ADD AX,DX ;乘以10后加上个位数字 MOV CL,1 ;记下已读入正确数值 INC SI ;指向下一字符 JMP rd5 rd10:CMP CL,1 JZ rd11 ;已读入正确数值转 STC ;输入有错 JMP rd13 rd11:CMP CH,1 JNZ rd12 ;已读入的数是正数转 NEG AX ;处理负数 rd12:CLC ;置正确读入标志 rd13:MOV CS:point,SI;记下读完后
46、的位置,供下次读入 使用 POP BX ;取回进入子程序时入栈保护的 PSW,送BX PUSHF ;当前的PSW入栈保存 TEST BX,1;判断进入子程序时的CF值 JNZ rd14 ;CF为1,保留多余符号转 MOV CS:bufin+2,13 MOV CS:point,0 rd14:POPF;取回入栈保存的PSW POP DS ;以下恢复各寄存器值并返回 POP SI POP DX POP CX POP BX RET bufin DB 128,0,13,127 dup(0);键盘输入缓冲区 point DW 0 ;用于记载下一次的读取位置 read ENDP6.5 子程序共享的方法子程序
47、共享的方法 6.5.1 复制子程序的源代码复制子程序的源代码 整理出每一个子程序的源代码清单,按汇编语言写注释的规定写上必备的说明,单独构成一个程序文件(文本文件)。把很多个这样的程序文件集中放在一个子目录(文件夹)中,构成一个子程序库。以后如果某个程序中需要一个子程序,只要从子程序库中以文件复制的方法,把所需要的子程序的清单复制到需要它的源程序中即可。为每个子程序建立一个文件,在管理上和使用上有如下特点:(1)需要人工管理。人工地从一些源程序中截取出子程序清单,建立单独的文件;如果在特定目录中的子程序文件遇到同名问题,就需要人工解决。(2)以复制方式共享。一个程序中需要用到某个子程序时,可以
48、从特定目录中挑选出需要的子程序文件,复制其中的程序清单到需要它的源程序中。(3)子程序库易于维护。如果需要更新某个子程序文件,一种方法是把更新版本的子程序以同名文件的形式复制过来,另一种方法是直接用编辑器对原有的文件进行修改。无论哪种方法,实现起来都很简便易行。(4)子程序清单需要多次汇编。由于子程序清单是以文本文件的形式存在,使用时是复制到源程序中,因此会随着源程序的汇编操作一道进行汇编。(5)可能存在标识符冲突。子程序中,除了子程序名之外有可能还会用到一些标识符,如子程序内需要使用的变量、标号等。当子程序清单复制到某个源程序之后,子程序内的标识符有可能与程序的其余部分的标识符同名,这种标识
49、符的重复定义是不符合汇编语言语法规定的,需要修改。6.5.2 INCLUDE伪指令伪指令 【伪指令格式】INCLUDE 文件名 【功能】告诉汇编程序,把“文件名”所指出的文本文件的内容调到INCLUDE伪指令所在处,对拼装后的源程序进行汇编。【例6.12】设存放在磁盘上有3个文件cr.asm、write.asm和read.asm,分别存放的是例6.10 的子程序write和例6.11的子程序read,以及回车换行子程序cr的程序清单。使用INCLUDE伪指令编写完整程序,从键盘上读入两个整数,求和。【解】data SEGMENTbuf DB 13,10,Input error.,13,10,$
50、data ENDScode SEGMENT ASSUMEDS:data,CS:codemain:MOVDX,data MOV DS,DX CLC CALL read;读入第1个整数 JC err;输入有错转 MOV CX,AX CALL read;读入第2个整数 JC err;输入有错转 MOV BX,AX CALL cr MOV AX,BX ADD AX,CX;计算两个整数的和 STC CALL write ;输出两个整数的和,带符号 JMP laberr:LEA DX,buf MOV AH,9 INT 21H;输出提示信息Input errorlab:MOV AH,4CH INT 21H