羽夏看Linux核心——段相關入門知識

2022-08-04 21:00:37

寫在前面

  此係列是本人一個字一個字碼出來的,包括範例和實驗截圖。如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Linux系統核心——簡述 ,方便學習本教學。

前置知識

  在開始正式介紹之前,有一些知識需要講解一下,否則基本就是聽天書。但是,有些知識是本教學的前置知識,也就是說,我不會在該教學介紹,但我們會去使用它:

  • 程式編寫和現代作業系統的基本概念,比如虛擬地址、記憶體、程序執行緒等;
  • C/C++ 編寫以及使用 GCC 編譯;
  • 8086組合的編寫以及兩種語法;
  • Make 的使用;

基礎知識

  下面我們來介紹一些基礎知識和硬體的「硬性規定」。

真真實模式與保護模式

  真真實模式是Intel 80286和之後的8086相容CPU的操作模式。真真實模式的特性是一個20位的記憶體地址空間,它定址具有1MB的記憶體的能力,可以直接軟體存取BIOS以及周邊硬體,沒有硬體支援的分頁機制和實時多工概念。從80286開始,所有的8086 CPU的開機狀態都是真真實模式。8086等早期的CPU只有一種操作模式,類似於真真實模式。

段暫存器

  當我們用組合讀寫某一個地址時,比如用下面的程式碼:

mov dword ptr ds:[0x123456], eax

  其實我們真正讀寫的地址是:ds.base + 0x123456。並不是0x123456,不過正好的是ds段暫存器的基址是0而已。
  段暫存器有這幾個:ES、CS、SS、DS、FS、GS、LDTR、TR,它們各有自己特殊的用途。
  段暫存器的結構可用下圖表示:

  段暫存器具有96位,但我們可見的只有16位元。我們可以用偵錯程式隨意載入一個程式,但由於我是64位元系統,無法編譯32位元程式,也找不到相應的程式,就不給圖了。
  既然是暫存器了,那就可以進行讀寫操作,如下將介紹讀寫段暫存器的操作:

  • Mov指令:MOV AX,ES,但只能讀16位元的可見部分;MOV DS,AX寫段暫存器,寫的是96位。
  • 讀寫LDTR的指令為:SLDT/LLDT
  • 讀寫TR的指令為:STR/LTR

CPU分級

  如果要講段描述符與段選擇子,先介紹CPU分級的概念。數值上越小,許可權越大。如果低許可權存取高許可權的東西,會導致失敗。0環被核心使用,雖然1環2環存在,但Windows只用了3環注意在學習保護模式是時候不要把作業系統的概念扯進去,還沒到作業系統層面。 CPU分級示意圖如下:

GDT 與 LDT

  GDT是全域性描述符表。LDT為區域性描述符表,但Windows並沒有使用它,故不再介紹,感興趣請查詢Intel白皮書。當我們執行類似MOV DS,AX指令時,CPU會查表,根據AX的值來決定查詢GDT還是LDT,並找到對應的段描述符。段描述符將會在後面部分進行介紹。

  GDT表存在於記憶體之中。CPU要想找到它,就必須知道它的位置。於是乎CPU有一個暫存器。它被稱之為GDTR,儲存了GDT表的位置和大小,是一個48位元的暫存器,用C語言表示如下:

struct GDTR
{
    DWORD GDTBase;    //GDT表的地址
    SHORT limit;      //GDT表的大小
}

段選擇子

  段選擇子結構簡單,那我先介紹它。它是一個16位元的描述符,指向了定義該段的段描述符(段描述符比較複雜,後面將會完整介紹)。段選擇子結構如下圖所示:

  它的成員解釋如下:

  • RPL:請求特權級別,通俗的講我用什麼許可權來請求。
  • TI:TI=0時,查GDT表;TI=1時,查LDT表。
  • Index:處理器將索引值乘以8在加上GDT或者LDT的基地址,就是要載入的段描述符。

段描述符

  既然提到段描述符,那我來介紹一下它的結構如下圖所示:

  段描述符有很多成員,它的成員將會在下面詳細介紹,學習的時候一定要按照我介紹的順序進行學習:

P位

  P = 1段描述符有效,P = 0段描述符無效。

Base

  Base被分成了三個部分,從圖可知:Base的低16位元被放到了段描述符的低四個位元組,高16位元被均分到段描述符的高四個位元組的頭和尾。把它們依次拼接起來就是完整的Base

Limit

  由圖可知,把段描述符中所有的Limit拼接起來就只有20位。上一節教學說它有32位元的Limit。那就是要看G位了。

G位

  如果G = 0,說明段描述符中的Limit的單位是位元組,段長度Limit範圍可從1B~1MB,即在20位的前面補3個0即可;如果G = 1,說明段描述符中的Limit的單位是位元組為4KB,即段長度Limit範圍可從4KB~4GB,在20位的後面補充FFF即可。舉個例子,如果Limit拼接後的為FFFFF,如果G為0則為000FFFFF,反之為FFFFFFF

S位

  S = 1程式碼段或者資料段描述符,S = 0系統段描述符。

TYPE域

  TYPE域是比較複雜的成員,它表示的含義受S位的影響。

  • 當S位為1時

  此時段描述符表示的是程式碼段或者資料段,如下圖所示:

  對於表格中Type域的屬性和含義,如下表格所示:

屬性 含義 屬性 含義
A 存取位 E 向下擴充套件位
R 可讀位 W 可寫位
C 一致位

  對於比較特殊的屬性,我們將進一步介紹:

C位

  C = 1:一致程式碼段;C = 0:非一致程式碼段。什麼是一致程式碼段,什麼是非一致程式碼段,將在後面的教學進行介紹。

E位

  什麼是向下拓展位,我們以fs為例來看一下如下示意圖:

  左邊表示向上拓展,右邊是向下拓展。即向上拓展basebase+limit之間區域有效,其餘無效;向下拓展basebase+limit之間的區域無效,其餘有效。這個位針對資料段有效。

  • 當S位為0時

  此時段描述符表示的是系統段,系統段有很多種,將會在後面的教學進行詳細講解。Type域每一個數值的含義如下圖所示:

DB位

  DB位對不同的段具有不同的影響,情況如下:

1️⃣ 對CS段的影響
  D = 1採用32位元定址方式,D = 0採用16位元定址方式。

2️⃣ 對SS段的影響
  D = 1隱式堆疊存取指令(如:PUSH POP CALL)使用32位元堆疊指標暫存器ESPD = 0隱式堆疊存取指令(如:PUSH POP CALL)使用16位元堆疊指標暫存器SP

3️⃣ 向下拓展的資料段
  D = 1段上線為4GBD = 0段上線為64KB。至於是什麼意思,我們來看下面一張圖。

  紅色表示向下拓展能定址的範圍。可以看出,如果D = 0,就算原來能定址4GB,因為DB位的限制導致最大範圍是64KB

DPL

  DPL(Descriptor Privilege Level),即描述符特權級別,規定了存取該段所需要的特權級別是什麼。如果通俗的理解,就是:如果你想存取我,那麼你應該具備什麼許可權

AVL

  AVL指示是否可供系統軟體使用,由作業系統來使用,CPU並不使用它。

載入段描述符至段暫存器

  除了MOV指令,我們還可以使用LESLSSLDSLFSLGS指令修改暫存器。CS不能通過上述的指令進行修改,CS為程式碼段,CS的改變會導致EIP的改變,要改CS,必須要保證CSEIP同時改,後面會講解。

CPL/RPL/DPL

  • CPL:CPU當前的許可權級別
  • DPL:如果你想存取我,你應該具備什麼樣的許可權(CPL)
  • RPL:用什麼許可權去存取一個段

