420 likes | 605 Views
第 6 章 子程序结构. 讲授要点. 子程序的定义、调用与返回。 子程序的参数传递方法。. 6.1 子程序 概述 6.1.1 过程的定义. 过程定义由 PROC 与 ENDP 伪指令实现,形式如下: 过程名 PROC [NEAR|FAR] < 过程体 > 过程名 ENDP 1. 过程名在整个程序中必须是唯一的。 2. 过程名本质上与标号一样,也具有 3 种属性:段地址、偏移地址和类型( NEAR 或 FAR )。 3. PROC 后用关键字 NEAR 、 FAR 或空,以表示过程的类型(缺省为 NEAR )。.
E N D
第6章 子程序结构 讲授要点 子程序的定义、调用与返回。 子程序的参数传递方法。
6.1子程序概述 6.1.1 过程的定义 过程定义由PROC与ENDP伪指令实现,形式如下: 过程名 PROC [NEAR|FAR] <过程体> 过程名 ENDP 1. 过程名在整个程序中必须是唯一的。 2. 过程名本质上与标号一样,也具有3种属性:段地址、偏移地址和类型(NEAR或FAR)。 3. PROC后用关键字NEAR、FAR或空,以表示过程的类型(缺省为NEAR)。
6.1.2 过程调用和返回 1.过程调用和返回指令 (1)CALL:过程调用 与JMP指令类似,CALL指令包括下列4种调用方式: • 段内直接调用(Intrasegment/Direct Call) • 段间直接调用(Intersegment/Direct Call) • 段内间接调用(Intrasegment/Indirect Call) • 段间间接调用(Intersegment/Indirect Call) 段内调用在同一代码段内进行,又称近(Near)调用; 段间调用可以在不同代码段之间进行,又称远(Far)调用。
语法格式: CALL ProcName 段内直接调用:IP进栈,IP = label的偏移地址; 段间直接调用:CS:IP 进栈,CS:IP = label的分段地址 CALL reg16/mem16 段内间接调用:IP进栈,IP = reg16 / [mem16] CALL mem32 段间间接调用: CS:IP 进栈,CS = mem32高字,IP = mem32低字 功能描述: (1)返回地址进栈。 远调用:CS与IP(下一条指令的地址)依次进栈。 近调用:IP(下一条指令的16位偏移地址)进栈。 (2)转移到过程的第1条指令去执行。 远调用:根据操作数,将32位地址送CS:IP。 近调用:根据操作数,将16位偏移地址送IP。 对标志位的影响:无。
(2)RET指令RET(Return):过程返回 语法格式: RET ; 近返回或远返回 RET imm16 ; 近返回或远返回,并调整堆栈:SP = SP + imm16 功能描述: RET:返回地址出栈,从而实现转移到返回地址处。其中, 远返回:POP 1个双字到CS:IP。 近返回:POP 1个字到IP。 RET imm16:在返回地址出栈后,CPU立即将imm16加到堆栈指针SP。这种机制用来在返回前将参数从栈中移出。 对标志位的影响:无。 说明:RET由汇编程序根据其所在过程的类型(NEAR或FAR)决定是近返回还是远返回。缺省为近返回。
2.使用过程应注意的问题 • 在过程体内必须有一条RET指令被执行到。如果在过程内没有执行到RET或其它转移指令,程序将继续执行ENDP后的指令。 • 正确选择过程的类型。通常基于下列原则: • 若过程只在同一代码段中被调用,则定义为NEAR。 • 若过程可以在不同代码段中被调用,则定义为FAR。 • 通常要保证RET指令执行前,栈顶内容正好是返回地址。 • 注意保护相关寄存器的值。通常,除了作为返回参数的寄存器外,过程不应改变其它寄存器的值。 • 可以将过程定义放在单独的代码段中。若过程定义与主程序处于同一代码段,则要保证其只有被调用时,才会执行。
3. 保存和恢复寄存器 例: SUBT PROC NEAR PUSH AX PUSH BX PUSH CX …… POP CX POP BX POP AX RET SUBT ENDP
【例6.1】分析下列程序,描述它的功能。 dseg SEGMENT buf DB 80,81 DUP(0) dseg ENDS sseg SEGMENT STACK DW 64 DUP(0) sseg ENDS cseg SEGMENT ASSUME CS:cseg,DS:dseg,SS:sseg
cr PROC (NEAR) MOV AH,2 MOV DL,13 INT 21H MOV DL,10 INT 21H RET cr ENDP
main:MOV AX,dseg MOV DS,AX LEA DX,buf MOV AH,10 INT 21H ;输入一个符号串 CALL cr MOV AH,1 INT 21H ;输入一个字符 MOV BL,AL ;用BL保存读入的字符
lab2: MOV DL,[SI] CMP DL,BL JZ lab1 ;等于第2次输入的符号则转 MOV AH,2 INT 21H INC SI LOOP lab2 lab1:MOV AH,4CH INT 21H cseg ENDS END main
【例6.2】编写一个子程序,对一个无符号的字型数组的各元素求和。在调用子程序之前,已把数组的段地址放在DS中,起始偏移地址放在寄存器SI中,数组元素个数(>0)放在CX中。要求子程序把计算结果以双字的形式存放,高位放在DX中,低位放在AX中。【例6.2】编写一个子程序,对一个无符号的字型数组的各元素求和。在调用子程序之前,已把数组的段地址放在DS中,起始偏移地址放在寄存器SI中,数组元素个数(>0)放在CX中。要求子程序把计算结果以双字的形式存放,高位放在DX中,低位放在AX中。 sum PROC NEAR PUSH BX ;保护用到的寄存器BX XOR AX,AX MOV DX,AX ;求和前先把存放结果的DX,AX清0 MOV BX,AX
s1: ADD AX,[BX+SI] ;把一个元素加到AX中 ADC DX,0 ;若有进位,DX加1 INC BX INC BX ;BX加2,指向数组的下一元素 LOOP s1 POP BX ;恢复寄存器BX的值 RET sum ENDP
6.2 过程的参数传递 参数的分类: • 入口参数:由调用者向过程传递的数据,作为过程的输入参数。 • 出口参数:由过程向调用者返回的数据,作为过程的输出参数。 根据问题的需要,过程可以只有入口参数或只有出口参数,也可以二者兼有。 对于过程与调用者之间的参数传递,可根据传递的数据量,选择采用寄存器、变量或堆栈等方式。由于过程是相对独立的功能块,因此,在定义过程时,通常要加上适当的注释,主要包括功能、入口参数与出口参数等。
6.2.1 用变量传递参数 在程序中定义全局变量,如放在数据段,过程直接按名访问该变量。 过程直接以变量作为参数,虽然方便,但通用性较差。 【例6.3】编写一个子程序,以放在DS段中year的公元年份为入口参数,判断该年是否为闰年。 另有一个应用程序,它已定义了一个字节型数组t,依次存放着12个月的每月天数,其中2月份的天数是28。应用程序已经在DS段中存放了年份值,利用前面编写的子程序,编写程序段调整数组t中2月份的天数。
【解】 ;功能:根据一个年份是否为闰年,设置该年2月份的天数 ;入口:DS段中的字型变量year =公元年份 ;出口:DS段中的字节型变量t+1=该年2月份天数 ;破坏寄存器:无 jud1 PROC NEAR PUSH AX PUSH BX PUSH CX PUSH DX MOV BYTE PTR [t+1],28 MOV AX,[year]
MOV DX,0 MOV BX,4 DIV BX ;除以4 CMP DX,0 JNZ lab1 ;不能整除4则不是闰年,转 MOV AX,[year] ;取回年份值 MOV BX,100 DIV BX ;除以100 CMP DX,0 JNZ lab2 ;不能整除100则是闰年,转 MOV AX,[year] MOV BX,400 DIV BX ;除以400
CMP DX,0 JZ lab2 lab2: INC BYTE PTR [t+1] ;是闰年,把天数加1,设置出口参数 lab1 : POP DX POP CX POP BX POP AX RET Jud1: ENDP
6.2.2 用寄存器传递参数 通过寄存器传递数据或数据地址。 通常选择AL、AX、DX:AX(或EAX)传递字节、字或双字。 传递16位偏移地址最好选择SI、DI或BX, 传递32位地址可以用DS:BX、DS:SI、DS:DI、ES:BX、ES:SI或ES:DI等。
【例6.4】用寄存器传递参数,编写例6.3要求的子程序。【例6.4】用寄存器传递参数,编写例6.3要求的子程序。 【解】 ;功能:判断一个年份是否为闰年 ;入口:AX = 公元年份 ;出口:CF,1表示是闰年,0表示非闰年 ;破坏寄存器:AX jud PROC NEAR PUSH BX PUSH CX PUSH DX MOV CX,AX ;临时保存年份值 MOV DX,0 MOV BX,4
DIV BX ;除以4,为预防溢出,用双字除以字 CMP DX,0 JNZ lab1 ;不能整除4则不是闰年,转 MOV AX,CX ;取回年份值 MOV BX,100 DIV BX ;除以100 CMP DX,0 JNZ lab2 ;不能整除100则是闰年,转 MOV AX,CX MOV BX,400 DIV BX ;除以400 CMP DX,0 JZ lab2
lab1: CLC ;把CF清0表示非闰年,设置出口参数 JMP lab3 lab2: STC ;把CF置1表示是闰年,设置出口参数 lab3: POP DX POP CX POP BX RET jud ENDP 对于DX中存放的年份值,需要先放到AX中,才能调用子程序jud,然后以调用返回后的CF值决定是否把t数组中表示2月份天数的[t+1]加1。程序段如下: MOV AX,DX CALL jud ADC BYTE PTR [t+1],0 ;原值+0+CF
6.2.3 用地址表传递参数 建立一个地址表,存放所有参数的地址,传递地址表的首地址给过程。 这种方法特别适合于参数较多的情况。
6.2.4 用堆栈传递参数 过程从堆栈得到入口参数,返回前将出口参数写入堆栈;调用者通过出栈得到返回参数。 过程从堆栈存取参数时,通常使用BP,因为其隐含的段地址在SS中。 采用堆栈传递参数时,典型的过程结构如下: StdProc proc near push bp mov bp, sp ; BP指向当前栈顶,用于取入口参数 ... pop bp ret ParmSize ; 返回前从堆栈移出入口参数 StdProc endp 其中,ParmSize是过程被调用前进栈的入口参数的字节数。
【例6.5】用堆栈传递入口参数,编写子程序,把接收的两个带符号整数中大的一个作为结果,出口参数放在AX中。【例6.5】用堆栈传递入口参数,编写子程序,把接收的两个带符号整数中大的一个作为结果,出口参数放在AX中。 【解】 ;功能:求两个带符号整数中大的一个 ;入口参数:调用前把两个带符号整数入栈 ;出口参数:AX ;破坏寄存器:AX
_max PROC NEAR PUSH BP ;暂时保存寄存器BP的值 MOV BP,SP MOV AX,WORD PTR [BP+6] ;取第1个参数到AX CMP AX,WORD PTR [BP+4] ;与第2个参数比较 JGE lab MOV AX,WORD PTR [BP+4] ;取第2个参数到AX lab: POP BP ;恢复寄存器BP的原值 RET _max ENDP
6.3 子程序举例 【例】编写子程序write,把整型数据以十进制形式显示到屏幕上。 【分析】参照高级语言中输出语句的功能,write子程序应具备这样一些特点:被显示的整数可以是无符号的,也可以是带符号的,但需要明确指出是哪一种情况;整数在计算机内部是字型数据,范围为-32768~+65535;被输出的数据是带符号数时,负号“-”必须输出,而正号“+”总是省略;输出数据的最大位数是十进制的5位,当计算出5位中的某一位是0时,需要判断这个0是否应该输出,输出条件是前面已经输出过非0数字或者这个0是个位数。输出的数必须是以ASCII码形式存放在DL中。
【解】 ; 功能: 在屏幕上输出整数值 ; 入口: AX =待输出的整数 ; CF=为0表示输出无符号数,为1则输出带符号数 ; 出口: 无 ; 破坏寄存器: 无 ;DX、AX-存放整数,BX-分离各整数位时除数,CX-分离各数位次数, ;SI-表示是否输出过非0数字,DI-暂存输出的整数 write PROC NEAR PUSH BX PUSH CX PUSH DX PUSH SI
PUSH DI MOV SI,0 ;SI清0表示还没有输出过非0数字 MOV DI,AX ;保存待输出的数值到DI中 JNC w1 ;作为无符号数输出转 CMP AX,0 JGE w1 ;AX是正数转 MOV DL, '-' MOV AH,2 INT 21H ;输出负号 NEG DI ;取绝对值放在DI中
w1: MOV BX,10000 ;第一次的除数 MOV CX,5 ;重复次数 w2: MOV AX,DI ;取回待输出数值 MOV DX,0 ;被除数高位清0 DIV BX ;做双字除以字的除法 MOV DI,DX ;余数保存在DI中 CMP AL,0 JNE w3 ;商非0转 CMP SI,0 ;商是0,判断前面是否输出过数字 JNE w3 ;前面已输出过数字,则当前的0应 该输出,转 CMP CX,1 ;判断是否是个位 JNE w4 ;不是个位则不输出当前的0,转
w3: MOV DL,AL ADD DL,30H MOV AH,2 INT 21H ;输出当前这一位数字 MOV SI,1 ;用SI记载已输出过数字 w4: MOV AX,BX MOV DX,0 MOV BX,10 DIV BX MOV BX,AX ;bx / 10 => bx,计算下一次的除数 LOOP w2
POP DI POP SI POP DX POP CX POP BX RET write ENDP
【例6.11】编写子程序read,从键盘上读入一个整数。 【分析】为了尽可能与高级语言中整数输入的情况一致,子程序不仅要能读入正确输入时的数据,还要能对不正确的输入做出适当的反应,因此设计上要注意几个问题:首先是要用字符串输入方式(DOS的10号子功能),因为这种方式支持退格键修改功能,因而需要准备相应的输入缓冲区;出口参数需要两个,以CF的设置表示输入是否正确,当输入正确时把整数值放在AX中作为输入结果;要能够跳过若干个连续的空格符;要能够处理正负号。
【解】 ; 功能: 从键盘读入整数值 ; 入口: CF =为0表示废弃多余符号。 ; 为1则把多余符号留作下一次输入。 ; 出口: CF =0表示正常读入,1表示输入有错 ; 破坏寄存器: 无 read PROC NEAR PUSH BX PUSH CX PUSH DX PUSH SI PUSH DS ;以上为寄存器保护
PUSHF PUSH CS POP DS ;令DS取CS的值 rd1: MOV BX,CS:[point] ;取上次输入后已读取到输入串的位置 rd2: INC BX CMP CS:[bufin+BX+1], ' ' JE rd2 ;跳过空格 CMP CS:[bufin+BX+1],13 JNZ rd4 ;不是回车键,转读入数值处理
rd3: LEA DX,CS:[bufin] MOV AH,10 INT 21H ;遇回车键要求再次输入 MOV AH,2 MOV DL,10 INT 21H ;换行 MOV CS:[point],0 JMP rd1 ;对新的输入再转去跳过前导空格 rd4: MOV SI,BX DEC SI ;令SI指向输入串的第一个有效字符 MOV AX,0 MOV BX,10 MOV CX,0
rd5: CMP CS:[bufin+SI+2], '+' JNZ rd6 ;不是正号转 CMP CL,1 JE rd10 ;已读到正确数值后,遇正号转 CMP CL,0 JE rd8 ;正号是第一个有效字符转 STC ;输入有错 JMP rd13
rd6: CMP CS:[bufin+SI+2], '-' JNZ rd9 CMP CL,1 ;已读到正确数值后,遇负号转 JE rd10 CMP CL,0 JE rd7 ;负号是第一个有效字符转 STC ;输入有错 JMP rd13
rd7: MOV CH,1 ;记下读入的是负数 rd8: MOV CL,2 ;记下已读入正/负号 INC SI ;指向下一字符 JMP rd5 rd9: CMP CS:[bufin+SI+2], '0' JB rd10 ;不是数字转 CMP CS:[bufin+SI+2], '9' JA rd10 ;不是数字转 MUL BX ;已读入的数值×10 MOV DL,CS:[bufin+SI+2] SUB DL,30h MOV DH,0 ADD AX,DX ;乘以10后加上个位数字 MOV CL,1 ;记下已读入正确数值 INC SI ;指向下一字符 JMP rd5
rd10: CMP CL,1 JZ rd11 ;已读入正确数值转 STC ;输入有错 JMP rd13 rd11: CMP CH,1 JNZ rd12 ;已读入的数是正数转 NEG AX ;处理负数 rd12: CLC ;置正确读入标志 rd13: MOV CS:[point],SI ;记下读完后的位置,供下次读入使用 POP BX ;取回进入子程序时入栈保护的 PSW, 送BX PUSHF ;当前的PSW入栈保存 TEST BX,1 ;判断进入子程序时的CF值 JNZ rd14 ;CF为1,保留多余符号转 MOV CS:[bufin+2],13 MOV CS:[point],0
rd14: POPF ;取回入栈保存的PSW POP DS ;以下恢复各寄存器值并返回 POP SI POP DX POP CX POP BX RET bufin DB 128,0,13,127 dup(0) ;键盘输入缓冲区 point DW 0 ; 用于记载下一次的读取位置 read ENDP
本章小结 • 过程的特点是一次定义、多次调用。对过程的合理运用,不仅可以缩短源程序的长度,更重要的是可显著改善程序的结构。 • 具有独立功能的通用性过程可为多个问题所利用。 • 过程的参数传递方法主要有变量、寄存器、堆栈、地址表等,这些方法也可结合使用。