oslab oranges 一個操作系統的實現 實驗二 認識保護模式


 https://github.com/yyu/osfs00

實驗目的:

理解x86架構下的段式內存管理

掌握實模式和保護模式下段式尋址的組織方式、

關鍵數據結構、代碼組織方式

掌握實模式與保護模式的切換

掌握特權級的概念,以及不同特權之間的轉移

 

實驗內容:

1. 認真閱讀章節資料,掌握什么是保護模式,弄清關鍵數據結構:

GDT、descriptor、selector、GDTR, 及其之間關系,閱讀

pm.inc文件中數據結構以及含義,寫出對宏Descriptor的分析

2. 調試代碼,/a/ 掌握從實模式到保護模式的基本方法,畫出代碼

流程圖,如果代碼/a/中,第71行有dword前綴和沒有前綴,編

譯出來的代碼有區別么,為什么,請調試截圖。

3. 調試代碼,/b/,掌握GDT的構造與切換,從保護模式切換回實

模式方法

4. 調試代碼,/c/,掌握LDT切換

5. 調試代碼,/d/掌握一致代碼段、非一致代碼段、數據段的權限

訪問規則,掌握CPL、DPL、RPL之間關系,以及段間切換的基

本方法

6. 調試代碼,/e/掌握利用調用門進行特權級變換的轉移

代碼對應iso中chapter3 

 

實驗解決問題與課后動手改:

1. GDT、Descriptor、Selector、GDTR結構,及其含義是什么?他

們的關聯關系如何?pm.inc所定義的宏怎么使用?

2. 從實模式到保護模式,關鍵步驟有哪些?為什么要關中斷?為

什么要打開A20地址線?從保護模式切換回實模式,又需要哪些

步驟?

3. 解釋不同權限代碼的切換原理,call, jmp,retf使用場景如何,

能夠互換嗎?

4. 課后動手改:

1. 自定義添加1個GDT代碼段、1個LDT代碼段,GDT段內要對一個內

存數據結構寫入一段字符串,然后LDT段內代碼段功能為讀取並打

印該GDT的內容;

2. 自定義2個GDT代碼段A、B,分屬於不同特權級,功能自定義,要

求實現A-->B的跳轉,以及B-->A的跳轉。

 

 

實驗環境:

 

VMwareWorkstationPro 15.5.0

 

Ubuntu 12.04.5 desktop i386 32位

 

bochs 2.6.9

 

 

 

關鍵技術:

 

  1. bochs使用
  2. 實模式,保護模式及其關鍵數據結構GDT,LDT,Descriptor、Selector等
  3. 特權級變換

 

 

 

實驗步驟:

 

1.認真閱讀章節資料,掌握什么是保護模式,弄清關鍵數據結構:

 

GDT、descriptor、selector、GDTR, 及其之間關系,閱讀

 

pm.inc文件中數據結構以及含義,寫出對宏Descriptor的分析

 

 

 

GDT即為Global Descriptor Table(全局描述符表)又叫段描述符表,為保護模式下的一個數據結構。其中包含多個descriptor,定義了段的起始地址,界限屬性等。

 

descriptor為段描述符,包含段基址,段界限,段屬性。其結構如圖

 

 

 

 

Selector為選擇子,有其數據結構。在pmtest1.asm程序中,其作用就是偏移,對應描述符相對於GDT基址的偏移。

 

 

 

 

GDTR為GDT寄存器。結構與GDTPTR類似,6字節,前兩字節GDT界限,后4字節GDT基地址。

 

 

 

四者關系:

GDT中包含多個descriptor,descriptor包含段的信息,包含段基址,界限屬性等。多個selector包含對應descriptor相對於GDT的偏移,於是selector發揮了類似 指向descriptor的作用。而GDTR中包含了GDT基地址與界限。四者綜合就可以獲得某個descriptor的地址。而保護模式下尋址就先靠GDTR找到GDT,然后根據descriptor找到對應段的地址,然后再加上段內偏移offset,就得到某個線性地址。

如圖所示

 

 

對宏Descriptor分析:

結構如圖:

 

 

8字節。從低地址開始前兩字節為段界限1,然后三個字節為段基址1,然后兩個字節byte5,byte6包含段屬性以及段界限2,最后一字節為段基址2.由於歷史原因,段界限和段基址都分開存放。程序中descriptor由pm.inc中的宏descriptor生成。

代碼:

%macro Descriptor 3 ;macro定義宏。 3表示有三個參數

    dw    %2 & 0FFFFh                ; 段界限1

    dw    %1 & 0FFFFh                ; 段基址1

    db    (%1 >> 16) & 0FFh            ; 段基址2

    dw    ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)    ; 屬性1 + 段界限2 + 屬性2

    db    (%1 >> 24) & 0FFh            ; 段基址3%endmacro ; 字節

 

macro代表宏開始。宏名Descriptor,3代表有三個參數。

參數1-3分別為段基址,界限,屬性。

比如LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, DA_DRW         ; 顯存首地址

利用宏Descriptor定義了基址為0B8000H的段LABEL_DESC_VIDEO.

0B8000H為顯存首地址。利用該段在屏幕中顯示數據。

之后第一行dw 為兩字節。   %2 & 0FFFFh, 相當於取段界限的低位,寫入這兩字節。

然后dw,dd去段基址1,2,構成三字節段基址,相當於上面結構圖的段基址1.

然后dw兩字節構成段屬性,段界限2.

然后dw兩字節構成段基址3.

其中段基址為該段起始地址,界限為長度。

 

2. 調試代碼,/a/ 掌握從實模式到保護模式的基本方法,畫出代碼

流程圖,如果代碼/a/中,第71行有dword前綴和沒有前綴,編

譯出來的代碼有區別么,為什么,請調試截圖。

 

流程圖:pmtest1.asm 用文字描述如下

1)定義GDT  [SECTION .gdt]

其中定義了一個空descriptor,一個32位代碼段,一個顯存descriptor

其中32位代碼段只初始化了段界限,段屬性

2)進入[SECTION .s16] 16位代碼段(實模式)

修改GDT值:修改32位段描述符值

LABEL_SEG_CODE32的物理地址(即 [SECTION .s32]這個段的物理地址)賦給eax,然后把它分成三部分賦給描述符DESC_CODE32中的相應位置。由於DESC_CODE32的段 界限和屬性已經指定,所以至此,DESC_CODE32的初始化全部完成。

(將段寄存器段界限段屬性由符合實模式要求到符合保護模式要求)

之后賦值gdtr寄存器:

GDT的物理地址填充到了GdtPtr這個6字節的數據結構中。

lgdt [GdtPtr] 將GdtPtr指示的6字節加載到寄存器gdtr

之后關中斷。

之后打開A20地址線。

修改cr0寄存器:PE位置1。

此時cs的值仍然是實模式下的值,把代碼段的選擇子裝入cs:

jmp dword SelectorCode32:0 ,進入32位代碼段[SECTION .s32]

3)進入32位代碼段[SECTION .s32]

進行屏幕顯示操作。

 

調試代碼a:

將程序編譯為.com文件,使用dos運行。(因為引導扇區只有512字節,程序高於512字節就不方便了)

代碼a有dword前綴調試:

(1)准備freedocs.img

(2)bximage生成pm.img

(3)修改bochs

 

 

重點是

floppya: 1_44=freedos.img, status=inserted

floppyb: 1_44=pm.img, status=inserted

boot: a

 

(1)bochs格式化B盤

Sudo bochs

dos format b:

 

 

(5)修改pmtest1,org改為0100h,並編譯為pmtest1.com

 

 

 

 

 

(6)pmtest1.com復制到pm.img

sudo mount -o loop pm.img /mnt/floppy

會出現了錯誤

mount point /mnt/floppy does not exist

先創建文件夾

 

 

然后
sudo losetup /dev/loop0 pm.img  創建loop設備,然后操作loop設備,就是對pm.img數據的操作了

 

 

sudo mount /dev/loop0/ /mnt/floppy loop設備掛載到/mnt/floppy上

 

 

然后

 sudo cp pmtest1.com /mnt/floppy/ 賦值

然后卸載

sudo umount /mnt/floppy/

 

 

之后再做一次遇到問題

 

 

 

解決,卸載

 

 

 

 另外發現了 sudo cp pmtest2.com /mnt/floppy/ 賦值並不是覆蓋。也就是說cp了先cp了pmtest1.com,然后不格式化(format b:),直接cp  pmtest2.com,那么兩個程序都可以運行。

(7)dos下運行pmtest1.com

Sudo bochs

B:\pmtest1.com 運行

可見右側出現一個紅色的P

 

 

代碼a無dword前綴調試:

(1)修改pmtest1.asm,刪掉第71行的dword,存為pmtestd.asm,並編譯為pmtestd.com

 

(2)dos運行

陷入循環並且無紅色的P在屏幕右側

 

 

失敗原因:

jmp dword SelectorCode32:0 ; 執行這一句會把 SelectorCode32 裝入 cs,  //selector16位,dword兩字節,高位selector,低位偏移0.(因為聲明了這段是16位代碼,所以一個字兩字節)
; 並跳轉到 Code32Selector:0 處

刪除dword 后只有16位。cs寄存器沒有正確設置,沒有跳轉到32位代碼段,故顯示失敗

3. 調試代碼,/b/,掌握GDT的構造與切換,從保護模式切換回實

模式方法

分析:pmtest2.asm

在前面程序的基礎上,新建一個段,這個段以5MB為基址,遠遠超出實模式下1MB的界限。我們

先讀出開始處8字節的內容,然后寫入一個字符串,再從中讀出8字節。如果讀寫成功的話,兩次讀出的內容應該是不同的,而且第

二次讀出的內容應該是我們寫進的字符串。字符串是保存在數據段中的,也是新增加的。

1)LABEL_DESC_STACK:  Descriptor 為全局堆棧段[SECTION .gs]的descriptor,初始化在[SECTION .gs]和[SECTION.16]完成。Descriptor屬性為DA_DRWA+DA_32,DA_32表明是32位堆棧段。

2)LABEL_DESC_DATA:Descriptor 為[SECTION .data1]  ; 數據段的descriptor,初始化在[SECTION .data1] 完成,其中包含了要寫入的字符串

3)LABEL_DESC_CODE32: Descriptor 32位代碼段(保護模式)[SECTION .s32]. 由實模式跳入.

[SECTION .s32]中我們改變了ss和esp(代碼3.5第174行到177行),這樣,在32位代碼段中所有的堆棧操作將會在新增的 堆棧段中進行。

這個段的開頭初始化了ds、es和gs,讓ds指向新增的數據段,es指向新增的5MB內存 的段,gs指向顯存(第167行到第172行)。接着顯示一行字符串,之后就開始讀寫大地址內存了(第198行到第200行)。由於要讀 兩次相同的內存,我們把讀的過程寫進一個函數TestRead,寫內存的內容也寫進函數TestWrite,這兩個函數的入口分別在第206行 和第222行。可以看到,在TestRead中還調用了DispAL和DispReturn這兩個函數(第253行和第286行),DispAL將al中的字節用十 六進制數形式顯示出來,字的前景色仍然是紅色;DispReturn模擬一個回車的顯示,實際上是讓下一個字符顯示在下一行的開頭 處。要注意的一個細節是,在程序的整個執行過程中,edi始終指向要顯示的下一個字符的位置。所以,如果程序中除顯示字符外 還用到edi,需要事先保存它的值,以免在顯示時產生混亂。

4)保護模式中字符串尋址:TestWrite中用到一個常量OffsetStrTest,它的定義在代碼3.4第47行。注意,我們用到這個字符串的時候並沒有用直接標 號StrTest,而是又定義了一個符號OffsetStrTest,它等於StrTest-$$。$$的含義代表當前 節(section)開始處的地址。所以StrTest-$$表示字符串StrTest相對於本節的開始處(即LABEL_DATA處)的偏移。容易發現數據段的基址便是LABEL_DATA的物理地址。於是OffsetStrTest既是字符串相對LABEL_DATA的偏移,也是其在數據段中的偏移。我們在保護模式下需要用到的正是這個偏移,而不再是實模式下的地址。前文中提到過的section的一點妙用指 的便是這里的$$,它不是沒有替代品,而是這樣做思路會比較清晰。OffsetPMMessage的情形與此類似。

6)返回實模式

概述:

先回憶開中斷:加載寄存器,之后關中斷。之后打開A20地址線。修改cr0寄存器:PE位置1。此時cs的值仍然是實模式下的值,把代碼段的選擇子裝入cs(修改段界限,段屬性。)

 

關中斷差不多就是完成上述的逆向操作:

加載一個合適的描述符選擇子到有關段寄存器,以使對應段描述符高速緩沖寄存器中含有合適的段界限和屬性,重新設置各個段寄存器的值,比如cr0PE位置0.恢復sp(堆棧指針寄存器)的值,修改段界限,段屬性,然后關閉A20,打開中斷,重新回到原來的樣子。

(將段寄存器段界限段屬性由符合保護模式要求到符合實模式要求)

為了能從保護模式恢復實模式的寄存器,需要先保存到系統自己的堆棧段。在[SECTION.16]中完成。

mov sp, 0100h

...

然后32位代碼段的操作在自定義的堆棧段[SECTION .STACK]完成。二者互不干擾,方便了恢復。

 

 

詳述:

從實模式進入保護模式時直接用一個跳轉就可以了,但是返回的時候卻稍稍復雜一些。因為在准備結束保護模式回到實模 式之前,需要加載一個合適的描述符選擇子到有關段寄存器,以使對應段描述符高速緩沖寄存器中含有合適的段界限和屬性。而 且,我們不能從32位代碼段返回實模式,只能從16位代碼段中返回這是因為無法實現從32位代碼段返回時cs高速緩沖寄存器中的 屬性符合實模式的要求(實模式不能改變段屬性)。

所以,在這里,我們新增一個Normal描述符(代碼3.4第15行)。在返回實模式之前把對應選擇子SelectorNormal加載到ds、 es和ss,就是上面所說的這個原因。

LABEL_DESC_NORMAL: Descriptor對應選擇子SelectorNormal。對應段 [SECTION .s16code],16 位代碼段. 由 32 位代碼段跳入, 跳出后到實模式。

這個段是由[SECTION .s32]中的jmp SelectorCode16:0跳進來的。開頭的語句把 SelectorNormal賦給ds、es、fs、gs和ss,完成我們剛剛提到的使命。然后就清cr0的PE位,接下來的跳轉看上去好像不太對,因 為段地址是0。其實這里只是暫時這樣寫罷了,在程序的一開始處可以看到代碼3.8中的這幾句。

67 mov ax, cs

...

73 mov [LABEL_GO_BACK_TO_REAL+3], ax

mov [LABEL_GO_BACK_TO_REAL+3], ax的作用就是為回到實模式的這個跳轉指令指定正確的段地址,這條指令的機器碼如圖3.9 所示。 

 

 

3.9告訴我們,LABEL_GO_BACK_TO_REAL+3恰好就是Segment的地址,而第73行執行之前ax的值已經是實模式下的cs(我們記 做cs_real_mode)了,所以它將把cs保存到Segment的位置,等到jmp指令執行時,它已經不再是:

jmp 0:LABEL_REAL_ENTRY

而變成了:

jmp cs_real_mode:LABEL_REAL_ENTRY

它將跳轉到標號LABEL_REAL_ENTRY處。

在跳回實模式之后,程序重新設置各個段寄存器的值,恢復sp的值,然后關閉A20,打開中斷,重新回到原來的樣子

144 LABEL_REAL_ENTRY: ; 從保護模式跳回到實模式就到了這里

...

159 int 21h ; / 回到 DOS

 

調試:

編譯pmtest2.asm為pmtest2.com

bochs dos 下運行

 

 

 

 

第一行為開始內存5MB處全是零。然后寫入了41,42,...48,也就是16進制的A,B,C,D...H,在代碼pmtest2.asm中DATA段的寫入的str。

同時看到,程序執行結束后不再像上一個程序那樣進入死循環,而是重新出現了DOS提示符。這說明我們重新回到了實模式下

DOS。

 4.調試代碼,/c/,掌握LDT切換

分析:

LDT與GDT都是描述符table,L代表Local,局部。簡單來說,LDT是一種描述符表,與GDT差不多,只不過它的選擇子的TI位必 須置為1。在運用它時,需要先用lldt指令加載ldtr,lldt的操作數selector是GDT中用來描述LDT的描述符。(也就是說LDT相當於GDT中描述的一個段,對應有特殊的寄存器ldtr,而該段中又有一些描述符描述一些LDT段,只屬於這個LDT。)

 

pmtest3.asm中增加了兩個節[SCTION .ldt][SECTION .la]。(原來有omtest2.asm中的各個段)。其中[SCTION .ldt]在GDT中有對應的descriptor和selector  LABEL_DESC_LDT: 。而[SECTION .la]是LDT描述的段,在GDT無定義。

[SCTION .ldt]是增加的LDT,其中有一個descriptor,對應[SECTION .la]。

[SECTION .la]中包含顯示的字符L,在屏幕顯示。實現時調用了GDT中 的SelectorVideo。

轉換到LDT的過程:先由實模式跳轉到GDT中的32位代碼段[SECTION .s32](保護模式),然后在[SECTION .s32]中

mov ax, SelectorLDT

lldt ax

加載ldtr(成為當前LDTR),

然后。jmp SelectorLDTCodeA。因為SelectorLDTCodeA的TI位為1,所以系統從當前LDT尋找相應描述符。跳轉到LDT中descriptor描述的段[SECTION .la]顯示L后,然后jmp SelectorCode16:0,跳回GDT中描述的16位代碼段,然后返回實模式。其中SelectorLDT在GDT中定義,指向LDT地址。

 

[SECTION .s32]第217行到第220行,指令lldt,功能和lgdt也差不多, 負責加載ldtr,它的操作數是一個選擇子,這個選擇子對應的就是用來描述LDT的那個描述符(標號LABEL_DESC_LDT)。

本例用到的LDT中只有一個描述符(標號LABEL_LDT_DESC_CODEA處),這個描述符跟GDT中的描述符沒什么分別。選擇子卻不一樣,多出了一個屬性SA_TIL。可以在pm.inc中找到它的定義:

SA_TIL EQU 4

由圖3.5可知,SA_TIL將選擇子SelectorLDTCodeA的TI位置為1。實際上,這一位便是區別GDT的選擇子和LDT的選擇子的關鍵所在。如果TI被置位,那么系統將從當前LDT中尋找相應描 述符。也就是說,當代碼3.10中用到SelectorLDTCodeA時,系統會從LDT中找到LABEL_LDT_DESC_CODEA描述符,並跳轉到相應的段中。

 

這個LDT很簡單,只有一個代碼段。我們還可以在其中增加更多的段,比如數據段、堆棧段等,這樣一來,我們可以把一個單獨的任務所用到的所有東西封裝在一個LDT中。

 

通過幾個簡單的例子,我們對IA32的分段機制大致已經有所了解了。“保護模式”中“保護”二字到底是什么含義? 在描述符中段基址和段界限定義了一個段的范圍,對超越段界限之外的地址的訪問是被禁止的,這無疑是對段的一種保護。另外,有點復雜的段屬性作為對一個段各個方面的定義規定和限制了段的行為和性質,從功能上來講,這仍然是一種保護。

 

調試:

編譯pmtest3.asm為pmtest3.com,在dos運行

 

 

5. 調試代碼,/d/掌握一致代碼段、非一致代碼段、數據段的權限

訪問規則,掌握CPL、DPL、RPL之間關系,以及段間切換的基

本方法

分析:

(1)特權級

IA32的分段機制中,特權級總共有4個特權級別,從高到低分別是0、1、2、3。數字越小表示的特權級越大,較為核心的代碼和數據,將被放在特權級較高的層級中。處理器將用這樣的機制來避免低特權級的任務在不被 允許的情況下訪問位於高特權級的段。如果處理器檢測到一個訪問請求是不合法的,將會產生常規保護錯誤(#GP)。

 

 

(2)CPL,DPL,RPL

CPL是存寄存器如CS中,

RPL是代碼中根據不同段跳轉而確定,以動態刷新CS里的CPL.

DPL是在GDT/LDT描述符表中,靜態的。

一致代碼段:

  簡單理解,就是操作系統拿出來被共享的代碼段,可以被低特權級的用戶直接調用訪問的代碼。通常這些共享代碼,是"不訪問"受保護的資源和某些類型異常處理。比如一些數學計算函數庫,為純粹的數學運算計算,被作為一致代碼段。

一致代碼段的限制作用:

特權級高的程序不允許訪問特權級低的數據:核心態不允許調用用戶態的數據.

特權級低的程序可以訪問到特權級高的數據.但是特權級不會改變:用戶態還是用戶態.

非一致代碼段:

為了避免低特權級的訪問而被操作系統保護起來的系統代碼.

非一致代碼段的限制作用

只允許同級間訪問.

絕對禁止不同級訪問:核心態不用用戶態.用戶態也不使用核心態.

 通常低特權代碼必須通過"門"來實現對高特權代碼的訪問和調用。不同級別代碼段之間轉移規則,是通過CPL/RPL/DPL來校驗。先來理解這幾個概念。

 

CPL(Current PrivilegeLevel)

CPL是當前執行的程序或任務的特權級。它被存儲在cs和ss的第0位和第1位上。在通常情況下,CPL等於代碼所在的段的 特權級。當程序轉移到不同特權級的代碼段時,處理器將改變CPL。

在遇到一致代碼段時,情況稍稍有點特殊,一致代碼段可以被相同或者更低特權級的代碼訪問。當處理器訪問一個與 CPL特權級不同的一致代碼段時,CPL不會被改變。

 DPL(Descriptor Privilege Level)

DPL表示段或者門的特權級。它被存儲在段描述符或者門描述符的DPL字段中,正如我們先前所看到的那樣。當當前代碼段試圖訪問一個段或者門時,DPL將會和CPL以及段或門選擇子的RPL相比較,根據段或者門類型的不同,DPL將會被區別 對待,下面介紹一下各種類型的段或者門的情況。

數據段DPL規定了可以訪問此段的最低特權級。比如,一個數據段的DPL是1,那么只有運行在CPL為0或者 1的程序才有權訪問它。

非一致代碼段(不使用調用門的情況下):DPL規定訪問此段的特權級。比如,一個非一致代碼段的特 權級為0,那么只有CPL為0的程序才可以訪問它。

調用門:DPL規定了當前執行的程序或任務可以訪問此調用門的最低特權級(這與數據段的規則是一致的)。

一致代碼段和通過調用門訪問的非一致代碼段DPL規定了訪問此段的最高特權級。比如,一個一致代 碼段的DPL是2,那么CPL為0和1的程序將無法訪問此段。

TSS:DPL規定了可以訪問此TSS的最低特權級(這與數據段的規則是一致的)。(TSS 全稱task state segment,是在操作系統進程管理的過程中,任務(進程)切換時的任務現場信息。)

 

RPL(Requested PrivilegeLevel)

RPL是通過段選擇子的第0位和第1位表現出來的。處理器通過檢查RPL和CPL來確認一個訪問請求是否合法。即便提出訪問請求的段有足夠的特權級,如果RPL不夠也是不行的。也就是說,如果RPL的數字比CPL大(數字越大特權級越低), 那么RPL將會起決定性作用,反之亦然。

操作系統過程往往用RPL來避免低特權級應用程序訪問高特權級段內的數據。當操作系統過程(被調用過程)從一個應用程序(調用過程)接收到一個選擇子時,將會把選擇子的RPL設成調用者的特權級。於是,當操作系統用這個選擇子 去訪問相應的段時,處理器將會用調用過程的特權級(已經被存到RPL中),而不是更高的操作系統過程的特權級(CPL)進行特權檢驗。這樣,RPL就保證了操作系統不會越俎代庖地代表一個程序去訪問一個段,除非這個程序本身是有權限的。

例子:

的數據段的選擇子的RPL改為3:

SelectorData equ LABEL_DESC_DATA-LABEL_GDT+SA_RPL3

再運行一下,發生了什么?

Bochs重啟了,系統崩潰了,在控制台你能看到這樣的字樣:

load_seg_reg(DS): RPL & CPL must be <= DPL

容易理解,崩潰的原因在於我們違反了特權級的規則,用RPL=3的選擇子去訪問DPL=1的段,於是引起異常。而我們又沒有相應 的異常處理模塊,於是最為嚴重的情況就發生了。 

(3)不同特權級代碼段間轉移

程序從一個代碼段轉移到另一個代碼段之前,目標代碼段的選擇子會被加載到cs中。作為加載過程的一部分,處理器將會檢查描述符的界限、類型、特權級等內容。如果檢驗成功,cs將被加載,程序控制將轉移到新的代碼段中,從eip指示的位置開始執 行。

程序控制轉移的發生,可以是由指令jmp、call、ret、sysenter、sysexit、int n 或iret引起的,也可以由中斷和異常機制 引起。

使用jmp或call指令可以實現下列4種轉移:

1. 目標操作數包含目標代碼段的段選擇子。

2. 目標操作數指向一個包含目標代碼段選擇子的調用門描述符。

3. 目標操作數指向一個包含目標代碼段選擇子的TSS。

4. 目標操作數指向一個任務門,這個任務門指向一個包含目標代碼段選擇子的TSS。

4 種方式可以看做是兩大類,一類是通過jmp和call的直接轉移(上述第1種),另一類是通過某個描述符的間接轉移(上述 第2、3、4種)。下面就來分別看一下。

 

(4)通過jmp或call直接轉移

如果目標是非一致代碼段,要求CPL必須等於目標段的

DPL,同時要求RPL小於等於DPL;如果目標是一致代碼段,則要求CPL大於或者等於目標段的DPL,RPL此時不做檢查。當轉移到一致

代碼段中后,CPL會被延續下來,而不會變成目標代碼段的DPL。也就是說,通過jmp和call所能進行的代碼段間轉移是非常有限

的,對於非一致代碼段,只能在相同特權級代碼段之間轉移。遇到一致代碼段也最多能從低到高,而且CPL不會改變。如果想自由

地進行不同特權級之間的轉移,顯然需要其他幾種方式,即運用門描述符或者TSS。

 

(5)基本的調用門進行段轉移(先不涉及特權級轉換,用門特權級轉換見6./e/)

門:門也是一種描述符,門描述符的結構如圖3.13

 

 

可以看到,門描述符和我們前面提到的描述符有很大不同,它主要是定義了目標代碼對應段的選擇子、入口地址的偏移和一些 屬性等。可是,雖然這樣的結構跟代碼段以及數據段描述符大不相同,我們仍然看到,第5個字節(BYTE5)卻是完全一致的,都表 示屬性。在這個字節內,各項內容的含義與前面提到的描述符也別無二致,這顯然是必要的,以便識別描述符的類型。在這里,S 位將是0

直觀來看,一個門描述了由一個選擇子和一個偏移所指定的線性地址,程序正是通過這個地址進 行轉移的。門描述符分為4種:

調用門(Call gates)

中斷門(Interrupt gates)

陷阱門(Trap gates)

任務門(Task gates)

其中,中斷門和陷阱門是特殊的調用門,將會在后面提到,我們先來介紹調用門。在這個例子中,我們用到調用門。為簡單起見,先不涉及任何特權級變換,而是先來關注它的工作方法。

pmtest3.asm的基礎上修改為pmtest4.asm

 

增加一個代碼段作為通過調用門轉移的目標段

添加[SECTION .sdset]:調用selectvideo在屏幕上顯示C。因為打算用call指令調用將要建立的調用門,所以,在這段代碼的結尾處調用了一個retf指令。

然后加入該段的descriptor以及selector,並初始化

 

 

然后添加調用門的descriptor以及selector

使用宏GATE(在pm.inc定義)初始化門的descriptor

SelectorCodeDest就是這個調用門要調用的段的selector,也就是我們剛剛在上面定義的段的selector

 

 

 

 

 

 

然后就准備好了要被調用的段以及調用門

 

下面進行調用

Call 測試調用門后retf,相當於繼續運行,從235行開始繼續。

調用門准備就緒,它指向的位置是SelectorCodeDest:0,即標號LABEL_SEG_CODE_DEST處的代碼

用一個call指令來使用這個調用門是個好主意 :

 

233 ; 測試調用門(無特權級變換),將打印字母'C'

⇒ 234 call SelectorCallGateTest:0

...

241 jmp SelectorLDTCodeA:0 ; 跳入局部任務,將打印字母'L'

 

這個call指令被放在進入局部任務之前,由於我們新加的代碼以指令retf結尾,所以最終代碼將會跳回 到call指令的下面繼續執行。所以,我們最終看到的結果應該是在pmtest3.exe執行結果的基礎上多出一個紅色的字母C。

 

其實調用門本質上只不過是個入口地址,只是增加了若干的屬性而已。在我們的例子中所用到的調用門完全等同於一個地址,我們甚至可以把使用調用門進行跳轉的指令修改為跳轉到調用門內指定的地址的指令:

call SelectorCodeDest:0

運行一下,效果是完全相同的。(下面是更復雜的情況)

6)使用調用門進行轉移時特權級檢驗的規則。

