Ch 30 真實模式的 DOS 使用 80386 及更高階 CPU

早在西元 1985 年,英特爾就已經推出具有 32 位元暫存器的 80386 CPU,但是那時候的作業系統仍是 16 位元的 DOS,大部分的軟體仍使用 16 位元的定址與暫存器,直到十年後的西元 1995 年,微軟才推出了 Windows 95,作業系統與軟體才逐漸成為 32 位元。

在這段期間的 10 年內,甚至是到現在,市面上有關組合語言的書籍大部分仍是談論 16 位元的 8086/8088 CPU,對 80386 及更高階的 CPU 而言,實在是大材小用。事實上,微軟所出品的 MASM 5.x 組譯器可以在 16 位元的 DOS 系統中,有限制的使用 80386 的特性,可以使得程式更有效率,例如使用 32 位元的暫存器運算,使用 80386 多出的兩個段暫存器,FS 與 GS,使用特殊定址方式等等。這一章將對這些主題做介紹。


使用 32 位元暫存器做乘法運算

有許多組合語言的書籍常有這樣的作業:寫一個 32 位元的乘法。其解法常常是把 32 位元拆成兩個 16 位元,然後利用類似直式乘法相乘。但此處小木偶提出另一種解法,原始程式,MUL32.ASM,如下:

;70000*80000=5600000000 (11170h*13880h=14DC93800h)
        .386            ;02 使用 32 位元暫存器與指令集
;***************************************
code    segment use16   ;04 使用 16 位元定址方式
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
x       dd      70000   ;09 被乘數
y       dd      80000   ;10 乘數
p_low   dd      ?       ;11 積的低位址字組
p_high  dd      ?       ;12 積的高位址字組
begin:  mov     eax,x   ;13 32 位元資料轉移
        mov     ebx,y
        mul     ebx     ;15 32 位元乘法
        mov     p_low,eax
        mov     p_high,edx

        lea     bx,x    ;19 印出被乘數
        mov     ah,0
        call    print_dword

        mov     ah,2
        mov     dl,'*'
        int     21h

        lea     bx,y    ;27 印出乘數
        mov     ah,0
        call    print_dword

        mov     ah,2
        mov     dl,'='
        int     21h

        cmp     p_high,0        ;35 印出乘積
        jz      no_pnt_high
        lea     bx,p_high
        mov     ah,0
        call    print_dword
no_pnt_high:
        mov     bx,offset p_low
        call    print_dword

        mov     ax,4c00h
        int     21h
;---------------------------------------
;以十六進位數印出 BX 所指位址的四個位元組(雙字組)
;輸入:BX:要印出雙字組的位址
;    :AH非零:顯示全部32位元,AH=0:由最大數不為零開始顯示
;輸出:螢幕上顯示十六進位數
;      AX、CX、DX 會被破壞
print_dword     proc    near
        add     bx,3    ;53 由較大位數開始印
        mov     cl,4    ;54 共印出 4 個位元組
nxt:    mov     dl,[bx]
        call    pnt_dl
        dec     bx
        dec     cl
        jnz     nxt
        ret

pnt_dl: mov     ch,dl
        shr     dl,4    ;63 只有在 286 以上的 CPU 才可以使用
        call    pt0
        mov     dl,ch
        and     dl,0fh
pt0:    add     dl,'0'
        cmp     dl,'9'
        jbe     pt1
        add     dl,7
pt1:    cmp     dl,'0'  ;71 檢查是否為零
        jne     pt2
        or      ah,ah   ;73 若為零,則檢查是否已經
        jz      pt3     ;   印過最大不為零的數了
pt2:    mov     ah,1
        push    ax
        mov     ah,2
        int     21h
        pop     ax
pt3:    ret
print_dword     endp
;---------------------------------------
code    ends
;***************************************
        end     start

此程式需以組譯器組譯並轉換成 MUL32.COM 檔,才能執行。底下小木偶大致解釋此程式的運作並說明如何除錯。

80386 及其較高等的 CPU 都可以使用 32 位元的暫存器,當然也可以直接做 32 位元的四則運算以及邏輯運算,例如 ADD、ADC、OR、AND 等運算。雖然 DOS 是十六位元的作業系統,但是作業系統只掌管輸出、輸入及管理磁碟、檔案、資源等工作,並不涉及運算,所以本程式可以在 DOS 中執行。當然在撰寫原始程式時,必須做某些宣告使得組譯器能按照程式設計師的意志工作,關鍵之處就是在 .386 假指令及 segment 假指令的 use16 這兩個地方。

.386 假指令與 .386P 假指令

.386 是表示可以用 80386 CPU 的指令集,而 .386P 則是表示可以使用 80386 CPU 的特權指令集,當然 80386 CPU 的指令集包含了所有比它等級低的 CPU,所以 8088/8086/80186/80286 等 CPU 的所有指令都能在 80386 使用,並且 80386 還多出一些它們所沒有的指令,這就是所謂的向下相容。類似的假指令還有 .8088、.186、.286/286P、.8087、.287、.387,詳細情形,請自行參考英特爾的 CPU 指令集手冊。

.386 或 .386P 以及其他類似的宣告一般都是放在原始程式的開頭,但是也可以放在原始程式的其他地方,不過如果您要放在其他地方時要注意有兩個限制。第一是必須放在段與段之間,換句話說,在同一段內的指令集無法變更。第二是高階的浮點指令及不能搭配低階的 CPU 指令集,例如,如果您在程式一開始宣告 .286,而後又指定 .387,就會被組譯器認為不合法。

對於使用 32 位元運算,還有一個問題需要解決。假如指定了 .386 或 .386P 雖然可以使用較高階的指令集,但是 .386 會指定此區段將以 32 位元方式定址,此時每個段最大可以有 4GB,這種定址方式無法為真實模式所接受。假如沒有宣告 .386 的話,那麼將不能使用 32 位元運算,並且組譯器會以內定的組譯方式,即使用 8088/8086 以及 8087 指令集組譯原始程式,同時使用十六位元的定址方式。所謂十六位元的定址方式就是在第一章第二章所提到的以『段:偏移』的方式來表示位址的,這時一個段最長只能有 64KB,這種定址方式才是真實模式下所能接受的方式。

那麼魚與熊掌是否可以兼得?請看下面說明。

SEGMENT 假指令

第十一章時,曾提到 segment 假指令,到了 MASM 5.x 版之後,segment 後面其實還有一個選項,就是『使用』,其完整語法是

區段名  segment  排列型式   合併型式   類別名   使用

『使用』只能在有宣告 .386 或 .386P 假指令時才能有這個選項,『使用』只有兩個選擇,use16 或 use32,顧名思義,use16 就表示此區段使用十六位元定址方式,use32 是表示此區段用 32 位元定址方式。

32 位元的 MOV 指令

程式第 13、14 行及第 16、17 行是利用在 80386 及其較高階的 CPU 直接進行 32 位元資料轉移指令,此指令的用法和 16 位元時相同,只是暫存器或變數『長』長了,當然要在 DOS 下用這些優勢,要先宣告 .386 和 use16 這兩樣東西。

32 位元的 MUL 指令

程式第 15 行是令用在 80386 及其較高階的 CPU 直接進行 32 位元運算。以 mul 為例,當乘數是 32 位元的暫存器或變數時,會把乘積存於 EDX:EAX 堙C

可以使用 32 位元運算元的指令還有 ADD、ADC、SUB、SBB、IMUL、DIV、IDIV 等等。這些指令的用法和 16 位元時相同,只是暫存器或變數『長』長了,當然要在 DOS 下用這些優勢,要先宣告 .386 和 use16 這兩樣東西。

加強的 SHR 指令

程式第 63 行的 shr dl,4 是使 DL 之值向右移四個位元,以得到 DL 較高的四個位元。8086/8088 等級的 CPU 可以使用 shr dl,1,但是如果是連續向右移數個位元時,必須把右移位元數先放在 CL 中,再執行 shr dl,cl。這樣很麻煩,所以到了 80286 及其較高階的 CPU 已經可以直接把右移次數接在運算元之後。類似的指令還有 SHL、ROL、ROR 等。

LEA 指令

LEA 指令是用來取得位址的 8086/8088 指令,其語法是:

lea     暫存器,變數

LEA 會把變數所在之位址計算出來,並存入前面的暫存器堙C其用途有點像 offset,但是它比 offset 多點彈性。使用 offset 時,必須在組譯階段就已經確定的變數位址才可以,lea 可以在執行時才知道變數位址。

