深入理解Linux内存映射机制
生活随笔
收集整理的這篇文章主要介紹了
深入理解Linux内存映射机制
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
Author: wzt
EMail: [email]wzt@xsec.org[/email]
Site: [url]http://www.xsec.org[/url]
Date: 2008-6-13
?
一. 緒 論
二. X86的硬件尋址方法
三. 內核對頁表的設置
四. 實例分析映射機制
一. 緒 論
我們經常在程序的反匯編代碼中看到一些類似0x32118965這樣的地址,操作系統中稱為
線性地址,或虛擬地址。虛擬地址有什么用?虛擬地址
又是如何轉換為物理內存地址的呢?本章將對此作一個簡要闡述。
1.1 Linux內存尋址概述
現代意義上的操作系統都處于32位保護模式下。每個進程一般都能尋址4G的物理空間。
但是我們的物理內存一般都是幾百M,進程怎么能獲得4G
的物理空間呢?這就是使用了虛擬地址的好處,通常我們使用一種叫做虛擬內存的技術
來實現,因為可以使用硬盤中的一部分來當作內存使用
。例外一點現在操作系統都劃分為系統空間和用戶空間,使用虛擬地址可以很好的保護
內核空間被用戶空間破壞。
對于虛擬地址如何轉為物理地址,這個轉換過程有操作系統和CPU共同完成. 操作系統為
CPU設置好頁表。CPU通過MMU單元進行地址轉換。
1.2 瀏覽內核代碼的工具
現在的內核都很大, 因此我們需要某種工具來閱讀龐大的源代碼體系,現在的內核開
發工具都選用vim+ctag+cscope瀏覽內核代碼,網上已有
現成的makefile文件用來生成ctags/cscope/etags。
一、用法:
????找一個空目錄,把附件Makefile拷貝進去。然后在該目錄中選擇性地運行如下make
命令:
$ make
將處理/usr/src/linux下的源文件,在當前目錄生成ctags, cscope
???注:SRCDIR用來指定內核源代碼目錄,如果沒有指定,則缺省為/usr/src/linux/
1) 只創建ctags
$ make SRCDIR=/usr/src/linux-2.6.12/ tags
2) 只創建cscope
$ make SRCDIR=/usr/src/linux-2.6.12/ cscope
3) 創建ctags和cscope
$ make SRCDIR=/usr/src/linux-2.6.12/
4) 只創建etags
$ make SRCDIR=/usr/src/linux-2.6.12/ TAGS
二、處理時包括的內核源文件:
1) 不包括drivers,sound目錄
2) 不包括無關的體系結構目錄
3) fs目錄只包括頂層目錄和ext2,proc目錄
三、最簡單的ctags命令
1) 進入
進入vim后,用
:tag func_name
跳到函數func_name
2) 看函數(identifier)
想進入光標所在的函數,用
CTRL + ]
3) 回退
回退用 CTRL + T
1.3 內核版本的選取
本次論文分析, 我選取的是linux-2.6.10版本的內核。最新的內核代碼為2.6.25。但
是現在主流的服務器都使用的是RedHat AS4的機器,它使
用2.6.9的內核。我選取2.6.10是因為它很接近2.6.9,現在紅帽企業Linux 4以Linux2.
6.9內核為基礎,是最穩定、最強大的商業產品。在2004
年期間,Fedora等開源項目為Linux 2.6內核技術的更加成熟提供了一個環境,這使得
紅帽企業 Linux v.4內核可以提供比以前版本更多更好的
功能和算法,具體包括:
* 通用的邏輯CPU調度程序:處理多內核和超線程CPU。
* 基于對象的逆向映射虛擬內存:提高了內存受限系統的性能。
* 讀復制更新:針對操作系統數據結構的SMP算法優化。
* 多I/O調度程序:可根據應用環境進行選擇。
* 增強的SMP和NUMA支持:提高了大型服務器的性能和可擴展性。
* 網絡中斷緩和(NAPI):提高了大流量網絡的性能。
Linux 2.6 內核使用了許多技術來改進對大量內存的使用,使得 Linux 比以往任何時
候都更適用于企業。包括反向映射(reverse mapping)
、使用更大的內存頁、頁表條目存儲在高端內存中,以及更穩定的管理器。因此,我選
取linux-2.6.10內核版本作為分析對象。
?
二. X86的硬件尋址方法
請參考Intel x86手冊^_^
?
三. 內核對頁表的設置
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把它轉換成對應的無符號整數
????#define pte_val(x) ((x).pte_low)
????#define pmd_val(x) ((x).pmd)
????#define pgd_val(x) ((x).pgd)
????#define pgprot_val(x) ((x).pgprot)
????把內核空間的線性地址轉換為物理地址
????#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
????把物理地址轉化為線性地址
????#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
???
????x是頁表項值, 通過pte_pfn得到其對應的物理頁框號, 最后通過pfn_to_page得
到對應的物理頁描述符
????#define pte_page(x) pfn_to_page(pte_pfn(x))
????如果對應的表項值為0, 返回1
????#define pte_none(x) (!(x).pte_low)
????x是頁表項值, 右移12位后得到其對應的物理頁框號
????#define pte_pfn(x) ((unsigned long)(((x).pte_low >>
PAGE_SHIFT)))
根據頁框號和頁表項的屬性值合并成一個頁表項值
????#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) |
pgprot_val(prot))
????根據頁框號和頁表項的屬性值合并成一個中間表項值
????#define pfn_pmd(pfn, prot) __pmd(((pfn) << PAGE_SHIFT) |
pgprot_val(prot))
????向一個表項中寫入指定的值
????#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
????#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)
????#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
????#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)
????根據線性地址得到高10位值, 也就是在目錄表中的索引
????#define pgd_index(address) (((address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1))
????根據頁描述符和屬性得到一個頁表項值
????#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
3.2內核頁表的初始化
內核在進入保護模式前, 還沒有啟用分頁功能, 在這之前內核要先建立一個臨時內核
頁表,因為在進入保護模式后, 內核繼續初始化直到建
立完整的內存映射機制之前, 仍然需要用到頁表來映射相應的內存地址。 臨時頁表的
初始化是在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
????????leal 0x007(%edi),%ecx
????????Movl %ecx,(%edx)
????????movl %ecx,page_pde_offset(%edx)
????????addl $4,%edx
????????movl $1024, %ecx
11:
????????stosl addl $0x1000,%eax
????????loop 11b
????????/* 內核到底要建立多少頁表, 也就是要映射多少內存空間, 取決于這個判
斷條件。在內核初始化程中內核只要保證能映射到包括內
核的代碼段,數據段, 初始頁表和用于存放動態數據結構的128k大小的空間就行 */
????????leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
????????cmpl %ebp,%eax
????????jb 10b
????????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 /* Clear prefetch and normalize %eip */
1:
????????lss stack_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選項的支持。
static void __init pagetable_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模式相關的
代碼:
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
????????unsigned long pfn;
????????pgd_t *pgd;
????????pmd_t *pmd;
????????pte_t *pte;
????????int pgd_idx, pmd_idx, pte_ofs;
????????pgd_idx = pgd_index(PAGE_OFFSET);
????????pgd = pgd_base + pgd_idx;
????????pfn = 0;
????????for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
????????????????pmd = one_md_table_init(pgd);
????????????????if (pfn >= max_low_pfn)
????????????????????????continue;
????????????????for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn <
max_low_pfn; pmd++, pmd_idx++) {
????????????????unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
????????????????……
????????????????pte = one_page_table_init(pmd);
????????????????for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn <
max_low_pfn; pte++, pfn++, 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中定義:
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{
????????pmd_t *pmd_table;
#ifdef CONFIG_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
????????return pmd_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中:
static pte_t * __init one_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();
????????return page_table;
????????}
????????return pte_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中定義:
static inline int is_kernel_text(unsigned long addr)
{
????????if (addr >= (unsigned long)_stext && addr <= (unsigned
long)__init_end)
????????????????return 1;
????????return 0;
}
_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);
#define load_cr3(pgdir) \
????????asm volatile("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@localhost temp]# cat test.c
#include <stdio.h>
void test(void)
{
????????printf("hello, world.\n");
}
int main(void)
{
????????test();
}
這段代碼很簡單, 我們故意要main調用test函數, 就是想看下test函數的虛擬地址是
如何映射成物理地址的。
4.2 段式映射分析
我們先編譯, 在反匯編下test文件
[root@localhost temp]# gcc -o test test.c
[root@localhost temp]# objdump -d test
08048368 <test>:
?8048368: 55 push %ebp
?8048369: 89 e5 mov %esp,%ebp
?804836b: 83 ec 08 sub $0x8,%esp
?804836e: 83 ec 0c sub $0xc,%esp
?8048371: 68 84 84 04 08 push $0x8048484
?8048376: e8 35 ff ff ff call 80482b0 <printf@plt>
?804837b: 83 c4 10 add $0x10,%esp
?804837e: c9 leave
?804837f: c3 ret
08048380 <main>:
?8048380: 55 push %ebp
?8048381: 89 e5 mov %esp,%ebp
?8048383: 83 ec 08 sub $0x8,%esp
?8048386: 83 e4 f0 and $0xfffffff0,%esp
?8048389: b8 00 00 00 00 mov $0x0,%eax
?804838e: 83 c0 0f add $0xf,%eax
?8048391: 83 c0 0f add $0xf,%eax
?8048394: c1 e8 04 shr $0x4,%eax
?8048397: c1 e0 04 shl $0x4,%eax
?804839a: 29 c4 sub %eax,%esp
?804839c: e8 c7 ff ff ff call 8048368 <test>
?80483a1: c9 leave
?80483a2: c3 ret
?80483a3: 90 nop
從上述結果可以看到, ld給test()函數分配的地址為0x08048368.在elf格式的可執行文
件代碼中,ld的實際位置總是從0x8000000開始安排程序
的代碼段, 對每個程序都是這樣。至于程序在執行時在物理內存中的實際位置就要由
內核在為其建立內存映射時臨時做出安排, 具體地址則
取決于當時所分配到的物理內存頁面。假設該程序已經運行, 整個映射機制都已經建
立好, 并且CPU正在執行main()中的call 8048368這條指
令, 要轉移到虛擬地址0x08048368去運行. 下面將詳細介紹這個虛擬地址轉換為物理
地址的映射過程.
首先是段式映射階段。由于0x08048368是一個程序的入口,更重要的是在執行的過程中
是由CPU中的指令計數器EIP所指向的, 所以在代碼段中
。 因此, i386CPU使用代碼段寄存器CS的當前值作為段式映射的選擇子, 也就是用它
作為在段描述表的下標.那么CS的值是多少呢?
用GDB調試下test:
?(gdb) info reg
eax 0x10 16
ecx 0x1 1
edx 0x9d915c 10326364
ebx 0x9d6ff4 10317812
esp 0xbfedb480 0xbfedb480
ebp 0xbfedb488 0xbfedb488
esi 0xbfedb534 -1074940620
edi 0xbfedb4c0 -1074940736
eip 0x804836e 0x804836e
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
可以看到CS的值為0x73, 我們把它分解成二進制:
0000 0000 0111 0011
最低2位為3, 說明RPL的值為3, 應為我們這個程序本省就是在用戶空間,RPL的值自
然為3.
第3位為0表示這個下標在GDT中。
高13位為14, 所以段描述符在GDT表的第14個表項中, 我們可以到內核代碼中去驗證
下:
在i386/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
可以看到段描述符的確就是GDT表的第14個表項中。
我們去GDT表看看具體的表項值是什么, GDT的內容在arch/i386/kernel/head.S中定
義:
ENTRY(cpu_gdt_table)
????????.quad 0x0000000000000000 /* NULL descriptor */
????????.quad 0x0000000000000000 /* 0x0b reserved */
????????.quad 0x0000000000000000 /* 0x13 reserved */
????????.quad 0x0000000000000000 /* 0x1b reserved */
????????.quad 0x0000000000000000 /* 0x20 unused */
????????.quad 0x0000000000000000 /* 0x28 unused */
????????.quad 0x0000000000000000 /* 0x33 TLS entry 1 */
????????.quad 0x0000000000000000 /* 0x3b TLS entry 2 */
????????.quad 0x0000000000000000 /* 0x43 TLS entry 3 */
????????.quad 0x0000000000000000 /* 0x4b reserved */
????????.quad 0x0000000000000000 /* 0x53 reserved */
????????.quad 0x0000000000000000 /* 0x5b reserved */
????????.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000
*/
????????.quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000
*/
????????.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000
*/
????????.quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000
*/
????????.quad 0x0000000000000000 /* 0x80 TSS descriptor */
????????.quad 0x0000000000000000 /* 0x88 LDT descriptor */
?????????/* Segments used for calling PnP BIOS */
????????.quad 0x00c09a0000000000 /* 0x90 32-bit code */
????????.quad 0x00809a0000000000 /* 0x98 16-bit code */
????????.quad 0x0080920000000000 /* 0xa0 16-bit data */
????????.quad 0x0080920000000000 /* 0xa8 16-bit data */
????????.quad 0x0080920000000000 /* 0xb0 16-bit data */
?????????/*
?????????* The APM segments have byte granularity and their bases
?????????* and limits are set at run time.
?????????*/
????????.quad 0x00409a0000000000 /* 0xb8 APM CS code */
????????.quad 0x00009a0000000000 /* 0xc0 APM CS 16 code (16 bit) */
????????.quad 0x0040920000000000 /* 0xc8 APM DS data */
????????.quad 0x0000000000000000 /* 0xd0 - unused */
????????.quad 0x0000000000000000 /* 0xd8 - unused */
????????.quad 0x0000000000000000 /* 0xe0 - unused */
????????.quad 0x0000000000000000 /* 0xe8 - unused */
????????.quad 0x0000000000000000 /* 0xf0 - unused */
????????.quad 0x0000000000000000 /* 0xf8 - GDT entry 31: double-fault TSS
*/
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
我們把這個值展開成二進制:
0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111
1111
根據上述對段描述符表項值的描述, 可以得出如下結論:
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展開成二進制:
0000 1000 0000 0100 1000 0011 0110 1000
對照線性地址的格式,可見最高10位為二進制的0000 1000 00, 也就是十進制的32,所
以MMU就以32為下標在其頁面目錄中找到其目錄項。這個
目錄項的高20位指向一個頁面表,CPU在這20位后添上12個0就得到頁面表的指針。找到
頁面表以后, CPU再來看線性地址中的中間10位,
0001001000,即十進制的72.于是CPU就以此為下標在頁表中找相應的表項。表項值的高
20位指向一個物理內存頁面,在后邊添上12個0就得到物
理頁面的開始地址。假設物理地址在0x620000的,線性地址的最低12位為0x368. 那么
test()函數的入口地址就為0x620000+0x368 = 0x620368
。
?
?
?
[Ph4nt0m] <[url]http://www.ph4nt0m.org/[/url]>
[Ph4nt0m Security Team]
???????????????????<[url]http://blog.ph4nt0m.org/[/url]> 刺@ph4nt0m
??????????Email: [email]axis@ph4nt0m.org[/email]
??????????PingMe:
<[url]http://cn.pingme.messenger.yahoo.com/webchat/ajax_webchat.php?yid=hanqin_wu[/url]
hq&sig=9ae1bbb1ae99009d8859e88e899ab2d1c2a17724>
??????????=== V3ry G00d, V3ry Str0ng ===
??????????=== Ultim4te H4cking ===
??????????=== XPLOITZ ! ===
??????????=== #_# ===
#If you brave,there is nothing you cannot achieve.#
EMail: [email]wzt@xsec.org[/email]
Site: [url]http://www.xsec.org[/url]
Date: 2008-6-13
?
一. 緒 論
二. X86的硬件尋址方法
三. 內核對頁表的設置
四. 實例分析映射機制
一. 緒 論
我們經常在程序的反匯編代碼中看到一些類似0x32118965這樣的地址,操作系統中稱為
線性地址,或虛擬地址。虛擬地址有什么用?虛擬地址
又是如何轉換為物理內存地址的呢?本章將對此作一個簡要闡述。
1.1 Linux內存尋址概述
現代意義上的操作系統都處于32位保護模式下。每個進程一般都能尋址4G的物理空間。
但是我們的物理內存一般都是幾百M,進程怎么能獲得4G
的物理空間呢?這就是使用了虛擬地址的好處,通常我們使用一種叫做虛擬內存的技術
來實現,因為可以使用硬盤中的一部分來當作內存使用
。例外一點現在操作系統都劃分為系統空間和用戶空間,使用虛擬地址可以很好的保護
內核空間被用戶空間破壞。
對于虛擬地址如何轉為物理地址,這個轉換過程有操作系統和CPU共同完成. 操作系統為
CPU設置好頁表。CPU通過MMU單元進行地址轉換。
1.2 瀏覽內核代碼的工具
現在的內核都很大, 因此我們需要某種工具來閱讀龐大的源代碼體系,現在的內核開
發工具都選用vim+ctag+cscope瀏覽內核代碼,網上已有
現成的makefile文件用來生成ctags/cscope/etags。
一、用法:
????找一個空目錄,把附件Makefile拷貝進去。然后在該目錄中選擇性地運行如下make
命令:
$ make
將處理/usr/src/linux下的源文件,在當前目錄生成ctags, cscope
???注:SRCDIR用來指定內核源代碼目錄,如果沒有指定,則缺省為/usr/src/linux/
1) 只創建ctags
$ make SRCDIR=/usr/src/linux-2.6.12/ tags
2) 只創建cscope
$ make SRCDIR=/usr/src/linux-2.6.12/ cscope
3) 創建ctags和cscope
$ make SRCDIR=/usr/src/linux-2.6.12/
4) 只創建etags
$ make SRCDIR=/usr/src/linux-2.6.12/ TAGS
二、處理時包括的內核源文件:
1) 不包括drivers,sound目錄
2) 不包括無關的體系結構目錄
3) fs目錄只包括頂層目錄和ext2,proc目錄
三、最簡單的ctags命令
1) 進入
進入vim后,用
:tag func_name
跳到函數func_name
2) 看函數(identifier)
想進入光標所在的函數,用
CTRL + ]
3) 回退
回退用 CTRL + T
1.3 內核版本的選取
本次論文分析, 我選取的是linux-2.6.10版本的內核。最新的內核代碼為2.6.25。但
是現在主流的服務器都使用的是RedHat AS4的機器,它使
用2.6.9的內核。我選取2.6.10是因為它很接近2.6.9,現在紅帽企業Linux 4以Linux2.
6.9內核為基礎,是最穩定、最強大的商業產品。在2004
年期間,Fedora等開源項目為Linux 2.6內核技術的更加成熟提供了一個環境,這使得
紅帽企業 Linux v.4內核可以提供比以前版本更多更好的
功能和算法,具體包括:
* 通用的邏輯CPU調度程序:處理多內核和超線程CPU。
* 基于對象的逆向映射虛擬內存:提高了內存受限系統的性能。
* 讀復制更新:針對操作系統數據結構的SMP算法優化。
* 多I/O調度程序:可根據應用環境進行選擇。
* 增強的SMP和NUMA支持:提高了大型服務器的性能和可擴展性。
* 網絡中斷緩和(NAPI):提高了大流量網絡的性能。
Linux 2.6 內核使用了許多技術來改進對大量內存的使用,使得 Linux 比以往任何時
候都更適用于企業。包括反向映射(reverse mapping)
、使用更大的內存頁、頁表條目存儲在高端內存中,以及更穩定的管理器。因此,我選
取linux-2.6.10內核版本作為分析對象。
?
二. X86的硬件尋址方法
請參考Intel x86手冊^_^
?
三. 內核對頁表的設置
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把它轉換成對應的無符號整數
????#define pte_val(x) ((x).pte_low)
????#define pmd_val(x) ((x).pmd)
????#define pgd_val(x) ((x).pgd)
????#define pgprot_val(x) ((x).pgprot)
????把內核空間的線性地址轉換為物理地址
????#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
????把物理地址轉化為線性地址
????#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
???
????x是頁表項值, 通過pte_pfn得到其對應的物理頁框號, 最后通過pfn_to_page得
到對應的物理頁描述符
????#define pte_page(x) pfn_to_page(pte_pfn(x))
????如果對應的表項值為0, 返回1
????#define pte_none(x) (!(x).pte_low)
????x是頁表項值, 右移12位后得到其對應的物理頁框號
????#define pte_pfn(x) ((unsigned long)(((x).pte_low >>
PAGE_SHIFT)))
根據頁框號和頁表項的屬性值合并成一個頁表項值
????#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) |
pgprot_val(prot))
????根據頁框號和頁表項的屬性值合并成一個中間表項值
????#define pfn_pmd(pfn, prot) __pmd(((pfn) << PAGE_SHIFT) |
pgprot_val(prot))
????向一個表項中寫入指定的值
????#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
????#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)
????#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
????#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)
????根據線性地址得到高10位值, 也就是在目錄表中的索引
????#define pgd_index(address) (((address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1))
????根據頁描述符和屬性得到一個頁表項值
????#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
3.2內核頁表的初始化
內核在進入保護模式前, 還沒有啟用分頁功能, 在這之前內核要先建立一個臨時內核
頁表,因為在進入保護模式后, 內核繼續初始化直到建
立完整的內存映射機制之前, 仍然需要用到頁表來映射相應的內存地址。 臨時頁表的
初始化是在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
????????leal 0x007(%edi),%ecx
????????Movl %ecx,(%edx)
????????movl %ecx,page_pde_offset(%edx)
????????addl $4,%edx
????????movl $1024, %ecx
11:
????????stosl addl $0x1000,%eax
????????loop 11b
????????/* 內核到底要建立多少頁表, 也就是要映射多少內存空間, 取決于這個判
斷條件。在內核初始化程中內核只要保證能映射到包括內
核的代碼段,數據段, 初始頁表和用于存放動態數據結構的128k大小的空間就行 */
????????leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
????????cmpl %ebp,%eax
????????jb 10b
????????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 /* Clear prefetch and normalize %eip */
1:
????????lss stack_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選項的支持。
static void __init pagetable_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模式相關的
代碼:
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
????????unsigned long pfn;
????????pgd_t *pgd;
????????pmd_t *pmd;
????????pte_t *pte;
????????int pgd_idx, pmd_idx, pte_ofs;
????????pgd_idx = pgd_index(PAGE_OFFSET);
????????pgd = pgd_base + pgd_idx;
????????pfn = 0;
????????for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
????????????????pmd = one_md_table_init(pgd);
????????????????if (pfn >= max_low_pfn)
????????????????????????continue;
????????????????for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn <
max_low_pfn; pmd++, pmd_idx++) {
????????????????unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
????????????????……
????????????????pte = one_page_table_init(pmd);
????????????????for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn <
max_low_pfn; pte++, pfn++, 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中定義:
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{
????????pmd_t *pmd_table;
#ifdef CONFIG_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
????????return pmd_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中:
static pte_t * __init one_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();
????????return page_table;
????????}
????????return pte_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中定義:
static inline int is_kernel_text(unsigned long addr)
{
????????if (addr >= (unsigned long)_stext && addr <= (unsigned
long)__init_end)
????????????????return 1;
????????return 0;
}
_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);
#define load_cr3(pgdir) \
????????asm volatile("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@localhost temp]# cat test.c
#include <stdio.h>
void test(void)
{
????????printf("hello, world.\n");
}
int main(void)
{
????????test();
}
這段代碼很簡單, 我們故意要main調用test函數, 就是想看下test函數的虛擬地址是
如何映射成物理地址的。
4.2 段式映射分析
我們先編譯, 在反匯編下test文件
[root@localhost temp]# gcc -o test test.c
[root@localhost temp]# objdump -d test
08048368 <test>:
?8048368: 55 push %ebp
?8048369: 89 e5 mov %esp,%ebp
?804836b: 83 ec 08 sub $0x8,%esp
?804836e: 83 ec 0c sub $0xc,%esp
?8048371: 68 84 84 04 08 push $0x8048484
?8048376: e8 35 ff ff ff call 80482b0 <printf@plt>
?804837b: 83 c4 10 add $0x10,%esp
?804837e: c9 leave
?804837f: c3 ret
08048380 <main>:
?8048380: 55 push %ebp
?8048381: 89 e5 mov %esp,%ebp
?8048383: 83 ec 08 sub $0x8,%esp
?8048386: 83 e4 f0 and $0xfffffff0,%esp
?8048389: b8 00 00 00 00 mov $0x0,%eax
?804838e: 83 c0 0f add $0xf,%eax
?8048391: 83 c0 0f add $0xf,%eax
?8048394: c1 e8 04 shr $0x4,%eax
?8048397: c1 e0 04 shl $0x4,%eax
?804839a: 29 c4 sub %eax,%esp
?804839c: e8 c7 ff ff ff call 8048368 <test>
?80483a1: c9 leave
?80483a2: c3 ret
?80483a3: 90 nop
從上述結果可以看到, ld給test()函數分配的地址為0x08048368.在elf格式的可執行文
件代碼中,ld的實際位置總是從0x8000000開始安排程序
的代碼段, 對每個程序都是這樣。至于程序在執行時在物理內存中的實際位置就要由
內核在為其建立內存映射時臨時做出安排, 具體地址則
取決于當時所分配到的物理內存頁面。假設該程序已經運行, 整個映射機制都已經建
立好, 并且CPU正在執行main()中的call 8048368這條指
令, 要轉移到虛擬地址0x08048368去運行. 下面將詳細介紹這個虛擬地址轉換為物理
地址的映射過程.
首先是段式映射階段。由于0x08048368是一個程序的入口,更重要的是在執行的過程中
是由CPU中的指令計數器EIP所指向的, 所以在代碼段中
。 因此, i386CPU使用代碼段寄存器CS的當前值作為段式映射的選擇子, 也就是用它
作為在段描述表的下標.那么CS的值是多少呢?
用GDB調試下test:
?(gdb) info reg
eax 0x10 16
ecx 0x1 1
edx 0x9d915c 10326364
ebx 0x9d6ff4 10317812
esp 0xbfedb480 0xbfedb480
ebp 0xbfedb488 0xbfedb488
esi 0xbfedb534 -1074940620
edi 0xbfedb4c0 -1074940736
eip 0x804836e 0x804836e
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
可以看到CS的值為0x73, 我們把它分解成二進制:
0000 0000 0111 0011
最低2位為3, 說明RPL的值為3, 應為我們這個程序本省就是在用戶空間,RPL的值自
然為3.
第3位為0表示這個下標在GDT中。
高13位為14, 所以段描述符在GDT表的第14個表項中, 我們可以到內核代碼中去驗證
下:
在i386/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
可以看到段描述符的確就是GDT表的第14個表項中。
我們去GDT表看看具體的表項值是什么, GDT的內容在arch/i386/kernel/head.S中定
義:
ENTRY(cpu_gdt_table)
????????.quad 0x0000000000000000 /* NULL descriptor */
????????.quad 0x0000000000000000 /* 0x0b reserved */
????????.quad 0x0000000000000000 /* 0x13 reserved */
????????.quad 0x0000000000000000 /* 0x1b reserved */
????????.quad 0x0000000000000000 /* 0x20 unused */
????????.quad 0x0000000000000000 /* 0x28 unused */
????????.quad 0x0000000000000000 /* 0x33 TLS entry 1 */
????????.quad 0x0000000000000000 /* 0x3b TLS entry 2 */
????????.quad 0x0000000000000000 /* 0x43 TLS entry 3 */
????????.quad 0x0000000000000000 /* 0x4b reserved */
????????.quad 0x0000000000000000 /* 0x53 reserved */
????????.quad 0x0000000000000000 /* 0x5b reserved */
????????.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000
*/
????????.quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000
*/
????????.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000
*/
????????.quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000
*/
????????.quad 0x0000000000000000 /* 0x80 TSS descriptor */
????????.quad 0x0000000000000000 /* 0x88 LDT descriptor */
?????????/* Segments used for calling PnP BIOS */
????????.quad 0x00c09a0000000000 /* 0x90 32-bit code */
????????.quad 0x00809a0000000000 /* 0x98 16-bit code */
????????.quad 0x0080920000000000 /* 0xa0 16-bit data */
????????.quad 0x0080920000000000 /* 0xa8 16-bit data */
????????.quad 0x0080920000000000 /* 0xb0 16-bit data */
?????????/*
?????????* The APM segments have byte granularity and their bases
?????????* and limits are set at run time.
?????????*/
????????.quad 0x00409a0000000000 /* 0xb8 APM CS code */
????????.quad 0x00009a0000000000 /* 0xc0 APM CS 16 code (16 bit) */
????????.quad 0x0040920000000000 /* 0xc8 APM DS data */
????????.quad 0x0000000000000000 /* 0xd0 - unused */
????????.quad 0x0000000000000000 /* 0xd8 - unused */
????????.quad 0x0000000000000000 /* 0xe0 - unused */
????????.quad 0x0000000000000000 /* 0xe8 - unused */
????????.quad 0x0000000000000000 /* 0xf0 - unused */
????????.quad 0x0000000000000000 /* 0xf8 - GDT entry 31: double-fault TSS
*/
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
我們把這個值展開成二進制:
0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111
1111
根據上述對段描述符表項值的描述, 可以得出如下結論:
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展開成二進制:
0000 1000 0000 0100 1000 0011 0110 1000
對照線性地址的格式,可見最高10位為二進制的0000 1000 00, 也就是十進制的32,所
以MMU就以32為下標在其頁面目錄中找到其目錄項。這個
目錄項的高20位指向一個頁面表,CPU在這20位后添上12個0就得到頁面表的指針。找到
頁面表以后, CPU再來看線性地址中的中間10位,
0001001000,即十進制的72.于是CPU就以此為下標在頁表中找相應的表項。表項值的高
20位指向一個物理內存頁面,在后邊添上12個0就得到物
理頁面的開始地址。假設物理地址在0x620000的,線性地址的最低12位為0x368. 那么
test()函數的入口地址就為0x620000+0x368 = 0x620368
。
?
?
?
[Ph4nt0m] <[url]http://www.ph4nt0m.org/[/url]>
[Ph4nt0m Security Team]
???????????????????<[url]http://blog.ph4nt0m.org/[/url]> 刺@ph4nt0m
??????????Email: [email]axis@ph4nt0m.org[/email]
??????????PingMe:
<[url]http://cn.pingme.messenger.yahoo.com/webchat/ajax_webchat.php?yid=hanqin_wu[/url]
hq&sig=9ae1bbb1ae99009d8859e88e899ab2d1c2a17724>
??????????=== V3ry G00d, V3ry Str0ng ===
??????????=== Ultim4te H4cking ===
??????????=== XPLOITZ ! ===
??????????=== #_# ===
#If you brave,there is nothing you cannot achieve.#
轉載于:https://blog.51cto.com/netwalk/82652
總結
以上是生活随笔為你收集整理的深入理解Linux内存映射机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php 参数 只用一次,php中,用函数
- 下一篇: python类的编写模板_python开