天天看點

深入了解計算機系統-之-記憶體尋址(四)--linux中分段機制的實作方式linux中的分段機制linux中的GDTlinux中的LDTlinux中GDT,LDT和IDT結構定義

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的代碼段和資料段。

深入了解計算機系統-之-記憶體尋址(四)--linux中分段機制的實作方式linux中的分段機制linux中的GDTlinux中的LDTlinux中GDT,LDT和IDT結構定義

相應的段描述符由宏__USER_CS, __USER_DS, __KERNEL_CS和__KERNEL_DS分别定義。是以為了對核心代碼段尋址,核心隻需要将__KERNEL_CS的值裝載進CS段寄存器即可。

注意

與段相關的線性位址從0開始,達到 232−1 的尋址長度。這就意味着在使用者态和核心态下所有進行均可使用相同的邏輯位址。

而所有的段都是從位址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的大小以及在系統中的線性基地

繼續閱讀