实验 10 编写子程序
本次实验将编写 3 个子程序。
一、显示字符串
这一子程序是个通用程序,用于实现显示字符串。程序提供灵活的调用接口,使调用者可以决定显示的位置(行,列)、内容和颜色。
子程序描述:
1. 名称:show_str
2. 功能:在指定的位置,用指定的颜色,显示一个用 0 结束的字符串。
3. 参数:(dh)=行号(取值范围 0~24),(dl)=列号(取值范围 0~79),(cl)=颜色
ds:si 指向字符串的首地址
4. 返回:无
5. 应用举例:在屏幕的 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 ; 字体颜色:绿色 —— 属性编码 00000010b,即 02h
mov ax,data
mov ds,ax
mov si,0
call show_str
mov ax,4c00h
int 21h
show_str: ...
...
code ends
end start
6. 提示:
(1) 子程序的入口参数是屏幕上的行号和列号。注意在子程序内部要将它们转化为显存中的地址,首先要分析一下屏幕上的行列位置和显存地址之间对应关系。
由于显示缓冲区第 0 页的地址为 B8000h~B8F9Fh,而一行显示 80 个字符,每个字符占用 2 个字节,可见第 1~7 行的占用的地址空间为 7*80*2=1120(460h)字节,因此第 8 行的起始地址偏移量为 460h,即起始地址为 B8460h。由于一个字符占用 2 个字节,可见第 1~2 列占用了 4 个字节,即 0~3 字节,因此第 3 列将占用 4~5 字节,所以第 3 列的偏移量为 04h~05h。于是第 8 行第 3 列的起始地址就是:B8460h+04h=B8464h。
字符串字体颜色为绿色,所以其属性数据为 00000010b,也就是 02h。
(2) 注意在子程序开始处要保存子程序中用到的相关寄存器所存储的数据,并在子程序返回时进行数据恢复。
(3) 这个子程序的内部处理和显存结构之间密切相关,但向外提供了与显存结构无关的接口。调用子程序进行字符串显示时,可以不必了解显存的结构,这一做法为编程提供了方便。在实验中,注意体会这种设计思想。
本实验程序代码在 Ubuntu 18.04 的 DOSBox-X 中运行时,不能实现改变显示缓冲区内存地址数据,从而显示指定信息内容的目的,但编译、链接和运行都不会报错,原因不明。在 Ubuntu 的 DOSBox 及 Windows 10 的 DOSBox-X 中都能正常运行。这些情况与实验 9 出现的情况相同。
7. 具体程序代码如下:
(1) 方法 1
只将子程序中用到的 DX、CX 寄存器中的 DX 寄存器所存储的数据压栈,CX 寄存器存储的数据未压栈。
; 实验 10 第 1 个子程序:显示字符串
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 ; 字体颜色:绿色 —— 属性码 00000010b,即 02h
mov ax,data
mov ds,ax
mov si,0
call show_str
mov ax,4c00h
int 21h
show_str: push dx ; DX 寄存器中存储着需要显示字符串的屏幕行、列信息,将这些信息压入栈中
; 将显示缓冲区第 0 页的段地址存入 ES 段寄存器中
mov ax,0B800h
mov es,ax
; 计算列偏移量,结果存储在 DI 寄存器中
dec dl
mov ax,2
mul dl
mov di,ax
; 计算行偏移量,将计算结果加到 DI 寄存器中
dec dh
mov ax,160
mul dh
add di,ax ; DI 寄存器中存储着显示缓冲区目标偏移地址
mov ah,cl ; 将属性编码存入 AH 寄存器中
mov bx,0
dis: mov cl,ds:[si]
mov ch,0
jcxz ok
mov al,cl ; 将字符 ASCII 码存入 AL 寄存器中
mov word ptr es:[bx+di],ax
inc si
add bx,2
jmp short dis
ok: pop dx ; 恢复需要显示的字符串所在屏幕的行、列信息数据
ret
code ends
end start
(2) 方法 2
将子程序中用到的两个寄存器 DX、CX 所存储的数据都进行压栈。
; 实验 10 第 1 个子程序:显示字符串
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 ; 字体颜色:绿色 —— 属性码 00000010b,即 02h
mov ax,data
mov ds,ax
mov si,0
call show_str
mov ax,4c00h
int 21h
show_str: push dx ; DX 寄存器中存储着需要显示字符串的屏幕行、列信息,将这些信息压入栈中
; 将显示缓冲区第 0 页的段地址存入 ES 段寄存器中
mov ax,0B800h
mov es,ax
mov bp,sp
mov bx,word ptr ss:[bp]
; 计算列偏移量,结果存储在 DI 寄存器中
dec bl
mov ax,2
mul bl
mov di,ax
; 计算行偏移量,将计算结果加到 DI 寄存器中
dec bh
mov ax,160
mul bh
add di,ax ; DI 寄存器中存储着显示缓冲区目标偏移地址
mov ch,0
push cx ; CX 寄存器中存储着需要显示字符串的属性信息,将这些信息压入栈中
mov ah,cl ; 将属性编码存入 AH 寄存器中
mov bx,0
dis: mov cl,ds:[si]
jcxz ok
mov al,cl ; 将字符 ASCII 码存入 AL 寄存器中
mov word ptr es:[bx+di],ax
inc si
add bx,2
jmp short dis
ok: pop cx ; 恢复需要显示的字符串属性信息数据
pop dx ; 恢复需要显示的字符串所在屏幕的行、列信息数据
ret
code ends
end start
二、解决除法溢出的问题
8.7 节已学习,div 指令可做除法。当进行 8 位除法时,用 al 存储结果的商,ah 存储结果的余数;进行 16 位除法时,用 ax 存储结果的商,dx 存储结果的余数。但如果结果的商大于 al 或 ax 所能存储的最大值,那么将如何?
比如下面的程序段:
mov bh,1
mov ax,1000
div bh
进行的是 8 位除法,结果的商为 1000,而 1000 在 al 中放不下。
又比如下面的程序段:
mov ax,1000h
mov dx,1
mov bx,1
div bx
进行的是 16 位除法,结果的商为 11000h,而 11000h 在 ax 中存放不下。
在用 div 指令做除法时,很可能发生上面的情况:结果的商过大,超出了寄存器所能存储的范围。当 CPU 执行 div 等除法指令时,如果发生上述情况,将引发 CPU 的一个内部错误,这个错误被称为“除法溢出”。可以通过特殊的程序来处理这个错误,但这里不讨论这个错误的处理,这在后面课程中学习。
8 位(8 bits)数值的范围是 0~255 共 256 个数字(有符号数范围是 -128~127),16 位(16 bits)数值的范围是 0~65535 共 65536 个数字(有符号数范围是 -32768~32767)。注意,余数总是小于除数的。
以下显示了除法溢出发生时的一些现象:
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B39 ES=0B39 SS=0B39 CS=0B39 IP=0100
0B39:0100 B80010 MOV AX,1000
-t
AX=1000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B39 ES=0B39 SS=0B39 CS=0B39 IP=0103
0B39:0103 BA0100 MOV DX,0001
-t
AX=1000 BX=0000 CX=0000 DX=0001 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B39 ES=0B39 SS=0B39 CS=0B39 IP=0106
0B39:0106 BB0100 MOV BX,0001
-t
AX=1000 BX=0001 CX=0000 DX=0001 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B39 ES=0B39 SS=0B39 CS=0B39 IP=0109
0B39:0109 F7F3 DIV BX
-t
Divide overflow
D:\>
以上展示了在 Windows 2000 中使用 Debug 执行相关程序段的结果,div 指令引发了 CPU 的除法溢出,系统对其进行了相关的处理。
既然 div 指令执行时可能产生除法溢出,那么在除法运算时就要注意除数和被除数的值,比如 1000000÷10 就不能用 div 指令来计算。那该怎么办呢?用下面的子程序 divdw 解决。
子程序描述
1. 名称:divdw
2. 功能:进行不会产生溢出的除法运算,被除数为 dword 型,除数为 word 型,结果为 dword 型。
3. 参数:(ax) = dword 型数据(被除数)的低 16 位
(dx) = dword 型数据(被除数)的高 16 位
(cx) = 除数
4. 返回:(dx) = 商的高 16 位,(ax) = 商的低 16 位
(cx) = 余数
注意,进行 16 位除法运算时,默认情况下被除数为 32 位,其中高 16 位存放于 DX 寄存器,低 16 位存放于 AX 在寄存器,除数存放于 reg 或某个内存字单元中(以上与 divdw 子程序的参数基本一致);计算的结果则是,商存放于 AX 寄存器,余数存放于 DX 寄存器(这些则与 divdw 子程序的返回情况不)。
5. 应用举例:计算 1000000÷10(F4240h÷0Ah)
mov ax,4240h
mov dx,000Fh
mov cx,0Ah
call divdw
6. 结果:(dx) = 0001h,(ax) = 86A0h,(cx) = 0
7. 提示:
给出一个公式:
X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
(1) X:被除数,范围:[0,FFFFFFFF]
(2) N:除数,范围:[0,FFFF]
(3) H:X 高 16 位,范围:[0,FFFF]
(4) L:X 低 16 位,范围:[0,FFFF]
(5) int():描述性运算符,取商,比如,int(38/10)=3
(6) rem():描述性运算符,取余数,比如,rem(38/10)=8
这个公式将可能产生溢出的除法运算 X/N 转变为多个不会产生溢出的除法运算。公式中,等号右边的所有除法运算都可以用 div 指令来实现,肯定不会导致除法溢出。
(关于这个公式的推导,有兴趣的读者请参看书本附注 5。)
也就是说,由于直接使用 div 指令执行会产生“除法溢出”的除法运算时会导致 CPU 产生“除法溢出”错误,所以需要通过间接使用 div 指令的方法来处理;在这种方法中使用 div 指令所执行的除法运算,任何情况下都不会产生“除法溢出”。“这种方法”就是调用上述的 divdw 子程序。
注意,只要被除数足够小,则无论除数是多少,只要除数大于等于 1,则商数就不可能大于被除数,所以用与存放被除数同样大小容量的寄存器来存放商数,就不可能产生“除法溢出”;另外,存储商数的寄存器容量只要足够大,也不会产生“除法溢出”。这就是 divdw 子程序设计的关键所在 —— 将原被除数分解成高 16 位和低 16 位(div 指令进行 16 位除法运算时,商被存放在 16 位寄存器 AX 中,原 32 位的被除数已被分解为 16 位,即用于存放商的寄存器容量与存放被除数的存储空间都是 16 位,因此不会产生“除法溢出”);然后逐次降级,循环调用,直至不会产生“除法溢出”为止。下面以十进制除法 100100÷3 为例,用这种设计思想进行分析如下:
被除数 100100 被平等分解为高 3 位数值 100 和低 3 位数值 500,分别与原除数 3 进行除法运算:
(1) (100 ÷ 3) х 1000 = (33) х 1000 = 33000
商数为 33000,余数为 1,该余数将加入低 3 位数值的除法运算当中。
注意,取 100 ÷ 3 的商数与 1000 相乘而得到高 3 位除法运算的商数 33000,因为被除数是原被除数的高3 位。为什么这里与高 3 位商数相乘的是 1000 而不是别的数值?因为高 3 位的最小值 1,就是实际原被除数的值 1000。
(2) (1 х 1000 + 100) ÷ 3 = 366
商数为 366,余数为 2。
注意高 3 位余数乘以 1000 后的值与低 3 位被除数相加,才构成实际的低 3 位被除数的真实值,这只需对原被除数与除数的除法运算进行模似计算就能验证。为什么与高 3 位余数相乘的是 1000 而不是别的数值?这与高 3 位商数相乘的数为 1000 的原因相同,都是因为高 3 位的最小值 1 就是实际原被除数的值 1000。
(3) 综合上述两项可知,100100 ÷ 3 的商数是高 3 位除法运算实际商数与低 3 位除法运算实际商数之和,而余数为低 3 位除法运算的余数;即商数为 33000+366=33366,余数为 2。
将此十进制除法运算的分解规则应用到十六进制,就构成了上述的公式。
8. 具体程序代码如下:
; 实验 10:解决除法溢出问题
; 计算 1000000÷10(F4240h÷0Ah)
assume cs:code
code segment
start: mov ax,4240h
mov dx,000Fh
mov cx,0Ah
call divdw
mov ax,4c00h
int 21h
divdw: push ax
push dx
push cx
; 计算高 16 位被除数与原 16 位除数的除法运算
mov bp,sp ; 当前 ss:sp 指向除数,本行代码使得 ss:[bp] 指向除数
mov ax,[bp+2] ; ss:[bp+2] 指向被除数的高 16 位
mov dx,0
div word ptr [bp] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的高 16 位),余数存放于 DX 寄存器
mov bx,ax ; 将高 16 位除法运算的商(商的高 16位)转存入 BX 寄存器
; 计算低 16 位被除数与原 16 位除数的除法运算
mov ax,[bp+4] ; ss:[bp+4] 指向被除数的低 16 位
div word ptr [bp] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的低 16 位),余数存放于 DX 寄存器
mov cx,dx ; 将最后结果余数转存入 CX 寄存器
mov dx,bx ; 将商的高 16 位转存入 DX 寄存器
pop cx
pop dx
pop ax
ret
code ends
end start
说明:说明:在 divdw 子程序中,当进行第一次 div 运算时,(dx)=0;也就是说,被除数的高 16 位值为 0。这说明被除数实际大小可以用 16 位寄存器来存储,而除法运算的结果,商是不会大于被除数的,因此所得的商数存储于 AX 寄存器是不会发生溢出的。当进行第二次 div 运算时,DX 寄存器默认已经存储着第一次 div 运算结果产生的余数,而这个余数在第二次 div 运算时本来就应该充当被除数的高 16 位 —— 被除数的高 16 位本来就应该存放在 DX 寄存器中。现在的问题在于,第二次 div 运算的被除数高 16 位如果不为 0(也就是第一次除法运算产生的余数如果不为 0),那么会不会产生除法溢出错误呢?(即第二次 div 运算得到的商数存储于 16 位的 AX 寄存器中,会不会产生溢出?)
16 位寄存器能存储的最大无符号数为 FFFFh,假设除数为 Nh(也存储于 16 位寄存器中),第一次 div 运算所得余数为 Mh,由于余数必定小于除数,则 Mh<Nh,再假设原被除数低 16 位的值是 Lh,则第二次 div 运算即为 :(Mh×(FFFFh+1)+Lh)÷Nh。由于 16 位寄存器能存储的最大值是 FFFFh,则第二次 div 运算,必有:
(Mh×(FFFFh+1)+Lh)÷Nh < (Mh×(FFFFh+1)+FFFFh)÷Nh
因为 Mh < Nh,所以:
Mh ≤ Nh-1
Mh×(FFFFh+1) ≤ (Nh-1)×(FFFFh+1)
Mh×(FFFFh+1)+FFFFh ≤ (Nh-1)×(FFFFh+1)+FFFFh = Nh×(FFFFh+1)-FFFFh-1+FFFFh = Nh×(FFFFh+1)-1
(Mh×(FFFFh+1)+FFFFh)÷Nh ≤ (Nh×(FFFFh+1)-1)÷Nh = FFFFh+1-1/Nh 因此:
(Mh×(FFFFh+1)+Lh)÷Nh < (Mh×(FFFFh+1)+FFFFh)÷Nh ≤ FFFFh+1-1/Nh 即:
(Mh×(FFFFh+1)+Lh)÷Nh < FFFFh+1-1/Nh
可见,第二次 div 运算所得的商数必定能存储于最大存储容量为 FFFFh 的 16 位寄存器中。
注:以上推导过程依据于书本附注 5 的公式证明。
三、数值显示
编程,将下述 data 段中的数据以十进制的形式显示出来:
data segment
dw 123,12666,1,8,3,38
data ends
这些数据在内存中都是二进制信息,标记了数值的大小。要把它们显示到屏幕上,成为能够读懂的信息,需要进行信息的转化。比如,数值 12666 在机器中存储为二进制信息 0011000101111010B(317AH),计算机可以理解它。而要在显示器上读到可理解的数值 12666,则应该显示为一串字符“12666”。由于显卡遵循的是 ASCII 编码,为了让能在显示器上看到这串字符,它在机器中应以 ASCII 码的形式存储为 31H、32H、36H、36H、36H(字符“0”~“9”对应的 ASCII 码为 30H~39H)。
经上述分析可知,在概念世界中的抽象数据 12666,它表示了一个数值的大小。在现实世界中它有多种表示形式,可在电子机器中以高低电平(二进制)的形式存储,也可在纸上、黑板上、屏幕上以人类的语言“12666”来书写。现在面临的问题就是,要将同一抽象的数据,从一种表示形式(计算机的存储形式)转化为另一种表示形式(由屏幕显示出来的人类语言形式)。
可见,要将数据用十进制形式显示到屏幕上,要进行两步工作:
(1) 将二进制信息存储的数据转变为十进制形式的字符串。
(2) 显示十进制形式的字符串。
第二步步骤在本次实验的第一个子程序中己经实现,只要调用 show_str 即可。重点讨论第一步,将二进制息转变为十进制形式的字符串是经常要用到的功能,因此应该为它编写一个通用的子程序。
子程序描述:
1. 名称:dtoc
名称 dtoc 就是 digit to character 的缩写,即将数字(digit)转换为(to)字符(character)。
2. 功能:将 word 型数据转变为表示十进制数的字符串,字符串以 0 作为结束符。
3. 参数:(ax) = word 型数据
ds:si 指向字符串的首地址。
4. 返回:无
5. 应用举例:编程,将数据 12666 以十进制的形式在屏幕的 8 行 3 列,用绿色显示出来。在显示时需调用本次实验中的第一个子程序 show_str。
assume cs:code
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
...
...
code ends
end start
6. 提示:
下面对这个问题进行一下简单的分析。
(1) 要得到字符串“12666”,就是要得到一列表示该字符串的 ASCII 码 31H、32H、36H、36H、36H。
十进制数码字符对应的 ASCII 码等于十进制数码值加 30H。
要得到表示十进制数的字符串,先求十进制数每位的值。
例:对于 12666,先求得每位的值:1、2、6、6、6。再将这些数分别加上 30H,便得到了表示 12666 的 ASCII 码串:31H、32H、36H、36H、36H。
(2) 那么,怎样得到每位的值呢?采用下面“除以数值 10,求其余数”的方法:
余数
10 | 12666 6
┗-------
10 | 1266 6
┗------
10 | 126 6
┗-----
10 | 12 2
┗----
10 | 1 1
┗---
0
可见,用 10 除 12666,共除 5 次,记下每次的余数,就得到了每位的值。
(3) 综合以上分析,设计处理过程如下:
用 12666 除以 10,循环 5 次,记下每次的余数:将每次的余数分别加 30H 便得到表示十进制数的 ASCII 码串,如下所示:
余数 +30H ASCII码串 字符串
10 | 12666 6 36H '6'
┗-------
10 | 1266 6 36H '6'
┗------
10 | 126 6 36H '6'
┗-----
10 | 12 2 32H '2'
┗----
10 | 1 1 31H '1'
┗---
0
(4) 对 (3) 的质疑
在己知数据是 12666 的情况下,可预先知道需要进行 5 次循环;但在实际问题中,数据的值是多少程序员并不能预先知道,也就是说,程序员不能事先确定循环次数。那么,如何确定数据各位的值是否己经全部执行了求余数的运算呢?观察上面的计算过程可知,只要是得到的商数为 0,则说明各位的值己经全部执行完毕;可以使用 jcxz 指令来实现相关的功能 —— 通过 jcxz 指令确定商数是否为 0。
(5) 特别注意,由于需要经过除法运算来取得“余数”,所以必然存在“除法溢出”问题(也就是本实验的第 2 个子程序 divdw 所解决的问题),因此有必要在设计时考虑将 divdw 子程序代码引入本程序之中。
7. 具体程序代码如下:
(1) 以 idata 方式给定一个需要显示的数值数据
; 实验 10 第 3 个子程序:显示值
; 本实验程序代码在 Ubuntu 18.04 的 DOSBox-X 中运行时,不能改变显示缓冲区内存地址数据、
; 以显示指定信息的内容,但编译、链接和运行都不会报错,原因不明。在 Ubuntu 的 DOSBox 及 Windows 10
; 的 DOSBox-X 中都能正常运行。这些情况与实验 9 出现的情况相同。
assume cs:code
data segment
db 10 dup (0)
data ends
code segment
start: mov ax,12366
mov bx,data
mov ds,bx
mov si,0
call dtoc ; 将存储在 AX 寄存器的数据转换为能显示在屏幕上的字符串数字,这些字符串数字的 ASCII 码被倒序存入 data 段空间
call dxpl ; 将倒序存储的数字字符串按原数字顺序重新存入 data 段空间
mov dh,8 ; 行:8
mov dl,3 ; 列:3
mov cl,2 ; 字体颜色:绿色 —— 属性码 00000010b,即 02h
call show_str
mov ax,4c00h
int 21h
dtoc: mov dx,0000h ; 将被除数的高 16 位存放到 DX 寄存器
push dx ; 将被除数的高 16 位压入栈中
push ax ; 将被除数的低 16 位压入栈中
mov bp,sp
mov di,0 ; ds:[di] 指向 data 段首地址
jd: mov cx,[bp] ; ss:[bp] 指向被除数的低 16 位
mov ax,cx ; 将被除数的低 16 位传送到 AX 寄存器
mov dx,[bp+2] ; ss:[bp+2] 指向被除数的高 16 位,将其传送到 DX 寄存器
jcxz md ; 检测被除数的低 16 位是否为 0
dvd: mov cx,0Ah ; 将除数存放到 CX 寄存器
call divdw
jmp jd
md: mov cx,[bp+2] ; ss:[bp+2] 指向被除数的高 16 位
jcxz ok1 ; 检测被除数的高 16 位是否为 0
jmp dvd
ok1: pop ax
pop dx
ret
divdw: push bp
push cx
; 计算高 16 位被除数与原 16 位除数的除法运算
mov bp,sp ; 当前 ss:sp 指向除数,本行代码使得 ss:[bp] 指向除数
mov bx,[bp+2] ; 将主调程序中的 (bp) 传送到 BX 寄存器,以便通过 [bx+idata] 指向栈中存储的被除数高、低 16 位的字单元
mov ax,ss:[bx+2] ; ss:[bx+2] 指向被除数的高 16 位 ; 不能省略段地址 ss,因为 [bx+idata] 默认段寄存器是 DS 而不是 SS
mov dx,0
div word ptr [bp] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的高 16 位),余数存放于 DX 寄存器
mov cx,ax ; 将高 16 位除法运算的商(商的高 16 位)转存入 BX 寄存器
; 计算低 16 位被除数与原 16 位除数的除法运算
mov ax,ss:[bx] ; ss:[bx] 指向被除数的低 16 位
div word ptr [bp] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的低 16 位),余数存放于 DX 寄存器
add dx,30h ; 将数值型的余数转换为字符型
mov byte ptr [di],dl ; 将最后结果余数转存入 ds:[di](data 段)内存字节单元
inc di ; (di)=(di)+1,令 ds:[di] 指向 data 段的下一个字节单元
mov dx,cx ; 将商的高 16 位转存入 DX 寄存器
mov ss:[bx],ax ; 更新栈中存储的商数的低 16 位的值
mov ss:[bx+2],dx ; 更新栈中存储的商数的高 16 位的值
pop cx
pop bp
ret
show_str: push dx ; DX 寄存器存储着需要显示字符串的屏幕行、列信息,将该信息压入栈中
; 将显示缓冲区第 0 页的段地址存入 ES 段寄存器中
mov ax,0B800h
mov es,ax
mov bp,sp
mov bx,word ptr [bp]
; 计算列偏移量,结果存储在 DI 寄存器中
dec bl
mov ax,2
mul bl
mov di,ax
; 计算行偏移量,将计算结果加到 DI 寄存器中
dec bh
mov ax,160
mul bh
add di,ax ; DI 寄存器中存储着显示缓冲区目标偏移地址
mov ch,0
push cx ; CX 寄存器中存储着需要显示字符串的属性信息,将这些信息压入栈中
mov ah,cl ; 将属性编码存入 AH 寄存器中
mov bx,0
dis: mov cl,[si]
cxz ok2
mov al,cl ; 将字符 ASCII 码存入 AL 寄存器中
mov word ptr es:[bx+di],ax
inc si
add bx,2
jmp short dis
ok2: pop cx ; 恢复需要显示的字符串所在屏幕的行、列信息数据
pop dx ; 恢复需要显示的字符串属性信息数据
ret
dxpl: push si
mov cx,0
push cx
mov di,0
s: mov cl,[si]
jcxz u
push cx
inc si
jmp s
u: pop cx
jcxz ok3
mov [di],cl
inc di
jmp u
ok3: pop si
ret
code ends
end start
(2) 在 data 段中给定一组需要显示的数值数据
; 实验 10 第 3 个子程序:显示值
; 本实验程序代码在 Ubuntu 18.04 的 DOSBox-X 中运行时,不能改变显示缓冲区内存地址数据、
; 以显示指定信息的内容,但编译、链接和运行都不会报错,原因不明。在 Ubuntu 的 DOSBox 及 Windows 10
; 的 DOSBox-X 中都能正常运行。这些情况与实验 9 出现的情况相同。
assume cs:code
data segment
dw 123,12666,1,8,3,38
db 10 dup (0)
data ends
code segment
start: mov bx,data
mov ds,bx
mov si,0
mov cx,6 ; 需要显示 6 个数值,所以 loop s 循环 6 次
mov dh,8 ; 行:8
s: mov ax,word ptr [si]
call dtoc ; 将存储在 AX 寄存器的数据转换为能显示在屏幕上的字符串数字,这些字符串数字的 ASCII 码被倒序存入 data 段空间
call dxpl ; 将倒序存储的数字字符串按原数字顺序重新存入 data 段空间
mov dl,3 ; 列:3
mov bx,cx ; 将 loop s 循环次数暂存到 BX 寄存器
mov cl,2 ; 字体颜色:绿色 —— 属性码 00000010b,即 02h
call show_str
add si,2 ; 在 data 段中给定的需要显示数值数据,是以 dw 方式声明,所以 si 自增步长为 2,而不是 1,不能“inc si”
inc dh
mov cx,bx ; 恢复 (cx) 为 loop s 的循环次数
loop s
mov ax,4c00h
int 21h
dtoc: push dx
push cx
mov bx,0000h ; 将被除数的高 16 位存放到 BX 寄存器
push bx ; 将被除数的高 16 位压入栈中
push ax ; 将被除数的低 16 位压入栈中
mov bp,sp
mov di,12 ; ds:[di] 指向 data 段中以 db 声明的数据段首地址
jd: mov cx,[bp] ; ss:[bp] 指向被除数的低 16 位
mov ax,cx ; 将被除数的低 16 位传送到 AX 寄存器
mov dx,[bp+2] ; ss:[bp+2] 指向被除数的高 16 位,将其传送到 DX 寄存器
jcxz md ; 检测被除数的低 16 位是否为 0
dvd: mov cx,0Ah ; 将除数存放到 CX 寄存器
call divdw
jmp jd
md: mov cx,[bp+2] ; ss:[bp+2] 指向被除数的高 16 位
jcxz ok1 ; 检测被除数的高 16 位是否为 0
jmp dvd
ok1: mov byte ptr ds:[di],0 ; 给 data 段中以 db 声明的数据段的字符串末尾添加字符串结束符 0
; 由于每次存入该数据段的字符数量不相同,所以必须在完成数值转换为字符而获得完整字符串的全部操作之后,在字符串末尾添加字符串结束符 0
pop ax
pop cx
pop dx
ret
divdw: push bp
push cx
push dx
; 计算高 16 位被除数与原 16 位除数的除法运算
mov bp,sp ; 当前 ss:sp 指向被除数高 16 位
mov bx,[bp+4] ; 将主调程序中的 (bp) 传送到 BX 寄存器,以便通过 [bx+idata] 指向栈中存储的被除数高、低 16 位的字单元
mov ax,ss:[bx+2] ; ss:[bx+4] 指向被除数的高 16 位。注意,不能省略段地址 ss,因为 [bx] 的默认段寄存器是 DS 而不是 SS
mov dx,0
div word ptr [bp+2] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的高 16 位),余数存放于 DX 寄存器
mov cx,ax ; 将高 16 位除法运算的商(商的高 16 位)转存入 CX 寄存器
; 计算低 16 位被除数与原 16 位除数的除法运算
mov ax,ss:[bx] ; ss:[bx+2] 指向被除数的低 16 位
div word ptr [bp+2] ; 16 位除法运算,默认商数存放于 AX 寄存器(商的低 16 位),余数存放于 DX 寄存器
add dx,30h ; 将数值型的余数转换为字符型
mov byte ptr [di],dl ; 将最后结果余数存入 ds:[di](data 段以 db 声明的数据段)字节单元
inc di ; (di)=(di)+1,令 ds:[di] 指向 data 段以 db 声明的数据段的下一个字节单元
mov dx,cx ; 将商的高 16 位转存入 DX 寄存器
mov ss:[bx],ax ; 更新栈中存储的商数的低 16 位的值
mov ss:[bx+2],dx ; 更新栈中存储的商数的高 16 位的值
pop dx
pop cx
pop bp
ret
show_str: push bx ; BX 暂存着主程序 loop s 的循环次数
push dx ; DX 寄存器存储着需要显示字符串的屏幕行、列信息,将该信息压入栈中
push si
; 将显示缓冲区第 0 页的段地址存入 ES 段寄存器中
mov ax,0B800h
mov es,ax
mov bp,sp
mov bx,word ptr [bp+2] ; 将行信息传送到 BX 寄存器
; 计算列偏移量,结果存储在 DI 寄存器中
dec bl
mov ax,2
mul bl
mov di,ax
; 计算行偏移量,将计算结果加到 DI 寄存器中
dec bh
mov ax,160
mul bh
add di,ax ; DI 寄存器中存储着显示缓冲区目标偏移地址
mov ch,0
push cx ; CX 寄存器中存储着需要显示字符串的属性信息,将这些信息压入栈中
mov ah,cl ; 将属性编码存入 AH 寄存器中
mov bx,0
mov si,12 ; 用 (si) 指明 data 段中以 db 声明的数据段首地址
dis: mov cl,[si]
jcxz ok2
mov al,cl ; 将字符 ASCII 码存入 AL 寄存器中
mov word ptr es:[bx+di],ax
inc si
add bx,2
jmp short dis
ok2: pop cx ; 恢复需要显示的字符串属性信息数据
pop si
pop dx ; 恢复需要显示的字符串所在屏幕的行、列信息数据
pop bx ; 恢复 BX 寄存器中暂存的主程序 loop s 的循环次数
ret
dxpl: push dx
push cx
push si
mov cx,0
push cx
mov si,12 ; 读取 data 段中以 db 声明的数据段首地址
mov di,12 ; 读取 data 段中以 db 声明的数据段首地址
s0: mov cl,[si]
jcxz u
push cx
inc si
jmp s0
u: pop cx
jcxz ok3
mov [di],cl
inc di
jmp u
ok3: pop si
pop cx
pop dx
ret
code ends
end start