用 DEBUG/SYMDEB 載入

用 DEBUG 載入這個程式時,因為 DEBUG 無法完全認得 80386 指令,因此反組譯時顯示不太正確:( 參考註一 )

-U 100 L1 [Enter]
1784:0100 EB10          JMP     0112
-U 112 [Enter]
1784:0112 66            DB      66
1784:0113 A10201        MOV     AX,[0102]
1784:0116 66            DB      66
1784:0117 8B1E0601      MOV     BX,[0106]
1784:011B 66            DB      66
1784:011C F7E3          MUL     BX
1784:011E 66            DB      66
1784:011F A30A01        MOV     [010A],AX
1784:0122 66            DB      66
1784:0123 89160E01      MOV     [010E],DX
1784:0127 BB0201        MOV     BX,0102
1784:012A B400          MOV     AH,00
1784:012C E83100        CALL    0160
1784:012F B402          MOV     AH,02
1784:0131 B22A          MOV     DL,2A
-U [Enter]
1784:0133 CD21          INT     21
1784:0135 BB0601        MOV     BX,0106
1784:0138 B400          MOV     AH,00
1784:013A E82300        CALL    0160
1784:013D B402          MOV     AH,02
1784:013F B23D          MOV     DL,3D
1784:0141 CD21          INT     21
1784:0143 66            DB      66
1784:0144 833E0E0100    CMP     WORD PTR [010E],+00
1784:0149 0F            DB      0F
1784:014A 8408          TEST    CL,[BX+SI]
1784:014C 00BB0E01      ADD     [BP+DI+010E],BH
1784:0150 B400          MOV     AH,00
1784:0152 E80B00        CALL    0160

您可以看見上面白色的部份都不是我們的程式,甚至您還看不懂,但是沒關係,它們都可以在 DUBUG 中被執行,例如您一進入 DEBUG 下

-T [Enter]

AX=0000  BX=0000  CX=00A0  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=0112   NV UP EI PL NZ NA PO NC
1784:0112 66            DB      66

這時已經從 1784:0100 跳到 1784:0116 的 mov eax,x,雖然 DEBUG 反組譯為 DB 66 及 mov ax,x ( x 位於位址 0102 處 ),但是我們不管它,仍然再下 T 指令追蹤它:

-T [Enter]

AX=1170  BX=0000  CX=00A0  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=0116   NV UP EI PL NZ NA PO NC
1784:0116 66            DB      66

結果是 DEBUG 仍能執行,且跳到下一個正確位址,mov ebx,y,這印證小木偶前述的話,雖然顯示不正確,但仍可正確執行。底下我們直接執行到位址 143:

-G 143 [Enter]
11170*13880=
AX=023D  BX=0105  CX=8000  DX=003D  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=0143   NV UP EI PL ZR NA PE NC
1784:0143 66            DB      66

您看上面紫色部份是執行的結果。好,底下再執行兩次追蹤指令:

-T [Enter]

AX=023D  BX=0105  CX=8000  DX=003D  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=0149   NV UP EI PL NZ NA PO NC
1784:0149 0F            DB      0F
-T [Enter]

AX=023D  BX=0105  CX=8000  DX=003D  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=014D   NV UP EI PL NZ NA PO NC
1784:014D BB0E01        MOV     BX,010E
-T [Enter]

AX=023D  BX=010E  CX=8000  DX=003D  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1784  ES=1784  SS=1784  CS=1784  IP=0150   NV UP EI PL NZ NA PO NC
1784:0150 B400          MOV     AH,00

您再觀察上述藍色的位址並對照,您會發現,即使反組譯時位址錯誤,但是追蹤時還是會在正確的位址停下來。不過您在追蹤此類程式時,還是得小心,尤其是下『G』指令時。如果您用 SYMDEB 除錯,結果也是一樣,反組譯可能會錯誤,但是追蹤或執行時不會錯誤。這個程式的其他部份都不難,小木偶就不解釋了。

用 DEBUG32.EXE 除錯

雖然 DEBUG/SYMDEB 可以除錯,但是無法正確顯示 386 指令來,總是美中不足,不過幸好這一切已經圓滿解決了,那就是 DEBUG32.EXE。可到大陸的AoGo 彙編小站下載,解壓縮後就可直接使用,不須安裝。

