子程序是由设计者定义的完成某种功能的程序模块。一旦定义了,该子程序可被任意调用。 例
|
SENDAT PROC FAR ;子程序定义伪指令语句 |
|
PUSH AX ;保护 AX 、 DX 、 SI 寄存器 |
|
PUSH DX |
|
PUSH SI |
|
LEA SI , BUFR ;子程序待输出的数据的首地址 |
|
GOON : MOV DX , 03FBH |
|
WAIT : IN AL , DX ;读端口 03FBH 读入数据 |
|
TEST AL , 20H |
|
JZ WAIT |
|
MOV AL , [SI] ;将缓冲区 BUFR 按字节装入 |
|
MOV DX , 03F 8H |
|
OUT DX , AL ;输出至端口 03F 8H |
|
INC SI |
|
CMP AL , 0AH ;判断输出数据是否为结束 |
|
JNE GOON ;不为 0AH 则转至 GOON |
|
POP SI ;恢复寄存器 |
|
POP DX |
|
POP AX |
|
RET |
|
SENDAT ENDP |
上面的子程序,可以把数据段 BUFR 缓冲区,以 OA 结束的数据,输出到 03F 8H 端口。 主程序在调用子程序时,一方面初始数据要传递给子程序,另一方面子程序运行的结果要传递给主程序。尽管没有初始数据或没有运行结果的情况也有,但一般情况下是必须考虑的。 在编写较为复杂的子程序时,可能出现子程序中调用子程序的情况,通常称这种情况叫子程序嵌套。子程序嵌套层次的深度受堆栈大小的影响,由于堆栈不仅在子程序中使用,还受多方面影响,必须保证整个程序运行过程中,堆栈不能溢出。 把功能相对独立的程序段单独编写和调试,作为一个相对独立的模块供程序使用,就形成子程序子程序可以实现源程序的模块化,可简化源程序结构,可以提高编程效率 1、程序定义伪指令 格式 : 过程名 proc [near|far] . 过程名 endp 过程名(子程序名)为符合语法的标识符 NEAR 属性(段内近调用)的过程只能被相同代码段的其他程序调用 FAR 属性(段间远调用)的过程可以被相同或不同代码段的程序调用 对简化段定义格式,在微型、小型和紧凑存储模式下,过程的缺省属性为 near ;在中型、大型和巨型存储模式下,过程的缺省属性为 far 对完整段定义格式,过程的缺省属性为 near 用户可以在过程定义时用 near 或 far 改变缺省属性
子程序常见格式: subname proc ; 具有缺省属性的 subname 过程 push ax ; 保护寄存器:顺序压入堆栈 push bx ;ax/bx/cx 仅是示例 push cx … ; 过程体 , 程序的主要功能 pop cx ; 恢复寄存器:逆序弹出堆栈 pop bx pop ax ret ; 过程返回 subname endp ; 过程结束 ; 子程序功能:实现光标回车换行 dpcrlf proc ; 过程开始 push ax ; 保护寄存器 AX 和 DX push dx mov dl,0dh ; 显示回车 mov ah,2 int 21h mov dl,0ah ; 显示换行 mov ah,2 int 21h pop dx ; 恢复寄存器 DX 和 AX pop ax ret ; 子程序返回 dpcrlf endp ; 过程结束 例 : 无参数传递的子程序 ALdisp proc ; 实现 al 内容的显示 push ax ; 过程中使用了 AX 、 CX 和 DX push cx push dx push ax ; 暂存 ax mov dl,al ; 转换 al 的高 4 位 mov cl,4 shr dl,cl or dl,30h ;al 高 4 位变成 3 cmp dl,39h jbe aldisp1 add dl,7 ; 是 0Ah ~ 0Fh ,还要加上 7 aldisp1: mov ah,2 ; 显示 int 21h 例 : 实现 AL 内容显示的子程序 pop dx ; 恢复原 ax 值到 dx and dl,0fh ; 转换 al 的低 4 位 or dl,30h cmp dl,39h jbe aldisp2 add dl,7 aldisp2: mov ah,2 ; 显示 int 21h pop dx pop cx pop ax ret ; 过程返回 ALdisp endp ... ; 主程序 mov bx,offset array; 调用程序段开始 mov cx,count displp: mov al,[bx] call ALdisp ; 调用显示过程 mov dl,',' ; 显示一个逗号,分隔数据 mov ah,2 int 21h inc bx loop displp ; 调用程序段结束 .exit 0 ... ; 过程定义 end
HTOASC proc ; 将 AL 低 4 位表达的一位 16 进制数转换为 ASCII 码 and al,0fh cmp al,9 jbe htoasc1 add al,37h ; 是 0AH ~ 0FH ,加 37H ret ; 子程序返回 htoasc1: add al,30h ; 是 0 ~ 9 ,加 30H ret ; 子程序返回 HTOASC endp 2、子程序的参数传递 入口参数(输入参数):主程序提供给子程序 出口参数(输出参数):子程序返回给主程序 参数的形式: ① 数据本身(传值) ② 数据的地址(传址) 传递的方法: ① 寄存器 ② 变量 ③ 堆栈 例:求校验和 子程序计算数组元素的“校验和” 校验和是指不记进位的累加 入口参数: 数组的逻辑地址(传址) 元素个数(传值) 出口参数: 求和结果(传值) 把参数存于约定的寄存器中,可以传值,也可以传址。 子程序对带有出口参数的寄存器不能保护和恢复(主程序视具体情况进行保护) 子程序对带有入口参数的寄存器可以保护,也可以不保护;但最好一致 例 : 入口参数: CX =元素个数, DS:BX =数组的段地址:偏移地址 出口参数: AL =校验和 用寄存器传递参数 .startup ; 设置入口参数(含有 DS ←数组的段地址) mov bx,offset array ;BX ←数组的偏移地址 mov cx,count ;CX ←数组的元素个数 call checksuma ; 调用求和过程 mov result,al ; 处理出口参数 .exit 0 checksuma proc xor al,al ; 累加器清 0 suma: add al,[bx] ; 求和 inc bx ; 指向下一个字节 loop suma ret checksuma endp end 主程序和子程序直接采用同一个变量名共享同一个变量,实现参数的传递 不同模块间共享时,需要声明 例 : 入口参数: count =元素个数, array =数组名(含段地址:偏移地址) 出口参数: result =校验和 用变量传递参数 ; 主程序 call checksumb
; 子程序 checksumb proc push ax push bx push cx xor al,al ; 累加器清 0 mov bx,offset array ;BX ←数组的偏移地址 mov cx,count ;CX ←数组的元素个数 sumb: add al,[bx] ; 求和 inc bx loop sumb mov result,al ; 保存校验和 pop cx pop bx pop ax ret checksumb endp 主程序将子程序的入口参数压入堆栈,子程序从堆栈中取出参数 子程序将出口参数压入堆栈,主程序弹出堆栈取得它们 例 : 入口参数: 顺序压入偏移地址和元素个数 出口参数: AL =校验和 用堆栈传递参数 .startup mov ax,offset array push ax mov ax,count push ax call checksumc add sp,4 mov result,al .exit 0 要注意堆栈的分配情况,保证参数存取正确、子程序正确返回,并保持堆栈平衡 checksumc proc push bp mov bp,sp ; 利用 BP 间接寻址存取参数 push bx push cx mov bx,[bp+6] ;SS:[BP+6] 指向偏移地址 mov cx,[bp+4] ;SS:[BP+4] 指向元素个数 xor al,al sumc: add al,[bx] inc bx loop sumc pop cx pop bx pop bp ret checksumc endp 堆栈区及参数 主程序实现平衡堆栈: add sp,n 子程序实现平衡堆栈: ret n 子程序的嵌套 1 .子程序的嵌套 2 .嵌套深度。
例:设从 BUF 开始存放若干无符号字节数据,找出其中的最小值并以 16 进制形式输出。 子程序内包含有子程序的调用就是子程序嵌套没有什么特殊要求 ALdisp proc push ax push cx ; 实现 al 内容的显示 push ax ; 暂存 ax mov cl,4 shr al,cl ; 转换 al 的高 4 位 call htoasc ; 子程序调用(嵌套) pop ax ; 转换 al 的低 4 位 call htoasc ; 子程序调用(嵌套) pop cx pop ax ret ALdisp endp 例 : 嵌套子程序 ; 将 AL 低 4 位表达的一位 16 进制数转换为 ASCII 码 HTOASC proc push ax push bx push dx mov bx,offset ASCII;BX 指向 ASCII 码表 and al,0fh ; 取得一位 16 进制数 xlat CS:ASCII ; 换码: AL ← CS:[BX + AL] ,注意数据在代码段 CS mov dl,al ; 显示 mov ah,2 int 21h pop dx pop bx pop ax ret ; 子程序返回 ; 子程序的数据区 ASCII db 30h,31h,32h,33h,34h,35h,36h,37h db 38h,39h,41h,42h,43h,44h,45h,46h HTOASC endp 这是一个具有局部变量的子程序。因为数据区与子程序都在代码段,所以利用了换码指令 XLAT 的另一种助记格式(写出指向缓冲区的变量名,目的是便于指明段超越前缀)。串操作 MOVS 、 LODS 和 CMPS 指令也可以这样使用,以便使用段超越前缀 除采用段超越方法外,子程序与主程序的数据段不同时,我们还可以通过修改 DS 值实现数据存取;但需要保护和恢复 DS 寄存器 子程序的递归 当子程序直接或间接地嵌套调用自身时称为递归调用,含有递归调用的子程序称为递归子程序 递归子程序必须采用寄存器或堆栈传递参数,递归深度受堆栈空间的限制 例:求阶乘 N!= N*(N-1)! 当 N>0 1 当 N=0 .model small .stack 256 .data N dw 3 result dw ? .code .startup mov bx,N push bx ; 入口参数: N call fact ; 调用递归子程序 pop result ; 出口参数: N ! .exit 0 例 : 求自然数 N(N>=1) 的阶乘 ; 计算 N! 的近过程 ; 入口参数:压入 N ; 出口参数:弹出 N! fact proc push ax push bp mov bp,sp mov ax,[bp+6] ; 取入口参数 N cmp ax,0 jne fact1 ;N > 0,N! = N × (N-1)! inc ax ;N = 0,N! = 1 jmp fact2 fact1: dec ax ;N-1 push ax call fact ; 调用递归子程序求 (N-1)! pop ax mul word ptr [bp+6] ; 求 N × (N-1)! fact2: mov [bp+6],ax ; 存入出口参数 N! pop bp pop ax ret fact endp 调用时进栈 返回时出栈 1 3! 2! 1! 子程序的重入 可重入子程序是指该子程序被某程序调用 , 但还未结束 , 又被另一个程序调用 , 这是在分时系统中 . 子程序的重入是指子程序被中断后又被中断服务程序所调用,能够重入的子程序称为可重入子程序。在子程序中,注意利用寄存器和堆栈传递参数和存放临时数据,而不要使用固定的存储单元(变量),就能够实现重入。 子程序的重入不同于子程序的递归。重入是被动地进入,而递归是主动地进入;重入的调用间往往没有关系,而递归的调用间却是密切相关的。递归子程序也是可重入子程序。 ASCII 码转换为二进制数 ① 首先判断输入为正或负数,并用一个寄存器记录 ② 接着输入 0 ~ 9 数字( ASCII 码),并减 30H 转换为二进制数 ③ 然后将前面输入的数值乘 10 ,并与刚输入的数字相加得到新的数值 ④ 重复②、③步,直到输入一个非数字字符结束 ⑤ 负数进行求补,转换成补码;否则直接保存数值
本例采用 16 位寄存器表达数据,所以只能输入+ 327677 ~- 32768 间的数值 但该算法适合更大范围的数据 例题 : 例:从键盘输入有符号十进制数 子程序从键盘输入一个有符号十进制数;子程序还包含将 ASCII 码转换为二进制数的过程 输入时,负数用“-”引导,正数直接输入或用“+”引导 子程序用寄存器传递出口参数,主程序调用该子程序输入 10 个数据 .data count = 10 array dw count dup(0) ; 预留数据存储空间 .code .startup mov cx,count mov bx,offset array again: call read ; 调用子程序输入一个数据 mov [bx],ax ; 将出口参数存放缓冲区 inc bx inc bx call dpcrlf ; 调用子程序,光标回车换行以便输入下一个数据 loop again .exit 0 ; 输入有符号 10 进制数的通用子程序 ; 出口参数: AX =补码表示的二进制数值 ; 说明:负数用“-”引导,正数用“+”引导或直接输入;数据范围是+ 32767 ~- 32768 read proc push bx push cx push dx xor bx,bx ;BX 保存结果 xor cx,cx ;CX 为正负标志, 0 为正,- 1 为负 mov ah,1 ; 输入一个字符 int 21h cmp al,'+' ; 是“+”,继续输入字符 jz read1 cmp al,'-' ; 是“-”,设置- 1 标志 jnz read2 ; 非“+”和“-”,转 read2 mov cx,-1 read1: mov ah,1 ; 继续输入字符 int 21h read2: cmp al,'0‘ ; 不是 0 ~ 9 之间的字符,则输入数据结束 jb read3 cmp al,'9' ja read3 sub al,30h ; 是 0 ~ 9 之间的字符,则转换为二进制数 ; 利用移位指令,实现数值乘 10 : BX ← BX × 10 shl bx,1 mov dx,bx shl bx,1 shl bx,1 add bx,dx ;bx 内容乘 10 mov ah,0 add bx,ax ; 已输入数值乘 10 后,与新输入数值相加 jmp read1 ; 继续输入字符 read3: cmp cx,0 jz read4 neg bx ; 是负数,进行求补 例:显示有符号十进制数 子程序在屏幕上显示一个有符号十进制数;子程序还包含将二进制数转换为 ASCII 码的过程 显示时,负数用“-”引导,正数直接输出、没有前导字符
子程序的入口参数用共享变量传递,主程序调用该子程序显示 10 个数据 .data count = 10 array dw 1234,-1234,0,1,-1,32767 dw -32768,5678,-5678,9000 wtemp dw ? ; 共享变量 .code .startup mov cx,count mov bx,offset array again: mov ax,[bx] mov wtemp,ax ; 将入口参数存入共享变量 call write ; 调用子程序显示一个数据 inc bx inc bx call dpcrlf ; 便于显示下一个数据 loop again .exit 0 ; 显示有符号 10 进制数的通用子程序 入口参数:共享变量 wtemp write proc push ax push bx push dx mov ax,wtemp ; 取出显示数据 test ax,ax ; 判断零、正数或负数 jnz write1 mov dl,'0' ; 是零,显示“ 0 ” 后退出 mov ah,2 int 21h jmp write5 write1: jns write2 ; 是负数,显示“-” mov bx,ax ;AX 数据暂存于 BX mov dl,'-' mov ah,2 int 21h mov ax,bx neg ax ; 数据求补(求绝对值) write2: mov bx,10 push bx ;10 压入堆栈,作为退出标志 write3: cmp ax,0 ; 数据(余数)为零 jz write4 ; 转向显示 sub dx,dx ; 扩展被除数 DX.AX div bx ; 数据除以 10 : DX.AX ÷ 10 add dl,30h ; 余数( 0 ~ 9 )转换为 ASCII 码 push dx ; 数据各位先低位后高位压入堆栈 jmp write3 write4: pop dx ; 数据各位先高位后低位弹出堆栈 cmp dl,10 ; 是结束标志 10 ,则退出 je write5 mov ah,2 ; 进行显示 int 21h jmp write4 write5: pop dx pop bx pop ax ret ; 子程序返回 write endp ; 使光标回车换行的子程序 dpcrlf proc ... ; 省略 dpcrlf endp end
|