Ch 12 字串

来源:百度文库 编辑:神马文学网 时间:2024/04/27 23:00:44
在撰寫程式時常常會遇到資料轉移﹑比較等狀況發生,80X86 家族的 CPU 裏有幾個指令是專門處理『字串』的,在這裡所謂的字串,是指在記憶體內連續的位元組或字組,並不一定是 ASCII 字元,也有可能是一段二進位數。
概論
80X86 指令集中字串處理的指令有搬移﹑掃描﹑比較三種,另外再加上由記憶體載入至暫存器與由暫存器存入記憶體兩種。
MOVSB﹑MOVSW 和 REP 指令
先說搬移字串。搬移字串指令有兩種,分別是 MOVSB 和 MOVSW,先說 MOVSB。MOVSB 的英文是 move string byte,意思是搬移一個位元組,它是把 DS:SI 所指位址的一個位元組搬移到 ES:DI 所指的位址上,搬移後原來的內容不變,但是原來 ES:DI 所指的內容會被覆蓋而且在搬移之後 SI 和 DI 會自動地址向下一個要搬移的位址。
一般而言,通常程式設計師一般並不會只搬一個位元組,通常都會重複許多次,如果要重複的話,就得把重複次數 ( 也就是字串長度 ) 先記錄在 CX 暫存器,並且在 MOVSB 之前加上 REP 指令,REP 是重複 (repeat) 的意思。這種寫法很是奇怪,一般而言組合語言原始檔的每一行都只有一個指令,但 REP MOVSB 卻可以在同一行寫兩個指令,當然您分開寫也是一樣的。
讓小木偶用 DEBUG 來觀察 REP MOVSB 執行情形:
C:\WINDOWS>debug [Enter]-a [Enter]1C6C:0100 mov cx,10 [Enter]1C6C:0103 mov si,200 [Enter]1C6C:0106 mov di,300 [Enter]1C6C:0109 rep movsb [Enter]1C6C:010B [Enter]-a 200 [Enter]1C6C:0200 db "I learn assembly" [Enter]1C6C:0210 [Enter]
上面的程式片段是把位於 1C6C:0200 的『I learn assembly』字串移搬到 1C6C:0300 處,此字串共 16 個字元,所以 CX 存入 10H。現在來追蹤看看。
-t [Enter]AX=0000 BX=0000 CX=0010 DX=0000 SP=FFEE BP=0000 SI=0200 DI=0000DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0106 NV UP EI PL NZ NA PO NC1C6C:0103 BE0002 MOV SI,0200-t [Enter]AX=0000 BX=0000 CX=0010 DX=0000 SP=FFEE BP=0000 SI=0200 DI=0000DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0106 NV UP EI PL NZ NA PO NC1C6C:0106 BF0003 MOV DI,0300-t [Enter]AX=0000 BX=0000 CX=0010 DX=0000 SP=FFEE BP=0000 SI=0200 DI=0300DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC1C6C:0109 F3 REPZ1C6C:010A A4 MOVSB-d 300 L10 [Enter]
在還未搬移之前,先看看 1C6C:0300 處的內容,再追蹤。此處您會看到我們輸入 rep 指令,但是 DEBUG 卻顯示 REPZ,事實上這兩個是一樣的。
1C6C:0300 E8 A3 F6 74 08 49 46 FE-06 D7 DC EB EF E8 C3 F9 ...t.IF.........-t [Enter]AX=0000 BX=0000 CX=000F DX=0000 SP=FFEE BP=0000 SI=0201 DI=0301DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC1C6C:0109 F3 REPZ1C6C:010A A4 MOVSB-d 300 L10 [Enter]1C6C:0300 49 A3 F6 74 08 49 46 FE-06 D7 DC EB EF E8 C3 F9 I..t.IF.........
在搬移一次之後,再看看 1C6C:0300 處的內容,發現上面已經和原來不一樣了 (紅色部份)。這是因為 movsb 已經把第零個位元組搬到 1C6C:0300 處,而覆蓋了原來的內容。而 CX 也減少一,SI﹑DI 也各增加一而指向下一個位址。好!再追蹤看看。
-t [Enter]AX=0000 BX=0000 CX=000E DX=0000 SP=FFEE BP=0000 SI=0202 DI=0302DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC1C6C:0109 F3 REPZ1C6C:010A A4 MOVSB
您有沒有發現,在搬移完之前,IP 都指向 REP MOVSB 指令 ( 即 REP MOVSB 所在位址 )。要追蹤這麼多次,太麻煩了,乾脆直接執行到搬移字串到結束。
-g 10b [Enter]AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0210 DI=0310DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=010B NV UP EI PL NZ NA PO NC1C6C:010B 06 PUSH ES-d 200 L10 [Enter]1C6C:0200 49 20 6C 65 61 72 6E 20-61 73 73 65 6D 62 6C 79 I learn assembly-d 300 L 10 [Enter]1C6C:0300 49 20 6C 65 61 72 6E 20-61 73 73 65 6D 62 6C 79 I learn assembly
搬移結束後,1C6C:0200 和 1C6C:0300 處的內容均相同,所以 MOVSB 事實上是把原來字串複製到要搬移之處,而原字串是原封不動的。
MOVSW 的作用方式都和 MOVSB 相同,所不同的是 MOVSW 每次搬移一個字組,所以每次搬運完 SI﹑DI 會增加 2,而 CX 仍然減少一。
CLD 和 STD 指令
此外,還有一點,小木偶在上面沒有提到。事實上我們也可以使每搬移一次之後,使 SI﹑DI 遞減,也就是往低位址搬移。方法是由『方向旗標』控制( 有關方向旗標請參考附錄二旗標暫存器 )。
當方向旗標清除時 (即方向旗標為零),搬移方向是向高位址處,SI﹑DI 會遞增,同時您可以看到在 DEBUG 顯示旗標處會有『UP』的字樣,表示向高位址搬移。這是大部分的情況。
當方向旗標設定時 (即方向旗標為一),搬移方向是向低位址處,SI﹑DI 會遞減。到在 DEBUG 顯示旗標處會有『DN』的字樣。
最後,方向旗標清除的指令是 CLD,意思是 clear direction flag;設定的指令是 STD,意思是 set direction flag。
CMPSB 和 CMPSW 指令
這兩個指令使用方法和 MOVSB﹑MOVSW 相同,而它的作用是將一個字串和另一處的字串比較。如果只有單獨的一個 CMPSB 或 CMPSW 時,CPU 只比較一個位元組或一個字組;當 CMPSB 或 CMPSW 前加上 REP 時,可以比較一個字串。您也可以用 REPE ( 表示 repeat while equal,如果兩字相等則重複 ) 來代替 REP,也可以用 REPZ ( 表示 repeat while zero,如果零旗標為 ZR,則重複 ) 來代替,換句話說 REP﹑REPE 和 REPZ 是相同的。
那您可能會問,如何才知道兩個字串相等?這時您就得檢查『零旗標』了,如果零旗標被設為一 (DEBUG 顯示 ZR),表示兩字串相等,此時兩字串會比較完畢所以 CX 也會一直減少至零。如果零旗標被設為零 (DEBUG 顯示 NZ),表示兩字串不相等,cmps 指令僅僅比較到第一個不相等的字元就停止了,所以 CX 不會為零,SI﹑DI 會指到第一個不相等的位元組或字組之後的位址。
與 MOVS 指令相同的是,CMPS 指令也可以用方向旗標來指定向高位址比較或向低位址比較。
SCASB 和 SCASW 指令
這是 scan string 的意思,中文是掃描字串,它的作用是在一個字串中找到特定的位元組或字組。而這特定的位元組或字組放在 AL 或 AX 暫存器中,被掃描字串的長度位於 CX,字串位址位於 ES:DI 所指的位址。同樣也可以用方向旗標來指定往高位址或低位址掃描。
同樣的 SCASB 或 SCASW 也可以用 REPE 來搭配使用。但是最常用的還是和 PRENE 搭配,它的意思是 repeat while not equal,意思是如果不相等則重複,試想當你在一個英文句子中,尋找英文字母『a』有沒有出現,直覺的方法是不是先看第一個字母,如果不是再看第二個字母。此處最常與 SCASB 搭配的 REPNE 也是如此,如果前面的字不相等,才找後面的字,所以用 REPNE ,而少用 REPE。您也可以用 REPNZ 來代替 REPNE。
LODSB 和 LODSW
LODSB 這個 80X86 指令是把 DS:SI 所指位址的記憶體內容載入一個位元組到 AL 裏,同樣視方向旗標而定,會使得 SI 暫存器增加一或遞少一。LODSB 之意思是 load string byte,但是它卻很少配合 REP 指令,因為通常我們用它是因為要處理該字串裏的每一個位元組,待處理完才能再次載入,所以兩次載入之間常還有其他指令,並不像 MOVS﹑CMPS﹑SCAS 這三個指令可以用一個指令就解決了。
LODSW 是載入 DS:SI 所指位址的內容一個字組到 AX 暫存器,同樣的 SI 會視方向旗標增加二或遞少二,這是因為一個字組佔兩個位元組。
STOSB 和 STOSW
這兩個指令和 LODSB﹑LODSW 類似,所不同的是這兩個指令是 AL 或 AX 的內容移到 ES:DI 所指的記憶體位址。DI 會視方向旗標增加或遞少。
印出 ASCII 字元的位元圖
字元圖案
在 BIOS 裏有一段記憶體空間 ( F000:FA6E 開始 ) 是存放 ASCII 字元由 0 開始到 127 共 128 個字元的圖案,每一個文字都可以看成許多『點』組成,這些點構成字元的圖案。如下圖是一個英文字『A』

最上面一行,由右而左共有 8 個點,這 8 個點構成一個位元組,其中有些點是紅色的,表示這個點必須在螢光幕上印出來,而相對應的位元為一:有些點是黑色表示這是背景,表示這個點不用印出來,相對應的位元為零。以英文字母『A』來說,第零個位元組應該就是 00 11 00 00,換算成十六進位就是 30H,同理其餘位元組分別是 78H﹑0CCH﹑0CCH﹑0FCH﹑0CCH﹑0CCH﹑0。這八個位元組就構成『A』的圖樣,其他的 ASCII 字元也都是類似。所以如果 DOS 要顯示英文字母,就到 BIOS 這個圖案表去尋找該字元的圖案位元組,然後用程式將它依樣畫葫蘆印在螢光幕上。
當然在螢光幕上每一個點都很小,所以您看不到鋸齒狀,也感覺不到點的樣子,在這裏小木偶想將這些字元圖案放大顯示在螢光幕上,這個程式稱為『CHAR_GRA.ASM』,執行結果如下:
H:\HomePage\SOURCE>char_grp [Enter]按任意鍵(Esc鍵離開): 按下 AAAAAAAAA AAAA AAAAAAAAAA AAAA AA按任意鍵(Esc鍵離開): 按下 11111111111111111111按任意鍵(Esc鍵離開): 按下 Esc 鍵H:\HomePage\SOURCE>原始程式
原始程式如下:
total_len equ 8*128 ;01;***************************************message segment ;03 資料段開始message0 db 0dh,0ah,‘按任意鍵(Esc鍵離開):$‘char_graph db total_len dup (?)message ends ;06 資料段結束;***************************************code segment ;08 程式碼區段開始assume cs:code,ds:message;---------------------------------------main proc far ;11 指程式開始start: push ds ;12 將返回DOS資訊存入堆疊sub ax,axpush axmov bx,0f000h ;16mov cx,total_lenmov ds,bx ;18 使DS指向BIOS段位址mov si,0fa6eh ;19 使SI指向BIOS中ASCII位元圖之偏移位址mov ax,messagemov di,offset char_graphmov es,ax ;22 使ES指向本程式的資料段rep movsb ;23 搬移mov ds,ax ;25 使DS指向本程式的資料段nxt_char:mov dx,offset message0mov ah,9int 21h ;29call crlfinput: mov ah,0 ;31 輸入按鍵int 16hcmp al,1bhje exitcmp al,07fhja input ;36mov dh,al ;38 保存該鍵的ASCII於DHmov si,offset char_graphcbw ;40 計算該ASCII之偏移位址mov cl,3shl ax,cladd si,ax ;43 並存於SIcld ;44 使LODSB往高位址處取得資料mov ch,8 ;45 每個ASCII字元圖以8位元組表示nxt_byte:lodsb ;47 取得該ASCII字元的其中一個位元組mov cl,8 ;48 每個位元組有8位元nxt_bit:mov dl,dh ;50 決定是要印出空白還是該字元shl al,1 ;51 決定方法是該位元為0則印空白jc print ;52 反之印出該ASCII字元mov dl,‘ ‘ ;53print: mov ah,2 ;54push ax ;55 為避免AL值改變,故存於堆疊int 21h ;56 印出pop ax ;57 取回AL值dec cljnz nxt_bit ;59 是否印下一位元call crlf ;61 否,則印出換行及歸位字元dec chjnz nxt_byte;63 是否印下一位元組jmp nxt_char;64 否,則跳到輸入按鍵exit: ret ;66 返回DOSmain endp ;67;---------------------------------------crlf proc near ;69push axmov ah,2mov dl,0dhint 21hmov dl,0ahint 21hpop axretcrlf endp ;78;---------------------------------------code ends ;80;***************************************stack segment stack ;82 堆疊段dw 80 dup (?)stack ends ;84;***************************************end start ;86 指定程式進入點程式解說
這個程式一開始,就是找到 BIOS 存放字元圖案的地方,把這些圖案資料移到本程式的資料段。此時來源字串由 DS:SI 指定,應該指到 F000:FA6E 的位址,而目的字串應指到本程式的資料段,也就是 message:char_graph 的位址。至於字串長度可以由從 0 到 127 共 128 個字元,每個字元佔 8 個位元組,因此總共佔用 8*128 個位元組。( 程式第 16 行到第 23 行)
第 25 行在執行完了搬移後,才將 DS 指向我們的資料區。接下來第 26 行到第 36 行是印出提示字串及輸入按鍵。第 38 行是保存按鍵的 ASCII 字元,等程式計算出來如何列印時,在銀幕上列印時需要用到。
第 39 行到第 43 行是計算該按鍵所代表的 ASCII 字元的 8 個位元組在那裏。可以想像,每一個字元都由 8 個位元組的圖案來表示,而這些圖案是放在 char_graph 開始的位址,所以 ASCII 為零的字元就是在 char_graph 位址上,ASCII 為一的字元就是在 char_graph 之後的第 8 個位址上,ASCII 為零的字元就是在 char_graph 之後的第 16 個位址上……,當使用者按下一個鍵時,該鍵的 ASCII 字元在 AL 暫存器,將其改成字組變成 AX ( 程式第 40 行 ),那該 ASCII 所在位址就應該是
8*AX + char_graph
程式第 41 行到第 43 行就是計算這一個算式,並將結果存於 SI 暫存器。
程式第 45 行到第 64 行是印出該字元的圖案來。因為一個字有 8 個位元組來表示其圖案,而每個位元組有 8 個位元來表示 8 個點,所以小木偶用兩個迴圈來解決。第一個迴圈比較大,由第 46 行到第 63 行,這 8 個位元組的個數存於 CH 中,每處理完一個位元組,CH 就減一 (第 62 行),這個迴圈一開始就取得代表該 ASCII 字元圖案的一個位元組,取得方式就是用 LODSB 指令 ( 第47行 ),此指令會自動使 SI 加一,然後將取得的資料放在 AL 中,交由第二個迴圈處理。因每一個位元組有 8 個位元所以進入第二個迴圈之前,先把 8 存入 CL ( 第48行 )。
第二個迴圈比較小,它包含在第一個迴圈裏,像這樣的處理方式,稱之為『巢狀』。在這個迴圈裏,先把 DH 內的數值拷貝到 DL 中,DH 的數值是使用者所按下鍵的 ASCII 字元,拷貝到 DL 中是要使印出在螢幕上。第二步是向左邊移一個位元到『進位旗標』,X86 指令集中有一個指令 SHL 恰好可以做這件事 ( 第51行 ),接下來就是檢查進位旗標 ( 第52行 ),如果剛才 SHL 後的位元為一,那進位旗標會被設定,應該印出 ASCII 字元;反之,若為零,進位旗標會被清除,應該印出空白 ( 第53行 )。接下來第 54 行到第 57 行是印出空白或 ASCII 字元的程式,第 59 行是檢查是否已將 8 個位元都處理完畢。
若處理完畢則又返回第一個迴圈,先印出換行字元 ( 第61行 ),再檢查是否已處理完 8 個位元組。這兩個迴圈的結構如下圖:

最後第 66 行是一個返回 DOS 的指令,但因為 main 副程式對 DOS 而言是遠程呼叫,因此這個 ret 指令會取出兩個字組,也就是程式第 12﹑14 行所推入的堆疊資料,而將控制權交還給 DOS,當然,如果您用 AH=4CH/INT 21H 來結束本程式也可以。
回到首頁,到第十一章,到第十三章