📄 汇007.txt
字号:
了解的内容:引入子程序的作用,制作用户子程序库的方法。
掌握的内容:在子程序中,保护和恢复寄存器的原因和方法。
熟练掌握的内容:子程序定义的一般格式,子程序的调用和返回指令,调用子程序常用的参数传递方法,模块间通信的方法。
建议学习时间:8小时。第7章 子程序和库
子程序是程序设计所常见的基本概念,汇编语言也提供了编写子程序的方法。
本章主要介绍子程序的定义、调用和返回、子程序的参数传递等知识。此后,还将讲解如何构造自己的子程序库。
7.1 子程序的定义
如果某程序段在源程序内反复出现,那么,就可把该程序段定义为子程序。这样可以缩短源程序长度、节省目标程序的存储空间,也可提高程序的可维护性和共享性。
定义子程序的一般格式如下:
子程序名 PROC [NEAR | FAR]
… ;子程序体
子程序名 ENDP
对子程序定义的具体规定如下:
、“子程序名”必须是一个合法的标识符,并前后二者要一致;
、PROC和ENDP必须是成对出现的关键字,它们分别表示子程序定义开始和结束;
、子程序的类型有近(NEAR)、远(FAR)之分,其缺省的类型是近类型;
、如果一个子程序要被另一段的程序调用,那么,其类型应定义为FAR,否则,其类型可以是NEAR。显然,NEAR类型的子程序只能被与其同段的程序所调用;
、子程序至少要有一条返回指令,也可有多条返回指令。返回指令是子程序的出口语句,但它不一定是子程序的最后一条语句;
、子程序名有三个属性:段值、偏移量和类型。其段值和偏移量对应于子程序的入口地址,其类型就是该子程序的类型。
编写子程序除了要考虑实现子程序功能的方法外,还要养成书写子程序说明信息的好习惯。其说明信息一般包括以下几方面内容:
、功能描述
、入口和出口参数
、所用寄存器 ;可选项,最好采用寄存器的保护和恢复方法,使之使用透明化
、所用额外存储单元 ;可选项,可以减少为子程序定义自己的局部变量
、子程序的所采用的算法 ;可选项,如果算法简单,可以不写
、调用时的注意事项 ;可选项,尽量避免除入口参数外还有其它的要求
、子程序的编写者 ;可选项,为将来的维护提供信息
、子程序的编写日期 ;可选项,用于确定程序是否是最新版本
这些说明性信息虽然不是子程序功能的一部分,但其他程序员可通过它们对该子程序的整体信息有一个较清晰认识,为准确地调用它们提供直接的帮助,与此同时,也为实现子程序的共享提供了必要的资料。
7.2 子程序的调用和返回指令
子程序的调用和返回是一对互逆操作,也是一种特殊的转移操作。
一方面,之所以说是转移,是因为当调用一个子程序时,程序的执行顺序被改变,CPU将转而执行子程序中的指令序列,在这方面,调用子程序的操作含有转移指令的功能,子程序的返回指令的转移特性与此类似;
另一方面,转移指令是一种“一去不复返”的操作,而当子程序完后,还要求CPU能转而执行调用指令之下的指令,它是一种“有去有回”的操作。
为了满足子程序调用和返回操作的特殊性,在指令系统中设置了相应的特定指令。
7.2.1 调用指令(CALL)
调用子程序指令的格式如下:
CALL 子程序名/Reg/Mem
子程序的调用指令分为近(near)调用和远(far)调用。如果被调用子程序的属性是近的,那么,CALL指令将产生一个近调用,它把该指令之后地址的偏移量(用一个字来表示的)压栈,把被调用子程序入口地址的偏移量送给指令指针寄存器IP即可实现执行程序的转移。近调用指令的堆栈操作如图7.1所示。
图7.1 近调用指令进栈操作示意图
如果被调用子程序的属性是远的,那么,CALL指令将产生一个远调用。这时,调用指令不仅要把该指令之后地址的偏移量压进栈,而且也要把段寄存器CS的值压进栈。在此之后,再把被调用子程序入口地址的偏移量和段值分别送给IP和CS,这样完成了子程序的远调用操作。远调用指令的堆栈操作如图7.2所示。
图7.2 远调用指令进栈操作示意图
子程序调用指令本身的执行不影响任何标志位,但子程序体中指令的执行会改变标志位,所以,如果希望子程序的执行不能改变调用指令前后的标志位,那么,就要在子程序的开始处保护标志位,在子程序的返回前恢复标志位。
例如:
CALL DISPLAY
;DISPLAY是子程序名
CALL BX
;BX的内容是子程序的偏移量
CALL WORD1
;WORD1是内存字变量,其值是子程序的偏移量
CALL DWORD1
;DWORD1是双字变量,其值是子程序的偏移量和段值
CALL word ptr [BX]
;BX所指内存字单元的值是子程序的偏移量
CALL dword ptr [BX]
;BX所指内存双字单元的值是子程序的偏移量和段值
7.2.2 返回指令(RET)
当子程序执行完时,需要返回到调用它的程序之中。为实现此功能,指令系统提供了一条专用的返回指令。其格式如下:
RET/RETN/RETF [Imm]
子程序的返回在功能上是子程序调用的逆操作。为了与子程序的远、近调用相对应,子程序的返回也分:远返回和近返回。返回指令在堆栈操作方面是调用指令的逆过程(如图7.3所示)。其具体规定如下:
、在近类型的子程序中,返回指令RET是近返回,其功能是把栈顶之值弹出到指令指针寄存器IP中,SP会被加2(如图7.3所示);
、在远类型的子程序中,返回指令RET是远返回,其功能是:先弹出栈顶之值到IP中,再弹出栈顶之值到CS之中,SP总共会被加4(如图7.4所示)。
图7.3 近返回指令的出栈操作示意图
图7.4 远返回指令的出栈操作示意图
如果返回指令后面带有立即数(其值通常为偶数),则表示在得到返回地址之后,SP还要增加的偏移量,它不是类似于高级语言中子程序的返回值(如图7.5所示)。
图7.5 带立即数的返回指令的出栈操作示意图
在MASM 5.0及其以后版本中,可用指令RETN或RETF来显式地告诉汇编程序是本子程序的返回是近返回,还是远返回。
例如:
RET
;可能是近返回,也可能是远返回
RETN
;近返回指令
RETF
;远返回指令
RET 6
;子程序返回后,(SP)←(SP) + 6
例7.1 编写一个子程序UPPER,实现把寄存器AL中存放的字符变大写。
解:
;子程序功能:把AL中存放的字符变大写
;入口参数:AL
;出口参数:AL
;算法描述:判断AL中字符必须在'a'~'z'之间才能把该字符变为大写
UPPER PROC
CMP AL, 'a' ;书写'a'的ASCII码61H也可以
JB over
CMP AL, 'z'
JA over
SUB AL, 20H ;书写指令AND AL, 0DFH也可以
over: RET
UPPER ENDP
例7.2 编写一个求字符串长度的子程序StrLen,该字符串以0为结束标志,其首地址存放在DS:DX,其长度保存在CX中返回。
解:
;子程序功能:求字符串的长度
;入口参数:DS:DX存放字符串的首地址,该字符串以0为结束标志
;出口参数:CX存放该字符串的长度
;算法描述:用BX来指针来扫描字符串中的字符,如果遇到其结束标志,则停止扫描字符串操作
StrLen PROC
PUSH AX
PUSH BX ;用堆栈来保存子程序所用到的寄存器内容
XOR CX, CX
XOR AL, AL
MOV BX, DX
again: CMP [BX], AL
JZ over
INC CX ;增加字符串的长度
INC BX ;访问字符串的指针向后移
JMP again
over: POP BX ;恢复在子程序开始时所保存的寄存器内容
POP AX
RET
StrLen ENDP
7.3 子程序的参数传递
子程序一般都是完成某种特定功能的程序段。当一个程序调用一个子程序时,通常都向子程序传递若干个数据让它来处理;当子程序处理完后,一般也向调用它的程序传递处理结果,我们称这种在调用程序和子程序之间的信息传递为参数传递。
用程序向子程序传递的参数称为子程序的入口参数,子程序向调用它的程序传递的参数称为子程序的出口参数。子程序的入口参数和出口参数都是任意项,对某个具体的子程序来说,要根据具体情况来确定其入口和出口参数,也可以二者都没有。
程序和被调用子程序之间的参数传递方法是程序员自己或和别人事先约定好的信息传递方法。这种信息传递方法可以是多种多样的,在本节,我们只介绍常用的、行之有效的参数传递方法有:寄存器传递参数、约定存储单元传递参数和堆栈传递参数等。如果对其它的参数传递方法感兴趣的话,可参考其它《汇编语言程序设计》书籍。
7.3.1 寄存器传递参数
一方面,由于CPU中的寄存器在任何程序中都是“可见”的,一个程序对某寄存器赋值后,在另一个程序中就能直接使用,所以,用寄存器来传递参数最直接、简便,也是最常用的参数传递方式。但另一方面,CPU中寄存器的个数和容量都是非常有限,所以,该方法适用于传递较少的参数信息。
例7.1是用寄存器传递参数的例子,子程序处理的数据被保存在寄存器AL中。假设有下列的程序段:
…
MOV AL, 'b'
CALL UPPER ;子返回时,(AL)='B'
…
MOV AL, '2'
CALL UPPER ;子返回时,AL的值不变,因为'2'不是字母
…
例7.3 按五位十进制的形式显示寄存器BX中的内容,如果BX的值小于0,则应在显示数值之前显示负号'-'。
例如:(BX)=123,显示:00123;(BX)=-234,显示:-00234;
解:
;子程序功能:把寄存器BX的内容按十进制有符号数显示出来
;入口参数:BX
;出口参数:无,只有显示信息
;算法描述: 1、定义6个字节的存储单元
2、先判断BX是否小于零,如果是,则先显示负号'-',再取BX的绝对值;
3、采用除10,得余数的方法,从低位向高位求出每位十进制位;
4、输出数据的字符串。
SubData SEGMENT
DB
5 DUP('0'), 0ah, 0dh, '$' ;0ah、0dh:换行、回车
SubData ENDS
DISPBX PROC
ASSUME DS:SubData
PUSH DS
PUSH DX
PUSH CX
PUSH AX
MOV AX, SubData ;取子程序所用的数据区段地址
MOV DS, AX
CMP BX, 0
JGE next
MOV DL, '-'
MOV AH, 2
INT 21H ;显示负号'-'
NEG BX ;求-BX,使其值为正数
next: MOV SI, 4
MOV AX, BX
MOV CX, 10D
again: XOR DX, DX
IDIV CX ;DX存放余数,AX存放商
ADD DL, '0'
MOV [SI], DL
DEC SI
JGE again
XOR DX, DX
MOV AH, 9
INT 21H ;调用中断21的功能9,显示DS:DX指向的字符串
POP AX
POP CX
POP DX
POP DS
RET
DISPBX ENDP
7.3.2 约定存储单元传递参数
在调用子程序时,当需要向子程序传递大量数据时,因受到寄存器容量的限制,就不能采用寄存器传递参数的方式,而要改用约定存储单元的传送方式。这种参数传递方式有点象情报人员和联络人员之间的传递信息方式,一个向指定地点放情报,另一个从指定地点取情报。
例7.2是采用约定存储单元传递参数的例子,所处理的数据不是直接传给子程序,而是把存储它们的地址告诉子程序。
例7.4:编写一个子程序分类统计出一个字符串中数字字符、字母和其它字符的个数。该字符串的首地址用DS:DX来指定(以0为字符串结束),各类字符个数分别存放BX、CX和DI中。
解:
;子程序功能:分类统计出字符串中数字字符、字母和其它字符的个数
;入口参数:DS:DX指向被统计的字符串
;出口参数:BX、CX和DI分别保存数字字符、字母和其它字符的个数
;算法描述: 1、当字符在'0'~'9'范围时,数字字符个数BX加1;
2、为了判断简单,先把字字母变成大写字母;
3、当字符在'A'~'Z'范围时,字母个数CX加1;
4、否则,其它字符个数DI加1。
COUNT PROC
PUSH AX
PUSH SI
XOR BX, BX
XOR CX, CX
XOR DI, DI ;上三条指令使各类字符计数清零
MOV SI, DX
again: MOV AL, [SI]
INC SI
CMP AL, 0
JE over
CMP AL, '0'
JL other
CMP AL, '9'
JG next
INC BX ;数字字符个数加1
JMP again
next: CALL UPPER ;调用子程序把AL中的字母变成大写字母
CMP AL, 'A'
JL other
CMP AL, 'Z'
JG other
INC CX ;字母个数加1
JMP again
other: INC DI ;其它字符个数加1
JMP again
over: POP SI
POP AX
RET
COUNT ENDP
例7.5 显示出任意字符串中数字字符、字母和其它字符的个数。
解:
.MODEL SMALL
.DATA
MSG
DB 'KSDJ L0984/[]3oiu OIU OIU (*&(5341', 0
.CODE
.STARTUP
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -