內存地址轉換與分段




原文標題:Memory Translation and Segmentation

原文地址:http://duartes.org/gustavo/blog/

 

[注:本人水平有限,僅僅好挑一些國外高手的精彩文章翻譯一下。

一來自己復習,二來與大家分享。]

 

本文是Intel兼容計算機(x86)的內存與保護系列文章的第一篇。延續了啟動引導系列文章的主題。進一步分析操作系統內核的工作流程。與曾經一樣。我將引用Linux內核的源碼。但對Windows僅僅給出演示樣例(抱歉,我忽略了BSDMac等系統。但大部分的討論對它們一樣適用)。文中假設有錯誤。請指教。

 

在支持Intel主板芯片上。CPU對內存的訪問是通過連接着CPU和北橋芯片的前端總線來完畢的。

在前端總線上傳輸的內存地址都是物理內存地址,編號從0開始一直到可用物理內存的最高端。

這些數字被北橋映射到實際的內存條上。物理地址是明白的、終於用在總線上的編號,不必轉換,不必分頁,也沒有特權級檢查。

然而,在CPU內部。程序所使用的是邏輯內存地址。它必須被轉換成物理地址后,才干用於實際內存訪問。從概念上講,地址轉換的步驟例如以下圖所看到的:

 x86 CPU開啟分頁功能后的內存地址轉換過程

x86 CPU開啟分頁功能后的內存地址轉換過程

 

此圖並未指出詳實的轉換方式。它只描寫敘述了在CPU的分頁功能開啟的情況下內存地址的轉換過程。假設CPU關閉了分頁功能,或執行於16位實模式。那么從分段單元(segmentation unit)輸出的就是終於的物理地址了。當CPU要運行一條引用了內存地址的指令時,轉換過程就開始了。第一步是把邏輯地址轉換成線性地址

可是。為什么不跳過這一步。而讓軟件直接使用線性地址(或物理地址呢?)其理由與:“人類為何要長有闌尾?它的主要作用不過被感染發炎而已”大致同樣。這是進化過程中產生的奇特構造。要真正理解x86分段功能的設計,我們就必須回溯到1978年。

 

最初的8086處理器的寄存器是16位的。其指令集大多使用8位或16位的操作數。

這使得代碼能夠控制216個字節(或64KB)的內存。然而Intel的project師們想要讓CPU能夠使用很多其它的內存。而又不用擴展寄存器和指令的位寬。於是他們引入了段寄存器segment register),用來告訴CPU一條程序指令將操作哪一個64K的內存區塊。

一個合理的解決方式是:你先載入段寄存器,相當於說“這兒!我打算操作開始於X處的內存區塊”;之后,再用16位的內存地址來表示相對於那個內存區塊(或段)的偏移量。總共同擁有4個段寄存器:一個用於棧(ss),一個用於程序代碼(cs),兩個用於數據(dses)。

在那個年代,大部分程序的棧、代碼、數據都能夠塞進相應的段中,每段64KB長。所以分段功能常常是透明的。

 

現今。分段功能依舊存在。一直被x86處理器所使用着。

每一條會訪問內存的指令都隱式的使用了段寄存器。比方,一條跳轉指令會用到代碼段寄存器(cs),一條壓棧指令(stack push instruction)會使用到堆棧段寄存器(ss)。

在大部分情況下你能夠使用指令明白的改寫段寄存器的值。

段寄存器存儲了一個16位的段選擇符(segment selector);它們能夠經由機器指令(比方MOV)被直接載入。唯一的例外是代碼段寄存器(cs)。它僅僅能被影響程序運行順序的指令所改變,比方CALLJMP指令。盡管分段功能一直是開啟的,但其在實模式與保護模式下的運作方式並不同樣的。

 

在實模式下,比方在導啟動的,段選擇符是一個16位的數值。指示出一個段的開始處的物理內存地址。這個數值必須被以某種方式放大。否則它也會受限於64K其中。分段就沒有意義了。比方,CPU可能會把這個段選擇符當作物理內存地址的高16位(只需將之左移16位,也就是乘以216)。這個簡單的規則使得:能夠按64K的段為單位。一塊塊的將4GB的內存都尋址到。遺憾的是,Intel做了一個非常詭異的設計,讓段選擇符只乘以24(或16),一舉將尋址范圍限制在了1MB,還引入了過度復雜的轉換過程。下述圖例顯示了一條跳轉指令,cs的值是0x1000

 實模式分段功能

實模式分段功能

 

實模式的段地址以16個字節為步長,從0開始編號一直到0xFFFF0(即1MB)。你能夠將一個從00xFFFF16位偏移量(邏輯地址)加在段地址上。在這個規則下。對於同一個內存地址,會有多個段地址/偏移量的組合與之相應,並且物理地址能夠超過1MB的邊界,僅僅要你的段地址足夠高(參見臭名昭著的A20)。相同的,在實模式的C語言代碼中,一個遠指針far pointer)既包括了段選擇符又包括了邏輯地址。用於尋址1MB的內存范圍。真夠“遠”的啊。

隨着程序變得越來越大,超出了64K的段,分段功能以及它古怪的處理方式,使得x86平台的軟件開發變得很復雜。這樣的設定可能聽起來有些詭異,但它卻把當時的程序猿推進了令人崩潰的深淵。

 

32位保護模式下,段選擇符不再是一個單純的數值,取而代之的是一個索引編號。用於引用段描寫敘述符表中的表項。這個表為一個簡單的數組。元素長度為8字節,每一個元素描寫敘述一個段。

看起來例如以下:

 段描寫敘述符

段描寫敘述符

 

有三種類型的段:代碼,數據,系統。為了簡潔明了。僅僅有描寫敘述符的共同擁有特征被繪制出來。基地址base address)是一個32位的線性地址,指向段的開始。段界限limit)指出這個段有多大。將基地址加到邏輯地址上就形成了線性地址。DPL是描寫敘述符的特權級(privilege level),其值從0(最高特權,內核模式)到3(最低特權,用戶模式),用於控制對段的訪問。

 

這些段描寫敘述符被保存在兩個表中:全局描寫敘述符表GDT)和局部描寫敘述符表LDT)。電腦中的每個CPU(或一個處理核心)都含有一個叫做gdtr的寄存器,用於保存GDT的首個字節所在的線性內存地址。為了選出一個段,你必須向段寄存器載入符合下面格式的段選擇符:

 段選擇符

段選擇符

 

GDTTI位為0。對LDTTI位為1index指出想要表中哪一個段描寫敘述符(譯注:原文是段選擇符,應該是筆誤)。對於RPL。請求特權級(Requested Privilege Level),以后我們還會具體討論。如今。須要好好想想了。

CPU執行於32位模式時,無論如何。寄存器和指令都能夠尋址整個線性地址空間,所以根本就不須要再去使用基地址或其它什么鬼東西。那為什么不干脆將基地址設成0,好讓邏輯地址與線性地址一致呢?Intel的文檔將之稱為“扁平模型”(flat model),並且在現代的x86系統內核中就是這么做的(特別指出,它們使用的是基本扁平模型)。基本扁平模型(basic flat model)等價於在轉換地址時關閉了分段功能。

如此一來多么美好啊。

就讓我們來看看32位保護模式下運行一個跳轉指令的樣例,當中的數值來自一個實際的Linux用戶模式應用程序:

 保護模式的分段

保護模式的分段

 

段描寫敘述符的內容一旦被訪問,就會被cache(緩存)。所以在隨后的訪問中。就不再須要去實際讀取GDT了,否則會有損性能。每一個段寄存器都有一個隱藏部分用於緩存段選擇符所相應的那個段描寫敘述符。假設你想了解很多其它細節。包含關於LDT的很多其它信息,請參閱《Intel System Programming Guide3A卷的第三章。2A2B卷講述了每個x86指令,同一時候也指明了x86尋址時所使用的各種類型的操作數:16位,16位加段描寫敘述符(可被用於實現遠指針),32位。等等。

 

Linux上。僅僅有3個段描寫敘述符在引導啟動過程被使用。他們使用GDT_ENTRY宏來定義並存儲在boot_gdt數組中。

當中兩個段是扁平的,可對整個32位空間尋址:一個是代碼段,載入到cs中,一個是數據段,載入到其它段寄存器中。第三個段是系統段,稱為任務狀態段(Task State Segment)。

在完畢引導啟動以后。每個CPU都擁有一份屬於自己的GDT

當中大部分內容是同樣的。僅僅有少數表項依賴於正在執行的進程。你能夠從segment.hLinux GDT的布局以及其實際的樣子。這里有4個基本的GDT表項:2個是扁平的,用於內核模式的代碼和數據。另兩個用於用戶模式。

在看這個Linux GDT時,請留意那些用於確保數據與CPU緩存線對齊的填充字節——目的是克服馮·諾依曼瓶頸

最后要說說。那個經典的Unix錯誤信息“Segmentation fault”(分段錯誤)並非由x86風格的段所引起的,而是因為分頁單元檢測到了非法的內存地址。

唉呀,下次再討論這個話題吧。

 

Intel巧妙的繞過了他們原先設計的那個拼拼湊湊的分段方法,而是提供了一種富於彈性的方式來讓我們選擇是使用段還是使用扁平模型。因為非常easy將邏輯地址與線性地址合二為一,於是這成為了標准,比方如今在64位模式中就強制使用扁平的線性地址空間了。可是即使是在扁平模型中,段對於x86的保護機制也十分重要。保護機制用於抵御用戶模式進程對系統內核的非法內存訪問。或各個進程之間的非法內存訪問。否則系統將會進入一個狗咬狗的世界!

在下一篇文章中,我們將窺視保護級別以及怎樣用段來實現這些保護功能。


免責聲明!

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



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