“段寄存器”的故事


一、 段寄存器的產生

段寄存器的產生源於Intel 8086 CPU體系結構中數據總線與地址總線的寬度不一致。

數據總線的寬度,也即是ALU(算數邏輯單元)的寬度,平常說一個CPU是“16位”或者“32位”指的就是這個。8086CPU的數據總線是16位。

地址總線的寬度不一定要與ALU的寬度相同。因爲ALU的寬度是固定的,它受限於當時的工藝水平,當時只能製造出16位的ALU;但地址總線不一樣,它可以設計得更寬。地址總線的寬度如果與ALU相同當然是不錯的辦法,這樣CPU的結構比較均衡,尋址可以在單個指令週期內完成,效率最高;而且從軟件的解決來看,一個變量地址的長度可以用整型或者長整型來表示會比較方便。

但是,地址總線的寬度還要受制於需求,因爲地址總線的寬度決定了系統可尋址的範圍,即可以支持多少內存。如果地址總線太窄的話,可尋址範圍會很小。如果地址總線設計爲16位的話,可尋址空間是2^16=64KB,這在當時被認爲是不夠的;Intel最終決定要讓8086的地址空間爲1M,也就是20位地址總線。

地址總線寬度大於數據總線會帶來一些麻煩,ALU無法在單個指令週期裏完成對地址數據的運算。有一些容易想到的可行的辦法,比如定義一個新的寄存器專門用於存放地址的高4位,但這樣增加了計算的複雜性,程序員要增加成倍的彙編代碼來操作地址數據而且無法保持兼容性。

Intel想到了一個折中的辦法:把內存分段,並設計了4個段寄存器,CS,DS,ES和SS,分別用於指令、數據、其它和堆棧。把內存分爲很多段,每一段有一個段基址,當然段基址也是一個20位的內存地址。不過段寄存器仍然是16位的,它的內容代表了段基址的高16位,這個16位的地址後面再加上4個0就構成20位的段基址。而原來的16位地址只是段內的偏移量。這樣,一個完整的物理內存地址就由兩部分組成,高16位的段基址和低16位的段內偏移量,當然它們有12位是重疊的,它們兩部分相加在一起,才構成完整的物理地址。

Base b15 ~ b12 b11 ~ b0  
Offset   o15 ~ o4 o3 ~ o0
Address a19 ~ a0

這種尋址模式也就是“實地址模式”。在8086中,段寄存器還只是一個單純的16位寄存器,而且操作寄存器的指令也不是特權指令。通過設置段寄存器和段內偏移,程序就可以訪問整個物理內存,無安全性可言。

總之一句話,段寄存器的設計是一個權宜之計,現在看來可以說是一個臨時性的解決方案,設計它的目的是爲了把地址空間從64KB擴展爲1MB,僅此而已。但是它的加入卻爲日後Intel系列芯片的發展帶來諸多不便,也爲理解i386體系帶來困擾。

二、 實現保護模式

到了80386問世的時候,工藝已經有了很大的進步,386的ALU已經從16位躍升爲32位,也就是說,80386是32位的CPU,而且結構也已經比較成熟,接下來的80486一直到Pentium系列雖然速度提高了幾個數量級,但並沒有質的變化,所以被統稱爲i386結構。

對於32位的CPU來說,只要地址總線寬度與數據總線寬度相同,就可以尋址2^32=4GB的內存空間,這已經足夠用,已經不再需要段寄存器來幫助擴展。但這時Intel已經無法把段寄存器從產品中去掉,因爲新的CPU也是產品系列中的一員,根據兼容性的需要,段寄存器必須保留下來。

這時,技術的發展需要Intel在其CPU中實現“保護模式”,用戶程序的可訪問內存範圍必須受到限制,不能再任意地訪問內存所有地址。Intel決定利用段寄存器來實現他們的保護模式,把保護模式建立在段寄存器的基礎之上。

對於段的描述不再只是一個20位的起始地址,而是全新地定義了“段描述項”。段描述項的結構如下:
B31 ~ B24 DES1 (4 bit) L19 ~ L16
DES2 (8 bit) B23 ~ B16
B15 ~ B0
L15 ~ L0

每一行是兩個字節,總共8個字節,64位。

DES1和DES2分別是一些描述信息,用於描述本段是數據段還是代碼段,以及讀寫權限等等。B0~B31是段的基地址,L0~L19是段的長度。

注意,規定段的長度是非常必要的,如果不限定段長度,“保護”就無從談起,用戶程序的訪問至少不能超過段的範圍。另外,段長度只有20位,所代表的最大可能長度爲2^20=1M,而整個地址空間是2^32=4GB,這樣來看,段的長度是不是太短了?其實,在DES1中,有一位用於表示段長度的單位,當它被置1時(一般情況下都是如此),表示長度單位爲4KB,這樣,一個段的最大可能尺寸就成了1M*4K=4G,與地址空間相穩合。4KB也正是一個內存頁的大小,說明段的大小也是向頁對齊的。

另外,注意到一個有趣的現象嗎?段描述項的結構被設計得不連續,不論是段基地址還是段長度,都被分成了兩節表示。這樣的設計與80286的過渡有關。上面的段描述項結構去掉第一行後剩下的三行正是286的段描述項。286被設計爲24位地址總線,所以段基址是24位,相應地段長是16位。在386的地址總線擴展爲32位之後,還必須兼容286產品的設計,所以只好在段描述項上“打補丁”。

在386中,段寄存器還是16位,那麼16位的段寄存器如何存放得下64位的段描述項? 段描述項不再由段寄存器直接持有。段描述項存放在內存裏,系統中可以有很多個段描述項,這些項連續存放,共同構成一張表,16位的段寄存器裏只是含有這張表裏的一個索引,但也並不僅是一個簡單的序號,而是存儲了一種數據結構,這種結構的定義如下:

index (b15 ~ b3) TI (b2) RPL (b1 ~ b0)

其中index是段描述表的索引,它指向其中的某一個段描述項。RPL表示權限,00最高,11最低。

還有一個關鍵的問題,內存中的段描述表的起始地址在哪裏?顯然光有索引是有不夠的。爲此,Intel又設計了兩個新的寄存器:GDTR(global descriptor table register)和LDTR(local descriptor table register),分別用來存儲段描述表的地址。段寄存器中的TI位正是用於指示使用GDTR還是LDTR。

當用戶程序要求訪問內存時,CPU根據指令的性質確定使用哪個段寄存器,轉移指令中的地址在代碼段,取數指令中的地址在數據段;根據段寄存器中的索引值,找到段描述項,取得段基址;指令中的地址是段內偏移,與段長比較,確保沒有越界;檢查權限;把段基址和偏移相加,構成物理地址,取得數據。

新的設計中處處有權限與範圍的限制,用戶程序只能訪問被授權的內存空間,從而實現了保護機制。就這樣,在段寄存器的基礎上,Intel實現了自己的“保護模式”。

三、 與頁式存管並存

現代操作系統的發展要求CPU支持頁式存儲管理。

頁式存管本身是與段式存管分立的,兩者沒有什麼關係。但對於Intel來說,同樣是由於“段寄存器”這個歷史的原因,它必須把頁式存管建立在段式存管的基礎之上,儘管這從設計的角度來說這是沒有道理,也根本沒有必要的。

在段式存管中,由程序發出的變量地址經映射(段基址+段內偏移)之後,得到的32位地址就是一個物理地址,是可以直接放到地址總線是去取數的。

在頁式存管中,過程也是相似的,由程序發出的變量地址並不是實際的物理地址,而是一個三層的索引結構,這個地址經過一系統的映射之後纔可以得到物理地址。

現在對於Intel CPU來說,以上兩個映射過程就要先後各做一次。由程序發出的變量地址稱爲“邏輯地址”,先經過段式映射成爲“線性地址”,線性地址再做爲頁式映射的輸入,最後得到“物理地址”。

Linux內核實現了頁式存儲管理,而且並沒有因爲兩層存管的映射而變得更復雜。Linux更關注頁式內存管理,對於段式映射,採用了特殊的方式把它簡化。讓每個段寄存器都指向同一個段描述項,即只設了一個段,而這個段的基地址爲0,段長度設爲最大值4G,這個段就與整個物理內存相重合,邏輯地址經映射之後就與線性地址相同,從而把段式存管變成“透明”的。

這就是Intel處理器中“段寄存器”的故事。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章