直接定址表
如何有效合理地組織數據,以及相關的編程技術。
描述了單元長度的標號
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、s都是標號。這些標號僅僅表示了內存單元的地址。
我們還可以使用一種標號,這種標號不但表示內存單元的地址,還表示了內存單元的長度,即表示在此標號處的單元,是一個字節單元,還是字單元,還是雙字單元。
比如:
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后面沒有“:”,它們是同時描述內存地址和單元長度的標號。
標號a,描述了地址code:0,和這個地址開始,以后的內存單元都是字節單元;
而標號b描述了地址code:8,和這個地址開始,以后的內存單元都是字單元。
因為這種標號包含了對單元長度的描述,所以,在指令中,它可以代表一個段中的內存單元。比如,對于程序中的b dw 0。
指令:mov ax,b
相當于:mov ax,cs:[8]
指令:mob b,2
相當于:mov word ptr cs:[8],2
指令:inc b
相當于:inc word ptr cs:[8]
在這些指令中,標號b代表了一個內存單元,地址為code:8,長度為2字節。
下面的指令會引起編譯錯誤:
mov al,b
因為b代表的內存單元是字單元,而al是8位寄存器。
如果我們將程序中的指令:add b,ax,寫為add b,al,將出現同樣的編譯錯誤。
對于程序中的a db 1,2,3,4,5,6,7,8:
指令:mov al,a[si]
相當于:mov al,cs:0[si] ;這種語法比較像高級語言中的數組語法。
相當于:mov al,cs:[si+0]
相當于:mov al,cs:[si].0
指令:mov al,a[3]
相當于:mov al,cs:0[3]
指令:mov al,a[bx+si+3]
相當于:mov al,cs:0[bx+si+3]
可見,使用這種包含單元長度的標號,可以使我們以簡潔的形式訪問內存中的數據。
以后,我們將這種標號稱為數據標號,它標記了存儲數據的單元的地址和長度。它不同于僅僅表示地址的地址標號。
檢測點16.1
下面的程序將code段中a處的8個數據累加,結果存儲到b處的dword中,補全程序。
assume cs:code
code segment
a dw 1,2,3,4,5,6,7,8
b dd 0
start: mov si,0
mov cs,8
s: mov ax, a[si]
add word ptr b ,ax
adc word ptr b[2] ,0 ;雙字單元的高字單元用來存放進位數值。
add si, 2 ;內存單元為字節單元。
loop s
mov ax,4c00h
int 21h
code ends
end start
在其他段中使用數據標號
一般來說,我們不在代碼段中定義數據,而是將數據定義到其他段中。在其他段中,我們也可以使用數據標號來描述存儲數據的單元的地址和長度。
注意:在后面加有“:”的地址標號,只能在代碼段中使用,不能在其他段中使用。
下面的程序將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 ex,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov 4c00h
int 21h
code ends
end start
注意,如果想在代碼段中,直接用數據標號訪問數據,則需要用偽指令assume將標號所在的段和一個段寄存器聯系起來。否則編譯器在編譯的時候,無法確定標號的段地 在哪一個寄存器中。當然,這種聯系是編譯器需要的,但絕對不是說,我們因編譯器的工作需要,用assume指令將段寄存器和某個段相聯系,段寄存器中就會真的存放該段的地址。我們在程序中還需要使用指令對段寄存器進行設置。
比如:在上面的程序中,我們要在代碼段code中用data段中的數據標號a、b訪問數據,則必須用assume將一個寄存器和data段相聯。在程序中,我們用ds寄存器和data段相聯,則編譯器對相關指令的編譯如下:
指令:mov al,a[si]
編譯為:mov al,[si+0]
指令:add b,ax
編譯為:add [8],ax
因為這些實際編譯出的指令,都默認所訪問單元的段地址在ds中,而實際要訪問的段為data,所以,若要訪問正確,在這些指令執行前,ds中必須為data段的段地址。則,我們在程序中使用指令:
mov ax,data
mov ds,ax
設置ds指向data段。
可以將標號當作數據來定義,此時,編譯器將標號所表示的地址當作數據的值。比如:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
data ends
數據標號c處存儲的兩個字型數據為標號a、b的偏移地址。相當于:
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
數據標號c處存儲的兩個雙字型數據為標號a的偏移地址和段地址、標號b的偏移地址和段地址。相當于:
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.2
下面的程序將data段中a處的8個數據累加,結果存儲到b處的字中。補全程序。
assume cs:code,es: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
int 21h
end start
直接定址表
現在,我們討論用查表的方法編寫相關程序的技巧。
編寫子程序,以十六進制的形式在屏幕中間顯示給定的byte型數據。
分析:一個字節需要用兩個十六進制數碼來表示,所以,子程序需要在屏幕上顯示兩個ASCII字符。用“0”、“1”、“2”、“3”、“4”、“5”、“6”、“7”、“8”、“9”、“A”、“B”、“C”、“D”、“E”、“F”這16個字符來顯示十六進制數碼。
我們可以將一個byte的高4位和低4位分開,分別用它們的值得到對應的數碼字符。比如2Bh,我們可以得到高4位的值為2,低4位的值為11,那么我們如何用這兩個數值得到對應的數碼字符“2”和“B”呢?
最簡單的辦法就是一個一個地比較,如下:
如果數值為0,則顯示“0”;
如果數值為1,則顯示“1”;
…
如果數值為11,則顯示“B”;
…
這樣做,程序中要使用多條比較、轉移指令。程序將比較長,混亂。
顯示,我們希望能夠在數值0~15和字符“0”~“F”之間找到一種映射關系。這樣我們用0~15間的任何數值,都可以通過這種映射關系直接得到“0”~“F”中對應的字符。
數值0~9和字符“0”~“9”之間的映射關系是很明顯的,即:
數值+30h = 對應字符的ASCII值。
但是,10~15和“A”~“F”之間的映射關系是:
數值+37h = 對應字符的ASCII值。
可見,我們可以利用數值和字符之間的這種原本存在的映射關系,通過高4位和低4位值得到對應的字符碼。但是由于映射關系的不同,我們在程序中必須進行一些比較,對于大于9的數值,我們要用不同的計算方法。
這樣做,雖然使程序得到了簡化。但是,如果我們希望用更簡捷的算法,就要考慮用同一種映射關系從數值得到字符碼。所以,我們就不能利用0~9和“0”~“9”之間與10~15和“A”~“F”之間原有的映射關系。
因為數值0~15和字符“0”~“F”之間沒有一致的映射關系存在,所以,我們應該在它們之間建立新的映射關系。
具體的做法是,我們建立一張表,表中依次存儲字符“0”~“F”,我們可以通過數值0~15直接查找到對應的字符。
子程序如下:
;用al傳送要顯示的數據
showtyte: 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
mov ah,table[bx] ;用高4位的值作為相對于table的偏移,取得對應的字符。
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah ;顯示高4位的十六進制字符碼
mov bl,al
mov bh,0
mov al,table[bx] ;用低4位的值作為相對于table的偏移,對得對應的字符。
mov es:[160*12+40*2+2],al ;顯示低4位的十六進制字符碼
pop es
pop bx
ret
可以看出,在子程序中,我們在數值0~15和字符“0”~“F”之間建立的映射關系為:
以數值N為table青史的偏移,可以找到對應的字符。
利用表,在兩個數據集合之間建立一種映射關系,使我們可以用查表現方法根據給出的數據得到其在另一個集合中的對應數據。這樣做的目的一般來說有三個:
1)為了算法的清晰和簡潔;
2)為了加快運算速度;
3)為了使程序易于擴充。
在上面的子程序中,我們更多的是為了算法的清晰和簡潔,而采用了查表的方法。
編程的時候要注意程序的容錯性,即對于錯誤的輸入要有處理能力。
上面的例子,我們通給出的數據進行計算或比較而得到結果的問題,轉化為用給出的數據作為查表的依據,通過查表得到結果的問題。具體的查表方法,是用查表的依據數據,直接計算出所要查找的元素在表中的位置。像這種可以通過依據數據,直接計算出所要找的元素的位置的表,我們稱其為:直接定址表。
程序入口地址的直接定址表
我們可以在直接定址表中存儲子程序的地址,從而方便地實現不同子程序的調用。
實現一個子程序setscreen,為顯示輸出提供如下功能:
1)清屏;
2)設置前景色;
3)設置背景色;
4)向上滾動一行。
入口參數說明:
1)用ah寄存器傳遞功能號:0表示清屏,1表示設置前景色,2表示設置背景色,3表示向上滾動一行;
2)對于2、3號功能,用al傳送顏色值,(al)∈{0,1,2,3,4,5,6,7}
下面,討論一下各種功能如何實現:
1)清屏:將顯存中當前屏幕中的字符設為空格符;
2)設置前景色:設置顯存中當前屏幕中處于奇地址的屬性字節的第0、1、2位;
3)設置背景色:設置顯存中當前屏幕中處于奇地址的屬性字節的第4、5、6位;
4)向上滾動一行:依次將第n+1行的內容復制到第n行處,最后一行為空。
我們將這4個功能分別寫為4個子程序:
;清屏
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
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;設置背景色
sub3: push bx
push cx
push es
mov cl,4
shr al,cl
mov bx,0b800h
mov es,bx
mov bx,1
sub3s: and byte ptr es:[bx],10001111b
shl al,1
shl al,1
shl al,1
shl al,1
or es:[bx],al
add bx,2
loop sub3s
pop es
pop cx
pop bx
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 ;一行的長度為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
當然,我們也可以將子程序setscreen如下實現:
setscreen: cmp ah,0
je do1
cmp ah,1
je do2
cmp ah,2
je do3
cmp ah,3
je do4
jmp short sret
do1: call sub1
jmp short sret
do2: call sub2
jmp short sret
do3: call sub3
jmp short sret
do4: call sub4
sret: ret
顯然,用通過比較功能號進行轉移的方法,程序結構比較混亂,不得功能的擴充。比如說,在setscreen中再加入一個功能,則需要修改程序的邏輯,加入新的比較、轉移指令。
用根據功能號查找地址表的方法,程序的結構清晰,便于擴充。如果加入一個新的功能子程序,那么只需要在地址表中加入它的入口地址就可以了。