0%

《汇编语言》(第四版) 实验 10

《汇编语言(第四版)》 . 王爽著 . 清华大学出版社 . 2019

实验 10 编写子程序

用 call 和 ret 来实现子程序的机制

ret 和 retf 指令

ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移

1
ret                           ;pop IP

ret 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移

1
2
retf                          ;pop IP
;pop CS

call 指令

call 指令进行两步操作,先将当前的 IP 或 CS 和 IP 压入栈中,再进行转移,call 指令不能实现短转移

1
2
3
4
;依据位移进行转移的 call 指令
;16 位位移
call 标号 ;push IP
;jmp near ptr 标号
1
2
3
4
5
;转移的目标地址在指令中的 call 指令
;段间转移
call far ptr 标号 ;push CS
;push IP
;jmp far ptr 标号
1
2
3
;转移地址在寄存器中的 call 指令
call 16 位 reg ;push IP
;jmp 16 位 reg
1
2
3
4
5
6
;转移地址在内存中的 call 指令
call word ptr 内存单元地址 ;push IP
;jmp word ptr 内存单元地址
call dword ptr 内存单元地址 ;push CS
;push IP
;jmp dword ptr 内存单元地址

子程序

子程序指一个具有一定功能的程序段,在需要调用子程序时,用 call 指令转去执行,call 指令后面的指令的地址将存储在栈中,在子程序结束时使用 ret 指令,用栈中的数据设置 IP 的值,从而转回 call 指令后面的代码处继续执行

利用 call 和 ret 指令可以用简捷的方法实现多个相互联系、功能独立的子程序来解决一个复杂的问题,这样就实现了程序的模块化设计

子程序的框架如下:

1
2
3
标号:
指令
ret

具有子程序的源程序的框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assume cs:code
code segment
main:
:
:
call sub1 ;调用子程序 sub1
:
:
mov ax,4c00h
int 21h

sub1: ;子程序 sub1 开始
:
call sub2 ;调用子程序 sub2
:
:
ret ;子程序返回

sub2: ;子程序 sub2 开始
:
:
ret ;子程序返回
code ends
end main

还有两个问题需要考虑:

参数和结果传递的问题
1)使用寄存器传递
2)批量数据放入内存,用寄存器传递内存空间的首地址
3)用栈传递

寄存器冲突的问题
用栈保存寄存器中的内容
编写子程序时使用这样的框架

1
2
3
4
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret, retf)

注意理解两个概念:
被调用者保存寄存器:
被调用者保存这些寄存器的值,保证这些寄存器的值在返回(调用结束)后不变
调用者保存寄存器:
调用者在调用之前负责先将这样的寄存器保存好,在调用过程中这些寄存器可以被随意修改,待调用结束后恢复

检测点 10.1

补全程序,实现从内存 1000:0000 处开始执行指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code

stack segment
db 16 dup (0)
stack ends

code segment

start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000h
push ax
mov ax,0
push ax
retf

code ends

end start

将段地址与偏移地址都压入栈中即可,注意先压段地址再压偏移地址,这样 retf 指令弹出时的顺序才是对的

检测点 10.2

下面的程序执行后,ax 中的数值为多少?

1
2
3
4
5
;内存地址         ;机器码           ;汇编指令
1000:0 b8 00 00 mov ax,0
1000:3 e8 01 00 call s
1000:6 40 inc ax
1000:7 58 s:pop ax

AX=0006H

call 指令会用栈保存其后的第一个字节的偏移地址,在转入标号 s 后执行 pop 指令,将刚刚的偏移地址从栈中弹入 ax 中

检测点 10.3

下面的程序执行后,ax 中的数值为多少?

1
2
3
4
5
6
7
8
;内存地址         ;机器码           ;汇编指令
1000:0 b8 00 00 mov ax,0
1000:3 9A 09 00 00 10 call far ptr s
1000:8 40 inc ax
1000:9 58 s:pop ax
add ax,ax
pop bx
add ax,bx

AX=1010H

call far ptr s 拆解为 push CSpush IPjmp far ptr s 三步,可知在 s 子程序中的两次弹出后的结果,ax=0008Hbx=1000H,最后结果为 1010H

检测点 10.4

下面的程序执行后,ax 中的数值为多少?

1
2
3
4
5
6
;内存地址         ;机器码           ;汇编指令
1000:0 b8 06 00 mov ax,6
1000:3 ff d0 call ax
1000:5 40 inc ax
1000:6 mov bp,sp
add ax,ss:[bp]

AX=000BH

call 指令会用栈保存其后的第一个字节的偏移地址,所以栈顶为 0005H,因为把 sp 中的值赋给了 bp,那么 ss:[bp] 即为栈顶元素,最后 ax=0006H+0005H=000BH

检测点 10.5

(1)

下面的程序执行后,ax 中的数值为多少?(注意:用 call 指令的原理来分析,不要在 Debug 中单步跟踪来验证你的结论。对于此程序,在 Debug 中单步跟踪的结果,不能代表 CPU 的实际执行结果。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code

stack segment
dw 8 dup (0)
stack ends

code segment

start: mov ax,stack
mov ss,ax
mov sp,16
mov ds,ax
mov ax,0
call word ptr ds:[0EH]
inc ax
inc ax
inc ax
mov ax,4c00h
int 21h

code ends

end start

AX=0003H

(2)

下面的程序执行后,ax 和 bx 中的数值为多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
assume cs:code

data segment
dw 8 dup (0)
data ends

code segment

start: mov ax,data
mov ss,ax
mov sp,16
mov word ptr ss:[0],offset s
mov ss:[2],cs
call dword ptr ss:[0]
nop

s: mov ax,offset s
sub ax,ss:[0ch]
mov bx,cs
sub bx,ss:[0eh]

mov ax,4c00h
int 21h

code ends

end start

AX=0001H, BX=0000H

这两题都可以通过画栈的示意图来得出答案,非常简单明了

问题 10.1

call 和 ret 的配合使用

下面程序返回前,bx 中的值是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code
code segment
start:mov ax,1
mov cx,3
call s
mov bx,ax ;(bx)=? bx=8
mov ax,4c00h
int 21h
s:add ax,ax
loop s
ret ;call 与 ret 的配合使用
code ends
end start

BX=0008H

程序返回前,(bx)=8。可以看出,从标号 s 到 ret 的程序段的作用是计算 2 的 N 次方,计算前,N 的值由 cx 提供

参数和结果传递的问题

编程,计算 data 段中第一组数据的 3 次方,结果保存在后面一组 dword 单元中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
assume cs:code

data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends

code segment

start: mov ax,data
mov ds,ax
mov si,0 ;ds:si 指向第一组 word 单元
mov di,16 ;ds:di 指向第二组 dword 单元

mov cx,8
s: mov bx,[si]
call cube
mov [di],ax
mov [di].2,dx
add si,2 ;ds:si 指向下一个 word 单元
add di,4 ;ds:di 指向下一个 dword 单元
loop s

mov ax,4c00h
int 21h

;说明:计算 N 的 3 次方
;参数:(bx)=N
;结果:(dx:ax)=N^3
cube: mov ax,bx
mul bx ;另一个乘数默认在 ax 中,进行的是 16 位的乘法
mul bx ;结果高位存放在 dx 中,低位存放在 ax 中
ret
code ends

end start

对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器

批量数据的传递

设计一个子程序,功能:将一个全是字母的字符串转化为大写。

1
2
3
4
capital:    and byte ptr [si],11011111b   ;将 ds:si 所指单元中的字母转化为大写
inc si ;ds:si 指向下一单元
loop capital
ret

编程,将 data 段中的字符串转化为大写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
assume cs:code

data segment
db 'conversation'
data ends

code segment

start: mov ax,data
mov ds,ax

mov si,0 ;ds:si 指向字符串(批量数据)所有空间的首地址
;将字符串在内存中的首地址放在寄存器中传递给子程序
mov cx,12 ;设置 cx 为字符串的长度
call capital
mov ax,4c00h
int 21h

;子程序需要知道字符串的内容和字符串的长度
capital: and byte ptr [si],11011111b
inc si
loop capital
ret

code ends

end start

栈在参数传递中的应用

通过一个 C 语言程序编译后的汇编语言程序,看一下栈在参数传递中的应用。
要注意的是,在 C 语言中,局部变量也在栈中存储。

C 程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void add(int, int, int);

main()
{
int a = 1;
int b = 2;
int c = 0;
add(a,b,c); ;该函数不改变 c(main) 的值
c++; ;c=1
}

void add(int a, int b, int c)
{
c = a + b;
}

编译后的汇编程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mov bp,sp
sub sp,6
mov word ptr [bp-6],0001 ;int a = 1;
mov word ptr [bp-4],0002 ;int b = 2;
mov word ptr [bp-2],0000 ;int c = 0;
push [bp-2]
push [bp-4]
push [bp-6] ;参数传递
call ADDR ;调用函数
add sp,6 ;释放函数中局部变量的空间
inc word ptr [bp-2] ;c++;

ADDR: push bp
mov bp,sp
mov ax,[bp+4]
add ax,[bp+6]
mov [bp+8],ax ;c=a+b=4;
mov sp,bp
pop bp
ret

可以用一个栈的示意图来帮助我们更好的理解

寄存器冲突的问题 - 问题 10.2

设计一个子程序,功能:将一个全是字母,以 0 结尾的字符串,转化为大写。

程序要处理的字符串以 0 作为结尾符,这个字符串可以如下定义:

1
db 'conversation',0

设计子程序如下

1
2
3
4
5
6
7
8
9
10
11
12
;说明:将一个全是字母,以 0 结尾的字符串,转化为大写
;参数:ds:si 指向字符串的首地址
;结果:没有返回值

capital: mov cl,[si]
mov ch,0
jcxz ok ;如果 (cx)=0,结束;如果不是 0,处理
and byte ptr [si],11011111b ;将 ds:si 所指单元中的字母转化为大写
inc si ;ds:di 指向下一个单元
jmp short capital

ok: ret

设计程序,将 data 段中的字符串全部转化为大写

利用循环,重复调用子程序 capital,完成对 4 个字符串的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
assume cs:code

data segment
db 'word',0
db 'unix',0
db 'wind',0
db 'good',0
data ends

code segment

start: mov ax,data
mov ds,ax
mov bx,0

mov cx,4 ;循环要使用 cx
s: mov si,bx
call capital
add bx,5
loop s

mov ax,4c00h
int 21h

capital: mov cl,[si] ;判断语句要使用 cx
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital

ok: ret

code ends

end start

不幸的是,这个程序在 cx 的使用上存在冲突

解决方法:
在子程序的开始将子程序中所有用到的寄存器中的内容都用栈保存起来,在子程序返回前再恢复

将子程序 capital 进行改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
capital:    push cx                       ;将 cx 入栈保存
push si

change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital

ok: pop si
pop cx ;cx 弹出
ret

注意寄存器出栈和入栈的顺序

实验任务

编写三个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。

1. 显示字符串

子程序描述
子程序编写

1)分析屏幕上的行列位置与显存地址的对应关系
2)注意保存子程序中用到的相关寄存器
3)层层抽象的设计思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
;参数:(dh)=行号,(dl)=列号,(cl)=颜色,ds:si 指向字符串的首地址
show_str: push dx ;保存子程序中要使用的寄存器
push cx
push si

