# 实验五:子程序设计 ## 一、实验目的 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. **模块化设计思想**:子程序是模块化程序设计的基础,可以提高代码的可重用性和可维护性