1. 程式人生 > >GDT、LDT、IDTR、TR(轉)

GDT、LDT、IDTR、TR(轉)

1、現在記憶體管理系統都是基於頁式管理的, 段式管理說白了可有可無, 那是Intel老古董階段留下來的遺毒, 以至於Intel的硬體構架白白地複雜了. Linux kernel直接讓段式管理透明. 建議LZ讀《Linux核心原始碼情景分析》上篇, 這在那書裡面是一開始就講明的事情.

      2、全域性描述符表GDT(Global Descriptor Table)在整個系統中,全域性描述符表GDT只有一張(一個處理器對應一個GDT),GDT可以被放在記憶體的任何位置,但CPU必須知道GDT的入口,也就是基地址放在哪裡,Intel的設計者門提供了一個暫存器GDTR用來存放GDT的入口地址,程式設計師將GDT設定在記憶體中某個位置之後,可以通過LGDT指令將GDT的入口地址裝入此暫存器,從此以後,CPU就根據此暫存器中的內容作為GDT的入口來訪問GDT了。GDTR中存放的是GDT在記憶體中的基地址和其表長界限。

基地址指定GDT表中位元組0線上性地址空間中的地址,表長度指明GDT表的位元組長度值。指令LGDT和SGDT分別用於載入和儲存GDTR暫存器的內容。在機器剛加電或處理器復位後,基地址被預設地設定為0,而長度值被設定成0xFFFF。在保護模式初始化過程中必須給GDTR載入一個新值。

 3、區域性描述符表LDT(Local Descriptor Table)區域性描述符表可以有若干張,每個任務可以有一張。我們可以這樣理解GDT和LDT:GDT為一級描述符表,LDT為二級描述符表。LDT和GDT從本質上說是相同的,只是LDT巢狀在GDT之中。LDTR記錄區域性描述符表的起始位置,與GDTR不同,LDTR的內容是一個段選擇子

。由於LDT本身同樣是一段記憶體,也是一個段,所以它也有個描述符描述它,這個描述符就儲存在GDT中,對應這個表述符也會有一個選擇子,LDTR裝載的就是這樣一個選擇子。LDTR可以在程式中隨時改變,通過使用lldt指令

由於每個程序都有自己的一套程式段、資料段、堆疊段,有了區域性描述符表則可以將每個程序的程式段、資料段、堆疊段封裝在一起,只要改變LDTR就可以實現對不同程序的段進行訪問。

當進行任務切換時,處理器會把新任務LDT的段選擇符和段描述符自動地載入進LDTR中。在機器加電或處理器復位後,段選擇符和基地址被預設地設定為0,而段長度被設定成0xFFFF。

除了GDTR、LDTR外還有IDTR和TR

(1)中斷描述符表暫存器IDTR 

與GDTR的作用類似,IDTR暫存器用於存放中斷描述符表IDT的32位線性基地址和16位表長度值。指令LIDT和SIDT分別用於載入和儲存IDTR暫存器的內容。在機器剛加電或處理器復位後,基地址被預設地設定為0,而長度值被設定成0xFFFF。

(2)任務暫存器TR

TR用於定址一個特殊的任務狀態段(Task State Segment,TSS)。TSS中包含著當前執行任務的重要資訊。

TR暫存器用於存放當前任務TSS段的16位段選擇符、32位基地址、16位段長度和描述符屬性值。它引用GDT表中的一個TSS型別的描述符。指令LTR和STR分別用於載入和儲存TR暫存器的段選擇符部分。當使用LTR指令把選擇符載入進任務暫存器時,TSS描述符中的段基地址、段限長度以及描述符屬性會被自動載入到任務暫存器中。當執行任務切換時,處理器會把新任務的TSS的段選擇符和段描述符自動載入進任務暫存器TR中。

4、邏輯地址到實體地址的轉換關係:

        5、本文為原創,轉載請註明:http://www.cnblogs.com/tolimit/

