簡介
我們經常在程式的反彙編代碼中看到一些類似0x32118965這樣的地址,作業系統中稱為線性地址,或虛擬地址。虛擬地址有什麼用?虛擬地址
又是如何轉換為物理記憶體地址的呢?本章將對此作一個簡要闡述。
Linux記憶體定址概述
現代意義上的作業系統都處於32位保護模式下。每個進程一般都能定址4G的物理空間。但是我們的物理記憶體一般都是幾百M,進程怎么能獲得4G的物理空間呢?這就是使用了虛擬地址的好處,通常我們使用一種叫做虛擬記憶體的技術來實現,因為可以使用硬碟中的一部分來當作記憶體使用。
另外一點現在作業系統都劃分為系統空間和用戶空間,使用虛擬地址可以很好的保護核心空間不被用戶空間破壞。
對於虛擬地址如何轉為物理地址,這個轉換過程有作業系統和CPU共同完成.作業系統為CPU設定好頁表。CPU通過MMU單元進行地址轉換。
瀏覽核心代碼的工具
現在的核心都很大,因此我們需要某種工具來閱讀龐大的原始碼體系,現在的核心開發工具都選用vim+ctag+cscope瀏覽核心代碼,網上已有
現成的makefile檔案用來生成ctags/cscope/etags。
一、用法:
找一個空目錄,把附屬檔案Makefile拷貝進去。然後在該目錄中選擇性地運行如下make命令:
$make
將處理/usr/src/linux下的源檔案,在當前目錄生成ctags,cscope
註:SRCDIR用來指定核心原始碼目錄,如果沒有指定,則預設為/usr/src/linux/
1)只創建ctags
$makeSRCDIR=/usr/src/linux-2.6.12/tags
2)只創建cscope
$makeSRCDIR=/usr/src/linux-2.6.12/cscope
3)創建ctags和cscope
$makeSRCDIR=/usr/src/linux-2.6.12/
4)只創建etags
$makeSRCDIR=/usr/src/linux-2.6.12/TAGS
二、處理時包括的核心源檔案:
1)不包括drivers,sound目錄
2)不包括無關的體系結構目錄
3)fs目錄只包括頂層目錄和ext2,proc目錄
三、最簡單的ctags命令
1)進入
進入vim後,用
:tagfunc_name
跳到函式func_name
2)看函式(identifier)
想進入游標所在的函式,用
CTRL+]
3)回退
回退用CTRL+T
核心版本的選取
本次論文分析,我選取的是linux-2.6.10版本的核心。最新的核心代碼為2.6.25。但是現在主流的伺服器都使用的是RedHatAS4的機器,它使
用2.6.9的核心。我選取2.6.10是因為它很接近2.6.9,現在紅帽企業Linux4以Linux2.6.9核心為基礎,是最穩定、最強大的商業產品。在2004
年期間,Fedora等開源項目為Linux2.6核心技術的更加成熟提供了一個環境,這使得紅帽企業Linuxv.4核心可以提供比以前版本更多更好的
功能和算法,具體包括:
1通用的邏輯CPU調度程式:處理多核心和超執行緒CPU。
2基於對象的逆向映射虛擬記憶體:提高了記憶體受限系統的性能。
3讀複製更新:針對作業系統數據結構的SMP算法最佳化。
4多I/O調度程式:可根據套用環境進行選擇。
5增強的SMP和NUMA支持:提高了大型伺服器的性能和可擴展性。
6網路中斷緩和(NAPI):提高了大流量網路的性能。
Linux2.6核心使用了許多技術來改進對大量記憶體的使用,使得Linux比以往任何時候都更適用於企業。包括反向映射(reversemapping) 、使用更大的記憶體頁、頁表條目存儲在高端記憶體中,以及更穩定的管理器。因此,我選取linux-2.6.10核心版本作為分析對象。
核心對頁表的設定
CPU做出映射的前提是作業系統要為其準備好核心頁表,而對於頁表的設定,核心在系統啟動的初期和系統初始化完成後都分別進行了設定。
3.1與記憶體映射相關的幾個宏
這幾個宏把無符號整數轉換成對應的類型
#define__pte(x)((pte_t){(x)})
#define__pmd(x)((pmd_t){(x)})
#define__pgd(x)((pgd_t){(x)})
#define__pgprot(x)((pgprot_t){(x)})
根據x把它轉換成對應的無符號整數
#definepte_val(x)((x).pte_low)
#definepmd_val(x)((x).pmd)
#definepgd_val(x)((x).pgd)
#definepgprot_val(x)((x).pgprot)
把核心空間的線性地址轉換為物理地址
#define__pa(x)((unsignedlong)(x)-PAGE_OFFSET)
把物理地址轉化為線性地址
#define__va(x)((void*)((unsignedlong)(x)+PAGE_OFFSET))
x是頁表項值,通過pte_pfn得到其對應的物理頁框號,最後通過pfn_to_page得到對應的物理頁描述符
#definepte_page(x)pfn_to_page(pte_pfn(x))
如果對應的表項值為0,返回1
#definepte_none(x)(!(x).pte_low)
x是頁表項值,右移12位後得到其對應的物理頁框號
#definepte_pfn(x)((unsignedlong)(((x).pte_low>>PAGE_SHIFT)))
根據頁框號和頁表項的屬性值合併成一個頁表項值
#definepfn_pte(pfn,prot)__pte(((pfn)<
根據頁框號和頁表項的屬性值合併成一個中間表項值
#definepfn_pmd(pfn,prot)__pmd(((pfn)<
向一個表項中寫入指定的值
#defineset_pte(pteptr,pteval)(*(pteptr)=pteval)
#defineset_pte_atomic(pteptr,pteval)set_pte(pteptr,pteval)
#defineset_pmd(pmdptr,pmdval)(*(pmdptr)=pmdval)
#defineset_pgd(pgdptr,pgdval)(*(pgdptr)=pgdval)
根據線性地址得到高10位值,也就是在目錄表中的索引
#definepgd_index(address)(((address)>>PGDIR_SHIFT)&(PTRS_PER_PGD-1))
根據頁描述符和屬性得到一個頁表項值
#definemk_pte(page,pgprot)pfn_pte(page_to_pfn(page),(pgprot))
核心頁表的初始化
核心在進入保護模式前,還沒有啟用分頁功能,在這之前核心要先建立一個臨時核心頁表,因為在進入保護模式後,核心繼續初始化直到建
立完整的記憶體映射機制之前,仍然需要用到頁表來映射相應的記憶體地址。臨時頁表的初始化是在arch/i386/kernel/head.S中進行的:
swapper_pg_dir是臨時頁全局目錄表,它是在核心編譯過程中靜態初始化的.
pg0是第一個頁表開始的地方,它也是核心編譯過程中靜態初始化的.
核心通過以下代碼建立臨時頁表:
ENTRY(startup_32)
…………
/*得到開始目錄項的索引,從這可以看出核心是在swapper_pg_dir的768個表項開始進行建立的,其對應的線性地址就是0xc0000000以上的地
址,也就是核心在初始化它自己的頁表*/
page_pde_offset=(__PAGE_OFFSET>>20);
/*pg0地址在核心編譯的時候,已經是加上0xc0000000了,減去0xc00000000得到對應的物理地址*/
movl$(pg0-__PAGE_OFFSET),%edi
/*將目錄表的地址傳給edx,表明核心也要從0x00000000開始建立頁表,這樣可以保證從以物理地址取指令到以線性地址在系統空間取指令
的平穩過渡,下面會詳細解釋*/
movl$(swapper_pg_dir-__PAGE_OFFSET),%edx
movl$0x007,%eax
leal0x007(%edi),%ecx
Movl%ecx,(%edx)
movl%ecx,page_pde_offset(%edx)
addl$4,%edx
movl$1024,%ecx
11:
stosladdl$0x1000,%eax
loop11b
/*核心到底要建立多少頁表,也就是要映射多少記憶體空間,取決於這個判斷條件。在核心初始化程中核心只要保證能映射到包括內
核的代碼段,數據段,初始頁表和用於存放動態數據結構的128k大小的空間就行*/
leal(INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl%ebp,%eax
jb10b
movl%edi,(init_pg_tables_end-__PAGE_OFFSET)
在上述代碼中,核心為什麼要把用戶空間和核心空間的前幾個目錄項映射到相同的頁表中去呢,雖然在head.S中核心已經進入保護模式,但是
核心現在是處於保護模式的段式定址方式下,因為核心還沒有啟用分頁映射機制,現在都是以物理地址來取指令,如果代碼中遇到了符號地址
,只能減去0xc0000000才行,當開啟了映射機制後就不用了現在cpu中的取指令指針eip仍指向低區,如果只建立核心空間中的映射,那么當
核心開啟映射機制後,低區中的地址就沒辦法定址了,應為沒有對應的頁表,除非遇到某個符號地址作為絕對轉移或調用子程式為止。因此
要儘快開啟CPU的頁式映射機制.
movl$swapper_pg_dir-__PAGE_OFFSET,%eax
movl%eax,%cr3/*cr3控制暫存器保存的是目錄表地址*/
movl%cr0,%eax/*向cr0的最高位置1來開啟映射機制*/
orl$0x80000000,%eax
movl%eax,%cr0
ljmp$__BOOT_CS,$1f/*Clearprefetchandnormalize%eip*/
1:
lssstack_start,%esp
通過ljmp$__BOOT_CS,$1f這條指令使CPU進入了系統空間繼續執行因為__BOOT_CS是個符號地址,地址在0xc0000000以上。
在head.S完成了核心臨時頁表的建立後,它繼續進行初始化,包括初始化INIT_TASK,也就是系統開啟後的第一個進程;建立完整的中斷處理程
序,然後重新載入GDT描述符,最後跳轉到init/main.c中的start_kernel函式繼續初始化.
3.3核心頁表的完整建立
核心在start_kernel()中繼續做第二階段的初始化,因為在這個階段中,核心已經處於保護模式下,前面只是簡單的設定了核心頁表,核心
必須首先要建立一個完整的頁表才能繼續運行,因為記憶體定址是核心繼續運行的前提。
pagetable_init()的代碼在mm/init.c中:
[start_kernel()>setup_arch()>paging_init()>pagetable_init()]
為了簡單起見,我忽略了對PAE選項的支持。
staticvoid__initpagetable_init(void)
{
……
pgd_t*pgd_base=swapper_pg_dir;
……
kernel_physical_mapping_init(pgd_base);
……
}
在這個函式中pgd_base變數指向了swapper_pg_dir,這正是核心目錄表的開始地址,pagetable_init()函式在通過
kernel_physical_mapping_init()函式完成核心頁表的完整建立。
kernel_physical_mapping_init函式同樣在mm/init.c中,我略去了與PAE模式相關的代碼:
staticvoid__initkernel_physical_mapping_init(pgd_t*pgd_base)
{
unsignedlongpfn;
pgd_t*pgd;
pmd_t*pmd;
pte_t*pte;
intpgd_idx,pmd_idx,pte_ofs;
pgd_idx=pgd_index(PAGE_OFFSET);
pgd=pgd_base+pgd_idx;
pfn=0;
for(;pgd_idx
pmd=one_md_table_init(pgd);
if(pfn>=max_low_pfn)
continue;
for(pmd_idx=0;pmd_idx
unsignedintaddress=pfn*PAGE_SIZE+PAGE_OFFSET;
……
pte=one_page_table_init(pmd);
for(pte_ofs=0;pte_ofs
if(is_kernel_text(address))
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL_EXEC));
else
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));
……
}
}
通過作者的注釋,可以了解到這個函式的作用是把整個物理記憶體地址都映射到從核心空間的開始地址,即從0xc0000000的整個核心空間中,
直到物理記憶體映射完畢為止。這個函式比較長,而且用到很多關於記憶體管理方面的宏定義,理解了這個函式,就能大概理解核心是如何建立
頁表的,將這個抽象的模型完全的理解。下面將詳細分析這個函式:
函式開始定義了4個變數pgd_t*pgd,pmd_t*pmd,pte_t*pte,pfn;
pgd指向一個目錄項開始的地址,pmd指向一個中間目錄開始的地址,pte指向一個頁表開始的地址pfn是頁框號被初始為0.pgd_idx根據
pgd_index宏計算結果為768,也是核心要從目錄表中第768個表項開始進行設定。從768到1024這個256個表項被linux核心設定成核心目錄項,
低768個目錄項被用戶空間使用.pgd=pgd_base+pgd_idx;pgd便指向了第768個表項。
然後函式開始一個循環即開始填充從768到1024這256個目錄項的內容。
one_md_table_init()函式根據pgd找到指向的pmd表。
它同樣在mm/init.c中定義:
staticpmd_t*__initone_md_table_init(pgd_t*pgd)
{
pmd_t*pmd_table;
#ifdefCONFIG_X86_PAE
pmd_table=(pmd_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd,__pgd(__pa(pmd_table)|_PAGE_PRESENT));
if(pmd_table!=pmd_offset(pgd,0))
BUG();
#else
pmd_table=pmd_offset(pgd,0);
#endif
returnpmd_table;
}
可以看出,如果核心不啟用PAE選項,函式將通過pmd_offset返回pgd的地址。因為linux的二級映射模型,本來就是忽略pmd中間目錄表的。
接著又個判斷語句:
>>if(pfn>=max_low_pfn)
>>continue;
這個很關鍵,max_low_pfn代表著整個物理記憶體一共有多少頁框。當pfn大於max_low_pfn的時候,表明核心已經把整個物理記憶體都映射到了系
統空間中,所以剩下有沒被填充的表項就直接忽略了。因為核心已經可以映射整個物理空間了,沒必要繼續填充剩下的表項。
緊接著的第2個for循環,在linux的3級映射模型中,是要設定pmd表的,但在2級映射中忽略,只循環一次,直接進行頁表pte的設定。
>>address=pfn*PAGE_SIZE+PAGE_OFFSET;
address是個線性地址,根據上面的語句可以看出address是從0xc000000開始的,也就是從核心空間開始,後面在設定頁表項屬性的時候會用
到它.
>>pte=one_page_table_init(pmd);
根據pmd分配一個頁表,代碼同樣在mm/init.c中:
staticpte_t*__initone_page_table_init(pmd_t*pmd)
{
if(pmd_none(*pmd)){
pte_t*page_table=(pte_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pmd(pmd,__pmd(__pa(page_table)|_PAGE_TABLE));
if(page_table!=pte_offset_kernel(pmd,0))
BUG();
returnpage_table;
}
returnpte_offset_kernel(pmd,0);
}
pmd_none宏判斷pmd表是否為空,如果為空則要利用alloc_bootmem_low_pages分配一個4k大小的物理頁面。然後通過set_pmd(pmd,__pmd
(__pa(page_table)|_PAGE_TABLE));來設定pmd表項。page_table顯然屬於線性地址,先通過__pa宏轉化為物理地址,在與上_PAGE_TABLE宏,
此時它們還是無符號整數,在通過__pmd把無符號整數轉化為pmd類型,經過這些轉換,就得到了一個具有屬性的表項,然後通過set_pmd宏設
置pmd表項.
接著又是一個循環,設定1024個頁表項。
is_kernel_text函式根據前面提到的address來判斷address線性地址是否屬於核心代碼段,它同樣在mm/init.c中定義:
staticinlineintis_kernel_text(unsignedlongaddr)
{
if(addr>=(unsignedlong)_stext&&addr<=(unsignedlong)__init_end)
return1;
return0;
}
_stext,__init_end是個核心符號,在核心連結的時候生成的,分別表示核心代碼段的開始和終止地址.
如果address屬於核心代碼段,那么在設定頁表項的時候就要加個PAGE_KERNEL_EXEC屬性,如果不是,則加個PAGE_KERNEL屬性.
#define_PAGE_KERNEL_EXEC\
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED)
#define_PAGE_KERNEL\
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED|_PAGE_NX)
最後通過set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));來設定頁表項,先通過pfn_pte宏根據頁框號和頁表項的屬性值合併成一個頁表項值,
然戶在用set_pte宏把頁表項值寫到頁表項里。
當pagetable_init()函式返回後,核心已經設定好了核心頁表,緊著調用load_cr3(swapper_pg_dir);
#defineload_cr3(pgdir)\
asmvolatile("movl%0,%%cr3"::"r"(__pa(pgdir)))
將控制swapper_pg_dir送入控制暫存器cr3.每當重新設定cr3時,CPU就會將頁面映射目錄所在的頁面裝入CPU內部高速快取中的TLB部分.現
在記憶體中(實際上是高速快取中)的映射目錄變了,就要再讓CPU裝入一次。由於頁面映射機制本來就是開啟著的,所以從這條指令以後就擴大
了系統空間中有映射區域的大小,使整個映射復蓋到整個物理記憶體(高端記憶體)除外.實際上此時swapper_pg_dir中已經改變的目錄項很可能還
在高速快取中,所以還要通過__flush_tlb_all()將高速快取中的內容沖刷到記憶體中,這樣才能保證記憶體中映射目錄內容的一致性。
3.4對如何構建頁表的總結
通過上述對pagetable_init()的剖析,我們可以清晰的看到,構建核心頁表,無非就是向相應的表項寫入下一級地址和屬性。在核心空間
保留著一部分記憶體專門用來存放核心頁表.當cpu要進行定址的時候,無論在核心空間,還是在用戶空間,都會通過這個頁表來進行映射。對於
這個函式,核心把整個物理記憶體空間都映射完了,當用戶空間的進程要使用物理記憶體時,豈不是不能做相應的映射了?其實不會的,核心
只是做了映射,映射不代表使用,這樣做是核心為了方便管理記憶體而已。
實例分析映射機制
4.1示例代碼
通過前面的理論分析,我們通過編寫一個簡單的程式,來分析核心是如何把線性地址映射到物理地址的。
[root@localhosttemp]#cattest.c
#include
voidtest(void)
{
printf("hello,world.\n");
}
intmain(void)
{
test();
}
這段代碼很簡單,我們故意要main調用test函式,就是想看下test函式的虛擬地址是如何映射成物理地址的。
4.2段式映射分析
我們先編譯,在反彙編下test檔案
[root@localhosttemp]#gcc-otesttest.c
[root@localhosttemp]#objdump-dtest
08048368:
8048368:55push%ebp
8048369:89e5mov%esp,%ebp
804836b:83ec08sub$0x8,%esp
804836e:83ec0csub$0xc,%esp
8048371:6884840408push$0x8048484
8048376:e835ffffffcall80482b0
804837b:83c410add$0x10,%esp
804837e:c9leave
804837f:c3ret
08048380:
8048380:55push%ebp
8048381:89e5mov%esp,%ebp
8048383:83ec08sub$0x8,%esp
8048386:83e4f0and$0xfffffff0,%esp
8048389:b800000000mov$0x0,%eax
804838e:83c00fadd$0xf,%eax
8048391:83c00fadd$0xf,%eax
8048394:c1e804shr$0x4,%eax
8048397:c1e004shl$0x4,%eax
804839a:29c4sub%eax,%esp
804839c:e8c7ffffffcall8048368
80483a1:c9leave
80483a2:c3ret
80483a3:90nop
從上述結果可以看到,ld給test()函式分配的地址為0x08048368.在elf格式的執行檔代碼中,ld的實際位置總是從0x8000000開始安排程式
的代碼段,對每個程式都是這樣。至於程式在執行時在物理記憶體中的實際位置就要由核心在為其建立記憶體映射時臨時做出安排,具體地址則
取決於當時所分配到的物理記憶體頁面。假設該程式已經運行,整個映射機制都已經建立好,並且CPU正在執行main()中的call8048368這條指
令,要轉移到虛擬地址0x08048368去運行.下面將詳細介紹這個虛擬地址轉換為物理地址的映射過程.
首先是段式映射階段。由於0x08048368是一個程式的入口,更重要的是在執行的過程中是由CPU中的指令計數器EIP所指向的,所以在代碼段中
。因此,i386CPU使用代碼段暫存器CS的當前值作為段式映射的選擇子,也就是用它作為在段描述表的下標.那么CS的值是多少呢?
用GDB調試下test:
(gdb)inforeg
eax0x1016
ecx0x11
edx0x9d915c10326364
ebx0x9d6ff410317812
esp0xbfedb4800xbfedb480
ebp0xbfedb4880xbfedb488
esi0xbfedb534-1074940620
edi0xbfedb4c0-1074940736
eip0x804836e0x804836e
eflags0x282642
cs0x73115
ss0x7b123
ds0x7b123
es0x7b123
fs0x00
gs0x3351
可以看到CS的值為0x73,我們把它分解成二進制:
0000000001110011
最低2位為3,說明RPL的值為3,應為我們這個程式本省就是在用戶空間,RPL的值自然為3.
第3位為0表示這個下標在GDT中。
高13位為14,所以段描述符在GDT表的第14個表項中,我們可以到核心代碼中去驗證下:
在i386/asm/segment.h中:
#defineGDT_ENTRY_DEFAULT_USER_CS14
#define__USER_CS(GDT_ENTRY_DEFAULT_USER_CS*8+3)
可以看到段描述符的確就是GDT表的第14個表項中。
我們去GDT表看看具體的表項值是什麼,GDT的內容在arch/i386/kernel/head.S中定義:
ENTRY(cpu_gdt_table)
.quad0x0000000000000000/*NULLdescriptor*/
.quad0x0000000000000000/*0x0breserved*/
.quad0x0000000000000000/*0x13reserved*/
.quad0x0000000000000000/*0x1breserved*/
.quad0x0000000000000000/*0x20unused*/
.quad0x0000000000000000/*0x28unused*/
.quad0x0000000000000000/*0x33TLSentry1*/
.quad0x0000000000000000/*0x3bTLSentry2*/
.quad0x0000000000000000/*0x43TLSentry3*/
.quad0x0000000000000000/*0x4breserved*/
.quad0x0000000000000000/*0x53reserved*/
.quad0x0000000000000000/*0x5breserved*/
.quad0x00cf9a000000ffff/*0x60kernel4GBcodeat0x00000000*/
.quad0x00cf92000000ffff/*0x68kernel4GBdataat0x00000000*/
.quad0x00cffa000000ffff/*0x73user4GBcodeat0x00000000*/
.quad0x00cff2000000ffff/*0x7buser4GBdataat0x00000000*/
.quad0x0000000000000000/*0x80TSSdescriptor*/
.quad0x0000000000000000/*0x88LDTdescriptor*/
/*SegmentsusedforcallingPnPBIOS*/
.quad0x00c09a0000000000/*0x9032-bitcode*/
.quad0x00809a0000000000/*0x9816-bitcode*/
.quad0x0080920000000000/*0xa016-bitdata*/
.quad0x0080920000000000/*0xa816-bitdata*/
.quad0x0080920000000000/*0xb016-bitdata*/
/*
*TheAPMsegmentshavebytegranularityandtheirbases
*andlimitsaresetatruntime.
*/
.quad0x00409a0000000000/*0xb8APMCScode*/
.quad0x00009a0000000000/*0xc0APMCS16code(16bit)*/
.quad0x0040920000000000/*0xc8APMDSdata*/
.quad0x0000000000000000/*0xd0-unused*/
.quad0x0000000000000000/*0xd8-unused*/
.quad0x0000000000000000/*0xe0-unused*/
.quad0x0000000000000000/*0xe8-unused*/
.quad0x0000000000000000/*0xf0-unused*/
.quad0x0000000000000000/*0xf8-GDTentry31:double-faultTSS*/
.quad0x00cffa000000ffff/*0x73user4GBcodeat0x00000000*/
我們把這個值展開成二進制:
0000000011001111111110100000000000000000000000001111111111111111
根據上述對段描述符表項值的描述,可以得出如下結論:
B0-B15,B16-B31是0,表示基地址全為0.
L0-L15,L16-L19是1,表示段的上限全是0xffff.
G位是1表示段長度單位均為4KB。
D位是1表示對段的訪問都是32位指令
P位是1表示段在記憶體中。
DPL是3表示特權級是3級
S位是1表示為代碼段或數據段
type為1010表示代碼段,可讀,可執行,尚未收到訪問
這個描述符指示了段從0地址開始的整個4G虛存空間,邏輯地址直接轉換為線性地址。
所以在經過段式映射後就把邏輯地址轉換成了線性地址,這也是在linux中,為什麼邏輯地址等同於線性地址的原因了。
4.3頁式映射分析
現在進入頁式映射的過程了,Linux系統中的每個進程都有其自身的頁面目錄PGD,指向這個目錄的指針保存在每個進程的mm_struct數據結構
中。每當調度一個進程進入運行的時候,核心都要為即將運行的進程設定好控制暫存器cr3,而MMU的硬體則總是從cr3中取得指向當前頁面目
錄的指針。當我們在程式中要轉移到地址0x08048368去的時候,進程正在運行,cr3早以設定好,指向我們這個進程的頁面目錄了。先將線性
地址0x08048368展開成二進制:
00001000000001001000001101101000
對照線性地址的格式,可見最高10位為二進制的0000100000,也就是十進制的32,所以MMU就以32為下標在其頁面目錄中找到其目錄項。這個
目錄項的高20位指向一個頁面表,CPU在這20位後添上12個0就得到頁面表的指針。找到頁面表以後,CPU再來看線性地址中的中間10位,
0001001000,即十進制的72.於是CPU就以此為下標在頁表中找相應的表項。表項值的高20位指向一個物理記憶體頁面,在後邊添上12個0就得到物
理頁面的開始地址。假設物理地址在0x620000的,線性地址的最低12位為0x368.那么test()函式的入口地址就為0x620000+0x368=0x620368
。