方法 2
table 段中只设置一组字符串,全部用于存储一年的数据。下一年的数据覆盖前一年的数据字符串。
Ubuntu 18.04 系统安装的 DOSBox-X 无法运行,只能在其 DOSBox 中运行;而 Widows 中则两者都可运行
assume cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
; 以上是表示 21 年的 21 个字符串,每个字符串包含 4 个字符,所以共计 84 个字符,占用 84 字节
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
; 以上是表示 21 年公司总收入的 21 个 dword 型数据,每个数据占用 4 字节,所以共计占用 84 字节
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
; 以上是表示 21 年公司雇员人数的 21 个 word 型数据,每个数据占用 2 字节,所以共计占用 42 字节
data ends
string segment
db 10 dup (' ')
db 0
string ends
code segment
start: mov ax,data
mov es,ax
mov bx,0 ; BX 寄存器存储着 data 段中 db 部分数据的偏移地址起始值,标记年份数据
mov di,168 ; DI 寄存器存储着 data 段中 dw 部分数据的偏移地址起始值,标记雇员人数数据
mov dh,3 ; 设置需要在屏幕上显示的字符串的起始行
mov dl,25 ; 设置需要在屏幕上显示的字符串的起始列
mov ax,string
mov ds,ax
mov si,0 ; SI 寄存器存储着 string 段字符串起始偏移地址
mov cx,21
; loop u 循环执行 21 次,每次对一年一组数据进行处理
u: push cx ; 将 loop u 循环次数值压栈暂存
push dx ; 将显示字符串的屏幕行、列信息压栈
mov bp,sp ; 需要将栈中元素复制使用(非出栈)。由于没有 [sp] 这样的用法,
; 而 BP 寄存器默认的段寄存器是 SS,因此可以利用 [bp+idata] 来调用栈元素
; 复制年份字符串到 string 段
mov ax,es:[bx]
mov [si],ax
mov ax,es:[bx+2]
mov [si+2],ax
call show_str
call cls
; 转换年收入数值为数字字符串,并复制到 string 段:先设置参数,再调用 dtoc 子程序
mov ax,es:[bx+84] ; 读取 data 段中年收入值的低 16 位到 AX 寄存器中
mov dx,es:[bx+84+2] ; 读取 data 段中年收入值的高 16 位到 DX 寄存器中
mov cx,10 ; 设置将数值转换为字符串时要用到的除数数值 10,并存入 CX 寄存器
call dtoc
add byte ptr [bp],11 ; 更新字符串的屏幕列信息(每个字符串包含 11 个字符,即 11 列)
mov dx,[bp] ; 恢复 DX 寄存器中存储的显示字符串的屏幕行、列信息
call show_str
call cls
; 转换雇员人数数值为数字字符串,并复制到 string 段相应位置:先设置参数,再调用 dtoc 子程序
mov ax,es:[di] ; 读取 data 段中雇员人数数值的(低)16 位到 AX 寄存器中
mov dx,0 ; 读取(设置)data 段中雇员人数数值的高 16 位(0)到 DX 寄存器中
mov cx,10 ; 设置将数值转换为字符串时要用到的除数数值 10,并存入 CX 寄存器
call dtoc
add byte ptr [bp],11 ; 更新字符串的屏幕列信息(每个字符串包含 11 个字符,即 11 列)
mov dx,[bp] ; 恢复 DX 寄存器中存储的屏幕显示字符串的行、列信息
call show_str
call cls
; 计算人均收入,将计算结果转换为字符串并存入 string 段相应位置
mov ax,es:[bx+84] ; 读取 data 段中年收入值的低 16 位到 AX 寄存器中
mov dx,es:[bx+84+2] ; 读取 data 段中年收入值的高 16 位到 DX 寄存器中
mov cx,es:[di] ; 设置 data 段中雇员人数作为除数数值,并存入 CX 寄存器
call divdw ; 调用 divdw 子程序,计算人均收入
; 以上述 divdw 所得的人均收入作为被除数,以数值 10 作为除数,调用 dtoc 子程序,
; 将人均收入(被除数)由数值形式转换为字符串形式,并存入 string 段相应位置
mov cx,10
call dtoc
add byte ptr [bp],11 ; 更新字符串的屏幕列信息(每个字符串包含 11 个字符,即 11 列)
mov dx,[bp] ; 恢复 DX 寄存器中存储的屏幕显示字符串的行、列信息
call show_str
call cls
add bx,4 ; 令 es:[bx] 指向 data 段下一年年份字符串的首字符
add di,2 ; 令 es:[di] 指向 data 段下一年的雇员人数值
pop dx ; 让行、列信息先出栈,是为了执行下一条 pop 指令
pop cx ; 恢复 loop u 循环次数值
inc dh ; 让行数自增 1,以便在屏幕上显示下一年的字符串
mov dl,25 ; 重置需要在屏幕上显示的字符串的起始列
loop u
mov ax,4c00h
int 21h
; begin dtoc
dtoc: push si ; 将 ds:[si] 指向 string 段字符串首字符位置信息压栈
push bx ; 将 es:[bx] 指向 data 段年份字符串首字符位置信息压栈
mov bx,cx ; 将除数 10 暂存到 BX 寄存器
; 将数值 0 压栈,确定在 ok 标号处进行原序复制数值数字字符串时的最后一个结束符,
; 以作为数字字符串的长度界限(注意,数字字符串后面还有空格字符,但这不影响此处的设置)
mov cx,0
push cx ; 在本 dtoc 子程序的 over 标号处将会通过 pop 操作将该 0 值出栈
mca: mov cx,ax ; 判断被除数的低 16 位值是否为 0
jcxz mcd ; 如果为 0,则跳至标号 mcd 处,以对高 16 位值是否为 0 作进一步判断
; 如果不为 0,则调用 divdw 子程序
cd: mov cx,bx ; 恢复 CX 寄存器存储的除数数值
call divdw ; 调用 divdw 子程序,执行能解决“除法溢出”问题的除法运算
add cx,30h ; 将数值转换为字符
push cx ; 将数值的数字字符压栈
jmp mca ; 跳至 mca 标号处,对下一轮除法运算的被除数低 16 位进行是否为 0 的判断
mcd: mov cx,dx ; 判断被除数的高 16 位值是否为 0
jcxz over ; 如果为 0,则跳至 over 标号处恢复数字字符串正常顺序后,返回主程序;
; 如果不为 0,则通过其后的 jmp 指令跳至 cd 标号处,调用 divdw 子程序
jmp cd
over: ; 将倒序显示的数字字符串恢复为原来数值的数字字符串正常序列
pop cx ; 将数值的数字字符出栈存入 CX 寄存器
jcxz ok ; 如果出栈字符的 ASCII 码值为 0,则说明数值的数字字符串已全部出栈完毕,
; 应该跳至 ok 标号处返回主调程序
mov [si],cl ; 将 CL 寄存器中存储的字符转存入 string 段相应位置;注意,不是 CX 寄存器
inc si ; si 后移 1 位,指向 string 段下一个字符的存储位置
jmp over ; 跳至 over 标号处继续执行出栈操作,再次判断出栈字符 ASCII 码值是否为 0
ok: pop bx
pop si
ret
; end dtoc
; begin divdw
; 执行 divdw 子程序前已预先设置参数:
; (ax) = 被除数的低 16 位 (dx) = 被除数的高 16 位 (cx) = 除数
; 执行 divdw 子程序后返回的情况(指执行返回前的系列 pop 指令和 ret 指令之前):
; (ax) = 商数的低 16 位 (dx) = 商数的高 16 位 (cx) = 余数
divdw: push bx
push bp
; 执行高 16 位被除数与原 16 位除数之间的除法运算
mov bx,ax ; 将被除数的低 16 位暂存到 BX 寄存器
mov ax,dx ; 将被除数的高 16 位转存到 AX 寄存器
mov dx,0 ; 设置原被除数高 16 位进行的除法运算所需要使用的高 16 位被除数
div cx ; 执行高 16 位除法运算,计算结果的余数默认存储于 DX 寄存器
mov bp,ax ; 将高 16 位除法运算所得的商数转存到 BP 寄存器
mov ax,bx ; 将被除数的低 16 位传送回 AX 寄存器
div cx ; 执行低 16 位除法运算,计算结果的商数默认存储于 AX 寄存器
mov cx,dx ; 将低 16 位除法运算所得的余数(也是最终除法运算的余数)转存到 CX 寄存器
mov dx,bp ; 将高 16 位除法运算所得的商数转存到 DX 寄存器
; 经 divdw 子程序上述操作后,AX、DX 和 CX 寄存器分别存储着商数的低 16 位、高 16 位和余数
pop bp
pop bx
ret
; end divdw
; begin show_str
show_str: push es ; 因为本子程序需要将显示缓冲区段地址传送到 ES 寄存器,
; 因此需要先将原 ES 段寄存器中存储的 data 段地址压栈,
push si
push di
mov ax,0B800h ; 再将显示缓冲区第 0 页的段地址存入 ES 段寄存器
mov es,ax
; 计算列偏移量,结果存储在 DI 寄存器中
dec dl ; dec 指令与 inc 指令功能相反,执行自减操作,参检测点 9.3 所述
mov ax,2
mul dl ; 执行 8 位(8-bits)乘法运算,计算结果默认存放在 AX 寄存器中
mov di,ax
; 计算行偏移量,将计算结果加到 DI 寄存器中
dec dh
mov ax,160 ; 在显示缓冲区需要用一个字内存单元来存储一个字符的信息,
; 其中低 8 位存储字符的 ASCII 码,高 8 位存储属性信息,
; 因此在屏幕显示一行信息(80 个字符)就需要 160 个字节(80 个字)
mul dh ; 执行 8 位(8-bits)乘法运算,计算结果默认存放在 AX 寄存器中
add di,ax ; DI 寄存器中存储着显示缓冲区目标偏移地址
dis: mov cl,[si] ; ds:[si] 存储着需要显示在屏幕的字符串当前的字符 ASCII 码
mov ch,0
jcxz ok0 ; 判断当前需要显示的字符的 ASCII 码值是否为 0
mov ch,2 ; 设置需要在屏幕上显示的字符串的属性信息 —— 字体颜色:绿色
mov es:[di],cx ; 将存储着字符信息(ASCII 码)和属性信息的 (cx) 传送到显示缓冲区
inc si ; 自增 (si),使其指向 string 段中下一个需要显示的字符
; 最后 ds:[si] 将指向字符串结束符 '\0'
add di,2 ; 自增 (di),使其指向下一个显示缓冲区中所显示字符的位置
jmp short dis
ok0: pop di
pop si
pop es
ret
; end show_str
; 还原 string 段存储的字符串原始状态,以便在后面的复制数字字符串后,不会保留前面字符串的任何字符
; begin cls
cls: push si
push cx
mov cx,0
docls: mov cl,[si]
jcxz ok1
sub cl,' '
jcxz ok1
mov byte ptr [si],' '
inc si
jmp docls
ok1: pop cx
pop si
ret
; end cls
code ends
end start