第 6 章 子程序结构

42
第 6 第 第第第第第 第第第第 第第第第第第 第第第第第第 、。 第第第第第第 第第第

Upload: elsa

Post on 19-Jan-2016

81 views

Category:

Documents


0 download

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

Page 1: 第 6 章 子程序结构

第 6 章 子程序结构

讲授要点

子程序的定义、调用与返回。

子程序的参数传递方法。

Page 2: 第 6 章 子程序结构

6.1 子程序概述 6.1.1 过程的定义

过程定义由 PROC 与 ENDP 伪指令实现,形式如下:

过程名 PROC [NEAR|FAR]

< 过程体 >

过程名 ENDP

1. 过程名在整个程序中必须是唯一的。

2. 过程名本质上与标号一样,也具有 3 种属性:段地址、偏移地址和类型( NEAR 或 FAR )。

3. PROC 后用关键字 NEAR 、 FAR 或空,以表示过程的类型(缺省为 NEAR )。

Page 3: 第 6 章 子程序结构

6.1.2 过程调用和返回

1 .过程调用和返回指令

( 1 ) CALL :过程调用

与 JMP 指令类似, CALL 指令包括下列 4 种调用方式: 段内直接调用( Intrasegment/Direct Call ) 段间直接调用( Intersegment/Direct Call ) 段内间接调用( Intrasegment/Indirect Call ) 段间间接调用( Intersegment/Indirect Call )

段内调用在同一代码段内进行,又称近( Near )调用;

段间调用可以在不同代码段之间进行,又称远( Far )调用。

Page 4: 第 6 章 子程序结构

语法格式: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 。

对标志位的影响:无。

Page 5: 第 6 章 子程序结构

( 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 )决定是近返回还是远返回。缺省为近返回。

Page 6: 第 6 章 子程序结构

2 .使用过程应注意的问题 在过程体内必须有一条 RET 指令被执行到。如果在过程内没有执行到 RET 或其它转移指令,程序将继续执行 ENDP 后的指令。 正确选择过程的类型。通常基于下列原则:

• 若过程只在同一代码段中被调用,则定义为 NEAR 。• 若过程可以在不同代码段中被调用,则定义为 FAR 。

通常要保证 RET 指令执行前,栈顶内容正好是返回地址。 注意保护相关寄存器的值。通常,除了作为返回参数的寄存器外,过程不应改变其它寄存器的值。 可以将过程定义放在单独的代码段中。若过程定义与主程序处于同一代码段,则要保证其只有被调用时,才会执行。

Page 7: 第 6 章 子程序结构

3. 保存和恢复寄存器

例 : SUBT PROC NEAR PUSH AX PUSH BX PUSH CX …… POP CX POP BX POP AX RET SUBT ENDP

Page 8: 第 6 章 子程序结构

【例 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

Page 9: 第 6 章 子程序结构

cr PROC ( NEAR )

MOV AH,2

MOV DL,13

INT 21H

MOV DL,10

INT 21H

RET

cr ENDP

Page 10: 第 6 章 子程序结构

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 保存读入的字符

Page 11: 第 6 章 子程序结构

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

Page 12: 第 6 章 子程序结构

【例 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

Page 13: 第 6 章 子程序结构

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

Page 14: 第 6 章 子程序结构

6.2 过程的参数传递

参数的分类: 入口参数:由调用者向过程传递的数据,作为过程的输入参数。 出口参数:由过程向调用者返回的数据,作为过程的输出参数。

根据问题的需要,过程可以只有入口参数或只有出口参数,也可以二者兼有。

对于过程与调用者之间的参数传递,可根据传递的数据量,选择采用寄存器、变量或堆栈等方式。由于过程是相对独立的功能块,因此,在定义过程时,通常要加上适当的注释,主要包括功能、入口参数与出口参数等。

Page 15: 第 6 章 子程序结构

6.2.1 用变量传递参数

在程序中定义全局变量,如放在数据段,过程直接按名访问该变量。 过程直接以变量作为参数,虽然方便,但通用性较差。

【例 6.3 】编写一个子程序,以放在 DS 段中 year 的公元年份为入口参数,判断该年是否为闰年。

另有一个应用程序,它已定义了一个字节型数组 t ,依次存放着 12 个月的每月天数,其中 2月份的天数是 28 。应用程序已经在 DS 段中存放了年份值,利用前面编写的子程序,编写程序段调整数组 t 中 2月份的天数。

Page 16: 第 6 章 子程序结构

【解】 ; 功能:根据一个年份是否为闰年,设置该年 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]

Page 17: 第 6 章 子程序结构

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

Page 18: 第 6 章 子程序结构

CMP DX,0

JZ lab2

lab2: INC BYTE PTR [t+1] ; 是闰年,把天数加 1 ,设置出口参数

lab1 : POP DX

POP CX

POP BX

POP AX

RET

Jud1: ENDP

Page 19: 第 6 章 子程序结构

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 等。

Page 20: 第 6 章 子程序结构

【例 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

Page 21: 第 6 章 子程序结构

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

