电工学习网

 找回密码
 立即注册

子程序设计

2015-3-22 09:10| 编辑:电工学习网| 查看: 8210| 评论: 0

    子程序是由设计者定义的完成某种功能的程序模块。一旦定义了,该子程序可被任意调用。

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

上一篇:循环程序设计下一篇:宏汇编 收藏

看过《子程序设计》的人还看了以下文章:

发表评论

最新评论

电工学习网 ( )

GMT+8, 2023-4-7 14:52

Powered by © 2011-2022 www.shop-samurai.com 版权所有 免责声明 不良信息举报

技术驱动未来! 电工学习网—专业电工基础知识电工技术学习网站。

栏目导航: 工控家园 | 三菱plc | 西门子plc | 欧姆龙plc | plc视频教程

返回顶部