Ch 37 保護模式

在談了許多在真實模式 ( real mode ) 下的程式設計之後,小木偶打算談談保護模式 ( protected mode )。這兩種模式的差別在於可使用的記憶體多寡。在民國 70 年 ( 也就是西元 1981 年 ) 時,使用 8088 為 CPU 的 IBM PC 剛推出時,主流電腦上的記憶體多為 64KB,而 8088 有 20 條位址線,每條位址線可以以高電壓或低電壓表示 0 或 1,因此只能表示 220 個位址,也就是說 8088 能存取 1MB 以內的記憶體。在那個時候,這是很大的記憶體,但是很快就不夠用了。繼起的 80286 有 24 條位址線,因此最多能定址 224 個不同位址,亦即 16M 個位元組。80286 有兩種工作模式:真實模式與保護模式。這兩種模式定址的方式不同,所以能存取的記憶體多寡也不同。在真實模式時,80286 仍然只能存取 1MB 以內的記憶體,但是速度比 8088 快多了,軟體也不須修改就能直接執行,能達成與 8088 向下相容的目的。如果 80286 切換到保護模式時,就能存取最多 16MB 的記憶體。雖然 80286 定址空間是 8086/8088 的 16 倍,但是仍然不夠。想想,如今一首 MP3 壓縮的歌曲,就要數個 MB 了。到了民國 74 年 ( 西元 1985 年 ),英特爾發售新一代的 CPU,80386,把位址線擴充為 32 條,可以存取 232 個位元組,亦即 4GB,大約足夠應付一般需要了,80386 的保護模式也與 80286 的保護模式不太相同,可以定址到 4GB。以後英特爾所發售的 CPU,一直到 Pentium 4 的部份型號為止,其定址方式都與 80386 相同,這些 CPU 架構相似,被稱為「80x86」、「x86」或「IA32」( 不過 IA32 的命名會出現麻煩,因為它也包括 16 位元的 CPU )。硬體,諸如 CPU 已進入 32 位元保護模式時代,而作業系統進入保護模式,仍有一段很長的路。

在 80286 已經普及的時候 ( 民國 72 年左右 ),常用的作業系統是 MS-DOS 3.x,MS-DOS 3.x 是在真實模式下執行的系統,那時候的記憶體也很貴,大約每 MB 要價一萬台幣,再加上那時作業系統並非圖形界面,而是文字導向,因此對記憶體需求不大,所以似乎不覺得使用超過 1MB 的記憶體有迫切的需要。這種情形一直到西元 1990 年代初期,Windows 等圖形界面的作業系統發展後,才逐漸改觀。到了西元 1995 年,微軟發售 Windows 95 之後,Windows 才真正進入保護模式。

英特爾所出品的 CPU 只有在 80286 或更高等級的 CPU 才有保護模式,但是 80286 與 80386 的保護模式不太相同,80286 保護模式的偏移位址仍使用 16 位元,因此每個區段仍然有 64KB 的限制,這會導致處理大量資料時的複雜性,而 80386 保護模式的偏移位址採用 32 位元,因此減低了這個缺點。再加上現今大概沒人仍然使用 80286 了,所以底下介紹的是以 80386 保護模式為主。在 80386 以後的 CPU,如 80486、Pentium 直到現在 ( 民國 102 年,西元 2013 年 ),保護模式仍然存在,只是現在的作業系統,從 Windows 9x 到 Windows 7,都是一開機,就切換到保護模式,讓使用者察覺不出來。

由真實模式進入保護模式後,除了可以使用超過 1MB 記憶體之外,保護模式還對記憶體做到「保護」的責任。意思是在保護模式下,程式無法任意存取其他區段的資料,所以各個程式之間不會互相干擾或破壞。如果有一個設計不好的程式進入無窮迴圈,使用者只需把這個程式關閉即可,不會影響其他程式,也不需要因這個程式當掉而重新啟動電腦。


進入保護模式的原始碼,PM1.ASM

小木偶將在下面幾章堙A介紹保護模式。小木偶先實作一個真實模式切換到保護模式的程式為例子。底下的程式碼是由真實模式切換到保護模式:

                .386P           ;001

desc            STRUC
limit_l         DW      0       ;區段邊界(BIT0-15)
base_l          DW      0       ;區段位址(BIT0-15)
base_m          DB      0       ;區段位址(BIT16-23)
attributes      DB      0       ;區段屬性
limit_h         DB      0       ;區段邊界(BIT16-19)(含區段屬性的高4位)
base_h          DB      0       ;區段位址(BIT24-31)
desc            ENDS    0       ;10

pdesc           STRUC
limit           DW      0       ;全域描述器表格大小
base            DD      0       ;32位元基底位址
pdesc           ENDS

jmp2pm          MACRO   s,o
                DB      66h,0eah;操作碼
                DW      o,0     ;32位元偏移位址
                DW      s       ;區段值或區段選擇子
                ENDM            ;021

;*******************************************************************************
code    SEGMENT USE16
        ASSUME  cs:code,ds:code
        ORG     100h
;-------------------------------------------------------------------------------
start:  jmp     SHORT begin
ALIGN           8
gdt             LABEL   BYTE        ;030                ;全域描述器表格
dummy           desc    <0,0,0,0,0,0>                   ;空描述器
pm_code         desc    <pm_c_len-1,0,0,98h,0,0>        ;程式碼區段描述器
pm_vedio        desc    <0ffffh,8000h,0bh,92h,0,0>      ;顯示記憶體區段描述器
pm_data         desc    <pm_d_len-1,0,0,92h,0,0>        ;資料區段描述器
gdt_len         =       $-gdt                           ;全域描述器表格的大小
gdt_ptr         pdesc   <gdt_len-1,0>                   ;全域描述器表格資料
code_selector   =       pm_code-gdt                     ;程式碼區段選擇器
video_selector  =       pm_vedio-gdt                    ;顯示記憶體區段選擇器
data_selector   =       pm_data-gdt                     ;資料區段選擇器
;------------------------------------040----------------------------------------
begin:  sub     eax,eax             ;041
;設置程式碼區段描述器
        mov     ebx,eax
        mov     ax,cs
        mov     bx,OFFSET pm
        shl     eax,4
        add     eax,ebx             ;程式碼區段偏移位址為 0
        mov     pm_code.base_l,ax
        shr     eax,10h
        mov     pm_code.base_m,al   ;050
        mov     pm_code.base_h,ah

