1)虛擬記憶體的解釋:
虛擬記憶體的核心概念是指代碼所用的記憶體位址與物理地址沒有關係.
在使用者空間中,一個進程的虛擬位址A指向不同的實體記憶體,而不是另一個進程的地址A.
任何時候CPU發送指令向記憶體存取資料時,通過軟體將虛擬位址的資料變為物理地址.
將虛擬位址變為物理地址變為物理地址的工作是由記憶體管理單元(MMU)完成的.
虛擬記憶體地址也可以稱為邏輯地址.
2)記憶體管理單元:
記憶體管理單元是CPU功能的一部份,如果CPU有cache,它將有一個記憶體管理單元,反之亦然.
記憶體管理單元可以將兩個進程對同一記憶體邏輯地址的訪問映射到不同的物理地址.
記憶體管理單元同快取密切協作,在RAM和快取之間按要求傳遞記憶體.
記憶體管理單元將記憶體分成許多頁,它是可利用實體記憶體的最小單位,每頁包含4KB位元組的地址空間.
3)虛擬記憶體到實體記憶體的映射:
3.1)映射的過程:
虛擬位址到物理地址的轉化是與體繫結構相關的,在X86 CPU上是以分段,分頁兩種方式轉化的.
虛擬位址(邏輯地址)---段式映射---線性地址---頁式映射---物理地址
Linux採用段頁式管理方式是由於intel的X86 CPU的硬體體繫結構決定的.這樣的雙重新對應本身毫無必要,在Linux中段式映射不起什麼作用.
可以理解為虛擬位址就是線性地址.
通過以下的程式來分析虛擬記憶體到線性地址再到實體記憶體的映射,我們還以X86為例:
vi hello.c
#include <stdio.h>
int greeting(){
printf("Hello world!/n");
return 0;
}
int
main (){
greeting();
return 0;
}
編寫一個HELLO WORLD程式,用gcc hello.c -o hello編譯
objdump -xd hello
這裡我們主要看main和greeting的調用:
08048354 <greeting>:
8048354: 55 push %ebp
8048355: 89 e5 mov %esp,%ebp
8048357: 83 ec 08 sub $0x8,%esp
804835a: c7 04 24 70 84 04 08 movl $0x8048470,(%esp)
8048361: e8 2e ff ff ff call 8048294 <puts@plt>
8048366: b8 00 00 00 00 mov $0x0,%eax
804836b: c9 leave
804836c: c3 ret
0804836d <main>:
804836d: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048371: 83 e4 f0 and $0xfffffff0,%esp
8048374: ff 71 fc pushl 0xfffffffc(%ecx)
8048377: 55 push %ebp
8048378: 89 e5 mov %esp,%ebp
804837a: 51 push %ecx
804837b: 83 ec 04 sub $0x4,%esp
804837e: e8 d1 ff ff ff call 8048354 <greeting>
8048383: b8 00 00 00 00 mov $0x0,%eax
8048388: 83 c4 04 add $0x4,%esp
804838b: 59 pop %ecx
804838c: 5d pop %ebp
804838d: 8d 61 fc lea 0xfffffffc(%ecx),%esp
8048390: c3 ret
函數main()通過call 8048354 <greeting>調用了greeting函數.
首先可以看到ld給greeting分配的地址是0x08048354,在elf格式的可執行代碼中,ld總是從0x08000000開始安排程式碼片段,對每個程式都這樣.
而程式在執行時在實體記憶體中的實際位置就要由核心在為其建立記憶體映射時臨時作出安排,具體地址則取決於當時所分配的實體記憶體頁面.這對於我們完全是透明的.
映射機制在程式運行時就已經建立起來了.
3.2)段式映射
從上例中,調用greeting()函數時,當前的地址是0x08048354,也就是EIP指標寄存器的值,那麼CS的值是什麼呢?
CS寄存器存放的是段式映射的選擇碼,可以理解為這是一個索引.
在LINUX中,選擇碼只有4個,也就是說只可能是以下4個其中1個,這4個選擇碼分別是:
段寄存器類型 數值 索引 TI RPL
__KERNEL_CS 0x10 0000 0000 00010 0 00
__KERNEL_DS 0x18 0000 0000 00011 0 00
__USER_CS 0x23 0000 0000 00100 0 11
__USER_DS 0x2B 0000 0000 00101 0 11
與上面的對照:
__KERNEL_CS index=2 TI=0 RPL=0
__KERNEL_DS index=3 TI=0 RPL=0
__USER_CS index=4 TI=0 RPL=3
__USER_DS index=5 TI=0 RPL=3
對選擇碼進行解釋說明:
1)關於段寄存器的賦值,依據以下的原則:
CS=__USER_CS
DS=__USER_DS
ES=__USER_DS
SS=__USER_DS
因為我們的程式在使用者空間中運行,所以無論是程式碼片段還是資料區段都是__USER_XX
2)關於TI的值,TI可以是GDT(全域段描述表),也可以是LDT(局部段描述表).
GDT對映的是0
LDT對映的是1
LINUX的TI幾乎都是0,LINUX核心中基本上不使用局部描述表LDT,LDT只是在vm86模式中運行wine以及其它在linux上類比運行windows
或DOS軟體的程式中才使用.
3)關於RPL,LINUX只用了0,3兩種層級.
0代表核心進程,3代表使用者進程.
通過以上的分析,我們的程式顯然是使用者進程,所以對映的就是__USER_CS,
最後CS寄存器的值就是0x23,而索引就是4.二進位(100)=十進位(4)
而在GDT全域描述表中4對映的是什麼呢?
我們先來看看gdt全域描述表:
ENTRY(gdt_table)
.quad 0x0000000000000000 /*NULL desccriptor*/
.quad 0x0000000000000000 /*not used*/
.quad 0x00cf9a000000ffff /*0x10 kernel 4GB code at 0x00000000*/
.quad 0x00cf92000000ffff /*0x18 kernel 4GB code at 0x00000000*/
.quad 0x00cffa000000ffff /*0x23 user 4GB code at 0x00000000*/
.quad 0x00cff2000000ffff /*0x2b user 4GB code at 0x00000000*/
.quad 0x0000000000000000 /*not used*/
.quad 0x0000000000000000 /*not used*/
可以看到索引為4的GDT就是
.quad 0x00cffa000000ffff /*0x23 user 4GB code at 0x00000000*/
現在把這4項描述符展開:
63-60 59-56 55-52 51-48 47-44 43-40 39-36 35-32 31-28 27-24 23-20 19-16 15-12 11-8 7-4 3-0
Kernel_CS:0x00cf9a000000ffff -->0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
Kernel_DS:0x00cf92000000ffff -->0000 0000 1100 1111 1001 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
User_CS: 0x00cffa000000ffff -->0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
User_DS: 0x00cff2000000ffff -->0000 0000 1100 1111 1111 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
以下對描述符各位進行解析:
描述符格式如下:
63-56位存放的是基地址31-24位,基地址都為0
55位也叫G位,在LINUX中都為1,等於1時段長以4k位元組為單位,等於0時以位元組為單位
54位也叫D位,在LINUX中都為1,等於1表示對該段的訪問為32位指令,等於0為16位指令
53位等於0
52位,CPU忽略該位,可由軟體使用.
51-48位存放的是段地址上限19-16位,都是1
47位也叫P位,在LINUX中都是1,表示4個段都在記憶體中.
46-45位是DPL位,表示特權層級.分別有00(0級)和11(3級)兩種組合.
44位也叫S位,等於1時表示一般的程式碼片段或資料區段,等於0時表示用於系統管理的系統段,如各類描述表.
43-41位叫做type位,因為各位之間有著緊密聯絡:
43位也叫E位,等於1時表示程式碼片段,這時第42位叫C位,C位等於0時會忽視特權層級,C位等於1時會依照特權層級.這時41位叫R位,等於1時為可讀,為0時不可讀.
43位等於0時表示資料區段,這時第42位叫ED位,ED位等於0時向上伸(資料區段),ED位等於1時向下伸(堆棧段),這時41位叫W位,等於1時為可寫,為0時不可寫.
40位叫A位,在LINUX中都是1,表示以被訪問過.
39-16位存放的是基地址23到0位,基地址都為0.
15-0位存放的是段地址上限15-0位,都是1
結論:每個段都是從0地址開始的整個4GB虛存空間,虛地址到線線地址的映射保持原值不變.
因此,LINUX核心的頁式映射,可以直接將線性地址當作虛擬位址.二者完全一致.
3.3)頁式映射
3.3.1)頁式映射的概念:
1)在I386 CPU中頁式儲存的基本思路是:通過頁面目錄和頁面表分兩個層次實現從線性地址到物理地址的映射.
2)在LINUX中要考慮到各種不同的CPU,它以一種假想的,虛擬CPU和MMU為基礎,設計出一種通用的模型,再把它分別落實到各種具體的CPU上.
因此,LINUX核心的映射機制設計成三層,在頁面目錄和頁面表中間增設了一層"中間目錄".
邏輯上的三層映射對於i386 CPU和MMU就變成了二層映射,把中間目錄PMD這一層跳過了,但是軟體的結構卻還保持著三層映射的架構.
3)頁面目錄稱為PGD,中間目錄稱為PMD,頁面表則稱為PT.PT的表項則稱為PTE.
頁面目錄,中間目錄,頁目表三者均為數組.
4)邏輯上把線性地址分成4個段位,分別用在頁面目錄PGD的位移,中間目錄PMD中的位移,頁表PT中的位移以及物理頁面內的位移,而如果是I386的CPU則沒有中間目錄.
也就是被分成3個段位,分別是頁面目錄PGD的位移,頁表pt中的位移以及物理頁內的位移量.
5)每個進程都有自己的頁目錄表和頁表,進程的切換就是將當前進程的頁目錄表起始地址儲存到CR3寄存器.
3.3.2)線性地址到物理地址的映射:
1)將一個進程的頁面目錄起始地址裝入寄存器CR3.
2)用線性地址的第1個段位即PGD的位移,找到頁面表的物理地址.頁目錄表的大小為4k,剛好一個頁的大小,包含1024項,每項4個位元組(32位)
3)用線性地址的第2個段位即PT的位移,找到表項,頁面表的大小也是4k,同樣包含1024項,每項4個位元組(32位)
4)得到表項的高20位+低12位0組成,這個高20就是物理地址的高20位,再加上線性地址的第3段位即12位的位移就得到了最終的物理地址.
3.3.3)用執行個體來說明映射的過程:
第一步:通過頁目錄表找到頁面表
還是以上面的程式為例:
hello程式執行後,調用函數grreeting,這裡的虛擬位址也就是線性地址為0x08048354
call 08048354 <greeting>
分解後的結果是:
0000 1000 0000 0100 1000 0011 0101 0100
第1個段位(高10位):
0000 1000 00
對映十進位的32,也就是在頁目錄表的位移32找到其頁面表的物理地址,也就是頁面表的指標,它的低12位是0,因為頁面表是4KB大小,所以肯定是邊界對齊了.
第二步:通過頁面表找到頁的起始物理地址高
接下來是線性地址的第二個段位(中間10位):
00 0100 10 00
對映十進位的72,也就是在剛才找到的頁面表的位移72找到目標頁的起始物理地址,高20位有效地址,低12位填充為0.
第三步:得到最終的物理地址
通過找到的頁起始物理地址,加上線性地址的第三個段位的位移地址得到最終的物理地址.
例如:
第三個段位:0011 0101 0100
對映16進位為0x354
如果目標頁的起始物理地址為:0x740000,那麼最終的物理地址就是:
0x740000+0x354=0x740354
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/wishfly/archive/2010/05/21/5613931.aspx