352 lines
9.6 KiB
Markdown
352 lines
9.6 KiB
Markdown
# 实验五:子程序设计
|
||
|
||
## 一、实验目的
|
||
|
||
1. 掌握子程序的基本结构
|
||
2. 学会子程序的各种参数传递方法
|
||
3. 理解递归调用的概念和实现方法
|
||
4. 掌握子程序的嵌套调用
|
||
|
||
---
|
||
|
||
## 二、基础性实验
|
||
|
||
### 实验5_1 子程序设计一:寄存器传参
|
||
|
||
#### 实验目的
|
||
掌握子程序的基本结构,学会通过寄存器传递参数的方法。
|
||
|
||
#### 实验内容
|
||
编写子程序完成两个16位数的相加,并将结果通过寄存器返回。
|
||
|
||
#### 程序代码
|
||
```asm
|
||
; 实验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
|
||
```
|
||
|
||
#### 程序分析
|
||
1. **子程序结构**:子程序ADD_PROC包含PROC和ENDP伪指令,之间是子程序的功能代码
|
||
2. **参数传递方式**:采用寄存器传参,AX传递第一个加数,BX传递第二个加数
|
||
3. **结果返回**:结果通过AX寄存器返回给调用者
|
||
4. **指令说明**:
|
||
- `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寄存器传递字符串地址,完成字符串的显示。
|
||
|
||
#### 程序代码
|
||
```asm
|
||
; 实验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
|
||
```
|
||
|
||
#### 程序分析
|
||
1. **参数传递方式**:通过DX寄存器传递字符串的首地址
|
||
2. **DOS功能调用**:使用INT 21H的09H功能显示以'$'结尾的字符串
|
||
3. **LEA指令**:`LEA DX, MSG1`将MSG1的偏移地址加载到DX
|
||
4. **子程序嵌套**:主程序调用DISPLAY和NEWLINE两个子程序,实现换行显示
|
||
5. **程序结构**:
|
||
- DISPLAY子程序:显示DX指向的字符串
|
||
- NEWLINE子程序:输出回车换行(0DH=CR,0AH=LF)
|
||
|
||
#### 验证结果
|
||
运行程序后,屏幕依次显示:
|
||
```
|
||
Hello, World!
|
||
Welcome to Assembly!
|
||
```
|
||
|
||
---
|
||
|
||
## 三、加强性实验
|
||
|
||
### 实验5_3 子程序设计三:递归阶乘计算
|
||
|
||
#### 实验目的
|
||
掌握较复杂的子程序设计方法,学习递归调用的实现思想。
|
||
|
||
#### 实验内容
|
||
编写子程序计算N!,通过递归调用实现。
|
||
|
||
#### 程序代码
|
||
```asm
|
||
; 实验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
|
||
```
|
||
|
||
#### 程序分析
|
||
1. **递归终止条件**:当CX<=1时,返回AX=1,结束递归
|
||
2. **递归公式**:N! = N × (N-1)!
|
||
3. **寄存器保护**:
|
||
- `PUSH CX`:在递归调用前保存当前N值
|
||
- `POP CX`:在递归返回后恢复N值
|
||
4. **乘法运算**:MUL CX实现AX与CX的16位无符号乘法,结果存入AX
|
||
5. **堆栈作用**:递归调用时,堆栈保存返回地址和中间变量
|
||
6. **结果显示**:通过除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
|
||
```
|
||
|
||
---
|
||
|
||
## 五、实验总结
|
||
|
||
1. **子程序的基本结构**:子程序由PROC/ENDP伪指令定义,使用RET指令返回调用点
|
||
|
||
2. **参数传递的重要性**:参数传递是子程序设计的核心,决定了主程序与子程序之间的数据交换方式
|
||
|
||
3. **寄存器传参的特点**:简单直接,适合少量参数(1-3个),但受寄存器数量限制
|
||
|
||
4. **堆栈传参的特点**:适合参数较多的情况,可以传递任意数量参数,但需要谨慎管理栈平衡
|
||
|
||
5. **递归调用的要点**:
|
||
- 必须有明确的递归终止条件
|
||
- 需要妥善保护现场(寄存器)防止数据被破坏
|
||
- 递归深度受堆栈空间限制,应注意避免堆栈溢出
|
||
|
||
6. **嵌套调用的实现**:通过CALL指令和RET指令配合实现,每层调用都有自己的返回地址
|
||
|
||
7. **模块化设计思想**:子程序是模块化程序设计的基础,可以提高代码的可重用性和可维护性 |