假設我們想由代碼A轉移到代碼B,運用一個調用門G,即調用門G中的目標選擇子指向代碼B的段。實際上,我們涉及了這么幾個要素:CPL、RPL、代碼B的DPL(記做DPL_B)、調用門G的DPL(記做DPL_G)。根據3.2.3.1中提到的,A訪問G這個調用門時,規則相當於訪問一個數據段,要求CPL和RPL都小於或者等於DPL_G。換句話說,CPL和RPL需在更高的特權級上。

除了這一步要符合要求之外,系統還將比較CPL和DPL_B。如果是一致代碼段的話,要求DPL_B≤CPL;如果是非一致代碼段的話,call指令和jmp指令又有所不同。在用call指令時,要求DPL_B≤CPL;在用jmp指令時,只能是DPL_B=CPL。

綜上所述,調用門使用時特權檢驗的規則如表所示。

 

 

 

也就是說,通過調用門和call指令,可以實現從低特權級到高特權級的轉移,無論目標代碼段是一致的還是非一致的。

 

 

調試:

編譯pmtest4.asm為pmtest4.com,在dos運行

pmtest3.asm的基礎上又多顯示了C。是調用門調用的段的輸出

 

 

6.調試代碼,/e/掌握利用調用門進行特權級變換的轉移

分析:

(1)跳轉與堆棧

通過調用門和call指令,可以實現從低特權級到高特權級的轉移,無論目標代碼段是一致的還是非一致的。 那么如何進行高特權級向低特權級轉換?

有特權級變換的轉移的復雜之處,不但在於嚴格的特權級檢驗,還在於特權級變化的時候,堆棧也要發生變化。處理器的這種 機制避免了高特權級的過程由於棧空間不足而崩潰。而且,如果不同特權級共享同一個堆棧的話,高特權級的程序可能因此受到有意或無意的干擾。

在我們的程序中,指令call DispReturn和call SelectorCodeDest:0顯然不同。與在實模式下類似,如果一個調用或跳轉指 令是在段間而不是段內進行的,那么我們稱之為“長”的(Far jmp/call),反之,如果在段內則是“短”的(Near jmp/call)。  (與windows不同)

那么長的和短的jmp或call有什么分別呢?對於jmp而言,僅僅是結果不同罷了,短跳轉對應段內,而長跳轉對應段間;而call 則稍微復雜一些,因為call指令是會影響堆棧的,長調用和短調用對堆棧的影響是不同的。我們下面的討論只考慮32位的情況.

 

對於短調用來說,call指令執行時下一條指令的eip壓棧,到ret指令執行時,這個eip會被從堆棧中彈出,如圖所示。

先從右向左壓棧參數,然后壓棧下一條指令eip,(從高地址到低地址壓棧)eip寄存器存儲着我們cpu要讀取指令的地址每次cpu執行都要先讀取eip寄存器的值,然后定位eip指向的內存地址。Esp是當前堆棧的指針寄存器,指向當前堆棧的底部位置。

 

 

可以看出,調用者的eip被壓棧,而在此之前參數已經入棧。圖中的“調用者

eip”對應nop指令地址。而在函數foo調用最后一條指令ret(帶有參數)返回之前和之后,堆棧的變化如圖所示。可見esp指向的內存中,存放着call后下一條指令的地址(nop)

 

 

長調用的情況與此類似,容易想到,返回的時候跟調用的時候一樣也是“長”轉移,所以返回的時候也需

要調用者的cs,於是call指令執行時被壓棧的就不僅有eip,還應該有cs,如圖所示。

 

 

帶參數的ret指令執行前后的情形如圖所示。

 

 

(2)通過調用門進行特權級轉換

call一個調用門也是長調用,情況跟上面 所說的長調用差不多。可是由於一些原因堆棧發生了切換,也就是說,call指令執行前后的堆棧已經 不再是同一個。我們在堆棧A中壓入參數和返回時地址,等到需要使用它們的時候堆棧已經變成B了。Intel提供了這樣一種機制,將堆棧A的諸多內容復制到堆棧B中,如圖所示。 

 

 

事實上,由於每一個任務最多都可能在4個特權級間轉移,所以,每個任務實際上需要4個堆棧。可 是,我們只有一個ss和一個esp,那么當發生堆棧切換,我們該從哪里獲得其余堆棧的ss和esp呢?這里涉及一樣TSS(Task-State Stack),它是一個數據結構,里面包含多個字段,32位TSS如圖所示。

 

 

可以看出,TSS包含很多個字段,但是在這里,我們只關注偏移4到偏移27的3個ss和3個esp。當發生堆棧切換時,內層的ss和 esp就是從這里取得的。

比如,我們當前所在的是ring3,當轉移至ring1時,堆棧將被自動切換到由ss1和esp1指定的位置。由於只是在由外層到內層 (低特權級到高特權級)切換時新堆棧才會從TSS中取得,所以TSS中沒有位於最外層的ring3的堆棧信息。

 

新堆棧的問題已經解決,下面就是CPU在整個過程中所做的工作:

1. 根據目標代碼段的DPL(新的CPL)從TSS中選擇應該切換至哪個ss和esp。

2. 從TSS中讀取新的ss和esp。在這過程中如果發現ss、esp或者TSS界限錯誤都會導致無效TSS異常(#TS)。

3. 對ss描述符進行檢驗,如果發生錯誤,同樣產生#TS 異常。

4. 暫時性地保存當前ss和esp的值。

5. 加載新的ss和esp。

6. 將剛剛保存起來的ss和esp的值壓入新棧。

7. 從調用者堆棧中將參數復制到被調用者堆棧(新堆棧)中,復制參數的數目由調用門中Param Count一項來決定。如果 Param Count是零的話,將不會復制參數。

8. 將當前的cs和eip壓棧。

9. 加載調用門中指定的新的cs和eip,開始執行被調用者過程。

 

在第7步中,解釋了調用門中Param Count的作用,Param Count只有5位,也就是說,最多只能復制31個參數。如果參數多於31個該怎么辦呢?這時可以讓其中的某個參數變成指向一 個數據結構的指針,或者通過保存在新堆棧里的ss和esp來訪問舊堆棧中的參數。

 

此刻結合TSS結構和上述步驟,可以理解通過調用門進行由外層到內層調用的全過程。那么,正如call指令對 應ret,調用門也面臨返回的問題。通過長短call和ret的堆棧變化這兩組對比,我們發現,ret基本上是call的反過程,只

是帶參數的ret指令會同時釋放事先被壓棧的參數。

實際上,ret這個指令不僅可以實現短返回和長返回,而且可以實現帶有特權級變換的長返回。由被調用者到調用者的返回過 程中,處理器的工作包含以下步驟:

1. 檢查保存的cs中的RPL以判斷返回時是否要變換特權級。

2. 加載被調用者堆棧上的cs和eip(此時會進行代碼段描述符和選擇子類型和特權級檢驗)。

3. 如果ret指令含有參數,則增加esp的值以跳過參數,然后esp將指向被保存過的調用者ss和esp。注意,ret的參數必須 對應調用門中的Param Count 的值。

4. 加載ss和esp,切換到調用者堆棧,被調用者的ss和esp被丟棄。在這里將會進行ss描述符、esp以及ss段描述符的檢驗。

5. 如果ret指令含有參數,增加esp的值以跳過參數(此時已經在調用者堆棧中)。

6. 檢查ds、es、fs、gs的值,如果其中哪一個寄存器指向的段的DPL小於CPL(此規則不適用於一致代碼段),那么一個空描述符會被加載到該寄存器。

如圖所示

 

 

綜上所述,使用調用門的過程實際上分為兩個部分,一部分是從低特權級到高特權級,通過調用門和call指令來實現;另一部

分則是從高特權級到低特權級,通過ret指令來實現。

(3)進入ring3

ret指令執行前,堆棧中應該已經准備好了目標代碼段的cs、eip,以及ss和esp,另外,還可能有參數。這些可以是處理器壓入棧的,也可以由我們自己壓棧。在我們的例子中,在ret前的堆棧如圖3.22所示。 

 

 

這樣,執行ret之后就可以轉移到低特權級代碼中了。在(pmtest4.asm)基礎上做一下修改(形成 pmtest5a.asm)。如上面的圖3.22所示,我們至少要添加一個ring3的代碼段和一個ring3的堆棧段。

 

(4)pmtest5a.asm 由ring0到ring3轉移

首先,我們之前的代碼都運行在ring0!

添加一個ring3代碼段[SECTION .ring3],一個ring3堆棧段[SECTION .s3]

這個ring3代碼段非常簡單,跟[SECTION .la]和[SECTION .sdest]的內容差不多,同樣是打印一個字符。

需要注意,由於這段代碼運行在ring3,而在其中由於要寫顯存而訪問到了VIDEO段,為了不會產生錯誤,我們把VIDEO段的DPL 修改為3。

25 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW + DA_DPL3

 

392行讓程序不再繼續執行。392 jmp $

之所以這樣做,是為了先驗證一下由ring0到ring3的轉移是否成功。如果屏幕上出 現紅色的3,並且停住不動,不再返回DOS,則說明轉移成功。

 

新段對應的描述符LABEL_DESC_CODE_RING3的屬性加上了DA_DPL3,讓它的DPL變成了3

相應選擇子SelectorCodeRing3的SA_RPL3將RPL也設成了3。

同時有堆棧段的descriptor LABEL_DESC_STACK3以及selector SelectorStack3,以及初始化,在此略去。

 

這樣,代碼段和堆棧段都已經准備好了。讓我們將ss、esp、cs、eip依次壓棧,並且執行retf指令。

266 push SelectorStack3

267 push TopOfStack3

107268 push SelectorCodeRing3

269 push 0

270 retf

此段代碼放在顯示完字符串“In Protect Mode now.”后立即執行。

編譯,運行。

會看到了紅色的3在“In Protect Mode now.”下方顯示。在這表明我們由ring0到ring3的轉移成功完成。

(5)pmtest5b.asm 在ring3中使用調用門

修改pmtest4中提到的調用門的selectorSelectorCallGateTest以及descriptorLABEL_CALL_GATE_TEST:的DPL,RPL

然后修改[SECTION .ring3]代碼,在死循環前添加

call SelectorCallGateTest:0。

修改描述符和選擇子是為了滿足CPL和RPL 都小於等於調用門DPL的條件。

編譯運行

出現錯誤。因為從低特權級到高特權級轉移的時候,需要用到 TSS。

 

(6)pmtest5c.asm 添加TSS,在ring3中使用調用門

因為從低特權級到高特權級轉移的時候,需要用到 TSS,在pmtest5c.asm中准備一個TSS

TSS作為數據結構有其descriptor LABEL_DESC_TSS,selector SelectorTSS以及段[SECTION .TSS]。定義及初始化見代碼

可以看出,除了0級堆棧之外,其他各個字段我們都沒做任何初始化。因為在本例中,我們只用到這一部分。

添加初始化TSS描述符的代碼之后,TSS就准備好了,我們需要在特權級變換之前加載它

311 call DispReturn

312

⇒ 313 mov ax, SelectorTSS

⇒ 314 ltr ax

315

316 push SelectorStack3

317 push TopOfStack3

318 push SelectorCodeRing3

319 push 0

320 retf

之后編譯運行,成功。顯示call調用門的C以及ring3段的3.

 

(7)pmtest5.asm 返回實模式

到目前為止,我們已經成功實現了兩次從高特權級到低特權級以及一次從低特權級到高特權級的轉移(ring0-ring3-ring-0-ring3,ring0打印“In protect mode”,然后到ring3打印3,然后ring3callgate到ring0打印L,然后返回ring3),最終在低特權級的代碼[SECTION .ring3] 中讓程序停住。我們已經具備了在各種特權級下進行轉移的能力,並且熟悉了調用門這種典型門描述符的用法。

 

為了讓我們的程序能夠順利地返回實模式,我們將調用局部任務的代碼加入到調用門的目標代碼([SECTION .sdest])。最后,程序將由這里進入局部任務,然后經由原路返回實模式。(ring3打印3,調用門,調用門打印C,調用局部任務LDT打印L,然后在局部任務jmp SelectorCode16:0返回16位代碼段,之后返回實模式)

346 [SECTION .sdest]; 調用門目標段

347 [BITS 32]

...

⇒ 359 mov ax, SelectorLDT

⇒ 360 lldt ax

361

⇒ 362 jmp SelectorLDTCodeA:0 ; 跳入局部任務,將打印字母'L'

編譯運行,結果應為顯示in protect mode ,3,c,l,然后返回實模式可以繼續運行

 

調試:

編譯為.com文件運行

pmtest5a

 

 

pmtest5b

 

 

 

pmtest5c

 

 

pmtest5

 

 

7.課后手動改:

1)自定義添加1個GDT代碼段、1個LDT代碼段,GDT段內要對一個內

