這一篇,是重點!我們將去講解作業系統根據程式碼(邏輯)地址去訪問真實實體地址的全過程。
將把全面幾節的東西全部用上,並完全梳理,完善細節。
前面講了分段、分頁機制,他們都可以實現,從虛擬地址(地址空間)向實體地址的轉換。但是,實際使用過程中,使用的是分段+分頁機制,段頁結合。
段頁結合
全過程分析(高能)
我們現在採用邊實驗邊講解翻譯全過程。
寫了一段 c 程式碼,編譯,然後在 Linux 0.11 中,進行除錯
#include <stdio.h>
int i = 0x12345678;
int main(void)
{
printf("The logical/virtual address of i is 0x%08x", &i);
fflush(stdout);
while (i)
;
return 0;
}
注意:我們程式中的變數 i 的大小為 0x12345678。
我們想做的是,通過編譯,找到變數 i 的邏輯地址,然後經過一系列的地址轉換,獲得實體地址。通過檢視實體地址的內容,是否是 0x12345678。
獲得虛擬地址
將執行的程式碼進行反編譯,可以看到 cmp dword ptr 這一部分。這一部分,對應的就是上面c語言的 while(i) 部分。
可以看到熟悉的 ds:0x3004
,這是什麼?
這就是我們之前分段章節裡面的間接定址。也就是說,我們要找到 ds 段的基址,然後加上3004的偏移量。
這裡的 ds:0x3004 就是這一部分。你會發現 0x3004 只有16位啊,下圖的偏移量標記的是32位。
因為在 Linux 0.11中,給每個程式劃分了 64M 的虛擬記憶體,2的16次方就是64M。
下圖中的偏移量位32位,是給每個程式劃分了 4G 的虛擬記憶體。
注意:看下圖紅色方框部分,其中的0-15位選擇符用來選擇程式中的段的。後面的0-31偏移值,是每個段中的偏移量。
分段機制,假設一個程式中有很多個段(個數由選擇符的位數決定),而且每個段都可以佔有一個大小的空間(由偏移值位數決定)。
在下圖中,由於選擇符0-15中只有14位用來指定段的,所以下圖中的虛擬地址,可以指定214個段,每個段可以有4G(232)的大小空間。
虛擬地址解讀
從上面,我們獲得變數 i 的虛擬地址為 ds: 0x3004。
通過下圖,我們檢視暫存器,可以獲得ds=0x0017,所以ds:0x3004=0x0017: 3004。
我們來看ds=0x0017的解讀。
這其實也叫選擇符,看下圖。
重點看,TI 位,也就是2號位。0x0017=0x 17 = 0x 0001 0111,也就是 TI 位為1。
當 TI 為0時,說明我們要找的有關段表資訊就在 GDT表中,我們可以通過繼續對 0x0017的3-15位進行解讀,獲取有關段表資訊在 GDT表中的索引。
當 TI 為1的時候,說明我們要找的 有關段表資訊 在 LDT表中。
段描述符(段表的相關資訊)
段描述符
每個段都有一個段描述符。
段描述符指定段的大小、訪問許可權和段的特權級、段型別以及段的第一位元組線上性地址空間中的位置(也就是段基址)。
GDT表
GDT表,是全域性描述表。從這裡的 描述 二字與上面的 段描述符可以看出:GDT表中儲存著上面提到的段描述符。
LDT表
LDT表,是區域性描述表。裡面也儲存著段描述符。
GDTR暫存器
此暫存器,記錄著 GDT表的基址。
LDTR暫存器
跟我們之前說的選擇符是一樣的,它表明了 LDT表在 GDT表中的位置。
我們可以這樣理解GDT和LDT:GDT為一級描述符表,LDT為二級描述符表。
LDT和GDT從本質上說是相同的,只是LDT巢狀在GDT之中。LDTR記錄區域性描述符表的起始位置,與GDTR不同,LDTR的內容是一個段選擇子。由於LDT本身同樣是一段記憶體,也是一個段,所以它也有個描述符描述它,這個描述符就儲存在GDT中,對應這個表述符也會有一個選擇子,LDTR裝載的就是這樣一個選擇子。
注意,LDT表中也儲存著描述符,是我們需要的。
也就是說,我們首先要獲取 LDT表的描述符,然後在 LDT表中獲取我們需要的段描述符。
獲得LDT段描述符
我們已經知道,我們的段選擇符為ldtr。
所以,我們現在得獲得 GDTR 和 LDTR暫存器中的內容。
可以看到,LDTR暫存器中的值為 0x0068, GDTR暫存器中的值為 0x00005cb8。
所以,我們將0x0068=0000 0000 0110 1000,我們保留3-15位,1101=13。
所以我們現在知道了,我們需要的段描述符在GDT表開始位置的第13個位置處。
我們在GDT表獲得偏移13個位置處的內容。
解讀段描述符
我們已經獲得了段描述符的內容了,離目標越來越近了。只要解讀出段描述符的內容,我們就可以獲得段表的基址了。
其解讀如下,我們利用上面的結果,並結合下圖,去獲得基址。
所以,我們獲得 LDT表的實體地址為0x00fd52d0。
獲取我們所需段表的描述符
就像我們之前談到的,LDT表儲存的也是段描述符。
所以我們也需要像之前那樣,去獲取相應位置的段描述符,然後進行解讀。
還記得我們之前的ds=0x0017嘛?
0x0017=0x 17 = 0x 0001 0111,其中索引為2。
現在我們獲得 LDT表基址開始處的內容。
因為索引都是從0開始的,所以獲取的段描述符為 0x00003ffff 0x10c0f300。
解讀方式如上,這樣我們求得段表的基址為0x10000000。
之後,將段基址與偏移量相加,即可獲得線性地址。0x10003004。
線性地址解讀
因為採用了多級頁表,所以分頁頁目錄和頁表。其中位數解讀,如圖所示。
注意,頁目錄的基址儲存在 cr3 暫存器中。
如下圖,我們獲得的頁目錄表的基址為0x0。
說明頁目錄表的基址為 0。
因為0x10003004=0x 0001 0000 0000 0000 0011 0000 0000 0100。
所以知道,目錄為 0001 0000 00 ,為64。
頁面為 00 0000 0011,為3。
偏移為0000 0000 0100,為4。
獲取頁目錄項
我們要獲得頁目錄號為64的內容:
解讀頁表項
可以看到基址為12到31為,所以地址為0x00fa5000。
獲取頁表項
頁表所在物理頁框為0x00fa5000位置,從該位置開始查詢3號頁表項,得到:
這個解讀同上,所以最後獲得的基址為0x00f99000。
加上前面提到的偏移4,最終的實體地址為:0x00f99004。
驗證
最後,我們檢視這個實體地址的內容,發現,是我們程式中設定的i的值。