9.6 KiB
9.6 KiB
实验五:子程序设计
一、实验目的
- 掌握子程序的基本结构
- 学会子程序的各种参数传递方法
- 理解递归调用的概念和实现方法
- 掌握子程序的嵌套调用
二、基础性实验
实验5_1 子程序设计一:寄存器传参
实验目的
掌握子程序的基本结构,学会通过寄存器传递参数的方法。
实验内容
编写子程序完成两个16位数的相加,并将结果通过寄存器返回。
程序代码
; 实验5_1: 子程序设计一 - 寄存器传参(两个16位数相加)
; 功能:完成两个16位数的相加
; 入口参数:AX, BX
; 出口参数:AX(结果)
DATAS SEGMENT
A DW 1234H ; 第一个加数
B DW 5678H ; 第二个加数
RESULT DW ? ; 结果存储单元
DATAS ENDS
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
; 子程序:ADD_PROC
; 功能:完成两个16位数的相加
; 入口参数:AX = 第一个数, BX = 第二个数
; 出口参数:AX = 相加结果
ADD_PROC PROC
ADD AX, BX ; AX = AX + BX
RET ; 返回调用点
ADD_PROC ENDP
START:
MOV AX, DATAS
MOV DS, AX
MOV AX, A ; 第一个数送AX
MOV BX, B ; 第二个数送BX
CALL ADD_PROC ; 调用子程序完成相加
MOV RESULT, AX ; 保存结果到RESULT单元
MOV AH, 4CH ; 程序退出
INT 21H
CODES ENDS
END START
程序分析
- 子程序结构:子程序ADD_PROC包含PROC和ENDP伪指令,之间是子程序的功能代码
- 参数传递方式:采用寄存器传参,AX传递第一个加数,BX传递第二个加数
- 结果返回:结果通过AX寄存器返回给调用者
- 指令说明:
CALL ADD_PROC:调用子程序,将返回地址入栈RET:子程序返回指令,从栈中弹出返回地址
验证结果
假设A=1234H,B=5678H:
- 入口:AX=1234H,BX=5678H
- 执行ADD后:AX=68ACH(1234H+5678H=68ACH)
- 结果:RESULT单元中保存68ACH
实验5_2 子程序设计二:DX传参
实验目的
掌握利用寄存器传递参数的方法,特别是DX寄存器在字符串显示中的应用。
实验内容
编写子程序,利用DX寄存器传递字符串地址,完成字符串的显示。
程序代码
; 实验5_2: 子程序设计二 - DX传参(字符串显示)
; 功能:显示以'$'结尾的字符串
; 入口参数:DX = 字符串地址
; 出口参数:无
DATAS SEGMENT
MSG1 DB 'Hello, World!$'
MSG2 DB 'Welcome to Assembly!$'
CRLF DB 0DH, 0AH, '$' ; 回车换行
DATAS ENDS
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
; 子程序:DISPLAY
; 功能:显示以'$'结尾的字符串
; 入口参数:DX = 字符串地址
; 出口参数:无
DISPLAY PROC
MOV AH, 09H ; DOS功能:显示字符串
INT 21H
RET
DISPLAY ENDP
; 子程序:NEWLINE
; 功能:输出回车换行
; 入口参数:无
; 出口参数:无
NEWLINE PROC
LEA DX, CRLF
MOV AH, 09H
INT 21H
RET
NEWLINE ENDP
START:
MOV AX, DATAS
MOV DS, AX
; 显示第一条消息
LEA DX, MSG1
CALL DISPLAY
CALL NEWLINE
; 显示第二条消息
LEA DX, MSG2
CALL DISPLAY
CALL NEWLINE
MOV AH, 4CH ; 程序退出
INT 21H
CODES ENDS
END START
程序分析
- 参数传递方式:通过DX寄存器传递字符串的首地址
- DOS功能调用:使用INT 21H的09H功能显示以'$'结尾的字符串
- LEA指令:
LEA DX, MSG1将MSG1的偏移地址加载到DX - 子程序嵌套:主程序调用DISPLAY和NEWLINE两个子程序,实现换行显示
- 程序结构:
- DISPLAY子程序:显示DX指向的字符串
- NEWLINE子程序:输出回车换行(0DH=CR,0AH=LF)
验证结果
运行程序后,屏幕依次显示:
Hello, World!
Welcome to Assembly!
三、加强性实验
实验5_3 子程序设计三:递归阶乘计算
实验目的
掌握较复杂的子程序设计方法,学习递归调用的实现思想。
实验内容
编写子程序计算N!,通过递归调用实现。
程序代码
; 实验5_3: 子程序设计三 - 递归阶乘计算
; 功能:计算N!(通过递归调用实现)
; 入口参数:CX = N(待计算的数)
; 出口参数:AX = N!结果
DATAS SEGMENT
N DW 5 ; 要计算阶乘的数
RESULT DW ? ; 结果存储单元
DATAS ENDS
STACKS SEGMENT
DW 100H DUP(?) ; 堆栈空间,用于递归调用
STACKS ENDS
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS, SS:STACKS
; 子程序:FACTORIAL
; 功能:计算N!(递归实现)
; 入口参数:CX = N
; 出口参数:AX = N!
FACTORIAL PROC
CMP CX, 1 ; 比较N与1
JBE DONE ; 如果N<=1,跳转到DONE
PUSH CX ; 保存当前N值
DEC CX ; 计算N-1
CALL FACTORIAL ; 递归调用,计算(N-1)!
POP CX ; 恢复N值
MUL CX ; AX = AX * CX = (N-1)! * N = N!
RET
DONE:
MOV AX, 1 ; 递归终止,返回1
RET
FACTORIAL ENDP
START:
MOV AX, DATAS
MOV DS, AX
MOV CX, N ; 将N送入CX作为入口参数
CALL FACTORIAL ; 调用递归子程序计算阶乘
MOV RESULT, AX ; 保存结果
; 以下代码用于显示结果(将数字转换为ASCII显示)
MOV CX, 0 ; 计数寄存器清零
MOV BX, 10 ; 除数10
DIVIDE_LOOP:
XOR DX, DX ; DX清零(32位除法需要)
DIV BX ; AX = AX / 10, DX = AX % 10
PUSH DX ; 保存余数(低位先入栈)
INC CX ; 计数+1
CMP AX, 0 ; 商是否为0?
JNE DIVIDE_LOOP ; 不为0则继续除法
; 显示结果
DISPLAY_LOOP:
POP DX ; 弹出数字
ADD DL, '0' ; 转换为ASCII字符
MOV AH, 02H ; DOS功能:显示字符
INT 21H
LOOP DISPLAY_LOOP
MOV AH, 4CH ; 程序退出
INT 21H
CODES ENDS
END START
程序分析
- 递归终止条件:当CX<=1时,返回AX=1,结束递归
- 递归公式:N! = N × (N-1)!
- 寄存器保护:
PUSH CX:在递归调用前保存当前N值POP CX:在递归返回后恢复N值
- 乘法运算:MUL CX实现AX与CX的16位无符号乘法,结果存入AX
- 堆栈作用:递归调用时,堆栈保存返回地址和中间变量
- 结果显示:通过除10取余的方法将二进制结果转换为ASCII码显示
验证结果
以N=5为例,执行过程如下:
- 5! = 5 × 4! = 5 × 24 = 120
- 结果显示为:120
四、思考题
4.1 子程序参数传递有哪些方式?各有何优缺点?
1. 寄存器传递方式
- 优点:执行速度快,不需要额外的存取操作
- 缺点:寄存器数量有限,只能传递少量参数
2. 堆栈传递方式
- 优点:参数数量不受限制,可以传递任意多参数
- 缺点:需要额外的栈操作,速度较慢,编程复杂度增加
3. 存储器传递方式
- 优点:适合传递大数据块(数组、结构等)
- 缺点:占用内存空间,需要事先分配存储单元
4. 混合传递方式
- 优点:综合使用多种传参方式,灵活高效
- 缺点:设计复杂度增加,需要明确约定各参数的传递方式
4.2 递归调用需要注意什么问题?
1. 递归终止条件
- 必须设置明确的递归终止条件,避免无限递归
- 每次递归调用都要趋近于终止条件
2. 现场保护
- 使用PUSH指令保存可能被修改的寄存器
- 在递归返回前用POP指令恢复寄存器
3. 堆栈空间
- 递归深度受堆栈大小限制
- 递归过深可能导致堆栈溢出
4. 参数传递
- 确保入口参数在递归调用时正确传递
- 注意参数的副本性,避免数据共享导致的错误
4.3 如何实现子程序的嵌套调用?
1. 基本原理
- 子程序A中可以调用子程序B,子程序B中又可以调用子程序C
- 通过CALL指令实现嵌套调用,每次调用自动保存返回地址
2. 返回地址管理
- 每层CALL都会将返回地址压入堆栈
- RET指令从堆栈弹出返回地址,确保正确返回
3. 寄存器保护
- 在子程序入口处PUSH需要保护的寄存器
- 在子程序出口处POP恢复寄存器
- 注意嵌套时寄存器保护的嵌套顺序
4. 示例结构
MAIN:
CALL SUB1
...
SUB1:
PUSH CX ; 保护寄存器
CALL SUB2 ; 嵌套调用子程序2
POP CX ; 恢复寄存器
RET
SUB2:
PUSH CX
CALL SUB3 ; 嵌套调用子程序3
POP CX
RET
五、实验总结
-
子程序的基本结构:子程序由PROC/ENDP伪指令定义,使用RET指令返回调用点
-
参数传递的重要性:参数传递是子程序设计的核心,决定了主程序与子程序之间的数据交换方式
-
寄存器传参的特点:简单直接,适合少量参数(1-3个),但受寄存器数量限制
-
堆栈传参的特点:适合参数较多的情况,可以传递任意数量参数,但需要谨慎管理栈平衡
-
递归调用的要点:
- 必须有明确的递归终止条件
- 需要妥善保护现场(寄存器)防止数据被破坏
- 递归深度受堆栈空间限制,应注意避免堆栈溢出
-
嵌套调用的实现:通过CALL指令和RET指令配合实现,每层调用都有自己的返回地址
-
模块化设计思想:子程序是模块化程序设计的基础,可以提高代码的可重用性和可维护性