這篇文章主要說一下linux對於分段機制的處理,雖然都說linux不使用分段機制,但是分段機制屬於CPU的一個功能,即使linux不使用,也要通過程式碼想辦法繞過它,況且linux也使用到了分段機制中的某些功能。

分段機制主要功能只有兩點:

將實體記憶體劃分為多個段,讓作業系統可以使用大於其地址線對應的實體記憶體(比如正常情況下32位地址線可以訪問4G大小的記憶體,但是有分段後則可訪問大於4G的記憶體)。許可權控制,將每個段設定許可權位,讓不同的程式訪問不同的段。

對於linux核心來說,它僅僅只使用了分段機制中的許可權控制功能,具體我們可以一起看看是如何做的。

CPU的段暫存器

在CPU中,跟段有關的CPU暫存器一共有6個:cs,ss,ds,es,fs,gs,它們儲存的是段選擇符。而同時這六個暫存器每個都有一個對應的非程式設計暫存器,它們對應的非程式設計暫存器中儲存的是段描述符。系統可以把同一個暫存器用於不同的目的,方法是先將其暫存器中的值儲存到記憶體中,之後恢復。而在系統中最主要的是cs,ds,ss這三個暫存器。

CS 程式碼段暫存器:指向包含程式指令的段,在CS暫存器中RPL用於表示當前CPU的特權級(CPL),CPL為0是最高許可權(核心態使用),CPL為3是使用者態使用。

SS棧段暫存器:指向當前程式的棧的段。

DS 資料段暫存器:指向儲存著靜態資料和全域性資料的段(靜態區)。

在段暫存器中主要儲存的是段選擇符,它的長度是16位,具體如下:


總結一下linux中的分段機制

索引號(index):所對應的段描述符處於GDT或LDT中的索引。

TI:TI=0表示對應段描述符儲存在GDT(全域性描述符表)中,TI=1表示對應的段描述符儲存在LDT(區域性描述符表)中。

RPL:當此對應的段選擇符裝入cs暫存器時,設定CPU當前的特權級的值為RPL,也就是cs暫存器中的RPL就是CPL。

段選擇符主要用途就是根據段索引號和TI標誌,去到GDT或者LDT中找到這個選擇符對應的段描述符,比如我們在核心程式碼中常見的__KERNEL_CS,__KERNEL_DS,__USER_CS,__USER_DS就是段選擇符,它們並不是段描述符。

全域性描述符表與區域性描述符表

全域性描述符表和區域性描述符表儲存的都是段描述符,記住要把段描述符和段選擇符區別開來,儲存在暫存器中的是段選擇符,這個段選擇符會到描述符表中獲取對於的段描述符,然後將段描述符儲存到對應暫存器的非程式設計暫存器中。

系統中每個CPU有屬於自己的一個全域性描述符表(GDT),其所在記憶體的基地址和其大小一起儲存在CPU的gdtr暫存器中。其大小為64K,一共可儲存8192個段描述符,不過第一個一般都會置空,也就是能儲存8191個段描述符。第一個置空的原因是防止加電後段暫存器未經初始化就進入保護模式而使用GDT。

而對於區域性描述符表,CPU設定是每個程序可以建立屬於自己的區域性描述符表(LDT),當前被使用的LDT的基地址和大小一起儲存在ldtr暫存器中。不過大多數使用者態的liunx程式都不使用區域性描述符表,所以linux核心只定義了一個預設的LDT供大多數程序共享。描述這個區域性描述符表的區域性描述符表描述符儲存在GDT中。


總結一下linux中的分段機制

對於表中的段描述符我們簡單說幾個特別的:

TLS段描述符:中文名字是區域性執行緒儲存段,這個會允許執行緒擁有自己的段,不過一般程式不經常會用到的,系統呼叫set_thread_area()與get_thread_area()為當前程序建立和撤銷一個TLS段。

TSS段描述符:叫做任務狀態段,這個描述符非常重要,每個處理器包含一個自己的tss段,這個tss段中的主要資料是一個tss_struct結構體,linux會將所有CPU的tss_struct結構體以init_tss陣列的形式儲存起來,這個tss_struct結構體中儲存的時當前執行程序的核心態堆疊棧頂地址和當前程序的IO許可許可權位。當程序切換時就會設定CPU的tss_struct結構體,CPU就可以從tss_struct中獲取當前程序的核心棧和IO許可許可權。

kernel code,kernel data,user code,user data:分別是核心程式碼段描述符,核心資料段描述符,使用者程式碼段描述符,使用者資料段描述符,不同的程序會使用同一個使用者程式碼段/資料段描述符,這個也之後介紹。

段描述符

段描述符就是儲存在全域性描述符表或者區域性描述符表中,當某個段暫存器試圖通過自己的段選擇符獲取對於的段描述符時,會將獲取到的段描述符放到自己的非程式設計暫存器中,這樣就不用每次訪問段都要跑到記憶體中的段描述符表中獲取。


總結一下linux中的分段機制

BASE(32位):段首地址的線性地址。

G:為0代表此段長度以位元組為單位,為1代表此段長度以4K為單位。

LIMIT(20位):此最後一個地址的偏移量,也相當於長度,G=0,段大小在1~1MB,G=1,段大小為4KB~4GB。

S:為0表示是系統段,否則為程式碼段或資料段。

Type:描述段的型別和存取許可權。

DPL:描述符特權級,表示訪問這個段CPU要求的最小優先順序(儲存在cs暫存器的CPL特權級),當DPL為0時,只有CPL為0才能訪問,DPL為3時,CPL為0為3都可以訪問這個段。

P:表示此段是否被交換到磁碟,總是置為1,因為linux不會把一個段都交換到磁碟中。

D或B:如果段的LIMIT是32位長,則置1,如果是16位長,置0。(詳見intel手冊)

AVL:忽略。

資料段描述符:

表示這個段描述符代表一個數據段,這種描述符可以放在GDT或者LDT。該描述符的S標誌位為1,也就是非系統段。需要注意核心資料段屬於資料段描述符,並不屬於系統段描述符。

程式碼段描述符:

表示這個段描述符代表一個數據段,這種描述符可以放在GDT或者LDT。該描述符的S標誌位為1,也就是非系統段。需要注意核心程式碼段屬於程式碼段描述符,並不屬於系統段描述符。

系統段描述符:

此描述符代表一個系統段,Type的值代表了是哪一種系統段,S標誌位為0。其中以下兩種都是系統段

區域性描述符表描述符(LDTD,系統段描述符的一種):

此種描述符代表一個包含有LDT的段,它只能儲存在GDT中,相應的Type為2,S為0。

任務狀態段描述符(TSSD,系統段描述符的一種):

這個描述符代表一個任務狀態段(TSS),這個段用於儲存部分處理器暫存器的內容(核心態棧地址和IO許可許可權位),它只儲存在GDT中,根據相應的程序是否正在CPU上執行,其Type欄位的值分別為11或9.這個描述符S標誌為0。

在所有段描述符中可能大家最關心的就是核心程式碼段描述符和核心資料段描述符以及使用者程式碼段描述符和使用者資料段描述符了,這裡也具體說說這幾個描述符,它們的構成如下:


總結一下linux中的分段機制

可以看出來它們的S都是1,都是非系統段,注意並不是核心用的段就是系統段,這裡的系統段的區分不是我們使用者態和核心態的這種劃分。所有的使用者程序都是使用同一個使用者程式碼段描述符和使用者資料段描述符,它們是__USER_CS和__USER_DS,也就是每個程序處於使用者態時,它們的CS暫存器和DS暫存器中的值是相同的。當任何程序或者中斷異常進入核心後,都是使用相同的核心程式碼段描述符和核心資料段描述符,它們是__KERNEL_CS和__KERNEL_DS。這裡要明確記得,核心資料段實際上就是核心態堆疊段。

