子程序(subroutine)是一组相对独立的程序代码,可以完成预定的一个或一组功能。需要执行这组程序代码时,由上一级程序(称为主程序或主调程序)通过调用指令CALL进入这个子程序执行。子程序执行完毕后,用返回指令RET回到主程序中,调用指令CALL的下一条指令执行。子程序调用和返回的过程如图11-5所示。
图11-5 子程序的调用和返回
由此可见,调用指令出现在主程序中,返回指令出现在子程序中。它们成对使用,但却出现在不同的程序中。
子程序调用指令和前面所学的JMP指令有相似之处,它们都是通过改变IP或IT和CS的值进行程序的转移。两者的不同之处在于调用指令要求返回,即子程序执行完成必须返回调用它的程序继续执行,而后者可以“一去不复返”。
子程序按照其入口地址长度可分为两种类型。
· 近程子程序:只能被同一个代码段里的程序调用的子程序。由于主程序和子程序处于同一个代码段,CS寄存器的值保持不变,调用和返回时只需要改变IP寄存器的值。近程子程序的入口地址用16位段内偏移地址表示。
· 远程子程序:能够被不同代码段的程序调用,也能被同一代码段的程序调用的子程序。调用这样的子程序时需要同时改变CS和IP寄存器的值,返回时,需要从堆栈里弹出32位的返回地址送入IP、CS寄存器。远程子程序的入口地址用16位段基址和16位段内偏移地址表示。
子程序的类型在它定义时说明。
CALL指令用来调用子程序,与JMP指令类似,有4种不同的寻址方式。
(1)段内直接调用。
格式:
CALL子程序名
操作:SP←SP-2,SS:[SP]←IP(保存16位返回地址),IP←子程序的偏移地址。
例如:
CALL PROC1 ; PROC1是近程子程序的入口标号
这条指令用来调用与主程序在同一代码段,并且定义为近程的子程序。
(2)段内间接调用。
格式:
CALL REG16/MEM16
操作:SP←SP-2,SS:[SP]←IP(保护16位返回地址),IP←REG16/MEM16。
这条指令用来调用近程子程序,子程序的入口偏移地址事先已存放在一个16位寄存器或者一个16位存储器字变量中。
例如:
LEA CX , PROC1
CALL CX ; 调用近程子程序PROC1
或者:
ADD_PROC1 DW PROC1 ; 子程序偏移地址放入存储器字变量
CALL ADD_PROC1 ; 调用近程子程序PROC1
或者:
LEA BX,ADD_PROC1
CALL WORD PTR[BX] ; 调用近程子程序PROC1
(3)段间直接调用。
格式:
CALL FAR PTR子程序名。
操作:SP←SP-2,SS:[SP]←CS(保存32位返回地址);SP←SP-2,SS:[SP]←IP(偏移地址IP保存在较小地址处);IP←子程序的偏移地址,CS←子程序的段基址。
例如:
CALL FAR PTR PROC2 ; PROC2是远程子程序的入口标号
这条指令用来调用处于不同代码段内,或者虽然处于同一代码段,但是被定义为远程的子程序。FAR PTR用来指明该子程序为远程子程序。远程子程序的入口地址和返回地址都是32位,包括16位的偏移地址和16位的段基址。
(4)段间间接调用。
格式:
CALL MEM32
操作:SP←SP-2,SS:[SP]←CS;SP←SP-2,SS:[SP]←IP;IP←[MEM32],CS←[MEM32+2]。
这条指令用来调用远程子程序,32位的子程序入口地址事先已存放在一个32位的存储器双字变量中。
例如:
ADD_PROC1 DD PROC2 ; 子程序入1:3地址放入存储器双字变量
CALL ADD_PROC1 ; 调用远程子程序PROC2
RET指令用来从子程序返回主程序,有以下4种返回方式。
(1)无参数段内返回。
格式:
RET
操作:IP←SS:[SP],SP←SP+2。
这条指令在近程子程序内使用,将执行CALL指令时保存在堆栈的16位返回地址送回IP,返回主程序。
(2)有参数段内返回。
格式:
RET D16
操作:IP←SS:[SP],SP←SP+2;SP←SP+D16。
这条指令除了将堆栈内16位偏移地址送入IP,还用一个16位的位移量(立即数或表达式)修改SP的值,这个操作用来废弃主程序存放在堆栈里的入口参数。
(3)无参数段间返回。
格式:
RET
操作:IP←SS:[SP],SP←SP+2;CS←SS:[SP],SP←SP+2。
这条指令在远程子程序内使用,将执行CALL指令时保存在堆栈的32位返回地址送回IP和CS,返回主程序。该指令的助记符与段内返回指令相同,但是它们汇编产生的机器代码是不同的,代表了两条不同的机器指令。这条指令也可以写作RETF。
(4)有参数段间返回。
格式:
RET D16
操作:IP←SS:[SP],SP←SP+2;CS←SS:[SP],SP←SP+2;SP←SP+D16。
这条指令在远程子程序内使用,将执行CALL指令时保存在堆栈的32位返回地址送回IP和CS,返回主程序,并且用16位位移量修改SP的值。该指令的助记符与段内返回指令相同,但是它们汇编产生的机器代码是不同的。
子程序的定义格式如下:
子程序名PROC [NEAR/FAR]
程序体
子程序名 ENDP
子程序名应为合法的标识符,子程序名不能与同一个源程序中的标号、变量名、其他子程序名相同。方括号中的内容是子程序的远近属性选项,两者可选其一,如果省略,默认为NEAR。用NEAR说明的子程序是近程子程序,它只能被与它同一代码段的程序调用。用FAR说明的子程序是远程子程序,它不仅能被与它同一代码段的程序调用,也能被其他代码段的程序调用。
子程序的程序体由一组指令组成,它完成一个特定的动作或运算。例如:
ZEROBYTES PROC ; 定义一个子程序(近程)
XOR AX, AX ; 将AX清零
MOV CX,128 ; 循环次数送CX
ZEROLOOP: MOV [BX],AX ; 将一个字存储单元清零
ADD BX,2 ; 修改地址
LOOP ZEROLOOP ; 循环控制
RET ; 结束子程序运行,返回主程序
ZEROBYTES ENDP ; 子程序结束
这段程序将BX寄存器间接寻址的连续256B的内存单元清零。使用这个子程序之前,需要在主程序中将需要清零的内存区域首地址放到BX,然后用CALL指令调用ZEROBYTES子程序。
子程序体中至少应包含一条返回指令,也可以有多于一条的返回指令。一般情况下,子程序的最后一条指令应该是返回指令。上述定义中,PROC和ENDP是伪指令,它们没有对应的机器码,它们用来向汇编程序报告一个子程序的开始和结束。这个子程序也可以简单地写成下面的形式:
ZEROBYTES:XOR AX,AX ; AX寄存器清零
LOOP ZEROLOOP ; 循环控制
RET ; 结束子程序运行,返回主程序
如果程序中有多个子程序,用这种方式定义后,它们的边界不容易清晰地区分。而且这种方式只能定义近程子程序,只能被同一代码段内的程序调用,因此不提倡。用户编写的主程序也可以看作是由操作系统调用的一个子程序。
.CODE ; 代码段开始
MAIN PROC FAR ; 主程序开始
PUSH DS ; 操作系统的返回点在DS:0
XOR AX,AX
PUSH AX ; 把32位返回点地址压入堆栈
…… ; 主程序的指令序列
RET ; 返回DOS
MAIN ENDP ; 主程序结束
…… ; 其他程序
END MAIN ; 源程序结束
也可以用4CH系统功能调用直接返回操作系统。
.COD ; 代码段开始
MAIN PROC FAR ; 主程序开始
…… ; 主程序的指令序列
MOV AX,4C00H
INT 21H ; 返回DOS
MAIN ENDP ; 主程序结束
…… ; 其他程序
END MAIN ; 源程序结束
采用这种方法后,不再需要把32位返回点地址压入堆栈。
下面程序中,子程序FRACTOR用来计算一个数的阶乘。主程序利用它计算1~5的阶乘,存入FRA数组。
.MODEL SMALL
.DATA
FRA DW 5 DUP(?)
.CODE
START: MOV AX,@DATA
MOV DS,AX
MOV BX,1 ; BX中存放待求阶乘的数
M0v SI,0 ; SI用作指针
MOV CX,5 ; 求阶乘次数(循环次数)
LOOP0: CALL FRACTOR ; 调用FRACTOR求阶乘
MOV FRA[SI],AX ; 保存结果(阶乘)
INC BX ; 产生下一个待求阶乘的数
ADD SI,2 ; 修改指针
LOOP LOOP0 ; 循环控制
MOV AX,4C00H
INT 21H
FRACTOR PROC NEAR
PUSH CX ; 把CX压入堆栈保护
MOV CX,BX ; 待求阶乘的数转入CX寄存器
MOV AX,1 ; 累乘器置初值1
FRALOOP: MUL CX ; 累乘
LOOP FRALOOP ; 循环控制
POP CX ; 从堆栈里弹出CX的原值
RET
FRACTOR ENDP
END START
在子程序入口处把相关寄存器的值入栈保护,程序返回前再恢复它们的值,这个操作称为保护现场和恢复现场。
那么,哪些寄存器需要入栈保护呢?从原理上说,只需要保护与主程序发生使用冲突的寄存器。但是,一个子程序可以为多个主程序调用,究竟哪些寄存器会发生使用冲突就不易确定了。所以,从安全角度出发,可以把子程序中所有使用到的寄存器都压入堆栈保护。但是有一点必须注意,不应包括带回运算结果的寄存器,例如上例中的AX寄存器。
另一方面,保护现场和恢复现场能否在主程序中进行?
从理论上说,保护现场和恢复现场可以在主程序中进行。但是,如果主程序中多次调用同一段子程序,就得有多组的PUSH和POP指令,这显然不如在子程序中进行保护现场和恢复现场方便,在那里只需写一次就可以了。
编写一个子程序的源代码之前,首先应该明确以下几点。
(1)子程序的名字。
(2)子程序的功能。
(3)入口参数:为了运行这个子程序,主程序为它准备了哪几个已知条件?这些参数存放在什么地方?
(4)出口参数:这个子程序的运行结果有哪些?存放在什么地方?
(5)影响寄存器:运行这个子程序会改变哪几个寄存器的值?
(6)其他需要说明的事项。
上述说明性文字加上子程序使用的变量说明、子程序的程序流程图和源程序清单,就构成了子程序文件。有了这样一个文档,程序员就可以放心地使用这个子程序,不必花更多的精力去了解它的内部细节。
许多时候,把上述内容以程序注释的方式书写在一个子程序的首部,以方便使用者。
【例11-3】一个名为SQUARE的子程序用来求一个数的平方根,源程序如下:
; 名称:SQUARE
; 功能:求16位无符号数的平方根
; 入口参数:16位无符号数在AX中
; 出口参数:8位平方根数在AL中
; 影响寄存器:AX(AL)
SQUARE PROC NEAR
PUSH CX ; 保护现场
PUSH BX
MOV BX,AX ; 要求平方根的数送BX
MOV AL,0 ; AL中存放平方根,初值0
MOV CX,1 ; CX置入第一个奇数1
NEXT:SUB BX,CX ; 利用公式:N^2=1+3+…+(2N-1)求平方根
JB DONE
ADD CX,2 ; 形成下一个奇数
INC AL ; AL存放已减去奇数的个数
JMP NEXT
DONE:POP BX ; 恢复现场
POP CX
RET ; 返回
SQUARE ENDP
准备好子程序文件之后,就可以编制主程序了。每调用一次子程序,主程序需要做3件事。
(1)为子程序准备入口参数。
(2)调用子程序。
(3)处理子程序的返回参数。
【例11-4】 为了求5个无符号数的平方根,可以编制主程序如下:
.DATA
X DW 59,3500,139,199,77 ; 欲求平方根的数组
ROOT DB 5 DUP(?) ; 存放平方根的内存区
.CODE
START: MOV AX,@DATA
MOV DS,AX
LEA BX,X ; 初始化指针
LEA SI,ROOT
MOV CX,5 ; 设置计数器初值
ONE: MOV AX,[BX] ; 设置入口参数
CALL SQUARE ; 调用子程序
MOV [SI],AL ; 保存返回参数(平方根)
ADD BX,2 ; 修改指针
INC SI ; 修改指针
LOOP ONE ; 循环控制
MOV AX,4C00H ; 返回DOS
INT 21H
SQUARE PROC NEAR
PUSH CX ; 保护现场
POP CX
RET ; 返回
SQUARE ENDP
END START
本例中,主程序把求平方根的数通过AX传送给子程序。子程序返回后,主程序把子程序的运算结果(平方根)保存到ROOT数组中。
主程序和子程序之间需要相互传递参数。传递的参数有两种类型。
(1)值传递:把参数的值放在约定的寄存器或存储单元传递给子程序,或者由子程序返回给主程序。如果一个入口参数是用值传递的,子程序可以使用这个值,但是无法改变这个入口参数自身的值。
(2)地址传递:把参数事先存放在某个存储单元,把这个存储单元的地址作为参数传递给子程序。如果一个参数使用它的地址来传递,子程序可以改变这个参数的值。例如,把存放结果的存储单元的地址作为入口参数传递给子程序,子程序就可以把运算结果直接存入这个单元。
参数按照其存放位置划分为3种类型。
· 参数存放在寄存器中。
· 参数存放在主、子程序共享的数据段内。
· 参数存放在堆栈内。
下面通过例子介绍这几种参数的传递方法。
【例11-5】 求菲波那契数列的前n项。菲波那契数列的前两项为1、1,以后的每一项都是其前两项之和。X0=1,X1=l,Xi=Xi-1+Xi-2(i≥2)。
.MODEL SMALL
DATA
FIBLST DW 1,1,18 DUP(?)
N DW 20
.CODE
START: MOV AX, @DATA
MOV DS, AX
LEA SI, FIBLST
MOV CX, N
SUB CX, 2
ONE: MOV AX, [SI+2]
MOV BX,[SI]
CALL FIB
MOV [SI+4],AX
ADD SI, 2
LOOP ONE
MOV AX, 4C00H
INT 21H
; 子程序FIB
; 功能:求菲波那契数列的一项
; 入口参数:AX=XH, BX=XH
; 出口参数:AX=Xi-1+Xi-2=(Xi)
FIB PROC
ADD AX, BX
RET
FIB ENDP
END START
本例使用寄存器AX、BX来传递参数,传递的是参数的值。
下面的程序仍然利用寄存器传递参数,但是传递的是参数的地址。
START: MOV AX, @DATA
MOV DS,AX
LEA SI,FIBLST
MOV CX,N
SUB CX,2
ONE: CALL FIB
ADD SI,2
LOOP ONE
; 子程序FIB
; 功能:求菲波那契数列的一项
; 入口参数:SI=Xi-2的段内偏移地址
; 出口参数:无(结果已由子程序存入数组内)
FIB PROC
PUSH AX
MOV AX,[SI]
ADD AX,[SI+2]
MOV [SI+4],AX
POP AX
RET
FIB ENDP
END START
由于传递的是参数的地址,子程序根据这个地址取出Xi-2和Xi-1,计算得到的结果直接存入变量Xi中。
仍以例11-5以求菲波那契数列程序为例,程序进行如下修改:
.MODEL SMALL
.DATA
FIBLST DW 1,1,18 DUP(?)
N DW 20
XI1 DW ?
XI2 DW 9
XI DW 9
.CODE
START: MOVAX,@DATA
MOV DS,AX
LEA SI,FIBLST ; 设置地址指针
MOV CX N
SUB CX,2 ; 设置计数器初值
ONE: MOV AX,[SI]
MOV X12,AX ; Xi-2置入Xi2
MOV AX,[SI+2]
MOV Xi1,AX ; Xi-1,置入Xi1
CALL FIB ; 调用子程序
MOV AX,Xl ; 取出子程序计算结果
MOV [SI+4],AX ; 取出Xi,置入FIBLST数组
ADD SI,2 ; 修改地址指针
LOOP ONE ; 循环控制
MOV AX,4C00H
INT 21H
;子程序FIB
;功能:计算菲波那契数列的一项
;入口参数:Xi1=Xi-1,Xi2=Xi-2
;出口参数:Xi=Xi-1+Xi-2
FIB PROC
PUSH AX ;保护现场
MOV AX, Xi1 ;AX=Xi-1
ADD AX, Xi2 ;AX=Xi-1+Xi-2
MOV Xl,AX ;XI←AX
POP AX ;恢复现场
RET
FIB ENDP
END START
本例中,主程序把数列的两个项先后送入与子程序共享的Xil和Xi2单元,子程序直接访问这两个单元,得到的结果存入共享的Xi,由主程序将它转存入数组。
仍以例11-5求菲波那契数列程序为例,程序进行如下修改:
.MODEL SMALL
.DATA
FIBLST DW 1,1,18 DUP(?)
N DW 20
.STACK
.CODE
START: MOVAX,@DATA
MOV DS,AX
LEA SI,FIBLST
MOV CX,N
SUB CX,2
ONE:PUSH AX ; 为保存结果,在堆栈预留单元
PUSH WORD PTR [SI] ; Xi-2入栈
PUSH WORD PTR[SI+2] ; Xi-1入栈
CALL FIB ; 调用子程序,执行后堆栈状态1
POP AX ; 从堆栈弹出结果,执行后堆栈状态4
MOV [SI+4],AX ; 把结果存入FIBLST数组
ADD SI,2
LOOP ONE
MOV AX, 4C00H
INT 21H
; 子程序FIB
; 功能:计算菲波那契数列的一项
; 入口参数:Xi-1,Xi-2在堆栈中
; 出El参数:Xi=Xi-1+Xi-2在堆栈中
FIB PROC
PUSH BP
MOV BP,SP ; 执行后堆栈状态2
MOV AX,[BP+4] ; 从堆栈取出Xi-1
ADD AX,[BP+6] ; AX=Xi-1+Xi-2
MOV [BP+8],AX ; 结果存入堆栈
POP BP ; 恢复BP
RET 4 ; 返回,SP=SP+4,执行后堆栈状态3
FIB ENDP
END START
本例中,主程序将子程序所需的参数压入堆栈,通过堆栈传递给子程序。图11-6给出了程序执行过程中堆栈的变化。在上面的源程序中,预留结果单元的操作PUSH AX可以用ADD SP,2代替,从堆栈弹出结果,存入数组的两条指令POP AX和MOV [SI+4],AX可以用一条指令POP WORD PTR[SI+4]代替。
|
|
BP |
|
BP |
|
BP |
IP(返回地址) |
IP(返回地址) |
IP(返回地址) |
IP(返回地址) | |||
Xi-1的值 |
Xi-1的值 |
Xi-1的值 |
Xi-1的值 | |||
Xi-2的值 |
Xi-2的值 |
Xi-2的值 |
Xi-2的值 | |||
预留结果单元 |
预留结果单元 |
预留结果单元 |
预留结果单元 | |||
原堆栈栈顶内容 |
原堆栈栈顶内容 |
原堆栈栈顶内容 |
原堆栈栈顶内容 | |||
堆栈状态1 |
堆栈状态2 |
堆栈状态3 |
堆栈状态4 |
图11-6 程序执行过程中堆栈的变化
不同的参数传递方法具有不同的特点和适用范围,可以根据需要灵活地选择使用。