總結一下linux中的分段機制


本文為原創,轉載請注明: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位,具體如下:

  • 索引號(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中。

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

  • 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:分別是內核代碼段描述符,內核數據段描述符,用戶代碼段描述符,用戶數據段描述符,不同的進程會使用同一個用戶代碼段/數據段描述符,這個也之后介紹。

 

 

段描述符

  段描述符就是保存在全局描述符表或者局部描述符表中,當某個段寄存器試圖通過自己的段選擇符獲取對於的段描述符時,會將獲取到的段描述符放到自己的非編程寄存器中,這樣就不用每次訪問段都要跑到內存中的段描述符表中獲取。

  • 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。

  在所有段描述符中可能大家最關心的就是內核代碼段描述符和內核數據段描述符以及用戶代碼段描述符和用戶數據段描述符了,這里也具體說說這幾個描述符,它們的構成如下:

  可以看出來它們的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個段描述符對應的段選擇符:

  可以看出來,它們的TI為0,表示都保存在全局段描述符表中。可能看到這里大家會有個疑問,既然用戶段的RPL為3,那怎么去訪問DPL為0的內核段呢,這就是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對應的非編程寄存器中獲取段描述符。

 

  

任務狀態段(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_CS
  • DS: __USER_DS
  • SS: 保存着用戶態棧基地址
  • ESP: 保存着用戶態棧頂地址
  • EIP: 保存下條將要執行的指令地址

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

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

  這些步驟執行完后,寄存器變化為:

  • CS: __KERNEL_CS
  • DS: __USER_DS
  • SS: 保存着內核態棧基地址
  • ESP: 保存着內核態棧頂地址
  • EIP: 保存着中斷處理程序入口地址

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

 

 

  

 


免責聲明!

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



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