1. 程式人生 > >深入理解計算機系統-之-記憶體定址(四)--linux中分段機制的實現方式

深入理解計算機系統-之-記憶體定址(四)--linux中分段機制的實現方式

linux中的分段機制

前面說了那麼多關於分段機制的實現,其實,Linux以非常有限的方式使用分段。因為,Linux基本不使用分段的機制(注:並不是不使用,使用分段方式還是必須的,會簡化程式的編寫和執行方式),或者說,Linux中的分段機制只是為了相容IA32的硬體而設計的。實際上,分段和分頁在某種程度上顯得有些多餘,因為它們都可以劃分程序的實體地址空間,分段可以給每一個程序分配不同的線性地址,而分頁可以把同一線性地址,對映到不同的實體地址空間。與分段相比,linux更喜歡分頁方式,因為:

  • 當所有程序使用相同的段暫存器值時,記憶體管理變得簡單,因為他們可以共享同樣的一組線性地址,或者更通俗的說,虛擬地址與線性地址一致。

  • linux設計目標之一是可以把它移植到絕大多數流行的處理器平臺。然而,RISC體系結構的對分段支援很有限。

Intel微處理器的段機制是從8086開始提出的, 那時引入的段機制解決了從CPU內部16位地址到20位實地址的轉換。為了保持這種相容性,386仍然使用段機制,但比以前複雜得多。因此,Linux核心的設計並沒有全部採用Intel所提供的段方案,僅僅有限度地使用了一下分段機制。這不僅簡化了Linux核心的設計,而且為把Linux移植到其他平臺創造了條件,因為很多RISC處理器並不支援段機制。但是,對段機制相關知識的瞭解是進入Linux核心的必經之路。

從2.2版開始,Linux讓所有的程序(或叫任務)都使用相同的邏輯地址空間,因此就沒有必要使用區域性描述符表LDT。但核心中也用到LDT,那只是在VM86模式中執行Wine,因為就是說在Linux上模擬執行Winodws軟體或DOS軟體的程式時才使用。2.6版的linux也只有在80x86結構下才使用分段。

在 IA32 上任意給出的地址都是一個虛擬地址,即任意一個地址都是通過“選擇符:偏移量”的方式給出的,這是段機制存訪問模式的基本特點。所以在IA32上設計操作 系統時無法迴避使用段機制。一個虛擬地址最終會通過“段基地址+偏移量”的方式轉化為一個線性地址。 但是,由於絕大多數硬體平臺都不支援段機制,只支援分頁機制,所以為了讓 Linux 具有更好的可移植性,我們需要去掉段機制而只使用分頁機制。但不幸的是,IA32規定段機制是不可禁止的,因此不可能繞過它直接給出線性地址空間的地址。 萬般無奈之下,Linux的設計人員乾脆讓段的基地址為0,而段的界限為4GB,這時任意給出一個偏移量,則等式為“0+偏移量=線性地址”,也就是說 “偏移量=線性地址”。另外由於段機制規定“偏移量<4GB”,所以偏移量的範圍為0H~FFFFFFFFH,這恰好是線性地址空間範圍,也就是說 虛擬地址直接對映到了線性地址,我們以後所提到的虛擬地址和線性地址指的也就是同一地址。看來,Linux在沒有迴避段機制的情況下巧妙地把段機制給繞過去了。

linux中的GDT

在單處理器的系統中只有一個GDT,但是在多處理器系統中每個CPU對應一個GDT。

所有的GDT均儲存在cpu_gdt_table陣列中,而所有GDT的地址和它們的大小被存放在cpu_gdt_descr陣列中。

每個GDT包含18個段和14個空的、未使用的或者保留的項。插入未使用的的目的是為了使經常一起訪問的描述符能夠在處於同一個32字的硬體告訴緩衝行中。

而那些被使用的18個段必定是如下幾種段型別

使用者態和核心態的資料段以及程式碼段4個段

