第十六章
16.1 描述了单元长度的标号
之前我们一直在代码段中使用标号来标记指令、数据、段的起始地址
assume cs:code
code segment
a: db 1,2,3,4,5,6,7,8
b: dw 0
start: mov si,offset a
mov bx,offset b
mov cx,8
s: mov al,cs:[si]
mov ah,0
add cs:[bx],ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
程序中的code、a、b、start都是标号,这些标号仅仅表示了内存单元的地址
还有一种标号,不但表示内存单元的地址,还表示了内存单元的长度,即表示在此标号处的单元,是一个字/字节/双字单元
上面的程序也可以写出这样
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
start: mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
标号a,描述了地址code:0以及从这个地址开始以后的内存单元都是字节单元
标号b,描述了地址code:8以及从这个地址开始,以后的内存单元都是字单元
这种标号可以代表一个段中的内存单元
; 两者等价
mov ax,b
mov ax,cs:[8]
; 下面的指令会引起编译错误,长度不匹配
mov al,b
; 两者等价
mov al,a[si]
mov al,cs:0[si]
我们将这种包含单元地址和长度的标号称为数据标号
下面的程序将code段中a处的8个数据累加,结果存储到b处的双字中,补全程序
assume cs:code
code segment
a dw 1,2,3,4,5,6,7,8
b dd 0
start: mov si,0
mov cx,8
s: mov ax,a[si]
add word ptr b,ax ; b虽然还包含着长度信息
adc word ptr b[2],0 ; 但其本身指的是cs:[16]这个内存单元
add si,2
loop s
mov ax,4c00h
int 21h
code ends
end start
16.2 在其他段中使用数据标号
我们一般将数据定义在其他段中,在其他段中同样可以使用数据标号
但是在后面有 : 的地址标号,只能在代码段中使用
将data段中a标号处的8个数据;欸加,结果存储到b标号处的字
assume cs:code,ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
iont 21h
code ends
end start
如果想在代码段中直接使用数据标号访问数据,则需要用伪指令assume将标号所在的段和一个段寄存器联系起来,否则编译的时候,无法确定标号的段地址在哪个寄存器中
在上面的程序中,我们用ds和data段关联,则编译器对相关指令的编译如下
mov al,a[si] ; 编译为 mov al.[si+0]
add b,ax ; 编译为 add [8],ax
我们可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
data ends
; 相当于
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a,offset b
data ends
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b
data ends
; 相当于
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, seg a, offset b, seg b
data ends
seg操作符的功能为取得某一标号的段地址
16.3 直接定址表
编写子程序,以十六进制的形式在屏幕中间显示给定的字节型数据
一个字节需要两个十六进制数码来表示,所以我们将一个字节的高4位和低4位分开,然后分别得到对应的ASCII码,但有一个问题
如2BH,2 + 30H = “2” 而11 + 37H = “B”
它们的映射关系是不同的
所以我们建立一张表,表中一次存储字符“0”~“F”,通过数值直接查找到对应的字符
; 用al转送要显示的数据
showbyte: jmp short show
table db '0123456789ABCDEF' ; 字符表
show: push bx
push es
mov ah,al
shr ah,1
shr ah,1
shr ah,1
shr ah,1 ; 右移4位,ah中得到高4位的值
and al,00001111b ; al中为低4位的值
mov bl,ah
mov bh,0 ; 高4位的值作为对应table的位移
mov ah,table[bx] ; 取得对应字符
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0 ; 高4位的值作为对应table的位移
mov al,table[bx] ; 取得对应字符
mov es:[160*12+40*2+2],al
pop es
pop bx
ret
利用表,在两个数据集合之间建立一种映射关系,使算法更简洁,运算更快,程序易于扩充
下例为了加快运算速度而采用查表
编写一个子程序,计算sin(x),x属于{0,30,60,90,120,150,180},并在屏幕中间显示计算结果
可以用麦克劳林公式计算,但是费时,因此可以建立一张表
; ax向子程序传递角度
showsin: jmp short show
table dw ag0,ag30,ag60,ag90,ag120,ag150,ag180
ag0 db '0',0 ; sin(0)对应的结果"0"
ag30 db '0.5',0
ag60 db '0.866',0
ag90 db '1',0
ag120 db '0.866',0
ag150 db '0.5',0
ag180 db '0',0
show: push bx
push es
push si
mov bx,0b800h
mov es,bx
; 用角度值/30作为对于table的偏移,取得对应字符串的偏移地址,放在bx中
mov ah,0
mov bl,30
div bl
mov bl,al ; bl保存除得的结果
mov bh,0
add bx,bx ; 因为ag0等占两个字节(值为标号ago的偏移地址)
; 因此bx要*2才是ag0等相对于table偏移地址
mov bx,table[bx]
; 显示sin(x)对应的字符串
mov si,160*12+40*2
shows: mov ah,cs:[bx]
cmp ah,0 ; 每个值的字符串长度不一样,因此在末尾加0
je showret ; 方便读取
mov es:[si],ah
inc bx
add si,2
jmp short shows
showret: pop si
pop es
pop bx
ret
同时,最好在程序中加上对角度值是否超出范围的检测
通过依据数据,直接计算要找的元素的位置的表,称为直接定址表
16.4 程序入口地址的直接定址表
我们可以直接在定址表中存储子程序的地址,方便实现不同子程序的调用
实现一个子程序setscreen,有如下功能:清屏,设置前景色,设置背景色,向上滚动一行
参数:ah传递功能号 0:清屏 1:设置前景色 2:设置背景色 3:向上滚动一行
al传送颜色值 (al)属于{0,1,2,3,4,5,6,7}
如何清屏:当前屏幕字符设置位空格符
向上滚动一行:第n+1行的内容复制到第n行,最后一行为空
; 各个子程序的实现
; 清屏
sub1: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s: mov byte ptr es:[bx],' '
add bx,2
loop sub1s
pop es
pop cx
pop bx
ret
; 设置前景色
sub2: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s: and byte ptr es:[bx],11111000b ; 第0、1、2位与前景色的有关
; 其他位和1与运算,保持不变
or es:[bx],al ;将前3位设置为对应的颜色
add bx,2 ; 奇数地址表示字符属性
loop sub2s
pop es
pop cx
pop bx
ret
; 设置背景色
sub3: push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s: and byte ptr es:[bx],10001111b ; 第4、5、6位与背景色的有关
; 其他位和1与运算,保持不变
or es:[bx],al ;设置为对应的颜色
add bx,2 ; 奇数地址表示字符属性
loop sub3s
pop es
pop cx
pop bx
ret
; 向上滚动一行
sub4: push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ; ds:si指向第n+1行
mov di,0 ; es:di指向第n行
cld
mov cx,24
sub4s: push cx
mov cx,160
rep movsb ; 复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1: mov byte ptr [160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret
; 将这些子程序入口存储在一个表中
; 功能和*2=对应子程序在表中的偏移
setscreen: jmp short set
table dw sub1,sub2,sub3,sub4
set: push bx
cmp ah,3 ; 判断功能号是否大于3
ja sret
mov bl,ah
mov bh,0
add bx,bx ; 根据ah提供的功能号找到对应子程序在table中的偏移
call word ptr table[bx] ; 调用
sret: pop bx
ret
这种方法便于扩充,如果加入一个新的功能子程序,只需要在地址表中加入它的入口地址即可