還可以看出這幾個段的BASE都是0x00000000,LIMIT都是0xfffff,並且G為1。也就是說,使用者程式碼段,使用者資料段,核心程式碼段,核心資料段這四個段它們的定址地址都是0x00000000~0xffffffff。也就是地址0到4G的大小。這也形成了為什麼所有程序都可以使用同一個使用者程式碼段和使用者資料段的條件。並且很清楚地可以看出,核心程式碼段和核心資料段都需要CPL為0時才能訪問,而使用者程式碼段和使用者資料段在CPL為0或者3時都可以訪問。

再看看這4個段描述符對應的段選擇符:


總結一下linux中的分段機制

可以看出來,它們的TI為0,表示都儲存在全域性段描述符表中。可能看到這裡大家會有個疑問,既然使用者段的RPL為3,那怎麼去訪問DPL為0的核心段呢,這就是linux精明的地方,它就是禁止使用者態訪問核心態的資料,但是核心為使用者態開了兩個小門,然使用者態能夠通過這兩個小門進入到核心態中,這兩個小門就是系統呼叫與中斷和異常。

快速訪問段描述符:

先看一下系統是如何將邏輯地址轉換為線性地址的:


總結一下linux中的分段機制

邏輯地址是由段選擇符(16位) + 段內偏移量offset(32位)得來。之前也說到,只有處於使用者態,CS和DS暫存器中的值都是__USER_CS和__USER_DS。只要處於核心態,CS和DS暫存器中的值都是__KERNEL_CS和__KERNEL_DS。在我們程式設計過程中,實際上提供的地址都是一個偏移量,系統會自動將這個偏移量與CS中的段選擇符進行結合。也就是我們使用的邏輯地址實際上只使用了offset這一段,段選擇符都為空。之前也說了這四個段描述符的BASE都為0x00000000,也得出當邏輯地址通過這樣的分段機制轉為線性地址後,實際上並沒有變化,也就是邏輯地址=線性地址(其實這兩個地址都是offset的值)。

也可以看出來,每次進行地址轉換時都要通過段描述符獲取段的基地址然後與偏移量運算得到線性地址,而段描述符是儲存在記憶體當中的,這樣每次轉換難道就要訪問一次記憶體或者cache嗎?當然不是,之前說到一共有6種段暫存器,它們每個都有屬於自己的一個非程式設計暫存器,專門用於存放現在的段描述符,比如拿cs段暫存器說,cs暫存器存放的是段選擇符,所以每次通過邏輯地址訪問這個段裡的內容時,都要通過這個段選擇符與gdtr(段描述符儲存在全域性描述符表中)或者ldtr(段描述符儲存在區域性描述符表中)結合然後從記憶體中得到對應的段描述符,然後根據段描述符的BASE和LIMIT將邏輯地址轉換為線性地址。如果進行連續訪問時(而且連續訪問的概率非常高),這樣的效率就非常低了,這個cs段暫存器對應的非程式設計暫存器就是用於儲存這個段描述符的,這樣就不用每次都從記憶體中獲取段描述符,而是直接從這個CS對應的非程式設計暫存器中獲取段描述符。


總結一下linux中的分段機制
任務狀態段(TSS)

任務狀態段的段選擇符儲存在tr暫存器中,核心為每個CPU準備了一個任務狀態段,其 主要儲存的是當前程序的IO許可許可權位和棧頂指標 ,其作用主要有兩個:

程序從使用者態切換到核心態時,系統會從該CPU的TSS中獲取該程序的核心態堆疊地址。當用戶態程序試圖通過in或out指令訪問一個IO埠時,CPU需要訪問存放在TSS中的IO許可許可權位以檢查該程序是否有許可權訪問該IO埠。

TSS段的儲存形式是一個tss_struct結構體,系統會將所有CPU的tss_struct結構體組成一個init_tss陣列的形式進行儲存,我們具體看一下tss_struct結構體:

struct tss_struct {
/*
* The hardware state:
*/
/* 存放暫存器的值的結構體,儲存有棧頂指標SP暫存器的值 */
struct x86_hw_tss x86_tss;
/*
* The extra 1 is there because the CPU will access an
* additional byte beyond the end of the IO permission
* bitmap. The extra byte must be all 1 bits, and must
* be within the limit.
*/
/* 當前程序的IO許可許可權位 */
unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
/*
* .. and then another 0x100 bytes for the emergency kernel stack:
*/
/* 緊急核心棧 */
unsigned long stack[64];
} ____cacheline_aligned;
struct x86_hw_tss {
u32 reserved1;
u64 sp0;
u64 sp1;
u64 sp2;
u64 reserved2;
u64 ist[7];
u32 reserved3;
u32 reserved4;
u16 reserved5;
u16 io_bitmap_base;
} __attribute__((packed)) ____cacheline_aligned;中斷或異常發生時的段切換

其實發生段的切換有兩種情況,一種是系統呼叫發生時,一種是中斷或異常發生時,但是這兩種情況都大同小異,這裡我們只拿中斷異常發生的情況進行說明。

這裡只說明系統大多數發生的情況,不討論個例。假定當前系統處於使用者態執行程式碼中,這時候各個段暫存器的值應該是這樣的:

CS: __USER_CSDS: __USER_DSSS: 儲存著使用者態棧基地址ESP: 儲存著使用者態棧頂地址EIP: 儲存下條將要執行的指令地址

當中斷或異常發生時,CPU會按照如下步驟進行執行:

讀取由idtr暫存器儲存的IDT(中斷向量表)中對應的門描述符。根據對應的門描述符,獲取其中儲存的段選擇符。(門描述符中儲存有一個段選擇符和一個門的DPL,這兩個部分是段切換的重要部分。具體可看我的部落格: http://www.cnblogs.com/tolimit/p/4415348.html )根據這個段選擇符獲取對於的段描述符(門描述符中儲存的段選擇符基本都是__KERNEL_CS)。這時CPU會使用CS暫存器中的CPL特權級與獲取的段描述符的DPL特權級比較,如果DPL<=CPL,則通過,否則產生“通用保護”異常,我們也看到,我們CS儲存的是__USER_CS,其CPL為3,門描述符中儲存的是__KERNEL_CS,其DPL為0,;也就是會通過檢查。如果是異常情況,這時還會多一步進行檢查,會檢查門描述符中的DPL特權級,當前特權級CPL的值 > DPL的值時,則通過檢查,否則不能通過檢查,而只有系統門和系統中斷門的DPL是3,其他的異常門的DPL都為0。這樣做的好處是避免了使用者程式訪問陷阱門、中斷門和任務門。到這裡檢查已經通過,如果特權級發生變化(使用者態產生的中斷和異常,肯定會發生特權級變化),則CPU會自動幫切換不同特權級使用的暫存器。從tr暫存器中獲取CPU的TSS段,從TSS段中獲取當前程序的核心態堆疊指標和SS暫存器的值並將它們裝載到SS和EIP暫存器。在當前程序的核心棧中儲存使用者態的SS暫存器和EIP暫存器的值。(注意,這裡是先裝載了SS和EIP暫存器,讓其指向核心棧,再在核心棧中儲存使用者態的SS和EIP暫存器值)如果故障已經發生,用引起異常的指令地址裝載到CS和EIP暫存器,從而使這條指令再次被執行。在核心棧中儲存使用者態的eflags、CS和EIP。CS和EIP的值就是返回後的下一條指令地址。如果有硬體出錯碼,也儲存到核心棧中。從中斷向量表的門中獲取CS和EIP值並裝載到CS和EIP暫存器。門中儲存的CS和EIP合起來就會是中斷處理程式入口地址。

這些步驟執行完後,暫存器變化為:

CS: __KERNEL_CSDS: __USER_DSSS: 儲存著核心態棧基地址ESP: 儲存著核心態棧頂地址EIP: 儲存著中斷處理程式入口地址

而核心棧中儲存的值有:使用者態CS,使用者態SS,使用者態ESP,使用者態EIP,使用者態eflags。當系統從中斷返回使用者態時,就會從核心棧中將這些值還原,最後會回到進入時的情況。至於為什麼不用修改DS暫存器的值,我也不清楚。