由於IA32段機制還規定,必須為程式碼段和資料段建立不同的段,所以Linux必須為程式碼段和資料段分別建立一個基地址為0,段界限為4GB 的段描述符。

不僅如此,由於Linux核心執行在特權級0,而使用者程式執行在特權級別3,根據IA32段保護機制規定,特權級3的程式是無法訪問特權級為 0的段的,所以Linux必須為核心使用者程式分別建立其程式碼段和資料段。這就意味著Linux必須建立4個段描述符——特權級0的程式碼段和資料段,特權級3的程式碼段和資料段。

這裡寫圖片描述

相應的段描述符由巨集__USER_CS, __USER_DS, __KERNEL_CS和__KERNEL_DS分別定義。因此為了對核心程式碼段定址,核心只需要將__KERNEL_CS的值裝載進CS段暫存器即可。

注意
與段相關的線性地址從0開始,達到2321的定址長度。這就意味著在使用者態和核心態下所有進行均可使用相同的邏輯地址。

而所有的段都是從地址0x00000000開始的,我們可以知道在linux下邏輯地址與線性地址一致(linux並沒有過多的使用分段技術),即邏輯地址的偏移量欄位與相應的線性地址欄位的值是一致的。

任務狀態段TSS

  TSS 全稱task state segment,是指在作業系統程序管理的過程中,任務(程序)切換時的任務現場資訊。

  TSS在任務切換過程中起著重要作用,通過它實現任務的掛起和恢復。所謂任務切換是指,掛起當前正在執行的任務,恢復或啟動另一任務的執行。在任務切換過程中,首先,處理器中各暫存器的當前值被自動儲存到TR所指定的TSS中;然後,下一任務的TSS的選擇子被裝入TR;最後,從TR所指定的TSS中取出各暫存器的值送到處理器的各暫存器中。由此可見,通過在TSS中儲存任務現場各暫存器狀態的完整映象,實現任務的切換。
 
  TSS的基本格式由104位元組組成。這104位元組的基本格式是不可改變的,但在此之外系統軟體還可定義若干附加資訊。基本的104位元組可分為連結欄位區域、內層堆疊指標區域、地址對映暫存器區域、暫存器儲存區域和其它欄位等五個區域。

暫存器儲存區域

  暫存器儲存區域位於TSS內偏移20H至5FH處,用於儲存通用暫存器、段暫存器、指令指標和標誌暫存器。當TSS對應的任務正在執行時,儲存區域是未定義的;在當前任務被切換出時,這些暫存器的當前值就儲存在該區域。當下次切換回原任務時,再從儲存區域恢復出這些暫存器的值,從而,使處理器恢復成該任務換出前的狀態,最終使任務能夠恢復執行。
  各通用暫存器對應一個32位的雙字,指令指標和標誌暫存器各對應一個32位的雙字;各段暫存器也對應一個32位的雙字,段暫存器中的選擇子只有16位,安排再雙字的低16位,高16位未用,一般應填為0。

內層堆疊指標區域

  為了有效地實現保護,同一個任務在不同的特權級下使用不同的堆疊。例如,當從外層特權級3變換到內層特權級0時,任務使用的堆疊也同時從3級變換到0級堆疊;當從內層特權級0變換到外層特權級3時,任務使用的堆疊也同時從0級堆疊變換到3級堆疊。所以,一個任務可能具有四個堆疊,對應四個特權級。四個堆疊需要四個堆疊指標。
 

  但是,當特權級由內層向外層變換時,並不把內層堆疊的指標儲存到TSS的內層堆疊指標區域。實際上,處理器從不向該區域進行寫入,除非程式設計者認為改變該區域的值。這表明向內層轉移時,總是把內層堆疊認為是一個空棧。因此,不允許發生同級內層轉移的遞迴,一旦發生向某級內層的轉移,那麼返回到外層的正常途徑是相匹配的向外層返回。

地址對映暫存器區域

  從虛擬地址空間到線性地址空間的對映由GDT和LDT確定,與特定任務相關的部分由LDT確定,而LDT又由LDTR確定。如果採用分頁機制,那麼由線性地址空間到實體地址空間的對映由包含頁目錄表起始實體地址的控制暫存器CR3確定。所以,與特定任務相關的虛擬地址空間到實體地址空間的對映由LDTR和CR3確定。顯然,隨著任務的切換,地址對映關係也要切換。

  但是,在任務切換時,處理器並不把換出任務但是的暫存器CR3和LDTR的內容儲存到TSS中的地址對映暫存器區域。事實上,處理器也從來不向該區域自動寫入。因此,如果程式改變了LDTR或CR3,那麼必須把新值人為地儲存到TSS中的地址對映暫存器區域相應欄位中。可以通過別名技術實現此功能。

連結欄位

  連結欄位安排在TSS內偏移0開始的雙字中,其高16位未用。在起連結作用時,地16位儲存前一任務的TSS描述符的選擇子。
 
  如果當前的任務由段間呼叫指令CALL或中斷/異常而啟用,那麼連結欄位儲存被掛起任務的 TSS的選擇子,並且標誌暫存器EFLAGS中的NT位被置1,使連結欄位有效。在返回時,由於NT標誌位為1,返回指令RET或中斷返回指令IRET將使得控制沿連結欄位所指恢復到鏈上的前一個任務。

其它欄位

  為了實現輸入/輸出保護,要使用I/O許可點陣圖。任務使用的I/O許可點陣圖也存放在TSS中,作為TSS的擴充套件部分。在TSS內偏移66H處的字用於存放I/O許可點陣圖在TSS內的偏移(從TSS開頭開始計算)。關於I/O許可點陣圖的作用,以後的文章中將會詳細介紹。
 
  在80386中,只定義了一種屬性,即除錯陷阱。該屬性是字的最低位,用T表示。該字的其它位置被保留,必須被置為0。在發生任務切換時,如果進入任務的T位為1,那麼在任務切換完成之後,新任務的第一條指令執行之前產生除錯陷阱。

3個區域性執行緒儲存(Thread-Local Storage,TLS)段

執行緒區域性儲存區(Thread Local Storage, TLS):將資料與一個正在執行的特定函式關聯起來。這種機制允許多執行緒應用程式使用最多3個區域性於執行緒的資料段。

linux系統可以使用set_thread_area()和get_thread_area()分別為正在執行的程序建立和撤銷一個TLS段。

執行緒區域性儲存是將現有函式變為執行緒安全的有用技巧。

當一個函式中訪問並修改全域性或靜態變數,那麼這個函式就是不可重入的。若使之變為可重入的函式,可以使用執行緒同步,也可以使用執行緒區域性儲存。執行緒區域性儲存為每一個訪問此變數的執行緒提供一個此變數獨立的副本,執行緒可以修改此變數,而不會影響到其他執行緒。

注:通過以上描述可以看出,執行緒區域性儲存不是用來共享變數的。
具體可參照 每天進步一點點——Linux中的執行緒區域性儲存

與高階電源管理(AMP)相關的3個段

由於BIOS程式碼使用了分段機制,所以當linux APM驅動程式呼叫BIOS函式來獲取或者設定APM裝置的狀態時,就可以使用自定義的程式碼段和資料段。

與支援即插即用(PnP)功能的BIOS服務程式相關的5個段

前面一種情況下,就像前述與APM相關的3個段的情況一樣,由於BIOS例程使用段,所以當linux的PnP裝置驅動程式呼叫BIOS函式來檢測PnP裝置使用的資源時,就可以使用自定義的程式碼段和資料段。

處理”雙重錯誤”異常的特殊TSS段

處理一個異常的時候可能會引發另外一個異常,在這種情況下產生雙重錯誤。

linux中的LDT

大多數使用者態下的linux程式不使用區域性描述符表,這樣核心就定義了一個預設的LDT供大多數程序共享。預設的區域性描述符表存放在default_ldt陣列中。

如果在某些情況下,程序仍然需要建立自己的區域性描述符表,(例如wine這樣的程式,他執行面向段的微軟windows應用程式),可以使用modify_ldt()系統呼叫允許程序建立自己的區域性描述符表。

modify_ldt() 讀取或一個程序寫入本地描述符表(ldt)。 ldt 是使用i386處理器每個程序的記憶體管理表。對於該表的詳細資訊,請參閱英特爾386處理器手冊。

任何被modify_ldt()建立的自定義區域性描述符表仍然需要他自己的段。當處理器開始執行擁有自定義區域性描述符表的程序時,該CPU的GDT副本中的LDT表項相應的就被修改了。

使用者態的程式同樣也利用modify_ldt()來分配新的段,但核心卻從不使用這些段,它也不需要了解相應的段描述符,因為這些段描述符被包含在程序自定義的區域性描述符表中。

linux中GDT,LDT和IDT結構定義

GDT描述符表gdt_page定義

struct gdt_page {
    struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));

DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);

段描述符結構定義

ia32機器上的定義

定義在arch/x86/include/asm/desc_defs.h檔案中

struct desc_struct {
  union {
    struct {
      unsigned int a;
      unsigned int b;
    };
    struct {
      u16 limit0;
      u16 base0;
      unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
      unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
    };
  };  
} __attribute__((packed));

聯合體——對成員域訪問和設定成為一種很優美的方法。上面第一個匿名結構體用來作為成員訪問取值的出口,下面第二個結構體對真實的成員設定值的入口。

欄位 描述
limit 段長度
base 段的首位元組的線性地址,有base0,base1,base2三部分構成
type 段的型別和存取許可權
s 系統標誌。1-系統段;0-普通段
dpl 描述符特權級
p segment-Present。linux下總是1
avl linux不用
d 區分程式碼段還是資料段
g 段大小粒度。以4K倍數計算

在32位機器上,這就是所有描述符的資料結構嘍,沒有細分門和非門!

typedef struct desc_struct gate_desc;
typedef struct desc_struct ldt_desc;
typedef struct desc_struct tss_desc;

由於三類描述符都是一個結構型別,從而一律使用下面巨集初始化在GDT中表項

#define GDT_ENTRY_INIT(flags, base, limit) { { { \
        .a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \
        .b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \
            ((limit) & 0xf0000) | ((base) & 0xff000000), \
    } } }

x64機器上的定義

但是在64位機器上,Linux則進行了細緻劃分:

/* 16byte gate */
struct gate_struct64 {
  u16 offset_low;
  u16 segment;
  unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
  u16 offset_middle;
  u32 offset_high;
  u32 zero1;
} __attribute__((packed));

16位元組LDT或TSS描述符結構

/* LDT or TSS descriptor in the GDT. 16 bytes. */
struct ldttss_desc64 {
  u16 limit0;
  u16 base0;
  unsigned base1 : 8, type : 5, dpl : 2, p : 1;
  unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8;
  u32 base3;
  u32 zero1;
} __attribute__((packed));
typedef struct gate_struct64 gate_desc;
typedef struct ldttss_desc64 ldt_desc;
typedef struct ldttss_desc64 tss_desc;

從上面程式碼看出無論是32位還是64位機器上,都使用typedef重新定義,以提供給系統其他使用此描述符的部分一致的型別名

區分描述符的列舉量

enum {
    GATE_INTERRUPT = 0xE,
    GATE_TRAP = 0xF,
    GATE_CALL = 0xC,
    GATE_TASK = 0x5,
};
enum {
    DESC_TSS = 0x9,
    DESC_LDT = 0x2,
    DESCTYPE_S = 0x10,  /* !system */
};

系統GDT,IDT指標描述結構

struct desc_ptr {
    unsigned short size;
    unsigned long address;
} __attribute__((packed)) ;

這個結構記錄了系統的GDT或者IDT的大小以及在系統中的線性基地