在我的上一篇博文中,我简单的向大家介绍了汇编语言程序设计的三种基本方式。在一个程序中的不同地方,常常需要多次非循环的使用完成特定功能的程序段,这些程序段除了某些变量的赋值不同外,具有相同的指令序列,这时,我们为了减少重复编写程序,缩短目标代码,节省内存空间,把视线这一功能的指令序列组成一个相对独立的程序段。这也就是我们这片文章中所要讨论的子程序。
子程序相当于高级语言(比如C语言)中的过程和函数,在汇编语言中子程序也称为过程。使用子程序的好处:
a、有利于程序模块化、结构化和自顶向下的程序设计方法,简化了程序设计过程。
b、增加了源程序的可读性,便于调试维护
c、减少了目标代码锁占用的空间
d、子程序一旦编制成功,在开发研制各种软件时都可使用,缩短了软件的开发周期。
一、子程序的调用与返回
1、子程序的定义
子程序必须定义在一个逻辑段内,子程序的定义由过程定义伪指令PROC/ENDP来实现,它们分别用在程序的子程序的前后,一般格式如下:
- PROC_NAME PROC [NEAR/FAR]
- ......
- PROC_NAME ENDP
其中PROC_NAME为子程序名,也极为CALL的操作数,自程序具有3个属性:段属性、偏移量属性和类型属性,段属性表示该子程序所在段的段基值。偏移量属性表示该子程序在段中的偏移量。类型属性也称为距离属性,可以是NEAR或FAR,属性为NEAR的子程序只能在本段内调用,属性为FAR的子程序则可以在本段以内以及其他段中调用。
2、调用指令
当主程序属性是NEAR的子程序时,CPU把当前指令指针IP的内容压入堆栈,作为返回地址保存起来,然后将子程序的偏移量送入IP,当从子程序返回时,将从堆栈弹出2个字节的返回地址送入IP,当调用属性是FAR的过程时,CPU把当前的段寄存器CS与指令指针IP的内容都压入堆栈,作为返回地址保存起来,然后将子程序的段基值与偏移量送入CS与IP,当子程序返回时,将从堆栈弹出4个字节的返回地址分别送入IP与CS。
我们容易知道,当主程序和子程序处于同一逻辑段时,可以把类型属性定义为NEAR,也可以把类型属性定义为FAR,然后进行调用。而当主程序与子程序不在同一逻辑段是,只可把过程的类型定义为FAR,然后调用。
二、返回指令
返回指令RET是子程序逻辑上的最后一条指令,也就是最后一条被执行的指令,它使子程序在完成功能后返回到调用它的CALL指令的后续指令处,即返回地址处继续执行。
三、子程序设计的基本要求
1、子程序必须有一定的通用性
2、注意寄存器的保存和恢复
3、正确使用堆栈
4、选用适当的方法在主程序与子程序间进行参数传递
5、编制子程序说明信息文件
四、子程序与主程序间的参数传递
在汇编语言中最常用的参数传递方式有3种,分别是:用寄存器传递参数、用堆栈传递参数和用地址表达式传递参数。
1、用寄存器传递参数
这种方式是通过通用寄存器来传递的参数,即在主程序调用子程序前,将入口参数送到约定的通用寄存器中,子程序可以直接从这些寄存器中取出参数进行加工处理,并将结果放在约定的通用寄存器中,返回主程序,主程序再从约定的寄存器中取出结果,我们一例子来说明问题:
例:将两个给定的二进制数(8位和16位)转换为ASCII码字符串。
分析:主程序提供呗转换的数据和转化后的ASCII码字符串的存储区的首地址。子程序完成二进制的转换。为了提高子程序的代码转换通用性,它可以完成8位或16位数的转换。设调用子程序时,入口参数为:被转换的数在DX中,若位数小于16,则从高到低存放,转换后的ASCII码的存放首地址在DI中。下面给出一种实现方法:
- DATA SEGMENT
- BIN1 DB 35H
- BIN2 DW 0AB48H
- ASCBUF DB 20H DUP (?)
- DATA ENDS
- STACK1 SEGMENT PARA STACK
- DW 20H DUP (0)
- STACK1 ENDS
- CODE SEGMENT
- ASSUME CS:CODE, DS:DATA, SS:STACK1
- BEGIN: MOV AX, DATA
- MOV DS, AX
- XOR DX, DX
- LEA DI, ASCBUF ;存放ASCII码的单元首地址送DI
- MOV DH, BIN1 ;待转换的第一个数据送DH
- MOV AX, 8 ;待转换的二进制数的位数送AX
- CALL BINASC
- MOV DX, BIN2
- MOV AX, 16
- LEA DI, ASCBUF
- ADD DI, 8 ;设置下一个数的存放首地址
- CALL BINASC
- MOV AH, 4CH
- INT 21H
- BINASC PROC
- MOV CX, AX
- LOP: ROL DX, 1 ;最高位移入最低位
- MOV AL, DL
- AND AL, 1 ;保留最低位,屏蔽其他位
- ADD AL, 30H
- MOV [DI], AL ;存结果
- INC DI ;修改地址指针
- LOOP LOP
- RET
- BINASC ENDP
- CODE ENDS
- END BEGIN
2、用堆栈传递参数
这种方法是主程序先将入口参数压入堆栈,子程序从堆栈中把参数读出,进行加工处理。这里要注意从堆栈中读取数据与从堆栈中弹出数据是有区别的,从堆栈中读取数据并不改变堆栈的栈顶指针SP,而从堆栈中弹出的数据,则需修改SP,在使用堆栈传递参数时,要保证堆栈状态的正确。
我们还以上面的例子来说明下问题,这次采用堆栈传递参数
分析:如果使用堆栈,一般用包括:
a、在主程序中,将待转换的数据、存放ASCII码的首地址和转换的位数压入栈中
b、在子程序中保存信息
下面我们依然用程序说明问题,在程序的必要处我已经做了注释
- DATA SEGMENT
- BIN1 DB 35H
- BIN2 DW 0AB48H
- ASCBUF DB 20H DUP (?)
- DATA ENDS
- STACK1 SEGMENT PARA STACK
- DW 20H DUP (0)
- STACK1 ENDS
- CODE SEGMENT
- ASSUME CS:CODE, DS:DATA, SS:STACK1
- BEGIN: MOV AX, DATA
- MOV DS, AX
- MOV AH, BIN1
- PUSH AX ;待转换数据压栈
- MOV AX, 8
- PUSH AX ;待转换位数压栈
- LEA DI, ASCBUF
- PUSH DI ;存放ASCII码的首地址压栈
- CALL BINASC ;调用转换子程序
- MOV AX, BIN2
- PUSH AX
- MOV AX, 10H
- PUSH AX
- ADD DI, 8
- PUSH DI
- CALL BINASC
- MOV AH, 4CH
- INT 21H
- BINASC PROC
- PUSH AX
- PUSH CX
- PUSH DX
- PUSH DI
- MOV BP, SP
- MOV DI, [BP+10] ;从堆栈取出入口参数
- MOV CX, [BP+12]
- MOV DX, [BP+14]
- LOP: ROL DX, 1
- MOV AL, DL
- AND AL, 1
- ADD AL, 30H
- MOV [DI], AL
- INC DI
- LOOP LOP
- POP DI
- POP DX
- POP CX
- POP AX
- RET 6 ;返回并从堆栈中弹出6个字节
- BINASC ENDP
- CODE ENDS
- END BEGIN
3、用地址表传递参数
当要传送的参数较多时,可在主程序中建立一个地址表,在调用子程序前,把所有参数的地址依次存放在该地址表中,然后把地址表的首地址通过寄存器传送到子程序中去,而在子程序中,按照地址表中给出的地址逐个取出参数,用地址表传递参数的方法,在入口参数比较多时很方便,当返回参数较多时,可用同样的方法传递参数,供主程序使用。