mov bl,dl ;取列号(0~79)
mov bh,0
add bx,bx ;算列偏移

mov al,dh ;取行号(0~24)
mov ah,160
mul ah
mov di,ax ;算行偏移

mov ax,0b800h
mov es,ax ;es 中存储显示缓冲区段地址

mov ah,cl ;取属性字节
s: mov al,ds:[si] ;取字符 ASCII 码

mov cl,al
mov ch,0

jcxz ok ;判断是否为结束符 0

mov es:[di+bx],ax ;送入显示缓冲区相应位置
add bx,2
inc si
loop s

ok: pop si ;恢复寄存器的值
pop cx
pop dx
ret ;子程序结束,返回调用者
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
assume cs:code
data segment
db 'welcome to masm!',0
data ends

code segment
start: mov dh,8
mov dl,3
mov cl,2
mov ax,data
mov ds,ax
mov si,0
call show_str

mov ax,4c00h
int 21h

show_str: push dx
push cx
push si

mov bl,dl
mov bh,0
add bx,bx

mov al,dh
mov ah,160
mul ah
mov di,ax

mov ax,0b800h
mov es,ax

mov ah,cl
s: mov al,ds:[si]

mov cl,al
mov ch,0

jcxz ok

mov es:[di+bx],ax
add bx,2
inc si
loop s

ok: pop si
pop cx
pop dx
ret
code ends
end start
用 Debug 跟踪程序运行

2. 解决除法溢出的问题

子程序描述
子程序编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;参数:(ax)=dword 型数据的低 16 位
; (dx)=dword 型数据的高 16 位
; (cx)=除数
;返回:(dx)=结果的高 16 位,(ax)=结果的低 16 位
; (cx)=余数
divdw: push ax ;高 16 位计算过程
mov ax,dx ;将被除数的高 16 位补零成 32 位放入 dx、ax 中
mov dx,0
div cx ;H/N, dx = rem(H/N), ax = int(H/N)
mov bx,ax ;将(最终结果的)商的前 16 位保存

pop ax ;低 16 位计算过程
div cx ;[rem(H/N)*65536+L]/N
mov cx,dx ;将余数放入 cx

mov dx,bx
ret
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
assume cs:code,ss:stack
stack segment
dw 8 dup (0)
stack ends

code segment
start: mov ax,stack
mov ss,ax
mov sp,16

mov ax,4240h
mov dx,000fh
mov cx,0ah
call divdw

mov ax,4c00h
int 21h

divdw: push ax
mov ax,dx
mov dx,0
div cx
mov bx,ax

pop ax
div cx
mov cx,dx

mov dx,bx
ret
code ends
end start
用 Debug 跟踪程序运行

3. 数值显示

子程序描述
子程序编写

用循环除 10 的方法,每次取余数得到数据从低位到高位的每一位数字,并检查商是否为 0,若为 0 则跳出循环,注意,数字加 30H 即为其 ASCII 码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
;参数:(ax)=word 型数据
ds:si 指向字符串的首地址
dtoc: push ax ;保存子程序中要使用的寄存器
push di
push si
push bx
push dx
push cx

mov bx,10 ;除数为 10
mov di,0
s: mov dx,0 ;向 dx 补零
div bx ;循环除 10
mov cx,ax ;取商
add dx,30h ;取余数,加 30H 成为 ASCII 码
push dx ;入栈保存
inc di ;记录长度
jcxz ok ;商为 0,结束循环
inc cx ;这里可以直接用 jmp short s
loop s

ok: mov cx,di ;长度为之后的循环次数

s1: pop ds:[si] ;弹出,放入内存
inc si
loop s1
mov byte ptr ds:[si],0 ;放置结束符 0

pop cx ;恢复寄存器的值
pop dx
pop bx
pop si
pop di
pop ax
ret
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
assume cs:code,ds:data
data segment
db 10 dup (0)
data ends

code segment
start: mov ax,12666
mov bx,data
mov ds,bx
mov si,0
call dtoc

mov dh,8
mov dl,3
mov cl,2
call show_str

mov ax,4c00h
int 21h

dtoc: push ax
push di
push si
push bx
push dx
push cx

mov bx,10
mov di,0
rem: mov dx,0
div bx
mov cx,ax
add dx,30h
push dx
inc di
jcxz ok1
inc cx
loop rem

ok1: mov cx,di

s1: pop ds:[si]
inc si
loop s1
mov byte ptr ds:[si],0

pop cx
pop dx
pop bx
pop si
pop di
pop ax
ret

show_str: push dx
push cx
push si

mov bl,dl
mov bh,0
add bx,bx

mov al,dh
mov ah,160
mul ah
mov di,ax

mov ax,0b800h
mov es,ax

mov ah,cl
s: mov al,ds:[si]

mov cl,al
mov ch,0

jcxz ok

mov es:[di+bx],ax
add bx,2
inc si
loop s

ok: pop si
pop cx
pop dx
ret
code ends
end start
用 Debug 跟踪程序运行


汇编语言实验合集

汇编语言实验合集

实验 1 查看 CPU 和内存,用机器指令和汇编指令编程
实验 2 用机器指令和汇编指令编程
实验 3 编程、编译、连接、跟踪
实验 4 [bx] 和 loop 的使用
实验 5 编写、调试具有多个段的程序
实验 6 实践课程中的程序
实验 7 寻址方式在结构化数据访问中的应用
实验 8 分析一个奇怪的程序
实验 9 根据材料编程
实验 10 编写子程序
课程设计 1
实验 11 编写子程序
实验 12 编写 0 号中断的处理程序
实验 13 编写、应用中断例程
实验 14 访问 CMOS RAM
实验 15 安装新的 int9 中断例程
实验 16 编写包含多个功能子程序的中断例程
实验 17 编写包含多个功能子程序的中断例程
课程设计 2