Page 22: 第 6 章 子程序结构

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

Page 23: 第 6 章 子程序结构

6.2.3 用地址表传递参数

建立一个地址表,存放所有参数的地址,传递地址表的首地址给过程。

这种方法特别适合于参数较多的情况。

Page 24: 第 6 章 子程序结构

6.2.4 用堆栈传递参数 过程从堆栈得到入口参数,返回前将出口参数写入堆栈;调用者通过出栈得到返回参数。 过程从堆栈存取参数时,通常使用 BP ,因为其隐含的段地址在 SS中。 采用堆栈传递参数时,典型的过程结构如下:

StdProc proc near

push bp

mov bp, sp ; BP 指向当前栈顶,用于取入口参数 ... pop bp

ret ParmSize ; 返回前从堆栈移出入口参数StdProc endp

其中, ParmSize 是过程被调用前进栈的入口参数的字节数。

Page 25: 第 6 章 子程序结构

【例 6.5 】用堆栈传递入口参数,编写子程序,把接收的两个带符号整数中大的一个作为结果,出口参数放在 AX 中。

【解】

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

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

; 出口参数: AX

;破坏寄存器: AX

Page 26: 第 6 章 子程序结构

_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

Page 27: 第 6 章 子程序结构

6.3 子程序举例

【例】编写子程序 write ,把整型数据以十进制形式显示到屏幕上。

【分析】参照高级语言中输出语句的功能, write 子程序应具备这样一些特点:被显示的整数可以是无符号的,也可以是带符号的,但需要明确指出是哪一种情况;整数在计算机内部是字型数据,范围为- 32768~ +65535 ;被输出的数据是带符号数时,负号“-”必须输出,而正号“+”总是省略;输出数据的最大位数是十进制的 5 位,当计算出 5 位中的某一位是 0 时,需要判断这个 0 是否应该输出,输出条件是前面已经输出过非 0 数字或者这个 0 是个位数。输出的数必须是以 ASCII 码形式存放在 DL 中。

Page 28: 第 6 章 子程序结构

【解】 ; 功能 : 在屏幕上输出整数值 ; 入口 : AX = 待输出的整数 ; CF= 为 0 表示输出无符号数,为 1 则输出带符号数 ; 出口 : 无 ; 破坏寄存器 : 无 ; DX 、 AX- 存放整数, BX- 分离各整数位时除数, CX- 分离各数位次数, ; SI- 表示是否输出过非 0 数字, DI-暂存输出的整数 write PROC NEAR

PUSH BX

PUSH CX

PUSH DX

PUSH SI

Page 29: 第 6 章 子程序结构

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 中

Page 30: 第 6 章 子程序结构

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 ,转

Page 31: 第 6 章 子程序结构

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

Page 32: 第 6 章 子程序结构

POP DI

POP SI

POP DX

POP CX

POP BX

RET

write ENDP

Page 33: 第 6 章 子程序结构

【例 6.11 】编写子程序 read ,从键盘上读入一个整数。

【分析】为了尽可能与高级语言中整数输入的情况一致,子程序不仅要能读入正确输入时的数据,还要能对不正确的输入做出适当的反应,因此设计上要注意几个问题:首先是要用字符串输入方式(DOS 的 10 号子功能 ) ,因为这种方式支持退格键修改功能,因而需要准备相应的输入缓冲区;出口参数需要两个,以 CF 的设置表示输入是否正确,当输入正确时把整数值放在 AX 中作为输入结果;要能够跳过若干个连续的空格符;要能够处理正负号。

Page 34: 第 6 章 子程序结构

【解】 ; 功能 : 从键盘读入整数值 ; 入口 : CF = 为 0 表示废弃多余符号。 ; 为 1 则把多余符号留作下一次输入。 ; 出口 : CF = 0 表示正常读入, 1 表示输入有错 ; 破坏寄存器 : 无 read PROC NEAR

PUSH BX

PUSH CX

PUSH DX

PUSH SI

PUSH DS ; 以上为寄存器保护

Page 35: 第 6 章 子程序结构

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 ; 不是回车键,转读入数值处理

Page 36: 第 6 章 子程序结构

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

Page 37: 第 6 章 子程序结构

rd5: CMP CS:[bufin+SI+2], '+'

JNZ rd6 ; 不是正号转

CMP CL,1

JE rd10 ; 已读到正确数值后,遇正号转

CMP CL,0

JE rd8 ; 正号是第一个有效字符转

STC ; 输入有错

JMP rd13

Page 38: 第 6 章 子程序结构

rd6: CMP CS:[bufin+SI+2], '-'

JNZ rd9

CMP CL,1 ; 已读到正确数值后,遇负号转

JE rd10

CMP CL,0

JE rd7 ;负号是第一个有效字符转

STC ; 输入有错

JMP rd13

Page 39: 第 6 章 子程序结构

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

Page 40: 第 6 章 子程序结构

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

Page 41: 第 6 章 子程序结构

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

Page 42: 第 6 章 子程序结构

本章小结

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

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

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