Ch 12 字串在撰寫程式時常常會遇到資料轉移﹑比較等狀況發生,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=0000 DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0106 NV UP EI PL NZ NA PO NC 1C6C:0103 BE0002 MOV SI,0200 -t [Enter] AX=0000 BX=0000 CX=0010 DX=0000 SP=FFEE BP=0000 SI=0200 DI=0000 DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0106 NV UP EI PL NZ NA PO NC 1C6C:0106 BF0003 MOV DI,0300 -t [Enter] AX=0000 BX=0000 CX=0010 DX=0000 SP=FFEE BP=0000 SI=0200 DI=0300 DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC 1C6C:0109 F3 REPZ 1C6C: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=0301 DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC 1C6C:0109 F3 REPZ 1C6C: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=0302
DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=0109 NV UP EI PL NZ NA PO NC
1C6C:0109 F3 REPZ
1C6C: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=0310 DS=1C6C ES=1C6C SS=1C6C CS=1C6C IP=010B NV UP EI PL NZ NA PO NC 1C6C: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 和 LODSWLODSB 這個 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鍵離開): 按下 A AA AAAA AA AA AA AA AAAAAA AA AA AA AA 按任意鍵(Esc鍵離開): 按下 1 11 111 11 11 11 11 111111 按任意鍵(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,ax push ax mov bx,0f000h ;16 mov cx,total_len mov ds,bx ;18 使DS指向BIOS段位址 mov si,0fa6eh ;19 使SI指向BIOS中ASCII位元圖之偏移位址 mov ax,message mov di,offset char_graph mov es,ax ;22 使ES指向本程式的資料段 rep movsb ;23 搬移 mov ds,ax ;25 使DS指向本程式的資料段 nxt_char: mov dx,offset message0 mov ah,9 int 21h ;29 call crlf input: mov ah,0 ;31 輸入按鍵 int 16h cmp al,1bh je exit cmp al,07fh ja input ;36 mov dh,al ;38 保存該鍵的ASCII於DH mov si,offset char_graph cbw ;40 計算該ASCII之偏移位址 mov cl,3 shl ax,cl add si,ax ;43 並存於SI cld ;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,‘ ‘ ;53 print: mov ah,2 ;54 push ax ;55 為避免AL值改變,故存於堆疊 int 21h ;56 印出 pop ax ;57 取回AL值 dec cl jnz nxt_bit ;59 是否印下一位元 call crlf ;61 否,則印出換行及歸位字元 dec ch jnz nxt_byte;63 是否印下一位元組 jmp nxt_char;64 否,則跳到輸入按鍵 exit: ret ;66 返回DOS main endp ;67 ;--------------------------------------- crlf proc near ;69 push ax mov ah,2 mov dl,0dh int 21h mov dl,0ah int 21h pop ax ret crlf 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 來結束本程式也可以。 |
|
来自: skywood > 《Daily Study》