《汇编语言》- 读书笔记 - 实验 10 编写子程序
- 1. 显示字符串
- 问题
- 子程序描述 show_str
- 提示
- 结果
- 演示
- 2. 解决除法溢出的问题
- 问题
- 子程序描述 divdw
- 提示
- 结果
- 演示
- 3. 数值显示
- 问题
- 子程序描述 dtoc
- 提示
- 结果
- 演示
在这次实验中,我们将要编写3个子程序
,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验一样,这个实验是必须独立完成的,在后面的课程中,将要用到这个实验中编写的3个子程序。
1. 显示字符串
问题
显示字符串是个常用功能,写个子程序来实现此功能。子程序要支持参数:显示的位置(行、列)、内容和颜色。
子程序描述 show_str
名称: | show_str |
---|---|
功能: | 在指定的位置,用指定的颜色,显示一个用0结束的字符串。 |
参数: | (dh ) = 行号(取值范围 0~24),( dl ) = 列号(取值范围 0~79), ( cl ) = 颜色,ds:si 指向字符串的首地址 |
返回: | 无 |
应用举例: | 在屏幕的8 行3 列,用绿色 显示data 段中的字符串 |
提示
- 子程序的入口参数是屏幕上的行号和列号,注意在子程序内部要将它们转化为显存中的地址,首先要分析一下屏幕上的行列位置和显存地址的对应关系;
- 注意保存子程序中用到的相关寄存器;
- 这个子程序的内部处理和显存的结构密切相关,但是向外提供了与显存结构无关的接口。通过调用这个子程序,进行字符串的显示时可以不必了解显存的结构,为编程提供了方便。在实验中,注意体会这种设计思想。
结果
assume cs:code
data segment
db 'Welcome to masm!',0
data ends
code segment
start: mov dh,8 ; 第 8 行
mov dl,3 ; 第 3 列
mov cl,2 ; 颜色
mov ax,data ; 设置数据段
mov ds,ax
mov si,0 ; 指向字符串首地址
call show_str ; 调用子程序
mov ax,4c00h ; 退出
int 21h
; ------------------- 子程序 -------------------
show_str:
push si ; 缓存寄存器
push di
push es
push dx
push cx
push bx
push ax
mov ax,0B800h ; 设置显存段地址
mov es,ax
mov al,160 ; 先算行偏移
dec dh ; 行号从 0 开始,所以这里要先减1
mul dh ; 行数 x 160算出行偏移
mov bx,ax ; 行偏移先存到 bx
mov al,2 ; 再算列偏移
dec dl ; 列号从 0 开始,所以这里要先减1
mul dl
add bx,ax ; 行+列 = 算出目标字符串(显存)的开始位置
mov dl,cl ; 字符属性换个地方存。cx下面要用来循环
mov ch,0 ; cx高位清空(后面我们只改 cl,就方便 jcxz 判断了)
mov di,0 ; 指向目标字符串首字符
sloop: mov cl,ds:[si] ; 取字符
mov es:[bx+di],cl ; 字符写入显存
mov es:[bx+di+1],dl ; 字符属性写入显存
jcxz ok ; 如果为 0 字符结束,跳出循环
inc si ; si 递增,源字符串指向下一字符
add di,2 ; di += 2,目标字符串指向下一字符
jmp short sloop
ok:
pop ax ; 还原寄存器
pop bx
pop cx
pop dx
pop es
pop di
pop si
ret
; ------------------- 子程序 -------------------
code ends
end start
演示
2. 解决除法溢出的问题
问题
div
指令执行除法时:
8位
除法的AL = 商
、AH = 余数
;
16位
除法则分别用AX
和DX
存储商
和余数
。
但若结果商
超出AL
或AX
的最大范围,该如何处理?
比如:
8位
除法:FFFF ÷ 1
,al
中容纳不下商 FFFFh
16位
除法:10000h ÷ 1
,ax
中容纳不下商 1 0000h
为解决这一问题,我们设计一个子程序来专门处理它。
子程序描述 divdw
名称: | divdw |
---|---|
功能: | 进行不会产生溢出的除法运算,被除数为 dword 型,除数为 word 型,结果为dword 型。 |
参数: | (ax ) = dword 型被除数的低 16 位,( dx ) = dword 型被除数的高 16 位, ( cx ) = 除数 |
返回: | (ax ) = 结果的低16位 ,( dx ) = 结果的高16位 , ( cx ) = 余数 |
应用举例: | 计算:1000000/10(F4240H/0AH) 结果: (dx)=0001H, (ax)=86A0H, (cx)=0 |
提示
书中给出的公式:
公式
X/N = int(H/N) * 65536 + [ rem(H/N) * 65536 + L ] / N
X 被除数,范围: [0,FFFFFFFF]
N 除数,范围: [0,FFFF]
H X 高 16 位,范围: [0,FFFF]
L X 低 16 位,范围: [0,FFFF]
int() 描述性运算符,取商,比如, int(38/10)=3
rem() 描述性运算符,取余数,比如, rem(38/10)=8
这个公式将可能产生溢出的除法运算:
XN
,转变为多个不会产生溢出的除法运算。公式中,等号右边的所有除法运算都可以用div
指令来做,肯定不会导致除法溢出。
(关于这个公式的推导,有兴趣的读者请参看附注5。)
结果
assume cs:code
code segment
start: mov ax,4240h ; dword 被除数的低 16位
mov dx,000Fh ; dword 被除数的高 16位
mov cx,0Ah ; word 除数
call divdw ; 调用子程序 dword 类型除法
mov ax,4c00h ; 退出
int 21h
; ------------------- 子程序 -------------------
divdw:
push si ; 缓存寄存器
push bx
mov si,ax ; 因为要用到 ax,所以被除数的低 16位,临时放在 si
mov ax,dx ; 先算高 16 位
mov dx,0 ; 高16补0
div cx ; 除数16位,则被除数32位。商ax,余dx
mov bx,ax ; 临时保存高16位商
mov ax,si ; 后算低 16 位
; 高16位的余数留在 dx不用动。相当于:余数 * 10000 + 低16位
div cx ; 除数16位,则被除数32位。商ax,余dx
mov cx,dx ; 得到余数
mov dx,bx ; 拿回临时保存的高16位商
pop bx ; 还原寄存器
pop si
ret
; ------------------- 子程序 -------------------
code ends
end start
演示
3. 数值显示
问题
编程,将 data
段中的数据以十进制的形式显示出来。
data seqment
dw 123,12666,1,8,3,38
data ends
但是数据在内存中都是二进制信息,
比如 12666
在内存保存的是二进制的0011 0001 0111 1010
等于16进制317A
可见,要将数据用十进制形式显示到屏幕上,要进行两步工作:
- 将用二进制信息存储的数据转变为十进制形式的字符串。(我们需要设计一个子程序)
- 显示十进制形式的字符串。(这个我们可以前面设计 好的
show_str
)
子程序描述 dtoc
名称: | dtoc |
---|---|
功能: | 将 word 型数据转变为表示十进制数的字符串,字符串以0 为结尾符。 |
参数: | (ax ) = word 型数据 ds:si 指向字符串的首地址 |
返回: | 无 |
应用举例 | 编程,将数据12666 以十进制 的形式在屏幕的8行3列 ,用绿色 显示出来。在显示时我们调用本次实验中的第一个子程序 show str 。 |
提示
- 十进制数码字符对应的
ASCII
码 = 十进制数码值
+30H
。
如12666
对应:
1 | 2 | 6 | 6 | 6 |
---|---|---|---|---|
31H | 32H | 36H | 36H | 36H |
- 要求得
12666
的每一位是多少,可以用除法
迭代取余
数:
- 借助
jcxz
指令,用商
是否为0
作为循环结束
的条件。 - 按这个顺序取余得到的是
66621
最后我们还要把它翻转过来才是12666
。
结果
assume cs:code
data segment
db 10 dup (0)
data ends
code segment
start:
mov bx,data ; 设置段地址
mov ds,bx
; 调用子程序 dtoc 将 word 转 10 进制数字
mov ax,12666 ; word 型数据
mov si,0 ; 循环变量从 0 开始
call dtoc
; 调用子程序 show_str 在屏幕 8行3列用绿色显示
mov dh,8
mov dl,3
mov cl,2
call show_str
mov ax,4c00h ; 退出
int 21h
; ------------------- 子程序 dtoc -------------------
dtoc:
push si ; 缓存寄存器
push di
push bx
; 用遍历取余的方式拿到 ascii 拿到的结果是倒的
; 比如 12666 遍历取余拿到的是 66621 所以先丢栈里,方便下一步翻转
mov bx,10
s: mov dx,0 ; 除数 bx 是 16 位,则被除数是32位。高16位 dx 没有数据要补 0
div bx ; 除完后 商在ax下次继续用,dx为余数
add dx,30H ; 将余数转 ascii
push dx ; 入栈备用(此时高8位是无意义的,之后我们只要取低8位用即可 )
inc si ; 循环变量 +1
mov cx,ax ; 取商来判断是否为 0
jcxz s_ok ; 如果商为0跳出循环
jmp s ; 否则继续循环
s_ok:
; 目前栈里是 66621 顺序不是我们想要的,遍历一下出栈到 ds:[0]... 就正了
mov di,0
r: pop ax
mov [di],al ; 写入目标内存地址。第 di 个字符(我们只取低8位有效值)
inc di
dec si
mov cx,si ; 出栈到第 si 个
jcxz r_ok ; 如果 si 为 0 表示出完,结束循环
jmp r ; 否则继续出栈
r_ok:
pop bx ; 还原寄存器
pop di
pop si
ret
; ------------------- 子程序 dtoc -------------------
; ------------------- 子程序 show_str -------------------
show_str:
push si ; 缓存寄存器
push di
push es
push dx
push cx
push bx
push ax
mov ax,0B800h ; 设置显存段地址
mov es,ax
mov al,160 ; 先算行偏移
dec dh ; 行号从 0 开始,所以这里要先减1
mul dh ; 行数 x 160算出行偏移
mov bx,ax ; 行偏移先存到 bx
mov al,2 ; 再算列偏移
dec dl ; 列号从 0 开始,所以这里要先减1
mul dl
add bx,ax ; 行+列 = 算出目标字符串(显存)的开始位置
mov dl,cl ; 字符属性换个地方存。cx下面要用来循环
mov ch,0 ; cx高位清空(后面我们只改 cl,就方便 jcxz 判断了)
mov di,0 ; 指向目标字符串首字符
sloop: mov cl,ds:[si] ; 取字符
mov es:[bx+di],cl ; 字符写入显存
mov es:[bx+di+1],dl ; 字符属性写入显存
jcxz ok ; 如果为 0 字符结束,跳出循环
inc si ; si 递增,源字符串指向下一字符
add di,2 ; di += 2,目标字符串指向下一字符
jmp short sloop
ok:
pop ax ; 还原寄存器
pop bx
pop cx
pop dx
pop es
pop di
pop si
ret
; ------------------- 子程序 show_str -------------------
code ends
end start