;設置資料區段描述器
        sub     eax,eax
        mov     ebx,eax
        mov     ax,cs
        mov     bx,OFFSET pm_data_seg
        shl     eax,4
        add     eax,ebx
        mov     pm_data.base_l,ax   ;060
        shr     eax,10h
        mov     pm_data.base_m,al
        mov     pm_data.base_h,ah

;填入正確的數值到 gdt_ptr 
        sub     eax,eax
        mov     ebx,eax
        mov     ax,cs
        mov     bx,OFFSET gdt
        shl     eax,4               ;070
        add     eax,ebx             ;計算 GDT 位址,存於EAX
        mov     gdt_ptr.base,eax    ;EAX=CS×4+(OFFSET gdt)
;載入 GDT
        lgdt    QWORD PTR gdt_ptr   ;若用 MASM 6.x 組譯,改用「lgdt gdt_ptr」

;關中斷
        cli

;開啟 A20 地址線
        in      al,92h              ;080
        or      al,00000010b
        out     92h,al

;準備切換到保護模式
        mov     eax,cr0
        or      eax,1
        mov     cr0,eax

;真正進入保護模式
        jmp2pm  code_selector,0     ;090

;以下到 pm_len 是在保護模式中執行的程式碼區段
pm:     mov     cx,video_selector
        mov     es,cx
        mov     cx,data_selector
        mov     ds,cx
        mov     esi,OFFSET string-OFFSET pm_data_seg
        mov     edi,(80*10+0)*2     ;螢幕第 10 列,第 0 行。
        mov     ah,0eh              ;黑底黃字
next:   lodsb                       ;100
        cmp     al,0
        jz      exit
        stosw
        jmp     next

exit:   jmp     $                   ;到此停止
pm_c_len        EQU     $-pm
;保護模式中的程式碼區段結束
;以下到 pm_d_len 是在保護模式中的資料區段
pm_data_seg:                        ;110
string          DB      'In protected mode.',0
pm_d_len        EQU     $-OFFSET pm_data_seg
;保護模式中的資料區段結束
;-------------------------------------------------------------------------------
code            ENDS
;*******************************************************************************
END     start

利用文書處理軟體,輸入上述原始碼後,存成 PM1.ASM 檔,然後開啟 Virtual PC ( 參考附錄十一 ),依照下面綠色框框的指令組譯、連結,別忘了每行輸入指令後要按「Enter」鍵:

組譯、連結成功後,輸入紫色框框的「pm1」,執行 PM1.COM 後,會看見在螢幕第 10 列,印出一行黃色的字串「In protect mode.」( 紅色框框內 ) 後,就當機了,這是因為在第 106 行的程式碼跳進了無窮迴圈中,故而當機;不過我們的程式確實是進入了保護模式。底下我們來看看 PM1.ASM 的原始碼。


進入保護模式:解說 PM1.ASM

區段描述器 ( Segment Descriptor ) 與區段描述器表格 ( Segment Descriptor Table )

PM1.ASM 的第 3 行到第 21 行定義了兩個結構體和一個巨集,其中一個結構體稱為 desc,它是由 8 個位元組所組成的,它也是保護模式的主角,這個結構體有個特別的名字,稱為「區段描述器」( segment descriptor,或簡稱描述器,大陸稱為描述符 )。第 30 行到 35 行,依據區段描述器,desc,結構體定義了四筆資料,這個四筆資料好比是一張表格,您也可以想成是一個陣列,這張表格稱為「區段描述器表格」( segment descriptor table,或簡稱描述器表格,大陸稱為描述符表格 )。到此為止,我們可以瞭解,區段描述表格是由一個一個的區段描述器所構成,每個區段描述器又由 8 個位元組所組成。

「區段描述器」,顧名思義,就是用來描述保護模式之下的區段,不過,在保護模式下和真實模式下,區段的意義是不同的,雖然兩者都稱為「segment」( 中文也都稱為「區段」,也有人稱為「節區」或「節」)。在真實模式下,每個區段最大只能有 64K 位元組;而程式可以讀取任意區段的資料,也能把任意資料寫入任意區段中;此外,位址的表示方式是以「區段:偏移位址」表示,如果您想獲得實體位址 ( physical address ),只需依照下面的公式:

實體位址=區段×16+偏移位址

就可以得到。( 對 32 位元的 CPU 而言,電腦主機板上所組成記憶體晶片堙A有許許多多的記憶體空間,每個空間被畫分為一個位元組的大小,由 0 開始編號一直到 4G 為止,每個標號代表一個位元組,這個編號就叫實體位址或絕對位址 )

在保護模式下,每個區段最大可以有 4G 位元組,當然也可以小於 4G 位元組,並且程式無法任意讀取或寫入其他區段,為了達到這些要求,顯然必須要有一塊記憶體記載著每個區段的資料,這些資料包含區段起始位址、區段大小、可以讀取或寫入的屬性等等,這樣的一塊記憶體就是前面所說的「區段描述器」,我們又把所有的區段描述器都集中起來,形成一個表格,就是「區段描述器表格」。我們先來看看「區段描述器」。「區段描述器」是由連續的 8 個位元組 ( 亦即 64 位元 ) 組成的記憶體,其內容如下:

上圖所標示的 0∼15 位元和 48∼51 位元,共 20 個位元 ( 淡藍色部份 ) 表明了區段的邊界 ( limit )。因為任何區段的開始位址都是由編號 0 的位址開始,每個位元組使用一個編號,邊界的意思就是表示這個區段最多可以使用到編號第幾個,意即到這個區段的「邊界」了。例如,有一個區段的區段描述器的邊界為 100,那表示這個區段由位址 0 開始到 100,如果把一個位元組的數值放到編號 100 的位址,就到達邊界了,但不能放到編號 101 的位址,故這個區段大小為 101 個位元組。也就是說,邊界與區段大小相差一個位元組,而剛剛所說得位址編號,就叫做「偏移位址」。

而第 16∼39、56∼63 位元,共 32 位元 ( 綠色部份 ) 表明了區段的基底位址 ( base ),基底位址就是區段由實體位址的哪一個地方開始。例如,如果基底位址是 30000H,那麼表示這個區段是由實體位址 30000H 開始,而區段的邊界就是區段大小減一。您可能會覺得奇怪,為什麼不把區段的基底位址擺在一塊,而要分開呢?這其實是為了與 286 保護模式相容所採取的不得已措施,因此造成程式的複雜性,魚與熊掌不可兼得,這也是沒辦法的事。第 40∼47 位元、第 52∼55 位元,共 12 個位元,則是表明此區段的屬性,區段的屬性較為複雜,稍後再細說。現在我們先來看看區段描述器的基底位址及大小如何使用。