存數據結構寫入一段字符串,然后LDT段內代碼段功能為讀取並打印該GDT的內容;

參考pmtest3.com

修改[SECTION .data1],修改字符串為StrTest: db "JUST MONIKA", 0

修改[SECTION .s32]; 32 位代碼段. 由實模式跳入.

改為如下,相當於直接跳到LDT中的descriptor

.........................

[SECTION .s32]; 32 位代碼段. 由實模式跳入.

[BITS 32]

LABEL_SEG_CODE32:

; Load LDT

mov ax, SelectorLDT

lldt ax

jmp SelectorLDTCodeA:0 ; 跳入局部任務

SegCode32Len equ $ - LABEL_SEG_CODE32

; END of [SECTION .s32]

........................

 

修改LDT中的段; CodeA (LDT, 32 位代碼段)[SECTION .la]

功能改為顯示GDT中[SECTION .DATA]段的字符串StrTest

........................

; CodeA (LDT, 32 位代碼段)

[SECTION .la]

ALIGN 32

[BITS 32]

LABEL_CODE_A:

mov ax, SelectorData

mov ds, ax ; 數據段選擇子

mov ax, SelectorVideo

mov gs, ax ; 視頻段選擇子

mov ax, SelectorStack

mov ss, ax ; 堆棧段選擇子

mov esp, TopOfStack

; 下面顯示一個字符串

mov ah, 0Ch ; 0000: 黑底    1100: 紅字

xor esi, esi

xor edi, edi

mov esi, OffsetStrTest ; 源數據偏移

mov edi, (80 * 10 + 0) * 2 ; 目的數據偏移。屏幕第 10 行, 第 0 列。

cld

.1:

lodsb

test al, al

jz .2

mov [gs:edi], ax

add edi, 2

jmp .1

.2: ; 顯示完畢

jmp SelectorCode16:0

CodeALen equ $ - LABEL_CODE_A

; END of [SECTION .la]

.........................

然后編譯運行。運行時順序為實模式跳轉保護模式[SECTION .s32],然后[SECTION .s32]加載LDT的ldtr,然后跳轉LDT的[SECTION .la]段,該段中先在屏幕顯示[SECTION .DATA]段的字符串StrTest,然后跳回實模式

代碼保存為pmtestmy.asm,編譯為pmtestmy.com。

編譯運行

(2)自定義2個GDT代碼段A、B,分屬於不同特權級,功能自定義,要求實現A-->B的跳轉,以及B-->A的跳轉。

參考pmtest5,實現了ring0->ring3->ring0->ring3d的跳轉

 

 

 

二.是書上內容的節選,代碼里有一點注釋。再翻翻書的保護模式那一章吧

 

x86 CPU的基本模式:實模式、保護模式

 

– 實模式

 

• 地址總線寬度:20bit

 

• 寄存器和數據總線寬度:16bit

 

• 尋址空間是多少?

 

• 實模式:PA=Segment*16+Offset

 

 

pmtest1.asm

; ==========================================
; pmtest1.asm
; 編譯方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================

%include    "pm.inc"    ; 常量, 宏, 以及一些說明

org    07c00h
    jmp    LABEL_BEGIN

[SECTION .gdt]  ;定義一個段,段名gdt
; GDT
;                              段基址,       段界限     , 屬性
LABEL_GDT:       Descriptor       0,                0, 0           ; 空描述符
LABEL_DESC_CODE32: Descriptor       0, SegCode32Len - 1, DA_C + DA_32; 非一致代碼段
LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, DA_DRW         ; 顯存首地址
; GDT 結束

GdtLen        equ    $ - LABEL_GDT    ; GDT長度 :equ相當於起個別名。S為當前位置。s-LABEL_GDT,就是當前位置減去.gdt起始位置,也就是.gdt長度
GdtPtr        dw    GdtLen - 1    ; GDT界限  。GdtPtr也是個小的數據結構,它有6字節,前2字節是GDT的界限,后4字節是GDT的基地址

        dd    0        ; GDT基地址

; GDT 選擇子
SelectorCode32        equ    LABEL_DESC_CODE32    - LABEL_GDT   ;直觀地看,它好像是DESC_VIDEO這個描述符相對於GDT基址的偏移。實際上有其數據結構,其名選擇子
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT  ;

; END of [SECTION .gdt]

[SECTION .s16]
[BITS 16]   ;表明是16位代碼
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h

; 初始化 32 位代碼段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah

; 為加載 GDTR 作准備
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址

; 加載 GDTR
lgdt [GdtPtr]

; 關中斷
cli

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

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

; 真正進入保護模式
jmp dword SelectorCode32:0 ; 執行這一句會把 SelectorCode32 裝入 cs,  //selector16位,dword兩字節,高位selector,低位偏移0.(因為聲明了這段是16位代碼,所以一個字兩字節)
; 並跳轉到 Code32Selector:0 處
; END of [SECTION .s16]


[SECTION .s32]; 32 位代碼段. 由實模式跳入.
[BITS 32]

LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 視頻段選擇子(目的)

mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 紅字
mov al, 'P'
mov [gs:edi], ax

; 到此停止
jmp $

SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

 

 

好了,首先看[SECTION .gdt]這個段,其中的Descriptor是在pm.inc中定義的宏(見代碼3.2)。先不要管具體的意義是什
么,看字面我們可以知道,這個宏表示的不是一段代碼,而是一個數據結構,它的大小是8字節。 

 

在段[SECTION.gdt]中並列有3個Descriptor,看上去是個結構數組,你一定猜到了,這個數組的名字叫做GDT。
GdtLen是GDT的長度,GdtPtr也是個小的數據結構,它有6字節,前2字節是GDT的界限,后4字節是GDT的基地址。
另外還定義了兩個形如SelectorXXXX的常量,至於是做什么用的,我們暫且不管它。
再往下到了一個代碼段,[BITS 16]明確地指明了它是一個16位代碼段。你會發現,這段程序修改了一些GDT中的值,然后執行
了一些不常見的指令,最后通過jmp指令實現一個跳轉(第71行)。正如代碼注釋中所說的,這一句將“真正進入保護模式”。實
際上,它將跳轉到第三個section,即[SECTION .s32]中,這個段是32位的,執行最后一小段代碼。這段代碼看上去是往某個地址
處寫入了2字節,然后就進入了無限循環。 
 
可以看到,在屏幕中部右側,出現了一個紅色的字母“P”,然后再也不動了。不難猜到,程序的最后一部分代碼中寫入的兩個字節是寫進了顯存中。
現在,大致的感性認識已經有了,但你一定有一些疑惑,什么是GDT?那些看上去怪怪的指令到底在做什么?現在我們先來總結一下,在這個程序中,我們了解到什么,有哪些疑問。
 
我們了解到的內容如下:
程序定義了一個叫做GDT的數據結構。
后面的16位代碼進行了一些與GDT有關的操作。
程序最后跳到32位代碼中做了一點操作顯存的工作。
 
我們不明就里的內容如下:
GDT是什么?它是干什么用的?
程序對GDT做了什么?
那個jmp SelectorCode32:0跟我們從前用過的jmp有什么不同? 
 
 
在IA32下,CPU有兩種工作模式:實模式和保護模式。直觀地看,當我們打開自己的PC,開始時CPU是工作在實模式下的,經過某種機制之后,才進入保護模式。在保護模式下,CPU有着巨大的尋址能力,並為強大的32位操作系統提供了更好的硬件保障。
 
我們先來回憶一下舊政策。Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的數據總線(Data Bus)以及20位的地址總線(Address Bus)和1MB的尋址能力。一個地址是由段和偏移兩部分組成的,物理地址遵循這樣的計算公式:
物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)
其中,段值和偏移都是16位的。
 
從80386開始,Intel家族的CPU進入32位時代。80386有32位地址線,所以尋址空間可以達到4GB。所以,單從尋址這方面說,使用16位寄存器的方法已經不夠用了。這時候,我們需要新的方法來提供更大的尋址能力。當然,慢慢地你能看到,保護模式的優點不僅僅在這一個方面。
 
在實模式下,16位的寄存器需要用“段:偏移”這種方法才能達到1MB的尋址能力,如今我們有了32位寄存器,一個寄存器就可以尋址4GB的空間,是不是從此段值就被拋棄了呢?實際上並沒有,新政策下的地址仍然用“段:偏移”這樣的形式來表示,只不過保護模式下“段”的概念發生了根本性的變化。
 
實模式下,段值還是可以看做是地址的一部分的,段值為XXXXh表示以XXXX0h開始的一段內存。
 
而保護模式下,雖然段值仍然由原來16位的cs、ds等寄存器表示,但此時它僅僅變成了一個索引,這個索引指向一個數據結構的一個表項,表項中詳細定義了段的起始地址、界限、屬性等內容。這個數據結構,就是GDT(實際上還可能是LDT,這個以后再介紹)。GDT中的表項也有一個專門的名字,叫做描述符(Descriptor)。
也就是說,GDT的作用是用來提供段式存儲機制,這種機制是通過段寄存器和GDT中的描述符共同提供的。為了全面地了解它,
我們來看一下圖3.4所示的描述符的結構。
 
 
 

 

 

 

 

這個示意圖表示的是代碼段和數據段描述符,此外,描述符的種類還有系統段描述符和門描述符,下文會有介紹。
除了BYTE5和BTYE6中的一堆屬性看上去有點復雜以外,其他三個部分倒還容易理解,它們分別定義了一個段的基址和界限。不過,由於歷史問題,它們都被拆開存放。
至於那些屬性,我們暫時先不管它。
好了,我們回頭再來看看代碼3.1,Descriptor這個宏用比較自動化的方法把段基址、段界限和段屬性安排在一個描述符中合適的位置,有興趣的讀者可以研究這個宏的具體內容。
本例的GDT中共有3個描述符,為方便起見,在這里我們分別稱它們為DESC_DUMMY、DESC_CODE32和DESC_VIDEO。
其中DESC_VIDEO的段基址是0B8000h,顧名思義,這個描述符指向的正是顯存。
現在我們已經知道,GDT中的每一個描述符定義一個段,那么cs、ds等段寄存器是如何和這些段對應起來的呢?你可能注意到了,在[SECTION.s32]這個段中有兩句代碼是這樣的(第80行和第81行):
 
mov ax, SelectorVideo
mov gs, ax
 
看上去,段寄存器gs的值變成了SelectorVideo,我們在上文中可以看到,SelectorVideo是這樣定義的(第25行):
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT
直觀地看,它好像是DESC_VIDEO這個描述符相對於GDT基址的偏移。實際上,它有一個專門的名稱,叫做選擇子(Selector),它也不是一個偏移,而是稍稍復雜一些,它的結構如圖3.5所示。

 

 

 

 

不難理解,當TI和RPL都為零時,選擇子就變成了對應描述符相對於GDT基址的偏移,就好像我們程序中那樣。
看到這里,讀者肯定已經明白了第86行的意思,gs值為SelectorVideo,它指示對應顯存的描述符DESC_VIDEO,這條指令將把
ax的值寫入顯存中偏移位edi的位置。
總之,整個的尋址方式如圖3.6所示。

 

 

 

注意圖3.6中“段:偏移”形式的邏輯地址(Logical Address)經過段機制轉化成“線性地址”(Linear Address),而不是“物理地址”(Physical Address),
其中的原因我們以后會提到。在上面的程序中,線性地址就是物理地址。另外,包含描述符的,不僅可以是GDT,也可以是LDT。 

 

明白了這些,離明白整個程序的距離已經只剩一層窗紙了。因為只剩下[SECTION .s16]這一段還沒有分析。不過,既然[SECTION .s32]是32位的程序,並且在保護模式下執行,那么[SECTION .s16]的任務一定是從實模式向保護模式跳轉了。下面我們就來看一下實模式是如何轉換到保護模式的。

 

讓我們到[SECTION .s16]這段,先看一下初始化32位代碼段描述符的這一段,代碼首先將LABEL_SEG_CODE32的物理地址(即
[SECTION .s32]這個段的物理地址)賦給eax,然后把它分成三部分賦給描述符DESC_CODE32中的相應位置。由於DESC_CODE32的段
界限和屬性已經指定,所以至此,DESC_CODE32的初始化全部完成。
 
接下來的動作把GDT的物理地址填充到了GdtPtr這個6字節的數據結構中,然后執行了一條指令(第55行):
 
lgdt [GdtPtr]

 

這一句的作用是將GdtPtr指示的6字節加載到寄存器gdtr,gdtr的結構如圖3.7所示。

 

 

 

 

 

 

 

 

 

 

 

pm.inc

; 描述符圖示

; 圖示一
;
;  ------ ┏━━┳━━┓高地址
;         ┃ 7  ┃ 段 ┃
;         ┣━━┫    ┃
;                  基
;  字節 7 ┆    ┆    ┆
;                  址
;         ┣━━┫ ② ┃
;         ┃ 0  ┃    ┃
;  ------ ┣━━╋━━┫
;         ┃ 7  ┃ G  ┃
;         ┣━━╉──┨
;         ┃ 6  ┃ D  ┃
;         ┣━━╉──┨
;         ┃ 50  ┃
;         ┣━━╉──┨
;         ┃ 4  ┃ AVL┃
;  字節 6 ┣━━╉──┨
;         ┃ 3  ┃    ┃
;         ┣━━┫ 段 ┃
;         ┃ 2  ┃ 界 ┃
;         ┣━━┫ 限 ┃
;         ┃ 1  ┃    ┃
;         ┣━━┫ ② ┃
;         ┃ 0  ┃    ┃
;  ------ ┣━━╋━━┫
;         ┃ 7  ┃ P  ┃
;         ┣━━╉──┨
;         ┃ 6  ┃    ┃
;         ┣━━┫ DPL┃
;         ┃ 5  ┃    ┃
;         ┣━━╉──┨
;         ┃ 4  ┃ S  ┃
;  字節 5 ┣━━╉──┨
;         ┃ 3  ┃    ┃
;         ┣━━┫ T  ┃
;         ┃ 2  ┃ Y  ┃
;         ┣━━┫ P  ┃
;         ┃ 1  ┃ E  ┃
;         ┣━━┫    ┃
;         ┃ 0  ┃    ┃
;  ------ ┣━━╋━━┫
;         ┃ 23 ┃    ┃
;         ┣━━┫    ┃
;         ┃ 22 ┃    ┃
;         ┣━━┫ 段 ┃
;
;   字節  ┆    ┆ 基 ┆
; 2, 3, 4
;         ┣━━┫ 址 ┃
;         ┃ 1  ┃ ① ┃
;         ┣━━┫    ┃
;         ┃ 0  ┃    ┃
;  ------ ┣━━╋━━┫
;         ┃ 15 ┃    ┃
;         ┣━━┫    ┃
;         ┃ 14 ┃    ┃
;         ┣━━┫ 段 ┃
;
; 字節 0,1┆    ┆ 界 ┆
;
;         ┣━━┫ 限 ┃
;         ┃ 1  ┃ ① ┃
;         ┣━━┫    ┃
;         ┃ 0  ┃    ┃
;  ------ ┗━━┻━━┛低地址
;


; 圖示二

; 高地址………………………………………………………………………低地址

; |   7   |   6   |   5   |   4   |   3   |   2   |   1   |   0    |
; |7654321076543210765432107654321076543210765432107654321076543210|    <- 共 8 字節
; |--------========--------========--------========--------========|
; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
; ┃31..24┃   (見下圖)   ┃     段基址(23..0)    ┃ 段界限(15..0)┃
; ┃      ┃              ┃                      ┃              ┃
; ┃ 基址2┃③│②│    ①┃基址1b│   基址1a     ┃    段界限1   ┃
; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫
; ┃   %6 ┃  %5  ┃  %4  ┃  %3  ┃     %2       ┃       %1     ┃
; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛
;         │                \_________
;         │                          \__________________
;         │                                             \________________________________________________
;         │                                                                                              \
;         ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
;         ┃ 7654321076543210  ┃
;         ┣━━╋━━╋━━╋━━╋━━┻━━┻━━┻━━╋━━╋━━┻━━╋━━╋━━┻━━┻━━┻━━┫
;         ┃ G  ┃ D  ┃ 0  ┃ AVL┃   段界限 2 (19..16)  ┃  P ┃   DPL    ┃ S  ┃       TYPE           ┃
;         ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫
;         ┃      ③: 屬性 2      ┃    ②: 段界限 2      ┃                   ①: 屬性1                  ┃
;         ┗━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┛
;       高地址                                                                                          低地址
;
;

; 說明:
;
; (1) P:    存在(Present)位。
;        P=1 表示描述符對地址轉換是有效的,或者說該描述符所描述的段存在,即在內存中;
;        P=0 表示描述符對地址轉換無效,即該段不存在。使用該描述符進行內存訪問時會引起異常。
;
; (2) DPL:  表示描述符特權級(Descriptor Privilege level),共2位。它規定了所描述段的特權級,用於特權檢查,以決定對該段能否訪問。 
;
; (3) S:   說明描述符的類型。
;        對於存儲段描述符而言,S=1,以區別與系統段描述符和門描述符(S=0)。 
;
; (4) TYPE: 說明存儲段描述符所描述的存儲段的具體屬性。
;
;         
;    數據段類型    類型值        說明
;            ----------------------------------
;            0        只讀 
;            1        只讀、已訪問 
;            2        讀/寫 
;            3        讀/寫、已訪問 
;            4        只讀、向下擴展 
;            5        只讀、向下擴展、已訪問 
;            6        讀/寫、向下擴展 
;            7        讀/寫、向下擴展、已訪問 
;
;        
;            類型值        說明
;    代碼段類型    ----------------------------------
;            8        只執行 
;            9        只執行、已訪問 
;            A        執行/讀 
;            B        執行/讀、已訪問 
;            C        只執行、一致碼段 
;            D        只執行、一致碼段、已訪問 
;            E        執行/讀、一致碼段 
;            F        執行/讀、一致碼段、已訪問 
;
;        
;    系統段類型    類型編碼    說明
;            ----------------------------------
;            0        <未定義>
;            1        可用286TSS
;            2        LDT
;            3        忙的286TSS
;            4        286調用門
;            5        任務門
;            6        286中斷門
;            7        286陷阱門
;            8        未定義
;            9        可用386TSS
;            A        <未定義>
;            B        忙的386TSS
;            C        386調用門
;            D        <未定義>
;            E        386中斷門
;            F        386陷阱門
;
; (5) G:    段界限粒度(Granularity)位。
;        G=0 表示界限粒度為字節;
;        G=1 表示界限粒度為4K 字節。
;           注意,界限粒度只對段界限有效,對段基地址無效,段基地址總是以字節為單位。 
;
; (6) D:    D位是一個很特殊的位,在描述可執行段、向下擴展數據段或由SS寄存器尋址的段(通常是堆棧段)的三種描述符中的意義各不相同。 
;           ⑴ 在描述可執行段的描述符中,D位決定了指令使用的地址及操作數所默認的大小。
;        ① D=1表示默認情況下指令使用32位地址及32位或8位操作數,這樣的代碼段也稱為32位代碼段;
;        ② D=0 表示默認情況下,使用16位地址及16位或8位操作數,這樣的代碼段也稱為16位代碼段,它與80286兼容。可以使用地址大小前綴和操作數大小前綴分別改變默認的地址或操作數的大小。 
;           ⑵ 在向下擴展數據段的描述符中,D位決定段的上部邊界。
;        ① D=1表示段的上部界限為4G;
;        ② D=0表示段的上部界限為64K,這是為了與80286兼容。 
;           ⑶ 在描述由SS寄存器尋址的段描述符中,D位決定隱式的堆棧訪問指令(如PUSH和POP指令)使用何種堆棧指針寄存器。
;        ① D=1表示使用32位堆棧指針寄存器ESP;
;        ② D=0表示使用16位堆棧指針寄存器SP,這與80286兼容。 
;
; (7) AVL:  軟件可利用位。80386對該位的使用未左規定,Intel公司也保證今后開發生產的處理器只要與80386兼容,就不會對該位的使用做任何定義或規定。 
;


;----------------------------------------------------------------------------
; 在下列類型值命名中:
;       DA_  : Descriptor Attribute
;       D    : 數據段
;       C    : 代碼段
;       S    : 系統段
;       R    : 只讀
;       RW   : 讀寫
;       A    : 已訪問
;       其它 : 可按照字面意思理解
;----------------------------------------------------------------------------

; 描述符類型
DA_32        EQU    4000h    ; 32 位段

DA_DPL0        EQU      00h    ; DPL = 0
DA_DPL1        EQU      20h    ; DPL = 1
DA_DPL2        EQU      40h    ; DPL = 2
DA_DPL3        EQU      60h    ; DPL = 3

; 存儲段描述符類型
DA_DR        EQU    90h    ; 存在的只讀數據段類型值
DA_DRW        EQU    92h    ; 存在的可讀寫數據段屬性值
DA_DRWA        EQU    93h    ; 存在的已訪問可讀寫數據段類型值
DA_C        EQU    98h    ; 存在的只執行代碼段屬性值
DA_CR        EQU    9Ah    ; 存在的可執行可讀代碼段屬性值
DA_CCO        EQU    9Ch    ; 存在的只執行一致代碼段屬性值
DA_CCOR        EQU    9Eh    ; 存在的可執行可讀一致代碼段屬性值

; 系統段描述符類型
DA_LDT        EQU      82h    ; 局部描述符表段類型值
DA_TaskGate    EQU      85h    ; 任務門類型值
DA_386TSS    EQU      89h    ; 可用 386 任務狀態段類型值
DA_386CGate    EQU      8Ch    ; 386 調用門類型值
DA_386IGate    EQU      8Eh    ; 386 中斷門類型值
DA_386TGate    EQU      8Fh    ; 386 陷阱門類型值


; 選擇子圖示:
;         ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
;         ┃ 1514131211109876543210  ┃
;         ┣━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━╋━━╋━━┻━━┫
;         ┃                                 描述符索引                                 ┃ TI ┃   RPL    ┃
;         ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━┻━━━━━┛
;
; RPL(Requested Privilege Level): 請求特權級,用於特權檢查。
;
; TI(Table Indicator): 引用描述符表指示位
;    TI=0 指示從全局描述符表GDT中讀取描述符;
;    TI=1 指示從局部描述符表LDT中讀取描述符。
;

;----------------------------------------------------------------------------
; 選擇子類型值說明
; 其中:
;       SA_  : Selector Attribute

SA_RPL0        EQU    0    ; ┓
SA_RPL1        EQU    1    ; ┣ RPL
SA_RPL2        EQU    2    ; ┃
SA_RPL3        EQU    3    ; ┛

SA_TIG        EQU    0    ; ┓TI
SA_TIL        EQU    4    ; ┛
;----------------------------------------------------------------------------



; 宏 ------------------------------------------------------------------------------------------------------
;
; 描述符
; usage: Descriptor Base, Limit, Attr
;        Base:  dd  ;段基址
;        Limit: dd (low 20 bits available) ;段界限
;        Attr:  dw (lower 4 bits of higher byte are always 0) ;段屬性
%macro Descriptor 3 ;macro定義宏。 3表示有三個參數
    dw    %2 & 0FFFFh                ; 段界限1
    dw    %1 & 0FFFFh                ; 段基址1
    db    (%1 >> 16) & 0FFh            ; 段基址2
    dw    ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)    ; 屬性1 + 段界限2 + 屬性2
    db    (%1 >> 24) & 0FFh            ; 段基址3
%endmacro ; 共 8 字節
;
; 門
; usage: Gate Selector, Offset, DCount, Attr
;        Selector:  dw
;        Offset:    dd
;        DCount:    db
;        Attr:      db
%macro Gate 4
    dw    (%2 & 0FFFFh)                ; 偏移1
    dw    %1                    ; 選擇子
    dw    (%3 & 1Fh) | ((%4 << 8) & 0FF00h)    ; 屬性
    dw    ((%2 >> 16) & 0FFFFh)            ; 偏移2
%endmacro ; 共 8 字節
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM