《汇编语言(第四版)》 . 王爽著 . 清华大学出版社 . 2019
实验 10 编写子程序
用 call 和 ret 来实现子程序的机制
ret 和 retf 指令
ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移
ret 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移
call 指令
call 指令进行两步操作,先将当前的 IP 或 CS 和 IP 压入栈中,再进行转移,call 指令不能实现短转移
1 2 3 4 5 6
| call word ptr 内存单元地址 call dword ptr 内存单元地址
|
子程序
子程序指一个具有一定功能的程序段,在需要调用子程序时,用 call 指令转去执行,call 指令后面的指令的地址将存储在栈中,在子程序结束时使用 ret 指令,用栈中的数据设置 IP 的值,从而转回 call 指令后面的代码处继续执行
利用 call 和 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 : : mov ax,4c00h int 21h
sub1: : call sub2 : : ret
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 CS
、push IP
、jmp far ptr s
三步,可知在 s 子程序中的两次弹出后的结果,ax=0008H
,bx=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 mov ax,4c00h int 21h s:add ax,ax loop s 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 mov di,16
mov cx,8 s: mov bx,[si] call cube mov [di],ax mov [di].2,dx add si,2 add di,4 loop s
mov ax,4c00h int 21h
cube: mov ax,bx mul bx mul bx ret code ends
end start
|
对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器
批量数据的传递
设计一个子程序,功能:将一个全是字母的字符串转化为大写。
1 2 3 4
| capital: and byte ptr [si],11011111b inc 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 mov cx,12 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 mov word ptr [bp-4],0002 mov word ptr [bp-2],0000 push [bp-2] push [bp-4] push [bp-6] call ADDR add sp,6 inc word ptr [bp-2]
ADDR: push bp mov bp,sp mov ax,[bp+4] add ax,[bp+6] mov [bp+8],ax mov sp,bp pop bp ret
|
可以用一个栈的示意图来帮助我们更好的理解
寄存器冲突的问题 - 问题 10.2
设计一个子程序,功能:将一个全是字母,以 0 结尾的字符串,转化为大写。
程序要处理的字符串以 0 作为结尾符,这个字符串可以如下定义:
设计子程序如下
1 2 3 4 5 6 7 8 9 10 11 12
|
capital: mov cl,[si] mov ch,0 jcxz ok and byte ptr [si],11011111b inc si 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 s: mov si,bx call capital add bx,5 loop s
mov ax,4c00h int 21h
capital: mov cl,[si] 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 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 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
| 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
|
示例
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
|
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
|
示例
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
| ds:si 指向字符串的首地址 dtoc: push ax push di push si push bx push dx push cx
mov bx,10 mov di,0 s: mov dx,0 div bx mov cx,ax add dx,30h push dx inc di jcxz ok inc cx loop s ok: 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
|
示例
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