在保護模式中,像 CS、DS、ES 等區段暫存器是用來指向「區段描述器表格」中的某個「區段描述器」,所以也可以說區段暫存器是某個「區段描述器」的指標或索引。例如,在保護模式中,CS 暫存器就應該指向程式碼區段的「區段描述器」,而這個程式碼區段的「區段描述器」內基底位址就是程式碼區段的起始位址,而此「區段描述器」內的邊界就是程式碼區段的大小減一。由 PM1.ASM 的第 31∼34 行看來,得知 PM1 進入保護模式後有四個區段描述器,dummy、pm_code、pm_vedio、pm_data,分別是空的區段描述器、程式碼區段描述器、顯示記憶體區段描述器、資料區段描述器。這四個區段描述器集中起來,就形成一個「區段描述器表格」。在進入保護模式之前,要先把這四個「區段描述器」的所有資料都設好。空的區段比較特別,以後再說,反正大部分情形下,「區段描述器表格」堛熔臚@個「區段描述器」為空的區段描述器。PM1.ASM 堛熔臚G個區段描述器是 pm_code,看它的名字,就知道它是保護模式下程式碼區段的「區段描述器」。PM1 在保護模式執行時,只把「In protect mode.」字串印在螢幕上,其原始碼位於第 92∼108 行,如下面所示:

;以下到 pm_len 是在保護模式中執行的程式碼區段
pm:     mov     cx,video_selector
        mov     es,cx
        mov     cx,data_selector
        mov     ds,cx
        mov     esi,OFFSET string-OFFSET pm_data_seg
        mov     edi,(80*10+0)*2     ;螢幕第 10 列,第 0 行。
        mov     ah,0eh              ;黑底黃字
next:   lodsb                       ;100
        cmp     al,0
        jz      exit
        stosw
        jmp     next

exit:   jmp     $                   ;到此停止
pm_c_len        EQU     $-pm
;保護模式中的程式碼區段結束

雖然程式碼區段僅短短十幾行,所佔位元組也不大,但是仍是一個區段,要為它準備一個區段描述器。所以在 pm_code 描述器內的基底位址需填入 pm: 標號的實體位址,其實體位址可以由「區段:偏移位址」得到。因為在進入保護模式之前,CPU 仍處於真實模式,所以實體位址就是「區段×16+偏移位址」,原始碼的第 41∼51 行 ( 下面的程式碼 ) 就是計算實體位址,及把實體位址填入程式碼區段描述器的過程:

begin:  sub     eax,eax             ;041
;設置程式碼區段描述器
        mov     ebx,eax
        mov     ax,cs
        mov     bx,OFFSET pm
        shl     eax,4
        add     eax,ebx             ;程式碼區段偏移位址為 0
        mov     pm_code.base_l,ax
        shr     eax,10h
        mov     pm_code.base_m,al   ;050
        mov     pm_code.base_h,ah

至於程式碼區段的長度則在第 107 行,由組譯器計算出來,並於第 32 行直接填入:

pm_code         desc    <pm_c_len-1,0,0,98h,0,0>        ;程式碼區段描述器

第三個區段描述器是用來指向顯示記憶體的區段,這是為了在進入保護模式後,直接把字串填入顯示記憶體內,就可以把字串顯示在螢幕上了。顯示記憶體是在 0B8000H 的地方,大小是 64K 位元組,因此我們在程式碼第 33 行直接定義:

pm_vedio        desc    <0ffffh,8000h,0bh,92h,0,0>      ;顯示記憶體區段描述器

第四個區段描述器是用來指向資料區段的,其實體位址的計算方式和程式碼區段計算方式相同。小木偶就不再贅述,請自行參考原始碼。

全域描述器表格 ( GDT )

進入保護模式後,我們把 DS、ES 暫存器分別指向 pm_data、pm_vedio,CPU 就可以找到相對應的區段描述器。但是剛才講過,DS、ES 等區段暫存器是一個索引值,是相對於區段描述器表格的索引。也就是說,DS、ES 等區段暫存器必須以某個位址為基準,而這個位址就是區段描述器表格的位址。那麼 CPU 又是如何得知區段描述器表格的實體位址呢?答案就在 80386 CPU 新增的一個暫存器,稱為 GDTR ( global descriptor table register,全域描述器表格暫存器 )。事實上區段描述器表格分為好幾種,全域描述器表格只是其中的一種,其他還有好幾種,但是以後再細說吧。GDTR 專門用來記錄全域描述器表格所在位址,載入此位址到 GDTR 暫存器的方法是用新的 80386 指令,LGDT。

LGDT 指令

LGDT 指令是 80286 以上才有的指令,其功能是把記憶體內的全域描述器表格的資料載入到 GDTR 暫存器堙C全域描述器表格的資料是一個 6 位元組長的結構體,PM1.ASM 命名為 pdesc,當然您可以依喜愛自行命名,並無硬性規定。此結構體有兩個欄位:第一個欄位長一個字組,是全域描述器表格的大小,以位元組為單位;第二個欄位長一個雙字組,是全域描述器表格的位址。如下:

pdesc           STRUC
limit           DW      0       ;全域描述器表格大小
base            DD      0       ;32位元基底位址
pdesc           ENDS

LGDT 語法為

LGDT    「pdesc 位址」

假如您用 MASM 5.x 組譯,必須使用「LGDT QWORD PTR pdesc位址」,否則會發生警告,但是其實載入到 LGDT 的位元組應為 6 個位元組,這是 MASM 5.x 的錯誤,不過不影響程式實際運作,參考微軟的網頁。MASM 6.x 已修正這個錯誤,不需要指定位元組長度。在以 LGDT 指令載入 pdesc 內的資料到 GDTR 暫存器前,一樣要先把正確的資料填入 pdesc 結構體,其程式碼在第 65∼74 行,請自行參考。

開啟 A20 位址線

雖然要進入保護模式不一定要開啟 A20 位址線,但是若不開啟它,則無法讀寫超過 1MB 以上的記憶體。什麼是 A20 位址線,又為什麼要開啟 A20 位址線才能讀寫超過 1MB 以上的記憶體呢?原來 8086/8088 只有 20 條位址線,從編號 AD0∼AD7、A8∼A19,可以定址 220 位元組,也就是 1MB,但是 8086/8088 的暫存器只有 16 位元,無法表示這麼多的位址,因此設計 8086/8088 的工程師想到一個方法,用兩個 16 位元的暫存器分別代表「區段」、「偏移位址」,並以「區段:偏移位址」表示。用這樣的表示方式,解決了以 16 位元的暫存器表示 20 位元的位址。但是這樣做會產生一個不合理的現象,假如有個指令讀取位址「FFFF:FFFF」的內容,位址「FFFF:FFFF」其實是實體位址 10FFEF,也就是 1114095,已經超過 8086/8088 20 根位址線的定址範圍了,不過「FFFF:FFFF」卻符合「區段:偏移位址」表示位址的規則。工程師也很快的解決了這個問題,那就是存取超過 1MB 的位址時,CPU 會自動先減去 1MB。也就是說,當存取類似「FFFF:FFFF」這種位址時,其實是存取「0000:FFEF」位址的內容。這就是所謂的回繞 ( wrap-around )。

後來的 80286 共有 24 條位址線,可定址 16MB,所以存取類似「FFFF:FFFF」這種位址時,有可能是真的要存取實體位址 10FFEF ( 保護模式下 );但是又希望 80286 在真實模式時能向下相容。為了解決這個問題,工程師們又想出一個方法,那就是在真實模式時,A20 位址線是是關閉的,迫使 CPU 發生回繞效果;當進入保護模式時,才開啟 A20 位址線,以便讀取 1MB 以上的記憶體。( 參考註一可閱讀更多的資料 )

一般開啟 A20 位址線的方法是利用 92H 埠,92H 埠位於南橋晶片上,將其第一個位元設為「1」,就能開啟 A20 位址線;若設為「0」,就能關閉。程式碼如下:

;開啟 A20 地址線
        in      al,92h              ;080
        or      al,00000010b
        out     92h,al

CR0 控制暫存器的 PE 位元

80286 及其較高級的 CPU,新增了一些控制暫存器:CR0、CR1 等等。其中 CR0 暫存器的第 0 位元叫做 PE 位元,英文稱為 protection enabled,意即「保護模式啟動」。當此位元為「0」時,CPU 處於真實模式;當此位元為「1」時,CPU 處於保護模式。

因此只要將 PE 位元變為「1」,CPU 就處於保護模式了,這段程式碼在第 84∼87 行,如下面程式片段。當然如果僅僅把這個位元變成「1」,而沒有設置正確的「區段描述器表格」,或者區段暫存器沒有指向正確的「區段描述器」,那麼電腦是會當機的。所以由真實模式切換到保護模式,不是僅僅把 PE 位元變成「1」就可以了。

;準備切換到保護模式
        mov     eax,cr0
        or      eax,1
        mov     cr0,eax

特殊的跳躍

到此已是萬事具備,只欠東風,距離進入保護模式僅一步之遙,執行程式碼的第 90 行就進入保護模式了。程式第 90 行,是一個巨集指令,jmp2pm。由真實模式進入保護模式,不僅僅 DS、ES 等區段暫存器需指向相對應的區段描述器,CS 暫存器也是如此。DS、ES 等暫存器可以用 MOV 指令,把適當的數值載入;但是 CS 無法這樣做,只能以 JMP、Jx、CALL、RET 等跳躍指令改變其值。在此處,小木偶採用 JMP 指令,所以此處的指令為「jmp code_selector:0」。但是,此處還面臨一個問題。在保護模式堙A偏移位址是以 32 位元表示,而真實模式堳o是 16 位元,因此這個跳躍指令應該是要跳躍到偏移位址「00000000」處,而不是「0000」;而這個指令又必須在 16 位元的區段,等執行跳躍後,才會變成 32 位元區段。換句話說,這個跳躍指令的程式碼是個很特別的程式碼,它混合了 16 位元與 32 位元。MASM 似乎是無法組譯,所以一般的做法是直接填上機械碼,就如同下面,以巨集方式填入機械碼:

jmp2pm          MACRO   s,o
                DB      66h,0eah;操作碼
                DD      o       ;32位元偏移位址
                DW      s       ;區段值或區段選擇子
                ENDM            ;021

JMP 的機械碼是 0EAH,如果真實模式下,要用到 32 位元的程式碼,則需加上 66H 作為前置碼,關於前置碼,可參考註一。又因為偏移位址為 32 位元,故「o」的資料形態為雙字組,以「DD」表示。最高位址的字組則是區段選擇器,「s」。

在保護模式下,共有三個區段:程式碼區段、資料區段、顯示記憶體區段。這些區段的真正位址並不是 CS、DS、ES 之數值,而是藉由此數值查詢「區段描述器表格」。

用 DEBUG32 觀察

底下以 DEBUG32 來觀察這些位址的關係。以 DEBUG32 載入 PM1.COM:

E:\HomePage\SOURCE\PM>h:debug32 pm1.com [Enter]
Debug32 - Version 1.0 - Copyright (C) Larson Computing 1994

CPU = ?86, Virtual 8086 Mode, Id/Step = 0F10, A20 enabled
-

第一個指令是跳躍,以留下一塊記憶體當作存放資料之用,先看程式會跳躍至何處,再檢查原來資料存了哪些。

-u 100 101 [Enter]
291C:0100 EB2C             JMP     Short 012E

-d 100 l40 [Enter]
291C:0100 EB 2C 90 90 90 90 90 90-00 00 00 00 00 00 00 00  k,..............
291C:0110 23 00 00 00 00 98 00 00-FF FF 00 80 0B 92 00 00  #...............
291C:0120 12 00 00 00 00 92 00 00-1F 00 00 00 00 00 66 2B  ..............f+
291C:0130 C0 66 8B D8 8C C8 BB A4-01 66 C1 E0 04 66 03 C3  @f.X.H;$.fA`.f.C

從 291C:0108 處開始到 291C:0127 為止,就是「區段描述器表格」,而上面紅色的部份是「程式碼區段描述器」。「23 00」其實是 23H 個位元組,這是因為 x86 架構的電腦資料安排方式是「數字位數大的,在高位址,排在右邊」,像這種排列方式稱之為「Little-Endian」。位址 291C:0112∼291C:0114 是程式碼區段的基底位址,不過還沒有設定好正確的數值,接下來一直到 014E 的程式就是在做這一件事。我們先下 4 個「t」指令追蹤看看。

-t [Enter]
AX=0000  BX=0000  CX=00DB  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=291C  ES=291C  SS=291C  CS=291C  IP=012E  NV UP DI PL NZ NA PO NC
291C:012E 662BC0           SUB     EAX,EAX
Trace Interrupt
-t [Enter]
AX=0000  BX=0000  CX=00DB  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=291C  ES=291C  SS=291C  CS=291C  IP=0131  NV UP DI PL ZR NA PE NC
291C:0131 668BD8           MOV     EBX,EAX
Trace Interrupt
-t [Enter]
AX=0000  BX=0000  CX=00DB  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=291C  ES=291C  SS=291C  CS=291C  IP=0134  NV UP DI PL ZR NA PE NC
291C:0134 8CC8             MOV     AX,CS
Trace Interrupt
-t [Enter]
AX=291C  BX=0000  CX=00DB  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=291C  ES=291C  SS=291C  CS=291C  IP=0136  NV UP DI PL ZR NA PE NC
291C:0136 BBA401           MOV     BX,01A4h
Trace Interrupt

到 0136 位址時,對照原始碼,得知移進 BX 的數值就是保護模式程式所在位址,291C:01A4,也就是在絕對位址 29364 處 ( 291C×10+1A4 )。您可自行追蹤,為了節省篇幅,直接執行到把正確的絕對位址填到程式碼區段描述器內,再看看記憶體內容,如下面淡藍色的地方。

-g 14e [Enter]
AX=0002  BX=01A4  CX=00DB  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=291C  ES=291C  SS=291C  CS=291C  IP=014E  NV UP DI PL NZ AC PO CY
291C:014E 662BC0           SUB     EAX,EAX
Trace Interrupt
-d 100 l40 [Enter]
291C:0100 EB 2C 90 90 90 90 90 90-00 00 00 00 00 00 00 00  k,..............
291C:0110 23 00 64 93 02 98 00 00-FF FF 00 80 0B 92 00 00  #.d.............
291C:0120 12 00 00 00 00 92 00 00-1F 00 00 00 00 00 66 2B  ..............f+
291C:0130 C0 66 8B D8 8C C8 BB A4-01 66 C1 E0 04 66 03 C3  @f.X.H;$.fA`.f.C

再到絕對位址 291C:01A4 反組譯看看,並對照原始碼,果真是保護模式下的程式片段,此片段由絕對位址 291C:01A4 起到 291C:01C6,共 36 個位元組,所以界限為 23H,就是上面白色的部份。


-u 1a4 1c6 [Enter]
291C:01A4 B91000           MOV     CX,0010h
291C:01A7 8EC1             MOV     ES,CX
291C:01A9 B91800           MOV     CX,0018h
291C:01AC 8ED9             MOV     DS,CX
291C:01AE 66BE00000000     MOV     ESI,00000000h
291C:01B4 66BF40060000     MOV     EDI,00000640h
291C:01BA B40E             MOV     AH,0Eh
291C:01BC AC               LODSB
291C:01BD 3C00             CMP     AL,00h
291C:01BF 0F840300         JZ      01C6
291C:01C3 AB               STOSW
291C:01C4 EBF6             JMP     Short 01BC
291C:01C6 EBFE             JMP     Short 01C6

分段轉換 ( Segment Translation )

事實上,在 80386 真實模式或保護模式下的位址有好幾種。此處所用到的有兩種:邏輯位址 ( logical address ) 與絕對位址。邏輯位址是指您在除錯器上所看到的位址,這也是程式所看到的位址,在真實模式中,邏輯位址就是以「區段:偏移位址」表示,在 DEBUG/SYMDEB 等除錯器看到的也是這種位址。在保護模式之下,似乎並沒有除錯器可用,但是在 Windows 系統堙A有好幾種除錯器可以使用,例如 Olly Debug,它們所顯示的位址也都是邏輯位址。絕對位址也稱為實體位址 ( physical address ),請參考前面的說明。一般而言,邏輯位址須經過轉換才能變成實體位址。在真實模式中,轉換方式就是前面所說的「實體位址=區段×16+偏移位址」公式。而在保護模式堙A有兩種轉換方式:分段轉換 ( segment translation ) 與分頁轉換 ( paging translation )。上面,小木偶以 PM1.COM 為例子,已簡單講完分段轉換。

在此做個小結論。分段轉換可參考右邊的簡圖。圖中左邊黃框部份是 CPU,只列出與 PM1.COM 有關的暫存器;圖中右邊紅色大框為實體記憶體,最右邊以紅色表示的 32 位元十六進位數值為實體位址,越底下位址越高。當然不可能把每個位元組及其實體位址標示出來,小木偶只標示重要的,而每個最小的,中間沒有分隔線的橫框都一樣大,代表 8 個位元組。在此紅色框內,有四部份圖有背景顏色,咖啡色、藍色、灰色、紫色,分別代表全域描述器表格 ( GDT ) 、程式碼區段、資料區段、顯示記憶體區段。在此紅色框左側,有四個黃色的數字,是相對於全域描述器表格的位址,這些數值應該填入適當的區段暫存器。

CPU 中的 GDTR 含有全域描述器表格的基底位址,此基底位址是以實體位址表達,並利用此一位址指向記憶體中的全域描述器表格,即藍色箭頭所指的路線。全域描述器表格中含有每個區段的資料,每個區段暫存器,如 CS、DS、ES 等,皆為相對於全域描述器表格的指標,以獲得該區段的資料。例如 CS 之值為 0008,即表示 CS 所指的區段描述器是在全域描述器表格的第 8 個位元組開始,即藏青色箭頭所指的路線。此描述器的基底位址為「0002 9364」,因此 CS 所指的實體位址是在「0002 9364」處 ( 深綠色箭頭所指 ),其大小為「23H+1」。在保護模式下的程式碼區段的偏移位址是 EIP,因此 CPU 就可以由 GDTR 和 CS 找到在全域描述器表格的程式碼區段描述器的位址,由這裡取出基底位址,再加上 EIP 就能找到下一行指令的位址。

資料也是這樣,CPU 藉由 GDTR 和 DS 找到全域描述器表格的資料區段描述器的位址,由此得到資料區段的基底位址,0002 9388,然後再由 32 位元的偏一位址,就能存取資料。PM1 堙A在保護模式埵s取的資料就只有 string 字串,它在資料區段的偏移位址可由下面程式 ( 第 97 行 ) 算出來,存到 ESI 堙G

        mov     esi,OFFSET string-OFFSET pm_data_seg