DEBUG32 用法與 DEBUG 類似,但功能強大,除了可進行對 80386 以上的 CPU 指令集除錯外,也增加了許多比 DEBUG 更強化的功能,例如可以對讀出或寫入記憶體時設定中斷點,以不同資料形態顯示記憶體內容等等,不過底下僅就這一章用得到的部份說明。首先用 DEBUG32.EXE 載入要除錯的程式,MUL32.COM,然後按『r』鍵觀察暫存器:

E:\HOMEPAGE\SOURCE>debug32 mul32.com [Enter]
Unable to take over DPMI address query
Debug32 - Version 1.0 - Copyright (C) Larson Computing 1994

CPU = Pentium, Virtual 8086 Mode, Id/Step = 058C, A20 enabled
-r [Enter]
AX=0000  BX=0000  CX=00A0  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=2739  ES=2739  SS=2739  CS=2739  IP=0100  NV UP DI PL NZ NA PO NC
2739:0100 EB10             JMP     Short 0112
-t [Enter] →追蹤到主程式
AX=0000  BX=0000  CX=00A0  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=2739  ES=2739  SS=2739  CS=2739  IP=0112  NV UP DI PL NZ NA PO NC
2739:0112 66A10201         MOV     EAX,[0102]
Trace Interrupt

此時將要把被乘數載入 EAX 堙A但是 DEBUG32 僅僅顯示暫存器較低位的 16 位元,可以下『r32』指令,使 DEBUG32 顯示暫存器的所有位元,同理『r16』會使得 DEBUG32 僅顯示暫存器較低位的 16 位元。見下面步驟:

-r32 [Enter] →使暫存器顯示所有位元
EAX=00000000  EBX=00000000  ECX=000000A0  EDX=00000000  EBP=00000000
ESI=00000000  EDI=00000000  FS=2739  GS=2739   SS=2739  ESP=0000FFFE
DS=2739  ES=2739   CS=2739  EIP=00000112   NV UP DI PL NZ NA PO NC
2739:0112 66A10201         MOV     EAX,[0102]
-t [Enter]
EAX=00011170  EBX=00000000  ECX=000000A0  EDX=00000000  EBP=00000000
ESI=00000000  EDI=00000000  FS=2739  GS=2739   SS=2739  ESP=0000FFFE
DS=2739  ES=2739   CS=2739  EIP=00000116   NV UP DI PL NZ NA PO NC
2739:0116 668B1E0601       MOV     EBX,[0106]
Trace Interrupt

底下小木偶不詳細追蹤了,直接執行到印出被乘數。先輸入『u』反組譯,看看要執行到那一位址。

-u [Enter] →反組譯程式碼
2739:011B 66F7E3           MUL     EBX
2739:011E 66A30A01         MOV     [010A],EAX
2739:0122 6689160E01       MOV     [010E],EDX
2739:0127 BB0201           MOV     BX,0102h
2739:012A B400             MOV     AH,00h
2739:012C E83100           CALL    0160
2739:012F B402             MOV     AH,02h
2739:0131 B22A             MOV     DL,2Ah
2739:0133 CD21             INT     21h
2739:0135 BB0601           MOV     BX,0106h
2739:0138 B400             MOV     AH,00h
2739:013A E82300           CALL    0160

與原始碼配合比對,很快就知道上面白色位址,012F,就是執行完印出被乘數後的下一指令。輸入『g 12f』表示執行到 12F 這個位址:

-g 12f →由現在的 IP 位址執行到 12F 處停止
EAX=4DC90100  EBX=00010101  ECX=00007000  EDX=00000030  EBP=00000000
ESI=00000000  EDI=00000000  FS=2739  GS=2739   SS=2739  ESP=0000FFFE
DS=2739  ES=2739   CS=2739  EIP=0000012F   NV UP DI PL ZR NA PE NC
2739:012F B402             MOV     AH,02h
Instruction Breakpoint

你可能看見螢幕上暫存器值,有好幾處反白,這是正常的,原來每次停下來時, DEBUG32 會把改變的數值以反白表示。但是照理說,應該會在螢幕上顯示出被乘數才對,不過卻沒看見,這也是正常的,原來 DEBUG32 的執行畫面與除錯畫面是分開來的,這樣才不會把畫面弄得一團亂。如果要觀察執行畫面可以輸入『FL』:

E:\HOMEPAGE\SOURCE>debug32 mul32.com
Unable to take over DPMI address query
Debug32 - Version 1.0 - Copyright (C) Larson Computing 1994

11170

如果要切回除錯畫面時,按下任意鍵就可以了。FL 是 flip,即切換之意。其他還有許多指令,例如設定中斷點、離開 DEBUG32 等等都與 DEBUG/SYMDEB 相似,也可以於 DEBUG32 提示符號『-』之後按『?』得到協助,小木偶就不多說了。


在 DOS 系統中的 32 位元定址方式

在 8086/8088/80286 等 CPU 只能用 BX、SI、DI、BP、SP 等暫存器定址,但是在 80386 及其高階的 CPU 都可以使用任何的通用暫存器定址,例如 EAX、ECX、EDX 都可以用來定址。例如

        mov     bx,[eax]
        mov     bl,[edx]

不過 AX、CX、DX 等暫存器即使已經宣告 .386 後,仍不能用來定址,所以下面的寫法,仍是錯誤的:

        mov     bx,[ax]
        mov     bl,[dx]

使用這些 32 位元暫存器定址還得注意這些暫存器是 32 位元的,而在真實模式中的位址只需 16 位元,較高的 16 位元是用不著的。所以如果要使用這些 32 位元暫存器定址時,必須確定它們較高的 16 位元之值為零,否則會造成不可預期之錯誤。

80386 對暫存器定址方式還做了最佳化的努力。因為在程式堭`用字組、雙字組等資料,所以為了改善效率,在 80386 的定址方式還可以寫成像底下的模式

        mov     cx,x[eax*n+y]
        mov     cx,x[ebx+eax*n+y]

上兩式的 n 可以是 1、2、4、8 四種數值,x 是變數 ( 陣列 ) 名,y 是常數。在這種定址方式堙A有『*n』的暫存器稱為索引暫存器,剩下的另一個暫存器稱為基底暫存器;假如沒有『*n』的運算,則寫在後面的是索引暫存器,前面的是基底暫存器;假如只有一個暫存器,則此暫存器是基底暫存器。用這種方式定址時,內定的段暫存器是由基底暫存器決定,基底暫存器為 EBP 時內定的段暫存器為 SS,否則均為 DS。請看以下例子:

        mov     ax,x[eax*2+ebp]     ;取得位址 SS:[EAX*2+EBP+x] 之值並存於 AX
        mov     ax,x[eax+ebp*2]     ;取得位址 DS:[EAX+EBP*2+x] 之值並存於 AX
        mov     ax,x[ebp*1]         ;取得位址 SS:[EBP] 之值並存於 AX
        mov     ax,x[ebx*1]         ;取得位址 DS:[EBX] 之值並存於 AX

以上四個例子都沒指定段暫存器,但是有時是自 DS 取得,有時是自 SS 取得,不可不慎。假如您想改變存取的段位址也可以,就是在位址前加上『段暫存器:』即可,例如您想取得位址 FS:[EDX*2] 的數值,可以用下面的方法:

        mov     ax,fs:[edx*2]

底下小木偶撰寫一個程式,ADDR32.ASM,此程式可以在螢幕上印出一個彩色字串,它除了利用 ECX、EDX 定址外,還用了一個 80386 新的段暫存器,GS,定址。

;***************************************
stack   segment stack   ;02 堆疊段
        dw      80h dup (?)
stack   ends
;***************************************
data    segment         ;06 資料段
string  db      2 dup ('0123456789ABCDEF'),0
data    ends
;***************************************
        .386            ;10 使用 80386
code    segment use16   ;11 使用 16 位元定址方式
        assume  cs:code,ds:data
;---------------------------------------
start:  push    ds      ;14 程式碼段開始
        push    0       ;15 推入立即值
        mov     ax,data
        mov     ds,ax

        mov     ax,0b800h
        mov     gs,ax   ;20 使 GS 指向顯示記憶體

        sub     edx,edx ;22 EDX 指向顯示記憶體
        mov     bh,0    ;23 BH 為字的顏色或底色
        mov     ecx,edx ;24 ECX 指向字串位址
        mov     cx,offset string

next:   mov     al,[ecx]
        mov     ah,bh
        and     ah,0fh  ;29 使顏色在 0∼0fh 之間,底色為黑色
        mov     gs:[10*160+edx*2],ax    ;30
        shl     ah,4    ;31 使文字為黑色,底色在 0∼0fh 之間
        mov     gs:[11*160+edx*2],ax    ;32
        inc     bh
        inc     edx
        inc     ecx
        or      al,al
        jnz     next

        mov     ax,4c00h
        int     21h
;---------------------------------------
code    ends
;***************************************
        end     start

組譯這個程式,以及執行畫面如下:

ADDR32 執行畫面

綜觀這個程式,都沒有發現任何在螢幕上印出字來的服務程式,但是卻真的印出字來,不僅如此,還是彩色的,這是怎麼做到的呢?

VGA/AGP 顯示記憶體位址

原來 IBM PC 及其相容機種執行 DOS 作業系統時,所顯示的內容都存在一塊稱為顯示記憶體 ( 也有人稱為視訊記憶體 ) 的記憶體內,所以如果我們直接改變記憶體內容,就可以改變顯示的文字。而這些 PC 機種對顯示模式分成文字模式與繪圖模式兩種模式,文字模式把螢幕分成 80*25 或 40*25 兩種。

常用的是 80*25 的文字模式,此模式每列可顯示 80 個文字總共 25 列 ( 由左而右稱為列,由上而下稱為行 ),所以一個畫面可以顯示 2000 個文字,用去 2000 個位元組,再加上每個文字都用一個位元組來表示顯示的顏色。這樣計算起來,在 80*25 文字模式下,顯示一個畫面需 4000 位元組,約 4KB 的記憶體。

對彩色顯示卡 ( CGA/EGA/VGA/AGP ) 來說,顯示記憶體由段位址 0B800H 處開始,螢幕上第零列第零行的文字在位址 B800:0000 處,其顏色在 B800:0001,第零列第一行在 B800:0002,其顏色在 B800:0003……。所以每一列需佔用 160 個位元組,故第一行便從位址 B800:00A0 開始 ( 160d=0A0h ),依此類推,第 R 列的第零個字是從位址 160*(R-1) 開始。請參考下圖:

顯示記憶體位址說明圖
上圖中的每一個黃色框框是表示螢幕上一個字元,圖上左緣及上緣有第幾行第幾列,而框框內的白色數字就是該行該列在顯示記憶體的位址,圖中的位址皆以十六進位表示。最後再看看第 24 列第 79 行的位址是 0F9E,其顏色放在位址 0F9F,從 0H 到 0F9FH 共 0FA0H 位元組,十六進位的 0FA0 即十進位的 4000。

至於所顯示字元的顏色是由比顯示記憶體高一個位址長度一個位元組的內容來決定。此位元組較高的四個位元決定背景顏色,較低的四個位元決定字的顏色。顏色如何表示請參考第十九章的說明

原始程式的第 22∼25 行是用來設定要顯示的字串位址、顯示記憶體位址及顏色出始值,第 27∼29 行取得字及顏色填入 AX 堙A第 30 行把 AX 之值填入顯示記憶體中,就能在螢幕上顯示帶有顏色的字來。

PUSH 數值

在 8088/8086 CPU 堙A只能把變數或暫存器推入堆疊,但在 80186 等級以上的 CPU 也可以直接使一個數推入堆疊。所以本來一進入程式時要保留原 DS:0000 的指令,稍作修改以增進效率,變成原始程式 14∼17 行。除此之外,在 80386 等級以上的 CPU 還可使用 PUSHA/PUSHAD 指令。

PUSHA/PUSHAD 與 POPA/POPAD 指令

PUSHA 指令是把 AX、BX、CX、DX、SP、BP、SI、DI 八個暫存器一次全部推入堆疊,您注意到它也可以把原來的 SP 推入堆疊,所謂原來是指還沒執行 PUSHA 時的 SP。POPA 是彈出堆疊頂的 8 個字組,並依序存入 DI、SI、BP、SP、DX、CX、BX、AX 堙C您可以把 PUSHA/POPA 的『A』想成是 all。

PUSHAD 是把 EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI 八個暫存器一次全部推入堆疊,同理 POPAD 是把堆疊堛漱K個雙字組彈出,並依序存入 EDI、ESI、EBP、ESP、EDX、ECX、EBX、EAX 堙APUSHAD/POPAD 堛滿yD』是指雙字組之意。


80386 其他新增指令

底下再介紹幾個 80386 新增的指令:

BT 指令

這個指令稱為「bit test」,位元測試之意,語法為:

        BT      變數或暫存器,暫存器
        BT      變數或暫存器,立即值

這兩種語法的「變數或暫存器」稱為目的運算元,後面的暫存器或立即值稱為「來源運算元」。BT 指令是在目的運算元中選定一個位元,將它複製到進位旗標,而所選定的是第幾個位元,由來源運算元指定。執行完後,原來的目的運算元與來源運算元的值都不變。在第一個語法中,目的運算元與來源運算元的長度,都要相同,可以都是 16 位元,或都是 32 位元。在第二個語法堙A變數或暫存器可以為 16 位元或 32 位元,而立即值最多只能是 8 個位元的長度 ( 事實上 8 個位元能表示的數,可從 0 到 255,早已能處理 16 位元或 32 位元的變數或暫存器了)。例如:

        mov     edx,1001b
        bt      edx,3

上面兩條指令執行完,EDX 仍為 1001B,而進位旗標會被設定,亦即進位旗標為 1,在 DEBUG 媗膆 CY,這是因為 EDX 的第三個位元為 1 ( 紅色的「1」),依照慣例,第幾個位元都是從 0 開始。

BTC 指令

這個指令稱為「bit test and complement」,位元測試並做補數之意,其語法是:

        BTC     變數或暫存器,暫存器
        BTC     變數或暫存器,立即值

BTC 指令是在目的運算元中選定一個位元,將它複製到進位旗標,並使該位元做補數運算 ( 如果原來該位元為 1,就變成 0;如果原來該位元為 0,就變成 1 ),而所選定的是第幾個位元,在來源運算元指定。目的運算元和來源運算元的限制,和 BT 指令一樣。例如:

        mov     edx,1001b
        btc     edx,3
        mov     ecx,1001b
        btc     ecx,2

經過前兩道指令後,EDX 變為 1,進位旗標被設定。經過後兩道指令後,ECX 變為 1101B,進位旗標被清除 ( 亦即進位旗標變為 0,在 DEBUG 顯示 NC )。

BTR 指令

BTR 是「bit test and reset」之意,意即位元測試並清除,其語法是:

        BTR     變數或暫存器,暫存器
        BTR     變數或暫存器,立即值

BTR 指令會在目的運算元中選定一個位元,將它複製到進位旗標,並清除該位元,使之變為 0,而所選定的是第幾個位元,在來源運算元指定。目的運算元和來源運算元的限制,和 BT 指令一樣。例如:

        mov     edx,1001b
        btr     edx,3
        mov     ecx,1001b
        btr     ecx,1

經過這兩道指令後,EDX 變為 1,進位旗標被設定。再經過後兩道指令,ECX 變為 1001B,進位旗標被清除。

BTS 指令

BTS 是「bit test and set」之意,意即位元測試並設定,其語法是:

        BTS     變數或暫存器,暫存器
        BTS     變數或暫存器,立即值

BTS 指令會在目的運算元中選定一個位元,將它複製到進位旗標,並設定該位元,( 設定該位元的意思是,使該位元變為 1 ),而所選定的是第幾個位元,在來源運算元指定。目的運算元和來源運算元的限制,和 BT 指令一樣。例如:

        mov     edx,1001b
        bts     edx,2

經過這兩道指令後,EDX 變為 1101B,進位旗標被清除。


註一:前置碼 ( 又叫指令字首,Instruction Prefixes )

這一章,提到了即使是在 DOS 這種 16 位元的作業系統中,也能夠一次處理 32 位元的資料,當然您得有 80386 或比它更高等級的 CPU,小木偶也已實作了這種程式。在前面我們以 DEBUG/SYMDEB 除錯這一類的程式時,會發現似乎這使用到 EAX 等 32 位元的暫存器或運算元時,前面都會有一個位元組,66H,是 DEBUG/SYMDEB 不認識的。這個位元組是什麼呢?

當我們在原始碼中,如果沒有定義「.386」之類的假指令,那麼 MASM 會假定這個程式是要在 16 位元的環境中,例如 DOS,執行的,所有的暫存器、運算元、位址等皆為 16 位元,如遇「MOV AX,1234H」,則會被編成「B8 34 12」,也就是說「MOV AX,」被 MASM 組譯成 B8,而運算元「1234H」組譯為「34 12」。如果在原始碼中定義了「.386」之類的假指令,那麼 MASM 會假定此程式會在 32 位元的環境中,例如 Windows 9x/Me/NT/XP 等,執行的,所有的暫存器、運算元、位址等皆為 32 位元,如遇「MOV EAX,1234H」,仍然會被編碼成「B8 34 12 00 00」,「MOV EAX,」仍被 MASM 組譯成 B8。

以上所說的是一般情形。不過在這一章堙A小木偶在原始碼堜w義「.386」,但是又在區段定義中使用「USE16」,表示可以使用 32 位元的暫存器、運算元等,卻又為 16 位元的程式。這時 MASM 會在編成機械碼時,內定為 16 位元的運算元、暫存器,故所組譯出來的「B8」視為「MOV AX,」。如果是使用 32 位元的暫存器、運算元,會自動在前面加上前置碼,66H,以視區別。例如,遇到「MOV EAX,1234H」這類指令,會被編碼為「66 B8 34 12 00 00」,表示此處的暫存器、運算元為 32 位元。當 CPU 執行到這裡,因為 CPU 知道此時是在 16 位元中,遇到前置碼 66H,就知道此時的暫存器、運算元為 32 位元。

反過來,如果在 32 位元的環境中,MASM 會內定使用 32 位元的暫存器、運算元等,當然也可以做 16 位元的運算,例如「MOV AX,1234H」,MASM 在編碼時,也會自動加上 66H 作為前置碼。因此,即使「MOV AX,」、「MOV EAX,」雖然在原始碼中均被組譯為「B8」,卻不會混淆不清。

講了這麼多,小木偶就是一句話。我們在原始碼中定義的區段,決定了這個區段內定為 16 位元或 32 位元,如果要在此區段內使用不同位元的暫存器、運算元等,就會被組譯器加上前置碼。在這章堙A小木偶使用完整的區段定義,如下表左邊的程式,第一是要宣告「.386」假指令,並在定義區段時使用「USE16」;也可以使用簡易區段定義,如下表右邊,先宣告「.MODEL」,再宣告「.386」:

        .386
;***************************************
code    SEGMENT USE16
        ASSUME  cs:code,ds:code
        ORG     100h
;---------------------------------------
main    PROC    FAR
        mov     eax,4c00h
        mov     ax,4c00h
        int     21h
main    ENDP
;---------------------------------------
code    ENDS
;***************************************
END     main
        .MODEL  TINY
        .386
        .CODE
        ASSUME  cs:@code
;---------------------------------------
main    PROC    FAR
        mov     eax,4c00h
        mov     ax,4c00h
        int     21h
main    ENDP
;***************************************
END     main

這兩種方法所得的程式碼皆相同,組譯、連接並轉變成 COM 檔後,以 DEBUG32 載入觀察,得到下面畫面:

Debug32 - Version 1.0 - Copyright (C) Larson Computing 1994

CPU = ?86, Virtual 8086 Mode, Id/Step = 0F10, A20 enabled
-u [Enter]
291C:0100 66B8004C0000     MOV     EAX,00004C00h
291C:0106 B8004C           MOV     AX,4C00h
291C:0109 CD21             INT     21h
291C:010B 0000             ADD     [BX+SI],AL
291C:010D 0000             ADD     [BX+SI],AL

當然,如果您不打算使用 80386 指令,作 32 位元的運算,那麼就不須宣告「.386」,那麼程式中,就無法出現像「EAX」這種 32 位元的暫存器或運算元了。

在 Windows 9x/NT/XP/Vista 等 32 位元的作業系統中,區段的設定方式,必須先宣告「.386」,再宣告「.MODEL」,這樣 MASM 就會以 32 位元的方式組譯,遇到「MOV EAX,」就編碼成「B8」。在 Windows 作業系統中,難免也會遇到「MOV AX,」這類指令,MASM 會將其編碼為「66 B8」。


回到首頁到第二十九章到第三十一章