您的位置: 网站首页 > 程序开发 > 汇编语言与微机原理教程 > 第11章 模块化程序设计 > 【11.2 子 程 序】

11.2 子 程 序

 

11.2     

子程序(subroutine)是一组相对独立的程序代码,可以完成预定的一个或一组功能。需要执行这组程序代码时,由上一级程序(称为主程序或主调程序)通过调用指令CALL进入这个子程序执行。子程序执行完毕后,用返回指令RET回到主程序中,调用指令CALL的下一条指令执行。子程序调用和返回的过程如图11-5所示。

11-5  子程序的调用和返回

由此可见,调用指令出现在主程序中,返回指令出现在子程序中。它们成对使用,但却出现在不同的程序中。

子程序调用指令和前面所学的JMP指令有相似之处,它们都是通过改变IPITCS的值进行程序的转移。两者的不同之处在于调用指令要求返回,即子程序执行完成必须返回调用它的程序继续执行,而后者可以“一去不复返”。

子程序按照其入口地址长度可分为两种类型。

·    近程子程序:只能被同一个代码段里的程序调用的子程序。由于主程序和子程序处于同一个代码段,CS寄存器的值保持不变,调用和返回时只需要改变IP寄存器的值。近程子程序的入口地址用16位段内偏移地址表示。

·    远程子程序:能够被不同代码段的程序调用,也能被同一代码段的程序调用的子程序。调用这样的子程序时需要同时改变CSIP寄存器的值,返回时,需要从堆栈里弹出32位的返回地址送入IPCS寄存器。远程子程序的入口地址用16位段基址和16位段内偏移地址表示。

子程序的类型在它定义时说明。

11.2.1  子程序的指令

1.调用(callCALL)指令

CALL指令用来调用子程序,与JMP指令类似,有4种不同的寻址方式。

1)段内直接调用。

格式:

CALL子程序名

操作:SPSP-2SS[SP]IP(保存16位返回地址),IP←子程序的偏移地址。

例如:

CALL PROC1              ; PROC1是近程子程序的入口标号

这条指令用来调用与主程序在同一代码段,并且定义为近程的子程序。

2)段内间接调用。

格式:

CALL    REG16/MEM16

操作:SPSP-2SS[SP]IP(保护16位返回地址),IPREG16/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子程序名。

操作:SPSP-2SS[SP]CS(保存32位返回地址);SPSP-2SS[SP]IP(偏移地址IP保存在较小地址处);IP←子程序的偏移地址,CS←子程序的段基址。

例如:

CALL    FAR PTR PROC2    ; PROC2是远程子程序的入口标号

这条指令用来调用处于不同代码段内,或者虽然处于同一代码段,但是被定义为远程的子程序。FAR PTR用来指明该子程序为远程子程序。远程子程序的入口地址和返回地址都是32位,包括16位的偏移地址和16位的段基址。

4)段间间接调用。

格式:

CALL        MEM32

操作:SPSP-2SS[SP]CSSPSP-2SS[SP]IPIP[MEM32]CS[MEM32+2]

这条指令用来调用远程子程序,32位的子程序入口地址事先已存放在一个32位的存储器双字变量中。

例如:

ADD_PROC1 DD PROC2      ; 子程序入13地址放入存储器双字变量

CALL  ADD_PROC1         ; 调用远程子程序PROC2

2.返回(returnRET)指令

RET指令用来从子程序返回主程序,有以下4种返回方式。

1)无参数段内返回。

格式:

RET

操作:IPSS[SP]SPSP+2

这条指令在近程子程序内使用,将执行CALL指令时保存在堆栈的16位返回地址送回IP,返回主程序。

2)有参数段内返回。

格式:

RET    D16

操作:IPSS[SP]SPSP+2SPSP+D16

这条指令除了将堆栈内16位偏移地址送入IP,还用一个16位的位移量(立即数或表达式)修改SP的值,这个操作用来废弃主程序存放在堆栈里的入口参数。

3)无参数段间返回。

格式:

RET

操作:IPSS[SP]SPSP+2CSSS[SP]SPSP+2

这条指令在远程子程序内使用,将执行CALL指令时保存在堆栈的32位返回地址送回IPCS,返回主程序。该指令的助记符与段内返回指令相同,但是它们汇编产生的机器代码是不同的,代表了两条不同的机器指令。这条指令也可以写作RETF

4)有参数段间返回。

格式:

RET    D16

操作:IPSS[SP]SPSP+2CSSS[SP]SPSP+2SPSP+D16

这条指令在远程子程序内使用,将执行CALL指令时保存在堆栈的32位返回地址送回IPCS,返回主程序,并且用16位位移量修改SP的值。该指令的助记符与段内返回指令相同,但是它们汇编产生的机器代码是不同的。

11.2.2  子程序的定义

子程序的定义格式如下:

子程序名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子程序。

子程序体中至少应包含一条返回指令,也可以有多于一条的返回指令。一般情况下,子程序的最后一条指令应该是返回指令。上述定义中,PROCENDP是伪指令,它们没有对应的机器码,它们用来向汇编程序报告一个子程序的开始和结束。这个子程序也可以简单地写成下面的形式:

ZEROBYTESXOR  AX,AX            ; AX寄存器清零

LOOP  ZEROLOOP                  ; 循环控制

RET                             ; 结束子程序运行,返回主程序

如果程序中有多个子程序,用这种方式定义后,它们的边界不容易清晰地区分。而且这种方式只能定义近程子程序,只能被同一代码段内的程序调用,因此不提倡。用户编写的主程序也可以看作是由操作系统调用的一个子程序。

.CODE                       ; 代码段开始

MAIN PROC FAR               ; 主程序开始

PUSH DS                     ; 操作系统的返回点在DS0

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用来计算一个数的阶乘。主程序利用它计算15的阶乘,存入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    CX5                 ; 求阶乘次数(循环次数)

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寄存器。

另一方面,保护现场和恢复现场能否在主程序中进行?

从理论上说,保护现场和恢复现场可以在主程序中进行。但是,如果主程序中多次调用同一段子程序,就得有多组的PUSHPOP指令,这显然不如在子程序中进行保护现场和恢复现场方便,在那里只需写一次就可以了。

11.2.3  子程序的文件

编写一个子程序的源代码之前,首先应该明确以下几点。

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

11.2.4  子程序的应用

准备好子程序文件之后,就可以编制主程序了。每调用一次子程序,主程序需要做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数组中。

11.2.5  参数的传递

主程序和子程序之间需要相互传递参数。传递的参数有两种类型。

1)值传递:把参数的值放在约定的寄存器或存储单元传递给子程序,或者由子程序返回给主程序。如果一个入口参数是用值传递的,子程序可以使用这个值,但是无法改变这个入口参数自身的值。

2)地址传递:把参数事先存放在某个存储单元,把这个存储单元的地址作为参数传递给子程序。如果一个参数使用它的地址来传递,子程序可以改变这个参数的值。例如,把存放结果的存储单元的地址作为入口参数传递给子程序,子程序就可以把运算结果直接存入这个单元。

参数按照其存放位置划分为3种类型。

·    参数存放在寄存器中。

·    参数存放在主、子程序共享的数据段内。

·    参数存放在堆栈内。

下面通过例子介绍这几种参数的传递方法。

1.使用寄存器传递参数

11-5 求菲波那契数列的前n项。菲波那契数列的前两项为11,以后的每一项都是其前两项之和。X0=1X1=lXi=Xi-1+Xi-2i2)。

.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

本例使用寄存器AXBX来传递参数,传递的是参数的值。

下面的程序仍然利用寄存器传递参数,但是传递的是参数的地址。

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-2Xi-1,计算得到的结果直接存入变量Xi中。

2.变量(共享数据段)传递参数

仍以例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                           XIAX

POP     AX                              ;恢复现场

RET

FIB     ENDP

END     START

本例中,主程序把数列的两个项先后送入与子程序共享的XilXi2单元,子程序直接访问这两个单元,得到的结果存入共享的Xi,由主程序将它转存入数组。

3.堆栈传递参数

仍以例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 SP2代替,从堆栈弹出结果,存入数组的两条指令POP AXMOV [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  程序执行过程中堆栈的变化

不同的参数传递方法具有不同的特点和适用范围,可以根据需要灵活地选择使用。