OFFSET string 是 string 的位址,OFFSET pm_data_seg 是資料區段的基底位址,兩者相減,就得到 string 字串的偏移位址。


區段的屬性

這一節來說說區段描述器內剩下沒講的部份,先看看區段描述器的樣子:

TYPE

其中基底位址、邊界均已解釋過了,接下來談談 TYPE。TYPE 共有四個位元,40∼43,它們所代表的意義,有點複雜。第 43 位元稱為 T 位元,而第 40∼42 位元的意義和 T 位元之數值有關;若 T=1,表示此區段為程式碼區段;若 T=0,表使此區段不是程式碼區段,如資料段、堆疊段等。如右圖所示。

  1. T=1,表示此區段為程式碼區段,那麼第 42 位元稱為 C 位元;第 41 位元稱為 R 位元:
  2. T=0,表使此區段不是程式碼區段,如資料段、堆疊段等,那麼第 42 位元稱為 E 位元;第 41 位元稱為 W 位元:

第 40 位元是 A 位元,是 accessed 的意思,判斷此區段是否曾經被存取過。若 A=0,未存取過;A=1,已被存取過。

S 位元、DPL 和 P 位元

再來,解釋 S 位元、DPL、P 位元三項:

  1. S ( system ) 位元:S=0,表示此區段為 LDT ( 區域描述器 )、TSS 描述器等,一般是給作業系統使用;S=1,表示此區段為程式碼、資料、堆疊等一般區段。
  2. DPL ( descriptor privilege level):表示此區段的特權等級,共有兩個位元,80386 的特權等級分為四級,0 是最高等級,3 是最低等級,以後再介紹。
  3. P ( present ) 位元:P=0,表示此區段在實體記憶體堙FP=1,表示此區段不在實體記憶體堙A可能放在虛擬記憶體堙C

TYPE、S 位元、DPL、P 位元亦合稱存取權限 ( access rights )。

其他位元:AVL、D、G 位元

  1. AVL ( available ) 位元:保留給系統程式使用。
  2. D 位元 ( default size ):此位元較為複雜,可分為三種情形:
  3. G ( granularity ) 位元:此位元決定邊界 ( limit ) 的單位,若 G=0,邊界的單位為位元組;若 G=1,邊界的單位為 4KB。

再回頭看看 PM1 的程式碼區段描述器,由低位元組到高位元組為「23 00 64 93 02 98 00 00」,綠色部份為邊界,白色部份為基底位址,就不細說了。紅色部份,「098」,為屬性,最高位的 4 個位元均為零,因此 G=0、D=0、AVL=0。由 G=0 可知,此區段邊界以位元組為單位,因此此由實體位址 29364H 開始,到 29387H 為止,共 24H 個位元組。由 D=0 可知,此區段預設為 16 位元的區段,因此暫存器、運算元預設為 16 位元,如果要用到 32 位元的暫存器,得在機械碼前面加上前置碼 66H,可對照 PM1 原始碼 pm: 標號後幾行的

        mov     esi,OFFSET string-OFFSET pm_data_seg
        mov     edi,(80*10+0)*2     ;螢幕第 10 列,第 0 行。

被組譯後的機械碼:

291C:01AE 66BE00000000     MOV     ESI,00000000h
291C:01B4 66BF40060000     MOV     EDI,00000640h

注意到是不是多了紅色的前置碼?( 閱讀第 30 章可知道更多的前置碼知識 )

再來的 P 位元、DPL 和 S 位元為「9」,9 的二進位為「1001」,因此 P、S 位元均為「1」,也就是此區段存在於實體記憶體中,並且為一般區段,即程式碼區段而非 LDT 等系統使用的區段。DPL 為 0,表示特權等即是最高等級。

最後的 TYPE 值為「8」,變成二進位是「1000」,因此 T=1,表示是程式碼區段,C、R、A 均為「0」,分別表示一致、不能讀取、未被存取過。


從保護模式返回真實模式

雖然藉由 PM1,我們介紹了如何進入保護模式,但是仍然沒有使用到保護模式巨大記憶體的好處,同時 PM1 也沒有從保護模式返回 DOS,只好讓 PM1 進入無窮迴圈而當機。底下小木偶再實作一個程式,PM2,這個程式進入保護模式後,先讀取實體位址 200000H 處 ( 由 2MB ) 開始的幾個位元組,將其內容印在螢幕上,再於相同位址寫入「I learn protected mode with assembly.」字串。接著再將位址 200000H 的內容印在螢幕上。如果真的進入保護模式,並且寫入成功,那麼螢幕上所顯示的內容,應當不同。底下是 PM2 的原始碼:

.386P
PAGE    ,132

tr_seg  EQU     200000h         ;目的資料區段位址為 2M 處
tr_len  EQU     string_pm_len-1 ;目的資料區段長度

jmp2pm  MACRO   s,o
        DB      66h,0eah;操作碼
        DW      o,0     ;32位元偏移位址
        DW      s       ;區段選擇器
        ENDM

jmp2_16 MACRO   s,o
        DB      66h,0eah;操作碼
        DW      o       ;16位元偏移位址
        DW      s       ;區段選擇器
        ENDM

pdesc   STRUC
limit   DW      0       ;全域描述器表格大小
base    DD      0       ;32位元基底位址
pdesc   ENDS

desc            STRUC
limit_l         DW      0       ;區段邊界(BIT0-15)
base_l          DW      0       ;區段位址(BIT0-15)
base_m          DB      0       ;區段位址(BIT16-23)
attributes      DB      0       ;區段屬性
limit_h         DB      0       ;區段邊界(BIT16-19)(含區段屬性的高4位)
base_h          DB      0       ;區段位址(BIT24-31)
desc            ENDS

;以巨集指令定義區段選擇描述器定義
;用法:descriptor       區段名,基底位址,區段大小,屬性
;descriptor巨集會自動把基底位址、區段大小、屬性歸類到各欄位
;,且descriptor巨集必須和 desc 結構體共用
descriptor      MACRO   desc_name,base,limit,attribute
limit_0         =       limit AND 0ffffh
limit_1         =       ( ( limit AND 0ffff0000h ) SHR 10h ) AND 0fh
attrib_0        =       attribute AND 0ffh
attrib_1        =       ( ( ( attribute SHR 8 ) SHL 4 ) OR limit_1 ) AND 0ffh
base_0          =       base AND 0ffffh
base_1          =       ( ( base AND 0ff0000h ) SHR 10h ) AND 0ffh
base_2          =       ( ( base AND 0ff000000h ) SHR 18h ) AND 0ffh
desc_name       desc    <limit_0,base_0,base_1,attrib_0,attrib_1,base_2>
                ENDM

;*******************************************************************************
data            SEGMENT USE16
gdt             LABEL   BYTE                            ;全域描述器表格
descriptor      dummy,0,0,0                             ;空描述器
descriptor      pm_code32,      0,   pm_c_len-1,498h    ;32位元程式碼區段描述器
descriptor      pm_datasr,      0,pm_d_sr_len-1, 92h    ;來源資料區段描述器
descriptor      pm_datatr, tr_seg,       tr_len, 92h    ;目的資料區段描述器
descriptor      pm_video, 0b8000h,       0ffffh, 92h    ;顯示記憶體區段描述器
descriptor      pm_stack,       0,  stack_len-1, 92h
descriptor      pm_code16,      0,       0ffffh, 98h    ;準備跳回真實模式的16位元程式碼區段描述器
descriptor      normal,         0,       0ffffh, 92h
gdt_len         =       $-gdt                   ;全域描述器表格的大小
gdt_ptr         pdesc   <gdt_len-1,0>           ;全域描述器表格資料
code32_selector =       pm_code32-gdt           ;程式碼區段選擇器
datasr_selector =       pm_datasr-gdt           ;來源資料區段選擇器
datatr_selector =       pm_datatr-gdt           ;目的資料區段選擇器
video_selector  =       pm_video-gdt            ;顯示記憶體區段選擇器
stack_selector  =       pm_stack-gdt            ;堆疊區段選擇器
code16_selector =       pm_code16-gdt           ;準備跳回真實模式的 16 位元程式碼區段選擇器
normal_selector =       normal-gdt
SaveSP          DW      ?                       ;用於保存SP暫存器
SaveSS          DW      ?                       ;用於保存SS暫存器
;-------------------------------------------------------------------------------
data            ENDS
;*******************************************************************************
pm_stack_seg    SEGMENT PARA STACK USE16
stack_len       =       256
                DB      stack_len DUP(0)
pm_stack_seg    ENDS
;*******************************************************************************
pm_datasr_seg   SEGMENT PARA USE16
string_pm       DB      'I learn protected mode with assembly.',0
string_pm_len   =       $-string_pm+1
char_per_line   DD      ?       ;螢幕上,每一列印出 16 個字元及該字元所代表的數值
x               DB      0       ;資料從螢幕第 9 列、第 0 行開始顯示
y               DB      9
pm_d_sr_len     =       $-string_pm
pm_datasr_seg   ENDS
;*******************************************************************************
code    SEGMENT USE16
        ASSUME  cs:code,ds:data
;-------------------------------------------------------------------------------
main    PROC
        mov     ax,data
        mov     ds,ax

;設置保護模式中,32 位元程式碼區段的描述器
        sub     eax,eax
        xor     ebx,ebx
        mov     ax,pm_code32_seg
        mov     bx,OFFSET pm_code32_start
        shl     eax,4
        add     eax,ebx
        mov     pm_code32.base_l,ax
        shr     eax,10h
        mov     pm_code32.base_m,al
        mov     pm_code32.base_h,ah

;設置保護模式中,16 位元程式碼區段的描述器
        sub     eax,eax
        xor     ebx,ebx
        mov     ax,pm_code16_seg
        mov     bx,OFFSET main16
        shl     eax,4
        add     eax,ebx
        mov     pm_code16.base_l,ax
        shr     eax,10h
        mov     pm_code16.base_m,al
        mov     pm_code16.base_h,ah

;設置保護模式中,來源資料區段的描述器
        xor     eax,eax
        mov     ax,pm_datasr_seg
        shl     eax,4
        mov     pm_datasr.base_l,ax
        shr     eax,10h
        mov     pm_datasr.base_m,al
        mov     pm_datasr.base_h,ah

;設置保護模式中,堆疊區段描述器
        mov     ax,ss
        mov     WORD PTR SaveSS,ax
        mov     WORD PTR SaveSP,sp
        sub     eax,eax
        mov     ax,pm_stack_seg
        shl     eax,4
        mov     WORD PTR pm_stack.base_l,ax
        shr     eax,10h
        mov     BYTE PTR pm_stack.base_m,al
        mov     BYTE PTR pm_stack.base_h,ah

;填入正確的數值到 gdt_ptr 
        sub     eax,eax
        xor     ebx,ebx
        mov     ax,ds
        mov     bx,OFFSET gdt
        shl     eax,4
        add     eax,ebx         ;計算並設置基位址
        mov     gdt_ptr.base,eax
;用 MASM 6.x 組譯時,改成「lgst gdt_ptr」;用 MASM 5.x 組譯時,
        lgdt    QWORD PTR gdt_ptr       ;改成「lgst QWORD PTR gdt_ptr」

        cli                     ;關中斷

;開起 A20 位址線
        in      al,92h
        or      al,00000010b
        out     92h,al

;切換到保護模式
        mov     eax,cr0
        or      al,1
        mov     cr0,eax

;清指令預取隊列,並真正進入保護模式
        jmp2pm   code32_selector,<OFFSET main32>

rm_entry:       ;回到真實模式時的進入點
        mov     ax,data
        mov     ds,ax
        mov     sp,SaveSP
        mov     ss,SaveSS

;關閉 A20 位址線
        in      al,92h
        and     al,11111101b
        out     92h,al
        sti

;結束程式,返回 DOS
        mov     ax,4c00h
        int     21h
main    ENDP
;-------------------------------------------------------------------------------
code    ENDS
;*******************************************************************************
pm_code32_seg   SEGMENT USE32
                ASSUME  cs:pm_code32_seg,ds:pm_datasr_seg
pm_code32_start:
;-------------------------------------------------------------------------------
;由 DL ( 第幾行,x )、DH ( 第幾列,y ) 求出顯示記憶體的位址,存於 EDI 堙C
;公式:EDI=160*DH+2*DL=128*DH+32*DH+2*DL
set_edi PROC
        movzx   eax,x
        movzx   edi,y
        mov     ebx,edi
        shl     eax,1   ;EAX=2*DL
        shl     ebx,7   ;EBX=128*DH
        shl     edi,5   ;EBX=32*DH
        add     edi,ebx
        add     edi,eax
        ret
set_edi ENDP
;-------------------------------------------------------------------------------
ascii   PROC
        and     al,0fh
        add     al,'0'
        cmp     al,'9'
        jbe     number
        add     al,7
number: stosw
        ret
ascii   ENDP
;-------------------------------------------------------------------------------
;在螢幕上 B8000:EDI 處印出 AL 之十六進位數值
print_char      PROC
        mov     bl,al
        shr     al,4
        call    ascii
        mov     al,bl
        call    ascii
        ret
print_char      ENDP
;-------------------------------------------------------------------------------
;此副程式會把 FS:ESI 所指位址之內容印在螢幕 B8000:EDI 上
print_line      PROC
        mov     ah,0eh
        push    ecx
        mov     edx,tr_seg
        xor     ebx,ebx
        add     edx,esi
        mov     ecx,4
next0:  shld    ebx,edx,8
        mov     al,bl
        call    print_char
        shl     edx,8
        loop    next0
        mov     al,' '
        stosw
        pop     ecx

        mov     char_per_line,10h
        push    esi
        push    ecx
next1:  mov     al,fs:[esi]
        inc     esi
        call    print_char
        mov     al,' '
        stosw
        dec     ecx
        jz      blank
        dec     char_per_line
        jnz     next1
        jmp     ok1

blank:  dec     char_per_line
        jz      ok1
        mov     al,' '
        stosw
        stosd
        jmp     blank

ok1:    pop     ecx
        pop     esi
        mov     char_per_line,10h
next2:  mov     al,fs:[esi]
        inc     esi
        cmp     al,20h
        jae     print
        mov     al,'.'
print:  stosw
        dec     ecx
        jz      ok2
        dec     char_per_line
        jnz     next2
ok2:    ret
print_line      ENDP
;-------------------------------------------------------------------------------
;此副程式先讀取 FS:00000000 開始的 string_len 個位元組,並把資料顯示在螢幕上,
;然後把 DS:string_pm 所址的字串寫入 FS:00000000,再把其內容印在螢幕上。
;FS:00000000 位址位於物理位址 tr_seg 處。
main32  PROC
        mov     ax,stack_selector
        mov     ss,ax
        mov     esp,stack_len
        mov     ax,datasr_selector
        mov     ds,ax
        mov     ax,video_selector
        mov     es,ax
        mov     ax,datatr_selector
        mov     fs,ax

        xor     esi,esi
        mov     ecx,string_pm_len
        cld
next3:  call    set_edi
        call    print_line
        jecxz   ok3
        mov     x,0
        inc     y
        loop    next3

ok3:    mov     esi,OFFSET string_pm
        xor     ebx,ebx
        mov     ecx,string_pm_len
next4:  lodsb
        mov     fs:[ebx],al
        inc     ebx
        loop    next4

        mov     x,0
        mov     y,13
        mov     ecx,string_pm_len
        xor     esi,esi
next5:  call    set_edi
        call    print_line
        jecxz   ok4
        mov     x,0
        inc     y
        loop    next5

ok4:    jmp2_16 code16_selector,<OFFSET main16>
main32  ENDP
;-------------------------------------------------------------------------------
pm_c_len        =       $-pm_code32_start
pm_code32_seg   ENDS
;*******************************************************************************
pm_code16_seg   SEGMENT USE16
                ASSUME  cs:pm_code16_seg
;-------------------------------------------------------------------------------
main16  PROC
        mov     ax,normal_selector
        mov     ds,ax
        mov     es,ax
        mov     ss,ax
        mov     eax,cr0
        and     al,11111110b
        mov     cr0,eax
        jmp     FAR PTR rm_entry
main16  ENDP
;-------------------------------------------------------------------------------
pm_code16_seg   ENDS
;*******************************************************************************
END     main

跟 PM1 一樣,利用文書處理軟體,輸入上述原始碼後,存成 PM2.ASM 檔,開啟 Virtual PC,依下圖綠框內①、②指令組譯並連結:

組譯並連結成功後,輸入③處紫框內指令,執行 PM2,會看見在銀幕中印出黃色內容,上面以紅框框住的是原來 2MB 處的 36 個位元組,內容均為「0」,而藍框框住的是 PM2 填入的內容。很明顯的,兩者不相同。圖中最下面⑥白框處可以見到,已返回 DOS,可以繼續輸入指令,不會當機。底下說明 PM2。

descriptor 巨集

每次定義區段描述器,都需要重新計算,很是麻煩,因此小木偶還撰寫了 descriptor 巨集,這個巨集可以輸入區段描述器的名稱、基底位址、邊界、屬性,然後轉變成英特爾所定義的那種很不直覺的區段描述器格式 ( 區段基底位址、區段邊界都分成兩部份 ),這個巨集是:

;以巨集指令定義區段選擇描述器定義
;用法:descriptor       區段名,基底位址,區段大小,屬性
;descriptor巨集會自動把基底位址、區段大小、屬性歸類到各欄位
;,且descriptor巨集必須和 desc 結構體共用
descriptor      MACRO   desc_name,base,limit,attribute
limit_0         =       limit AND 0ffffh
limit_1         =       ( ( limit AND 0ffff0000h ) SHR 10h ) AND 0fh
attrib_0        =       attribute AND 0ffh
attrib_1        =       ( ( ( attribute SHR 8 ) SHL 4 ) OR limit_1 ) AND 0ffh
base_0          =       base AND 0ffffh
base_1          =       ( ( base AND 0ff0000h ) SHR 10h ) AND 0ffh
base_2          =       ( ( base AND 0ff000000h ) SHR 18h ) AND 0ffh
desc_name       desc    <limit_0,base_0,base_1,attrib_0,attrib_1,base_2>
                ENDM

此處的 AND、SHR、SHL、OR 等並不是 x86 指令,而是運算子,在使用時,base、limit、attribute 都是已知數,不能是暫存器或變數,它們在組譯階段就已經確定了。有了這個巨集,以後要定義區段描述器,只需要像

descriptor      pm_video, 0b8000h,       0ffffh, 92h

就可以了。這是定義一個名為 pm_video 區段描述器,基底位址是 0B8000H,區段邊界是 0FFFFH,屬性為 92H。


註二: