線性地址轉換為物理地址是硬體實現還是軟體實現?具體過程如何?
線性地址轉換為物理地址是硬體實現還是軟體實現?具體過程如何?答:線性地址(Linear Address)是邏輯地址到物理地址變換之間的中間層。在分段部件中邏輯地址是段中的偏移地址,然後加上基地址就是線性地址。線性地址是一個32位無符號整數,可以用來表示高達4GB的地址,也就是,高達4294967296個內存單元。線性地址通常用十六進位數字表示,值的範圍從0x00000000到0xffffffff)。程序代碼會產生邏輯地址,通過邏輯地址變換就可以生成一個線性地址。如果啟用了分頁機制,那麼線性地址可以再經過變換以產生一個物理地址。當採用4KB分頁大小的時候,線性地址的高10位為頁目錄項在頁目錄表中的編號,中間十位為頁表中的頁號,其低12位則為偏移地址。如果是使用4MB分頁機制,則高10位頁號,低22位為偏移地址。如果沒有啟用分頁機制,那麼線性地址直接就是物理地址。那麼線性地址與分頁管理是怎麼實現的呢? CPU的頁式內存管理單元,負責把一個線性地址,最終翻譯為一個物理地址。從管理和效率的角度出發,線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個tatol_page[2^20]的大數組,共有2的20個次方個頁。這個大數組我們稱之為頁目錄。目錄中的每一個目錄項,就是一個地址——對應的頁的地址。如圖:
1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3寄存器中,是進行地址轉換的開始點。萬里長征就從此長始了。2、每一個活動的進程,因為都有其獨立的對應的虛似內存(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄地址。——運行一個進程,需要將它的頁目錄地址放到cr3寄存器中,將別個的保存下來。3、每一個32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位)依據以下步驟進行轉換:1、從cr3中取出進程的頁目錄地址(操作系統負責在調度進程的時候,把這個地址裝入對應寄存器);2、根據線性地址前十位,在數組中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數組),頁的地址被放到頁表中去了。3、根據線性地址的中間十位,在頁表(也是數組)中找到頁的起始地址;4、將頁的起始地址與線性地址中最後12位相加,得到最終我們想要的物理地址;參考網路~ 把以前的一篇博文貼過來。。
一、邏輯地址轉線性地址
機器語言指令中出現的內存地址,都是邏輯地址,需要轉換成線性地址,再經過MMU(CPU中的內存管理單元)轉換成物理地址才能夠被訪問到。
我們寫個最簡單的hello world程序,用gccs編譯,再反編譯後會看到以下指令:
mov 0x80495b0, %eax
這裡的內存地址0x80495b0 就是一個邏輯地址,必須加上隱含的DS 數據段的基地址,才能構成線性地址。也就是說 0x80495b0 是當前任務的DS數據段內的偏移。
在x86保護模式下,段的信息(段基線性地址、長度、許可權等)即段描述符佔8個位元組,段信息無法直接存放在段寄存器中(段寄存器只有2位元組)。Intel的設計是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT內的索引值(index)。
Linux中邏輯地址等於線性地址。為什麼這麼說呢?因為Linux所有的段(用戶代碼段、用戶數據段、內核代碼段、內核數據段)的線性地址都是從 0x00000000 開始,長度4G,這樣 線性地址=邏輯地址+ 0x00000000,也就是說邏輯地址等於線性地址了。
這樣的情況下Linux只用到了GDT,不論是用戶任務還是內核任務,都沒有用到LDT。GDT的第12和13項段描述符是 __KERNEL_CS 和__KERNEL_DS,第14和15項段描述符是 __USER_CS 和__USER_DS。內核任務使用__KERNEL_CS 和__KERNEL_DS,所有的用戶任務共用__USER_CS 和__USER_DS,也就是說不需要給每個任務再單獨分配段描述符。內核段描述符和用戶段描述符雖然起始線性地址和長度都一樣,但DPL(描述符特權級)是不一樣的。__KERNEL_CS 和__KERNEL_DS 的DPL值為0(最高特權),__USER_CS 和__USER_DS的DPL值為3。
用gdb調試程序的時候,用info reg 顯示當前寄存器的值:
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
可以看到ds值為0x7b, 轉換成二進位為 00000000 01111011,TI欄位值為0,表示使用GDT,GDT索引值為 01111,即十進位15,對應的就是GDT內的__USER_DATA 用戶數據段描述符。
從上面可以看到,Linux在x86的分段機制上運行,卻通過一個巧妙的方式繞開了分段。
Linux主要以分頁的方式實現內存管理。
二、線性地址轉物理地址
前面說了Linux中邏輯地址等於線性地址,那麼線性地址怎麼對應到物理地址呢?這個大家都知道,那就是通過分頁機制,具體的說,就是通過頁表查找來對應物理地址。
準確的說分頁是CPU提供的一種機制,Linux只是根據這種機制的規則,利用它實現了內存管理。
在保護模式下,控制寄存器CR0的最高位PG位控制著分頁管理機制是否生效,如果PG=1,分頁機制生效,需通過頁表查找才能把線性地址轉換物理地址。如果PG=0,則分頁機制無效,線性地址就直接做為物理地址。
分頁的基本原理是把內存劃分成大小固定的若干單元,每個單元稱為一頁(page),每頁包含4k位元組的地址空間(為簡化分析,我們不考慮擴展分頁的情況)。這樣每一頁的起始地址都是4k位元組對齊的。為了能轉換成物理地址,我們需要給CPU提供當前任務的線性地址轉物理地址的查找表,即頁表(page table)。注意,為了實現每個任務的平坦的虛擬內存,每個任務都有自己的頁目錄表和頁表。
為了節約頁表佔用的內存空間,x86將線性地址通過頁目錄表和頁表兩級查找轉換成物理地址。
32位的線性地址被分成3個部分:
最高10位 Directory 頁目錄表偏移量,中間10位 Table是頁表偏移量,最低12位Offset是物理頁內的位元組偏移量。
頁目錄表的大小為4k(剛好是一個頁的大小),包含1024項,每個項4位元組(32位),項目里存儲的內容就是頁表的物理地址。如果頁目錄表中的頁表尚未分配,則物理地址填0。
頁表的大小也是4k,同樣包含1024項,每個項4位元組,內容為最終物理頁的物理內存起始地址。
每個活動的任務,必須要先分配給它一個頁目錄表,並把頁目錄表的物理地址存入cr3寄存器。頁表可以提前分配好,也可以在用到的時候再分配。
還是以 mov 0x80495b0, %eax 中的地址為例分析一下線性地址轉物理地址的過程。
前面說到Linux中邏輯地址等於線性地址,那麼我們要轉換的線性地址就是0x80495b0。轉換的過程是由CPU自動完成的,Linux所要做的就是準備好轉換所需的頁目錄表和頁表(假設已經準備好,給頁目錄表和頁表分配物理內存的過程很複雜,後面再分析)。
內核先將當前任務的頁目錄表的物理地址填入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就找到了線性地址最終對應的物理內存單元。
我們知道Linux中用戶進程線性地址能定址的範圍是0 - 3G,那麼是不是需要提前先把這3G虛擬內存的頁表都建立好呢?一般情況下,物理內存是遠遠小於3G的,加上同時有很多進程都在運行,根本無法給每個進程提前建立3G的線性地址頁表。Linux利用CPU的一個機制解決了這個問題。進程創建後我們可以給頁目錄表的表項值都填0,CPU在查找頁表時,如果表項的內容為0,則會引發一個缺頁異常,進程暫停執行,Linux內核這時候可以通過一系列複雜的演算法給分配一個物理頁,並把物理頁的地址填入表項中,進程再恢復執行。當然進程在這個過程中是被蒙蔽的,它自己的感覺還是正常訪問到了物理內存。
怎樣防止進程訪問不屬於自己的線性地址(如內核空間)或無效的地址呢?內核里記錄著每個進程能訪問的線性地址範圍(進程的vm_area_struct 線性區鏈表和紅黑樹里存放著),在引發缺頁異常的時候,如果內核檢查到引發缺頁的線性地址不在進程的線性地址範圍內,就發出SIGSEGV信號,進程結束,我們將看到程序員最討厭看到的Segmentation fault。
Linux 從虛擬地址到物理地址-Bean_lee-ChinaUnix博客Linux內存地址映射
推薦閱讀: