上一篇聊到分段機制是為了提供了隔絕代碼、數據和堆棧區域的機制,能夠使得多個程序運行在同一個內存空間中不會相互干擾,這是對內存平坦模型的一種保護。內存經過分段機制后會變為一個個的段,這稱為多段模型。多段模型能夠利用分段機制的功能提供由硬件增強代碼、數據結構、程序和任務的保護措施。
現在我們知道了分段的目的是為了什么,但是我們好像還不知道什么是段,以及段有哪些特征。
段的定義
保護模式中的 80x86 架構提供了 4GB 的物理地址空間。這是 CPU 在地址總線上可以尋址的地址空間。這段地址空間是一種平坦模型地址空間,地址范圍從 0 到 0xFFFFFFFF。
平坦模型:相對于多個段的模型來說,平坦模型指的就是一個段,比如在實模式下,處理器最大可尋址 64 KB(2 ^ 16)的地址空間,在保護模式下,處理器最大可尋址 4GB (2 ^ 32)的地址空間,如果訪問超過最大地址空間的數據指令,需要重新指定段。
需要注意下的是,段地址 + 偏移地址確實能尋址 1MB 的地址空間,但這卻不是平坦模型的訪問方式,而是多段模型。
再來啰嗦一下分段機制的目的:分段機制就是把虛擬地址空間中的虛擬內存組織成一個個長度可變的段,這個段是虛擬地址到線性地址轉換的基礎,一般來說,段由三部分組成:
段基址(Base address):段的初始地址,可以認為是段的開始,段基址的段內偏移為 0 。
段限長(limit):該段最大可用的偏移位置,它定義了段的長度,也是段內偏移最大能夠尋址到的位置。
段屬性(Attributes):指的是該段的特性,比如段是否可讀、是否可寫、是否能夠作為程序執行,段的特權級等。
段基址和段限長一起定義了段所映射的線性地址空間的范圍。段內從 0 到 limit 地址范圍會對應線性地址空間中的 base 到 base + limit ,偏移量是無法大于段限長的,如果偏移地址大于段限長會引發異常,除此之外,如果訪問的這個段沒有符合段屬性,也會引發異常。
不同的段可以映射到相同的線性地址空間,這種映射是操作系統所允許的。也就是說不同的段可以在線性地址空間中覆蓋或者完全重疊,如下圖所示
段基址、段限長和段屬性都存儲在段描述符這個結構中,可以說段描述符就是能夠查找段重要信息的結構,在虛擬地址到線性地址的轉換過程中,就需要用到段描述符。那么段描述符被存儲在哪里呢?段描述符被存儲在段描述符表中,這個表就是一個數組,這個數組的下標就是段選擇子,還記得我們上篇文章聊過段選擇子嗎?段選擇子中的 Index 是這么描述的:
image-20230415223616884
為了把邏輯地址轉換成為線性地址,CPU 會執行以下操作:
使用段選擇子中的 Index 屬性通過查詢 GDT/LDT 表定位相應的段描述符。
利用段描述符檢驗段的訪問權限和范圍,用于確保該段是可訪問并且偏移量位于段界限內。
把段描述符中取得的段基地址加上 Index ,最后形成偏移地址。
如果沒有開啟分頁機制,那么此時的線性地址就等同于物理地址;如果開啟分頁機制,那么此時的線性地址會經過分頁機制轉換后才會把線性地址映射成為物理地址。
段選擇子
上篇文章提到了段選擇子,大致介紹了一下它的結構,并沒有細致說明,這篇文章就來細致說明一下。
段選擇子又稱段選擇符,它是一個 16 位的標識符,如下圖所示,段選擇子并不指向段,它指向段描述符表中的段描述符。
段選擇子總共分為三個部分:
RPL(Request Privilege Level):請求特權級,表示進程應該以什么權限來訪問段,數值越大權限越小。
TI(Table Indicator):表示應該查詢哪個表,TI = 0 查 GDT 表;TI = 1 查 LDT 表。
Index:CPU 會自動將 Index * 8,在加上 GDT 和 LDT 中的段基址,就是要加載的段描述符。
下面是幾幅段選擇子的示意圖,大家明白圖中所示也就明白段選擇子是如何表示的了。
需要注意的是,段選擇子 0x0008 和 段選擇子 0x000f 指向的是同一個段即段 1;段選擇子 0x0010 和 段選擇子 0x0017 也指向的是同一個段即段 2。段選擇子 0ffff 指向段 8191,而段選擇子 0x0000 指向的的是一個空段,因為 CPU 不使用 GDT 表中的第一項,所以指向該段的選擇子用作空選擇子。當把空選擇子加載到段寄存器(CS 和 SS 除外)中時,處理器不產生異常,但是當含有空選擇子的段寄存器用于訪問內存時,會產生異常。把空選擇子加載到 CS 和 SS 中也會產生異常。
段選擇子 a b c d 分別指向 linux 0.1x 中的內核代碼段、內核數據段、任務代碼段和任務數據段。
一般把段選擇子放在段寄存器中,每個寄存器支持特定類型的內容引用,這部分引用可以是代碼、數據或者堆棧;每個程序都會把有效的段選擇子加載到 CS、SS 或者是 DS 中,另外,處理器還提供了另外三個段寄存器即 FS、GS、ES 作為輔助,這三個寄存器提供當前 CPU 訪問段寄存器不夠時使用。
從上圖可以看到,每個段寄存器都由兩部分組成,一部分是段選擇子,一部分是 "段基址、段限長和段屬性信息",段選擇子是存在于段寄存器中顯示的部分,而段基址、段限長和段屬性是隱藏部分。
為什么會有隱藏部分呢?
隱藏部分也被稱為描述符緩沖或者是影子寄存器,當一個段選擇子被加載到段寄存器中可見部分時,處理器也會同時把段基址等信息加載到段寄存器的隱藏部分,緩存在段寄存器中隱藏部分使得處理器在進行地址轉換的時候不用再去段描述符中讀取段的相關信息。
段寄存器中的隱藏部分相當于是段描述符的一個鏡像,或者說是拷貝。因此操作系統必須要確保對段描述符的改動反映在描述符緩沖中,如果更改了段描述符卻沒有在描述符緩沖中進行修改,就會造成段不一致的現象。所以最快捷的方法就是在對描述附表做過改動之后就立刻重新加載 6 個段寄存器。這將會把描述附表中的相應段信息加載到描述符緩沖中。
處理器提供了兩類加載指令用于加載段的相關信息:
一類是 MOV、POP、LDS、LES、LSS、LGS 以及 LFS 指令,這些指令顯示的直接引用段寄存器;
一類是隱式加載指令,例如 CALL、JMP 和 RET 指令、IRET、INTn、INTO 和 INT3 等指令。這些指令在操作過程中會附帶改變 CS 寄存器的內容。
段描述符
段描述符是 GDT 和 LDT 表中的一個數據項,用于向處理器提供有關一個段的位置和大小信息以及訪問控制的狀態信息。每個段描述符長度是 8 字節,含有三個主要字段:段基址、段限長和段屬性,其他是一些細節字段。段描述符通常是由編譯器、鏈接器、加載器或者操作系統來創建。
這是一個比較詳細的段描述符的結構,下面來具體介紹一下這些字段的含義:
段限長字段 LIMIT --- Segment limit field
段限長用于指定段的長度,處理器會把段描述符中兩個段限長字段組合成一個 20 位的值,并且根據顆粒度標志 G 來指定段限長 Limit 值的實際含義。如果 G = 0,則段長度 Limit 范圍可以從 1 到 1MB 字節。如果 G = 1,則段長度 Limit 的范圍可以是從 4KB 到 4GB ,單位是 4KB。
基地址字段 BASE --- Base address field
這個字段定義在 4GB 線性地址空間中一個段字節 0 所處的位置。處理器會把 3 個分立的基地址字段組合成為一個 32 位的值,段基址應該對其 16 字節邊界,這樣做性能比較高。
段類型字段 TYPE --- Type field
類型字段指定段或門(Gate)的類型、說明段的訪問種類以及段的擴展方向。這個字段依賴與描述符類型標志 S 指明的是一個應用描述符還是系統描述。TYPE 字段的編碼對代碼、數據或系統描述符都不同。
描述符類型標志 S --- Descriptor type flag
表明描述符的類型,0 - 表示系統描述符,1 - 代碼或數據段描述符。
描述符特權級 --- DPL Descriptor priviledge level
DPL 表示描述符的特權級,特權級范圍從 0 - 3 ,3 最低,0 最高,DPL 用于控制對段的訪問;
我在內核訪問相關的描述中也提到了一個特權級,大家還記得是啥嗎?
段存在標志 --- P Segment present
P 標志位表示一個段是在內存中 p = 1 還是不在內存中 p = 0。當段描述符的 P 標志為 0 時,那么把指向這個段描述符的選擇符加載進段寄存器將導致產生一個段不存在異常。
D/B --- 默認操作大小/默認棧指針大小和/或上界限 Default operation size/default stack pointer size and/or upper bound
根據段描述符表示的是可執行代碼段、下擴數據段還是堆棧段,這個標志具有不同的功能(如果是 32 位,這個標志應該設置為 1,16 位應該設置為 0 )。如果是可執行代碼段時,這個標志是 D 標志;如果是棧段和下擴數據段,這個標志是 B 標志;
顆粒度標志 --- G Granularity
這個字段用于確定段限長字段 Limit 值的單位,如果顆粒度標志為 0 ,則段限長值的單位是字節;如果設置了顆粒度標志,則段限長使用 4KB 單位。
可用和保留比特位 --- Available and reserved bits
段描述符的第 2 個雙字的位 20 供系統軟件使用,位 21 是保留位并且設置為 0 。
段描述符表
段描述符表是存儲段描述符的一個數組,索引是由段選擇子提供。段描述符表的長度可變,最多可以包含 8192 個 8 字節的描述符,段描述符有兩種:即全局描述符表(Global descriptor table)和局部描述符表(Local descriptor table)。
描述符表由操作系統中的特殊數據結構來維護著,并且由內存管理硬件來引用。虛擬內存空間被分割成大小相等的兩半,一半由 GDT 來映射變換成為線性地址,一半由 LDT 來映射,由于段描述符表最大可以包含 8192 個 8 字節的描述符,也就是 2 ^ 13 = 8192 ,所以整個虛擬地址空間是 2 ^ 14 = 16384 個段了,通過指定 TI = 1 or 0 就可以查找到指令的段描述符。
當發生任務切換時,LDT 會更換成新任務的 LDT,但是 GDT 內容卻不會變。因此可以看出,GDT 相當于是全局共有的,系統中所有任務共享的段用 GDT 來映射,而 LDT 是當前任務特有的,可以把 LDT 看成是操作系統的數據。
下面是一副關于 GDT、LDT 的映射圖。
上圖中共有六個段,分別是用于任務 A 的 CodeA 、DataA,任務 B 的 CodeB、DataB,用于操作系統的 Codeos 和 Dataos,系統中的任務 A 和 任務B 分別是兩個不同的應用程序,并且每個任務都有自己的 Code 和 Data,在各自的 LDT 表中保存著 Code 和 Data。包含操作系統內核的兩個段 Codeos 和 Dataos 在 GDT 中映射,并且 GDT 表示任務 A 和任務 B 共同享有的全局映射,GDT 表還保存著 LDTA 和 LDTB。
當任務 A 在運行時,可訪問的段包括 LDTA 的 CodeA 和 DataA,加上 GDT 映射的 Codeos 和 Dataos,任務 B 運行時,可訪問的段包括 LDTB 的 CodeB 和 DataB,加上 GDT 映射的 Codeos 和 Dataos 。任務 A 在運行時,是無法訪問任務 B 的兩個段的;同樣的任務 B 在運行時,也是無法訪問任務 A 的,這正是虛擬地址提供的保護機制,還記得上篇文章寫到的嗎?
GDT 本身并不是一個段,它只是線性地址空間中的一個數據結構。GDT 的基地址 (Base Address)+ 段長度(Limit)會被直接加載進 GDTR 寄存器中。GDT 的基地址應該進行內存 8 字節對齊,用已得到最佳的處理性能。GDT 的限長以字節為單位。
處理器并不會使用 GDT 表中的第 1 個描述符,第 1 個描述符也是空描述符,把這個描述符加載進數據段寄存器 DS、FS、GS 和 ES 后不會產生異常,但是使用空描述符的段選擇符訪問內存時就肯定會產生一般保護性異常。
訪問 LDT 表需要使用其段選擇符,為了在訪問 LDT 時減少地址轉換次數,LDT 的段選擇符、基地址、段限長和訪問權限需要存儲在 LDTR 寄存器中。
審核編輯:湯梓紅
-
處理器
+關注
關注
68文章
19259瀏覽量
229653 -
寄存器
+關注
關注
31文章
5336瀏覽量
120231 -
cpu
+關注
關注
68文章
10854瀏覽量
211587 -
Linux
+關注
關注
87文章
11292瀏覽量
209331 -
數據結構
+關注
關注
3文章
573瀏覽量
40123
原文標題:圖文詳解 Linux 分段機制!
文章出處:【微信號:cxuangoodjob,微信公眾號:程序員cxuan】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論