RPL存在的意義

  舉個例子,我們本可以用讀寫的許可權去開啟一個檔案,但為了避免出錯,有些時候我們使用唯讀的許可權去開啟。

一致程式碼段與非一致程式碼段

對於一致程式碼段,也稱為共用段:

  • 特權級高的程式不允許存取特權級低的資料:核心態不允許存取使用者態的資料
  • 特權級低的程式可以存取到特權級高的資料,但特權級不會改變:使用者態還是使用者態

對於非一致程式碼段:

  • 只允許同級存取
  • 絕對禁止不同級別的存取:核心態不是使用者態,使用者態也不是核心態

資料段的許可權檢查

  數值上,CPL<=DPLRPL<=DPL。同時滿足上述條件才能通過。

程式碼段的許可權檢查

  下面的比較都是數值上的比較:

  • 如果是非一致程式碼段,要求:CPL==DPLRPL<=DPL
  • 如果是一致程式碼段,要求:CPL>=DPL

程式碼跨段基礎

  程式碼跨段本質就是修改CS段暫存器。前面的教學介紹過段暫存器讀寫,除CS外,其他的段暫存器都可以通過MOV/LES/LSS/LDS/LFS/LGS指令進行修改。但是CS為什麼不可以直接修改呢?CS的改變意味著EIP的改變,改變CS的同時必須修改EIP,故我們無法使用上面的指令來進行修改,這個也是CPU不允許的。

程式碼間的段間跳轉

  段間跳轉,有2種情況,即要跳轉的段是一致程式碼段還是非一致程式碼段,它們不同做的許可權檢查就不同。
  同時修改CSEIP的指令如下:JMP FAR/CALL FAR/RETF/INT/IRETED

  本篇只介紹段間跳轉,故只使用JMP FAR,即為長跳轉。下面我舉個範例來進行講解:

CPU如何執行這行程式碼JMP 0x20:0x004183D7

1️⃣ 段選擇子拆分
  0x20對應二進位制形式:0000 0000 0010 0000

  • 解析結果:
    • RPL = 0
    • TI = 0
    • Index = 4

2️⃣ 查表得到段描述符

  TI=0 所以查GDT表,Index=4找到對應的段描述符。注意四種情況可以跳轉:程式碼段、呼叫門、TSS任務段、任務門。後面的幾種將會在以後的教學詳細講解。

3️⃣ 許可權檢查

  請參考本節的程式碼段的許可權檢查

4️⃣ 載入段描述符
  通過上面的許可權檢查後,CPU會將段描述符載入到CS段暫存器中。

5️⃣ 程式碼執行
  CPUCS.Base + Offset的值寫入EIP然後跳轉到將要執行的CS:EIP處的程式碼,段間跳轉結束。

直接對程式碼段進行JMP或者CALL的操作,無論目標是一致程式碼段還是非一致程式碼段,CPL都不會發生改變。如果要提升CPL的許可權,只能通過呼叫門。

練習與思考

本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教學的捷徑。

  俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教學了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。

  1. 為什麼20位的定址可以達到1MB
  2. 拆分如下的段描述符:
00000000`00000000 00cf9b00`0000ffff
00cf9300`0000ffff 00cffb00`0000ffff
00cff300`0000ffff 80008b04`200020ab
ffc093df`f0000001 0040f300`00000fff
0000f200`0400ffff 00000000`00000000
80008955`22000068 80008955`22680068
00009302`2f40ffff 0000920b`80003fff
ff0092ff`700003ff 80009a40`0000ffff
80009240`0000ffff 00009200`00000000
  1. 拆分如下段選擇子:
002B 0023 0010 001B 003B
  1. 快速辨別問題2給定段描述符是否可用以及段基址、段長(至少10個)
  2. 記住程式碼段間跳轉的執行流程。

下一篇

  羽夏看Linux核心——門相關入門知識