我們說乙個cpu是16位的或32位的或64位的,指定是cpu中的alu單元(算術邏輯單元)的寬度,通常也就是資料匯流排的寬度。那麼位址匯流排呢?自然的,從程式設計的角度我們希望其與資料匯流排的寬度一致,這樣乙個位址也就是乙個指標,其與整數同寬,在進行指標運算時,直接拿整數運算指令就可了,不需要專門的「指標運算指令」。
在80286時代,當時資料匯流排的寬度是16位的,不出意外的話,位址匯流排也是16位的,也就是可以訪問64k的記憶體空間。但在當時intel的設計師預計到了記憶體需求的變化,決定將記憶體空間擴大。擴大到多少呢?決定擴大到1m。1m的記憶體空間需要20bit來表示,於是位址匯流排擴充套件到20位。可是我們的alu仍然是16位的,也就是說可以直接拿來運算的指標的長度也是16位的,超過16位就溢位了(我們可以想象下,當時的整形是16位,20位的數是無法表示和計算的),那怎麼填補這個空白?方法有很多種,比如增設20位的指令專用於位址運算與操作,但那樣又會造成cpu內部結構體的不均勻性。intel設計師採用一種巧妙的辦法,即分段的方法。
8086中設定四個段暫存器:cs/ds/ss/es,分別表示**段、資料段、堆疊段和其他段。每個段都是16位的,用作位址匯流排的高16位。每條訪存指令中的「內部位址」都是16位的,再將內部位址送到位址匯流排之前,要與對應的段暫存器進行拼接形成20位的位址,然後再送到位址匯流排上去定址。位址拼接的操作為:送往位址匯流排上的位址 = 段基址 << 4 + 內部位址。
這種定址方式給壞分子可乘之機。首先,修改段暫存器內容的指令不是特權指令,這樣誰都可以更改段基址;其次,段基址一旦確定,乙個程序就能所以訪問從此開始的64k記憶體。這樣一來,誰都可以訪問記憶體空間中的任何乙個記憶體單元,這是很危險的。8086這種定址方式缺乏對記憶體空間的保護,為了區別於後來的「保護模式」,就稱為「實位址模式」。
顯然,在「實位址模式」是無法構築現代的作業系統的。
到了386時代,資料匯流排和位址匯流排都擴充套件到32位了,可以定址4gb的記憶體空間了。按照我們的想法,intel該重新來過,可以摒棄段式記憶體管理了。可是因為286是自己的親哥哥,386不得不揹負起向前相容的包袱,依然採用段式記憶體管理。並且需要在段式記憶體管理的基礎實現保護模式,即對記憶體空間的保護。為了對記憶體空間進行保護,這樣幾項工作必須要做:
1、由段暫存器「確定」的基址不要透漏給使用者,即使用者無從讀取段基址
2、修改段基址的指令必須是特權指令
3、每個段上必須加許可權控制,許可權不夠,不許對記憶體進行訪問
有了這3個要求,286時代的「根據段暫存器確定段基址」方法已經行不通了,我們需要的不僅僅是基址,還需要訪問許可權等額外的資訊,而且我們不想把具體的基址暴露給使用者。
為了解決這些問題,intel引入乙個中間結構體,段描述符。並增設了兩個暫存器:gdtr(global descriptor talbe register)指向全域性段描述符陣列(表);ldtr(localdescriptor table register)執行區域性段描述符陣列(表)。而6個段暫存器,cs/ds/ss/es包括後來的fs/gs,其內容不在用作基址,而是用作索引去段描述符陣列中查詢對應的段描述符。
段描述符佔8個位元組,其定義以及其中各個標誌位的定義如下:
通過段描述符,我們能夠得到如下資訊:
①段的基址,由b31-b24/b23-b16/b15-b0構成,一共32位,基址可以是4gb空間中任意位址;
②段的長度,由l19-l16/l15-l0構成,一共20位。如果g位為0,表示段的長度單位為位元組,則段的最大長度是1m,如果g位為1,表示段的長度單位為4kb,則段的最大長度為1m*4k=4g。假定我們把段的基位址設定為0,而將段的長度設定為4g,這樣便構成了乙個從0位址開始,覆蓋整個4g空間的段。訪存指令中給出的「邏輯位址」,就是放到位址匯流排上的「實體地址」,這有別於「段基址加偏移」構成的「層次式」位址(其實應該算作「層次式」位址的特例),所以intel稱其為flat位址即平面位址,linux核心採用的就是平面位址)。
③段的型別,**段還是資料段,可讀還是可寫
描述符表儲存在由作業系統維護著的特殊資料結構中,並且由處理器的記憶體管理硬體來引用。這些特殊結構應該儲存在僅由作業系統軟體訪問的受保護的記憶體區域中,以防止應用程式修改其中的位址轉換資訊。同時,為了避免每次訪問記憶體時都通過段暫存器去查表、去讀和解碼乙個段描述符,每次更改段暫存器的內容時,cpu將段暫存器指向的段描述符中的段基址、長度以及訪問控制資訊等載入到cpu中的「影子結構」中快取起來。後續對該段的訪問控制都通過「影子結構體」來進行。
但是如果可以修改gdtr和ldtr的內容呢?我們不就可以隨便指定gdtr到我們自己偽造的段描述陣列從而掌控程式嗎?為了解決這個問題,intel將訪問這兩個暫存器的專門指令設為特權指令(lgdt/lldt,sgdt/sldt),這些指令只有當cpu處於系統狀態(即在作業系統核心中)才能使用,使用者空間無法訪問暫存器的內容。
這樣一來,工作1-2就完成了。
16位段暫存器中的內容,稱之為段選擇符,除了高13位用作段描述符陣列的索引外(因此理論上段描述符陣列最多可以8192個元素),低3位有其他的用途,如下所示:
由於有兩個描述符陣列,所以ti(table index)位用來確定從哪個陣列中索引。
在前面的段描述符結構中,我們看到了特權級別字段(dpl),為什麼還需要在這裡設定乙個特權字段(rpl)呢?
intel的cpu有四種特權級別,0級最高,3級最低。每條指令都有其適用級別,如前述的lgdt指令要求0級特權,通常使用者的應用程式都是3級。linux中對cpu特權進行了簡化,只區分使用者級別和系統級別,分別對應3級和0級,這是後話。一般應用程式的當前級別由其**段的區域性段描述符(即用段暫存器cs索引ldtr指向的區域性描述符項)中的dpl(descriptor privilege level)決定,當然,每個段描述符的dpl都是在0級狀態下由核心設定的。而全域性段描述符中的dpl有所不同,它表示所需的級別。段選擇符中的rpl也表示請求級別。這樣,當我們需要改變某個段暫存器(比如資料段ds)中的內容(段選擇符)來訪問一款新段空間時,cpu要做許可權檢查:
①當前程式有權訪問新的段嗎?比較當前程式的當前級別與新段描述符中的dpl
②新的段選擇符有權訪索引新的段嗎?比較新的段選擇符中的rpl與新段描述符的dpl。
當然,具體的許可權檢查比這要複雜,設計到段描述符中c位的取值,詳情情況請參考其他資料。
①根據指令性質確定該使用哪個段暫存器,如跳轉指令則目標位址在**段cs,取資料的指令目標位址在資料段;
②根據段暫存器的內容找到對應的段描述符。其實這一步不用找,前面介紹過了,段暫存器對應的段描述符已經在cpu的「影子結構」中了。
③從段描述符中獲得基址
④將指令中的「邏輯位址」與段的長度比較,確定是否越界
⑤根據指令的性質和段描述符中的訪問許可權確定是否越權
⑥將指令中的「邏輯位址」作為位移,與基位址相加得到實際的「實體地址」
之二 X86頁式記憶體管理
記憶體管理的目的是什麼?記憶體管理本身就像乙個外觀模式,它隱藏底層細節而給應用程式提供乙個統一易用的訪問記憶體的介面。程式可以訪問4g空間中的任意位址,但實際上物理記憶體可能只有幾百m,這之間的矛盾該怎麼解決?關鍵時刻,還是得抱硬碟的大腿。當可用記憶體不足時,將記憶體中不緊急的內容從記憶體中換出到磁...
x86架構段頁式記憶體管理機制
記憶體定址總體流程 段暫存器 值得注意的是,計算機在描述記憶體分段時至少需要如下資訊 段的大小 段的起始位址 段的屬性。在80386以後的cpu中,需要使用64位來儲存這些資訊,但段暫存器只有16位 intel為了保持向後相容 因此在保護模式下,需要使用段描述符來儲存這些資訊,而段暫存器儲存的是段選...
Linux記憶體管理之一 分段與分頁
現代作業系統的記憶體管理機制有兩種 段式管理和頁式管理。段式記憶體管理,就是將記憶體分成段,每個段的起始位址就是段基位址。位址對映的時候,由邏輯位址加上段基位址而得到實體地址。純粹的段式記憶體管理的缺點很明顯,就是靈活性和效率比較差。首先是段的長度是可變的,這給記憶體的換入換出帶來諸多不便,如何選擇...