內存尋址(一) —— 基本概念與機制


1. 內存地址

在編程中我們(編譯器)為各種數據分配的內存均為邏輯地址,邏輯地址通過操作系統轉換為物理地址。在使用Intel 80x86處理器時,應當分清以下三種“地址”:

  • 邏輯地址(logical address)
  • 線性地址(linear address)
  • 物理地址(physical address)

CPU控制單元通過一種稱為分段單元(segmentation unit) 的硬件電路把一個邏輯地址轉換成線性地址;
接着,第二個稱為 分頁單元(paging unit) 的硬件電路把一個線性地址轉換成物理地址。

image

2. 分段機制

最早的程序直接讀寫物理內存。在這種情況下,通常同時運行兩個程序是不可能的,因為它們很可能同時訪問同一個物理地址導致程序崩潰。

當Intel 8086處理器出現時,內存尋址迎來第一次飛躍,它引入了一個重要的概念——段,段式內存使得程序地址不再需要靜態重定位,支持了更大的地址。

8086處理器的數據總線是16位的,但其目標是尋址1MB的內存,這意味着需要20位的地址總線。為了解決這個問題,當時引入了分段的方法。

一個邏輯地址由兩部分組成:一個段標識符和一個指定段內相對地址的偏移量。段標識符是一個16 位長的字段,稱為段選擇符(Segment Selector),而偏移量是一個32 位長的字段。

image

2.1 段寄存器

為了方便找到段選擇符,處理器提供6個段寄存器專門用來存放段選擇符。它們是cs、ss、ds、es、fs、gs

  • cs為代碼段寄存器;
  • ss為棧段寄存器;
  • ds為數據段寄存器;

其他三個段寄存器作一般用途,6個段寄存器均為16位

2.2 段描述符

每個段由一個8字節的段描述符(segment descriptor)來具體描述。段描述符放在全局描述符表(Global Descriptor Table ,GDT)或局部描述符表(Local Descriptor Table, LDT)中。段描述符被存在一個非編程CPU寄存器中,非編程寄存器對程序員不可見,僅供6個可編程的段寄存器使用。

image

B31 ~ B24 和 B23 ~ B16分別為基地址的 bit16 ~ bit23 和 bit24 ~ bit31
L19 ~ L16和L15 ~ L0為段LIMIT的 bit10 ~ bit15 和 bit16 ~ bit19

4位type域描述了段的類型特征和存取權限,廣泛使用如下幾種:
1.代碼段描述符(描述符代表一個代碼段,可以放在GDT或LDT中,S標志位為1)
2.數據段描述符(描述符代表一個數據段,可以放在GDT或LDT中,S標志位為1)
3.任務狀態段描述符(代表一個任務狀態段(TSS),只能存放在GDT中,S標志位為9或11)
4.局部描述符表描述符(代表一個LDT段,只能存放在GDT中,S標志位為0)

2.3 段選擇符

之前提到16位的段選擇符存放在段寄存器中,它直接指向段描述符。每當一個段選擇符被裝入段寄存器,相應的段描述符就被裝入非編程寄存器。此時CPU只需直接引用段描述符所在寄存器即可,不需訪問主存中的GDT或LDT。僅當段寄存器的內容改變時,才有必要訪問GDT或LDT。

image

段選擇符包含以下域:

  • 13位的索引,指定了段描述符在GDT或LDT中的入口。
  • TI標志指明了段描述符是在GDT中(TI = 0)還是在LDT中( TI = 1)
  • 兩位RPL(請求特權級)

image

2.4 邏輯地址到線性地址的轉換

分段單元(segmentationunit)執行以下操作:

先檢查段選擇符的TI字段,以決定段描述符保存在哪一個描述符表中。TI字段指明描述符是在GDT中(在這種情況下,分段單元從GDTR寄存器中得到GDT的線性基地址)還是在LDT中(在這種情況下,分段單元從LDTR寄存器中得到LDT的線性基地址)

再來看一下index部分。我們可以將描述符表看成是一個數組,每個元素都存放一個段描述符,那index就表示某個段描述符在數組中的索引。

現在我們假設有一個段的段選擇符,它的TI=0,index=8。我們可以知道這個段的描述符是在GDT數組中,並且他的在數組中的索引是8。

假設GDT的起始位置是0x00020000,而一個段描述符的大小是8個字節,由此我們可以計算出段描述符所在的地址:0x00020000+8*index(只所以乘8是因為地址是連續的,假設內存地址最小單位為字節,index=8,一個段描述符大小為8字節,那么乘8是為了將前面7個段描述符跳過直接定位到第8個地址),從而我們就可以找到我們想要的段描述符,從而獲取某個段的首地址,然后再將從段描述符中獲取到的首地址與邏輯地址的偏移量相加就得到了線性地址。

image

3. 線性地址到物理地址轉換

分頁單元在分段之后運行,把線性地址轉換成物理地址

分頁單元的基本原理是把內存划分成大小固定的若干單元,每個單元稱為一頁(page),每頁包含4k字節的地址空間(為簡化分析,我們不考慮擴展分頁的情況)。這樣每一頁的起始地址都是4k字節對齊的。為了能轉換成物理地址,我們需要給CPU提供當前任務的線性地址轉物理地址的查找表,即頁表(page table)(把線性地址映射到物理地址的數據結構稱為頁表(page table)。頁表存放在主存中,並在啟用分頁單元之前必須由內核對頁表進行適當的初始化。)。注意,為了實現每個任務平攤虛擬內存,每個任務都有自己的頁目錄表和頁表。

為了節約頁表占用的內存空間,x86將線性地址通過頁目錄表和頁表兩級查找轉換成物理地址。

32 位的線性地址被分成 3 個域:

  • Directory(目錄):最高10 位
  • Table(頁表):中間10 位
  • Offset(偏移量):最低12 位

最高10位 Directory 頁目錄表偏移量,中間10位 Table是頁表偏移量,最低12位Offset是物理頁內的字節偏移量。

頁目錄表的大小為4k(剛好是一個頁的大小),包含1024項,每個項4字節(32位),項目里存儲的內容就是頁表的物理地址。如果頁目錄表中的頁表尚未分配,則物理地址填0。

頁表的大小也是4k,同樣包含1024項,每個項4字節,內容為最終物理頁的物理內存起始地址。

每個活動的任務,必須要先分配給它一個頁目錄表,並把頁目錄表的物理地址存入cr3寄存器。頁表可以提前分配好,也可以在用到的時候再分配。

還是以 mov 0x80495b0, %eax 中的地址為例分析一下線性地址轉物理地址的過程。

內核先將當前任務的頁目錄表的物理地址填入cr3寄存器。

線性地址 0x80495b0 轉換成二進制后是 0000 1000 0000 0100 1001 0101 1011 0000,最高10位0000 1000 00的十進制是32,CPU查看頁目錄表第32項,里面存放的是頁表的物理地址。

線性地址中間10位00 0100 1001 的十進制是73,頁表的第73項存儲的是最終物理頁的物理起始地址。

物理頁基地址加上線性地址中最低12位的偏移量,CPU就找到了線性地址最終對應的物理內存單元。

image

怎樣防止進程訪問不屬於自己的線性地址(如內核空間)或無效的地址呢?內核里記錄着每個進程能訪問的線性地址范圍(進程的vm_area_struct 線性區鏈表和紅黑樹里存放着),在引發缺頁異常的時候,如果內核檢查到引發缺頁的線性地址不在進程的線性地址范圍內,就發出SIGSEGV信號,進程結束,我們將看到程序員最討厭看到的Segmentation fault。


免責聲明!

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



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