第 6 章 子程序结构

Post on 19-Jan-2016

81 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

第 6 章 子程序结构. 讲授要点. 子程序的定义、调用与返回。 子程序的参数传递方法。. 6.1 子程序 概述 6.1.1 过程的定义. 过程定义由 PROC 与 ENDP 伪指令实现,形式如下: 过程名 PROC [NEAR|FAR] < 过程体 > 过程名 ENDP 1. 过程名在整个程序中必须是唯一的。 2. 过程名本质上与标号一样,也具有 3 种属性:段地址、偏移地址和类型( NEAR 或 FAR )。 3. PROC 后用关键字 NEAR 、 FAR 或空,以表示过程的类型(缺省为 NEAR )。. - PowerPoint PPT Presentation

TRANSCRIPT

第 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 中。

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 要求的子程序。 【解】 ;功能:判断一个年份是否为闰年 ;入口: 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 中。

【解】

; 功能:求两个带符号整数中大的一个

; 入口参数:调用前把两个带符号整数入栈

; 出口参数: 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

本章小结

过程的特点是一次定义、多次调用。对过程的合理运用,不仅可以缩短源程序的长度,更重要的是可显著改善程序的结构。

具有独立功能的通用性过程可为多个问题所利用。

过程的参数传递方法主要有变量、寄存器、堆栈、地址表等,这些方法也可结合使用。

top related