汇编基础:子程序设计
2017-06-10
在一个程序中的不同地方,常常需要多次非循环的使用完成特定功能的程序段,这些程序段除了某些变量的赋值不同外,具有相同的指令序列,这时,我们为了减少重复编写程序,缩短目标代码,节省内存空间,把视线这一功能的指令序列组成一个相对独立的程序段。这也就是我们这片文章中所要讨论的子程序。
子程序相当于高级语言(比如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、用地址表传递参数
当要传送的参数较多时,可在主程序中建立一个地址表,在调用子程序前,把所有参数的地址依次存放在该地址表中,然后把地址表的首地址通过寄存器传送到子程序中去,而在子程序中,按照地址表中给出的地址逐个取出参数,用地址表传递参数的方法,在入口参数比较多时很方便,当返回参数较多时,可用同样的方法传递参数,供主程序使用。