分段機制主要功能只有兩點:
對于linux內(nèi)核來說,它僅僅只使用了分段機制中的權(quán)限控制功能,具體我們可以一起看看是如何做的。
在CPU中,跟段有關(guān)的CPU寄存器一共有6個:cs,ss,ds,es,fs,gs,它們保存的是段選擇符。而同時這六個寄存器每個都有一個對應(yīng)的非編程寄存器,它們對應(yīng)的非編程寄存器中保存的是段描述符。系統(tǒng)可以把同一個寄存器用于不同的目的,方法是先將其寄存器中的值保存到內(nèi)存中,之后恢復(fù)。而在系統(tǒng)中最主要的是cs,ds,ss這三個寄存器。
CS 代碼段寄存器:指向包含程序指令的段,在CS寄存器中RPL用于表示當(dāng)前CPU的特權(quán)級(CPL),CPL為0是最高權(quán)限(內(nèi)核態(tài)使用),CPL為3是用戶態(tài)使用。
SS棧段寄存器:指向當(dāng)前程序的棧的段。
DS 數(shù)據(jù)段寄存器:指向保存著靜態(tài)數(shù)據(jù)和全局?jǐn)?shù)據(jù)的段(靜態(tài)區(qū))。
在段寄存器中主要保存的是段選擇符,它的長度是16位,具體如下:
索引號(index):所對應(yīng)的段描述符處于GDT或LDT中的索引。
TI:TI=0表示對應(yīng)段描述符保存在GDT(全局描述符表)中,TI=1表示對應(yīng)的段描述符保存在LDT(局部描述符表)中。
RPL:當(dāng)此對應(yīng)的段選擇符裝入cs寄存器時,設(shè)置CPU當(dāng)前的特權(quán)級的值為RPL,也就是cs寄存器中的RPL就是CPL。
段選擇符主要用途就是根據(jù)段索引號和TI標(biāo)志,去到GDT或者LDT中找到這個選擇符對應(yīng)的段描述符,比如我們在內(nèi)核代碼中常見的__KERNEL_CS,__KERNEL_DS,__USER_CS,__USER_DS就是段選擇符,它們并不是段描述符。
全局描述符表和局部描述符表保存的都是段描述符,記住要把段描述符和段選擇符區(qū)別開來,保存在寄存器中的是段選擇符,這個段選擇符會到描述符表中獲取對于的段描述符,然后將段描述符保存到對應(yīng)寄存器的非編程寄存器中。
系統(tǒng)中每個CPU有屬于自己的一個全局描述符表(GDT),其所在內(nèi)存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小為64K,一共可保存8192個段描述符,不過第一個一般都會置空,也就是能保存8191個段描述符。第一個置空的原因是防止加電后段寄存器未經(jīng)初始化就進入保護模式而使用GDT。
而對于局部描述符表,CPU設(shè)定是每個進程可以創(chuàng)建屬于自己的局部描述符表(LDT),當(dāng)前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不過大多數(shù)用戶態(tài)的liunx程序都不使用局部描述符表,所以linux內(nèi)核只定義了一個缺省的LDT供大多數(shù)進程共享。描述這個局部描述符表的局部描述符表描述符保存在GDT中。
對于表中的段描述符我們簡單說幾個特別的:
TLS段描述符:中文名字是局部線程存儲段,這個會允許線程擁有自己的段,不過一般程序不經(jīng)常會用到的,系統(tǒng)調(diào)用set_thread_area()與get_thread_area()為當(dāng)前進程創(chuàng)建和撤銷一個TLS段。
TSS段描述符:叫做任務(wù)狀態(tài)段,這個描述符非常重要,每個處理器包含一個自己的tss段,這個tss段中的主要數(shù)據(jù)是一個tss_struct結(jié)構(gòu)體,linux會將所有CPU的tss_struct結(jié)構(gòu)體以init_tss數(shù)組的形式保存起來,這個tss_struct結(jié)構(gòu)體中保存的時當(dāng)前運行進程的內(nèi)核態(tài)堆棧棧頂?shù)刂泛彤?dāng)前進程的IO許可權(quán)限位。當(dāng)進程切換時就會設(shè)置CPU的tss_struct結(jié)構(gòu)體,CPU就可以從tss_struct中獲取當(dāng)前進程的內(nèi)核棧和IO許可權(quán)限。
kernel code,kernel data,user code,user data:分別是內(nèi)核代碼段描述符,內(nèi)核數(shù)據(jù)段描述符,用戶代碼段描述符,用戶數(shù)據(jù)段描述符,不同的進程會使用同一個用戶代碼段/數(shù)據(jù)段描述符,這個也之后介紹。
段描述符就是保存在全局描述符表或者局部描述符表中,當(dāng)某個段寄存器試圖通過自己的段選擇符獲取對于的段描述符時,會將獲取到的段描述符放到自己的非編程寄存器中,這樣就不用每次訪問段都要跑到內(nèi)存中的段描述符表中獲取。
BASE(32位):段首地址的線性地址。
G:為0代表此段長度以字節(jié)為單位,為1代表此段長度以4K為單位。
LIMIT(20位):此最后一個地址的偏移量,也相當(dāng)于長度,G=0,段大小在1~1MB,G=1,段大小為4KB~4GB。
S:為0表示是系統(tǒng)段,否則為代碼段或數(shù)據(jù)段。
Type:描述段的類型和存取權(quán)限。
DPL:描述符特權(quán)級,表示訪問這個段CPU要求的最小優(yōu)先級(保存在cs寄存器的CPL特權(quán)級),當(dāng)DPL為0時,只有CPL為0才能訪問,DPL為3時,CPL為0為3都可以訪問這個段。
P:表示此段是否被交換到磁盤,總是置為1,因為linux不會把一個段都交換到磁盤中。
D或B:如果段的LIMIT是32位長,則置1,如果是16位長,置0。(詳見intel手冊)
AVL:忽略。
數(shù)據(jù)段描述符:
表示這個段描述符代表一個數(shù)據(jù)段,這種描述符可以放在GDT或者LDT。該描述符的S標(biāo)志位為1,也就是非系統(tǒng)段。需要注意內(nèi)核數(shù)據(jù)段屬于數(shù)據(jù)段描述符,并不屬于系統(tǒng)段描述符。
代碼段描述符:
表示這個段描述符代表一個數(shù)據(jù)段,這種描述符可以放在GDT或者LDT。該描述符的S標(biāo)志位為1,也就是非系統(tǒng)段。需要注意內(nèi)核代碼段屬于代碼段描述符,并不屬于系統(tǒng)段描述符。
系統(tǒng)段描述符:
此描述符代表一個系統(tǒng)段,Type的值代表了是哪一種系統(tǒng)段,S標(biāo)志位為0。其中以下兩種都是系統(tǒng)段
局部描述符表描述符(LDTD,系統(tǒng)段描述符的一種):
此種描述符代表一個包含有LDT的段,它只能保存在GDT中,相應(yīng)的Type為2,S為0。
任務(wù)狀態(tài)段描述符(TSSD,系統(tǒng)段描述符的一種):
這個描述符代表一個任務(wù)狀態(tài)段(TSS),這個段用于保存部分處理器寄存器的內(nèi)容(內(nèi)核態(tài)棧地址和IO許可權(quán)限位),它只保存在GDT中,根據(jù)相應(yīng)的進程是否正在CPU上運行,其Type字段的值分別為11或9.這個描述符S標(biāo)志為0。
在所有段描述符中可能大家最關(guān)心的就是內(nèi)核代碼段描述符和內(nèi)核數(shù)據(jù)段描述符以及用戶代碼段描述符和用戶數(shù)據(jù)段描述符了,這里也具體說說這幾個描述符,它們的構(gòu)成如下:
可以看出來它們的S都是1,都是非系統(tǒng)段,注意并不是內(nèi)核用的段就是系統(tǒng)段,這里的系統(tǒng)段的區(qū)分不是我們用戶態(tài)和內(nèi)核態(tài)的這種劃分。所有的用戶進程都是使用同一個用戶代碼段描述符和用戶數(shù)據(jù)段描述符,它們是__USER_CS和__USER_DS,也就是每個進程處于用戶態(tài)時,它們的CS寄存器和DS寄存器中的值是相同的。當(dāng)任何進程或者中斷異常進入內(nèi)核后,都是使用相同的內(nèi)核代碼段描述符和內(nèi)核數(shù)據(jù)段描述符,它們是__KERNEL_CS和__KERNEL_DS。這里要明確記得,內(nèi)核數(shù)據(jù)段實際上就是內(nèi)核態(tài)堆棧段。
還可以看出這幾個段的BASE都是0x00000000,LIMIT都是0xfffff,并且G為1。也就是說,用戶代碼段,用戶數(shù)據(jù)段,內(nèi)核代碼段,內(nèi)核數(shù)據(jù)段這四個段它們的尋址地址都是0x00000000~0xffffffff。也就是地址0到4G的大小。這也形成了為什么所有進程都可以使用同一個用戶代碼段和用戶數(shù)據(jù)段的條件。并且很清楚地可以看出,內(nèi)核代碼段和內(nèi)核數(shù)據(jù)段都需要CPL為0時才能訪問,而用戶代碼段和用戶數(shù)據(jù)段在CPL為0或者3時都可以訪問。
再看看這4個段描述符對應(yīng)的段選擇符:
可以看出來,它們的TI為0,表示都保存在全局段描述符表中。可能看到這里大家會有個疑問,既然用戶段的RPL為3,那怎么去訪問DPL為0的內(nèi)核段呢,這就是linux精明的地方,它就是禁止用戶態(tài)訪問內(nèi)核態(tài)的數(shù)據(jù),但是內(nèi)核為用戶態(tài)開了兩個小門,然用戶態(tài)能夠通過這兩個小門進入到內(nèi)核態(tài)中,這兩個小門就是系統(tǒng)調(diào)用與中斷和異常。
快速訪問段描述符:
先看一下系統(tǒng)是如何將邏輯地址轉(zhuǎn)換為線性地址的:
邏輯地址是由段選擇符(16位) + 段內(nèi)偏移量offset(32位)得來。之前也說到,只有處于用戶態(tài),CS和DS寄存器中的值都是__USER_CS和__USER_DS。只要處于內(nèi)核態(tài),CS和DS寄存器中的值都是__KERNEL_CS和__KERNEL_DS。在我們編程過程中,實際上提供的地址都是一個偏移量,系統(tǒng)會自動將這個偏移量與CS中的段選擇符進行結(jié)合。也就是我們使用的邏輯地址實際上只使用了offset這一段,段選擇符都為空。之前也說了這四個段描述符的BASE都為0x00000000,也得出當(dāng)邏輯地址通過這樣的分段機制轉(zhuǎn)為線性地址后,實際上并沒有變化,也就是邏輯地址=線性地址(其實這兩個地址都是offset的值)。
也可以看出來,每次進行地址轉(zhuǎn)換時都要通過段描述符獲取段的基地址然后與偏移量運算得到線性地址,而段描述符是保存在內(nèi)存當(dāng)中的,這樣每次轉(zhuǎn)換難道就要訪問一次內(nèi)存或者cache嗎?當(dāng)然不是,之前說到一共有6種段寄存器,它們每個都有屬于自己的一個非編程寄存器,專門用于存放現(xiàn)在的段描述符,比如拿cs段寄存器說,cs寄存器存放的是段選擇符,所以每次通過邏輯地址訪問這個段里的內(nèi)容時,都要通過這個段選擇符與gdtr(段描述符保存在全局描述符表中)或者ldtr(段描述符保存在局部描述符表中)結(jié)合然后從內(nèi)存中得到對應(yīng)的段描述符,然后根據(jù)段描述符的BASE和LIMIT將邏輯地址轉(zhuǎn)換為線性地址。如果進行連續(xù)訪問時(而且連續(xù)訪問的概率非常高),這樣的效率就非常低了,這個cs段寄存器對應(yīng)的非編程寄存器就是用于保存這個段描述符的,這樣就不用每次都從內(nèi)存中獲取段描述符,而是直接從這個CS對應(yīng)的非編程寄存器中獲取段描述符。
任務(wù)狀態(tài)段的段選擇符保存在tr寄存器中,內(nèi)核為每個CPU準(zhǔn)備了一個任務(wù)狀態(tài)段,其主要保存的是當(dāng)前進程的IO許可權(quán)限位和棧頂指針,其作用主要有兩個:
TSS段的保存形式是一個tss_struct結(jié)構(gòu)體,系統(tǒng)會將所有CPU的tss_struct結(jié)構(gòu)體組成一個init_tss數(shù)組的形式進行保存,我們具體看一下tss_struct結(jié)構(gòu)體:
struct tss_struct { /* * The hardware state: */ /* 存放寄存器的值的結(jié)構(gòu)體,保存有棧頂指針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. */ /* 當(dāng)前進程的IO許可權(quán)限位 */ unsigned long io_bitmap[IO_BITMAP_LONGS + 1]; /* * .. and then another 0x100 bytes for the emergency kernel stack: */ /* 緊急內(nèi)核棧 */ 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;
其實發(fā)生段的切換有兩種情況,一種是系統(tǒng)調(diào)用發(fā)生時,一種是中斷或異常發(fā)生時,但是這兩種情況都大同小異,這里我們只拿中斷異常發(fā)生的情況進行說明。
這里只說明系統(tǒng)大多數(shù)發(fā)生的情況,不討論個例。假定當(dāng)前系統(tǒng)處于用戶態(tài)執(zhí)行代碼中,這時候各個段寄存器的值應(yīng)該是這樣的:
當(dāng)中斷或異常發(fā)生時,CPU會按照如下步驟進行執(zhí)行:
這些步驟執(zhí)行完后,寄存器變化為:
而內(nèi)核棧中保存的值有:用戶態(tài)CS,用戶態(tài)SS,用戶態(tài)ESP,用戶態(tài)EIP,用戶態(tài)eflags。當(dāng)系統(tǒng)從中斷返回用戶態(tài)時,就會從內(nèi)核棧中將這些值還原,最后會回到進入時的情況。至于為什么不用修改DS寄存器的值,我也不清楚。