自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux內(nèi)存代碼需要某種工具來閱讀龐大的源代碼體系

運(yùn)維 系統(tǒng)運(yùn)維
例外一點(diǎn)現(xiàn)在操作系統(tǒng)都劃分為系統(tǒng)空間和用戶空間,使用虛擬地址可以很好的保護(hù)Linux內(nèi)核空間被用戶空間破壞。對(duì)于虛擬地址如何轉(zhuǎn)為物理地址,這個(gè)轉(zhuǎn)換過程有操作系統(tǒng)和CPU共同完成. 操作系統(tǒng)為CPU設(shè)置好頁(yè)表。CPU通過MMU單元進(jìn)行地址轉(zhuǎn)換。

我們經(jīng)常在程序的反匯編代碼中看到一些類似0x32118965這樣的地址,操作系統(tǒng)中稱為線性地址,或虛擬地址。虛擬地址有什么用?虛擬地址又是如何轉(zhuǎn)換為物理內(nèi)存地址的呢?本章將對(duì)此作一個(gè)簡(jiǎn)要闡述。

1.1 Linux內(nèi)存尋址概述現(xiàn)代意義上的操作系統(tǒng)都處于32位保護(hù)模式下。每個(gè)進(jìn)程一般都能尋址4G的物理空間。但是我們的物理內(nèi)存一般都是幾百M(fèi),進(jìn)程怎么能獲得4G的物理空間呢?這就是使用了虛擬地址的好處,通常我們使用一種叫做虛擬內(nèi)存的技術(shù)來實(shí)現(xiàn),因?yàn)榭梢允褂糜脖P中的一部分來當(dāng)作內(nèi)存使用。

例外一點(diǎn)現(xiàn)在操作系統(tǒng)都劃分為系統(tǒng)空間和用戶空間,使用虛擬地址可以很好的保護(hù)Linux內(nèi)核空間被用戶空間破壞。對(duì)于虛擬地址如何轉(zhuǎn)為物理地址,這個(gè)轉(zhuǎn)換過程有操作系統(tǒng)和CPU共同完成. 操作系統(tǒng)為CPU設(shè)置好頁(yè)表。CPU通過MMU單元進(jìn)行地址轉(zhuǎn)換。

1.2 瀏覽Linux內(nèi)核代碼的工具現(xiàn)在的Linux內(nèi)核都很大, 因此我們需要某種工具來閱讀龐大的源代碼體系,現(xiàn)在的Linux內(nèi)核開發(fā)工具都選用vim+ctag+cscope瀏覽Linux內(nèi)核代碼,網(wǎng)上已有現(xiàn)成的makefile文件用來生成ctags/cscope/etags。

一、用法:找一個(gè)空目錄,把附件Makefile拷貝進(jìn)去。然后在該目錄中選擇性地運(yùn)行如下make命令:$ make將處理/usr/src/linux下的源文件,在當(dāng)前目錄生成ctags, cscope
注:SRCDIR用來指定Linux內(nèi)核源代碼目錄,如果沒有指定,則缺省為/usr/src/linux/

1) 只創(chuàng)建ctags$ make SRCDIR=/usr/src/linux-2.6.12/ tags
2) 只創(chuàng)建cscope$ make SRCDIR=/usr/src/linux-2.6.12/ cscope
3) 創(chuàng)建ctags和cscope$ make SRCDIR=/usr/src/linux-2.6.12/
4) 只創(chuàng)建etags$ make SRCDIR=/usr/src/linux-2.6.12/ TAGS

二、處理時(shí)包括的Linux內(nèi)核源文件:

1) 不包括drivers,sound目錄
2) 不包括無關(guān)的體系結(jié)構(gòu)目錄
3) fs目錄只包括頂層目錄和ext2,proc目錄

三、最簡(jiǎn)單的ctags命令

1) 進(jìn)入進(jìn)入vim后,用:tag func_name跳到函數(shù)func_name
2) 看函數(shù)(identifier)想進(jìn)入光標(biāo)所在的函數(shù),用CTRL + ]
3) 回退回退用 CTRL + T

1.3 Linux內(nèi)核版本的選取
本次論文分析, 我選取的是linux-2.6.10版本的Linux內(nèi)核。最新的Linux內(nèi)核代碼為2.6.25。但是現(xiàn)在主流的服務(wù)器都使用的是RedHat AS4的機(jī)器,它使用2.6.9的Linux內(nèi)核。我選取2.6.10是因?yàn)樗芙咏?.6.9,現(xiàn)在紅帽企業(yè)Linux 4以Linux2.6.9Linux內(nèi)核為基礎(chǔ),是最穩(wěn)定、最強(qiáng)大的商業(yè)產(chǎn)品。在2004年期間,F(xiàn)edora等開源項(xiàng)目為L(zhǎng)inux 2.6Linux內(nèi)核技術(shù)的更加成熟提供了一個(gè)環(huán)境,這使得紅帽企業(yè) Linux v.4Linux內(nèi)核可以提供比以前版本更多更好的

功能和算法,具體包括:
• 通用的邏輯CPU調(diào)度程序:處理多Linux內(nèi)核和超線程CPU。
• 基于對(duì)象的逆向映射虛擬內(nèi)存:提高了內(nèi)存受限系統(tǒng)的性能。
• 讀復(fù)制更新:針對(duì)操作系統(tǒng)數(shù)據(jù)結(jié)構(gòu)的SMP算法優(yōu)化。
• 多I/O調(diào)度程序:可根據(jù)應(yīng)用環(huán)境進(jìn)行選擇。
• 增強(qiáng)的SMP和NUMA支持:提高了大型服務(wù)器的性能和可擴(kuò)展性。
• 網(wǎng)絡(luò)中斷緩和(NAPI):提高了大流量網(wǎng)絡(luò)的性能。
Linux 2.6 Linux內(nèi)核使用了許多技術(shù)來改進(jìn)對(duì)大量?jī)?nèi)存的使用,使得 Linux 比以往任何時(shí)候都更適用于企業(yè)。包括反向映射(reverse mapping)、使用更大的內(nèi)存頁(yè)、頁(yè)表?xiàng)l目存儲(chǔ)在高端內(nèi)存中,以及更穩(wěn)定的管理器。因此,我選取linux-2.6.10Linux內(nèi)核版本作為分析對(duì)象。

二. X86的硬件尋址方法請(qǐng)參考Intel x86手冊(cè)^_^

三. Linux內(nèi)核對(duì)頁(yè)表的設(shè)置CPU做出映射的前提是操作系統(tǒng)要為其準(zhǔn)備好Linux內(nèi)核頁(yè)表,而對(duì)于頁(yè)表的設(shè)置,Linux內(nèi)核在系統(tǒng)啟動(dòng)的初期和系統(tǒng)初始化完成后都分別進(jìn)行了設(shè)置。

3.1 與內(nèi)存映射相關(guān)的幾個(gè)宏這幾個(gè)宏把無符號(hào)整數(shù)轉(zhuǎn)換成對(duì)應(yīng)的類型
#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )

根據(jù)x把它轉(zhuǎn)換成對(duì)應(yīng)的無符號(hào)整數(shù)
#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)

把Linux內(nèi)核空間的線性地址轉(zhuǎn)換為物理地址
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)

把物理地址轉(zhuǎn)化為線性地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

x是頁(yè)表項(xiàng)值, 通過pte_pfn得到其對(duì)應(yīng)的物理頁(yè)框號(hào), 最后通過pfn_to_page得到對(duì)應(yīng)的物理頁(yè)描述符
#define pte_page(x) pfn_to_page(pte_pfn(x))

如果對(duì)應(yīng)的表項(xiàng)值為0, 返回1
#define pte_none(x) (!(x).pte_low)

x是頁(yè)表項(xiàng)值, 右移12位后得到其對(duì)應(yīng)的物理頁(yè)框號(hào)
#define pte_pfn(x) ((unsigned long)(((x).pte_low >> PAGE_SHIFT)))
根據(jù)頁(yè)框號(hào)和頁(yè)表項(xiàng)的屬性值合并成一個(gè)頁(yè)表項(xiàng)值
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

根據(jù)頁(yè)框號(hào)和頁(yè)表項(xiàng)的屬性值合并成一個(gè)中間表項(xiàng)值
#define pfn_pmd(pfn, prot) __pmd(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

向一個(gè)表項(xiàng)中寫入指定的值
#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)

根據(jù)線性地址得到高10位值, 也就是在目錄表中的索引
#define pgd_index(address) (((address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1))

根據(jù)頁(yè)描述符和屬性得到一個(gè)頁(yè)表項(xiàng)值
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
3.2Linux內(nèi)核頁(yè)表的初始化
Linux內(nèi)核在進(jìn)入保護(hù)模式前, 還沒有啟用分頁(yè)功能, 在這之前Linux內(nèi)核要先建立一個(gè)臨時(shí)Linux內(nèi)核頁(yè)表,因?yàn)樵谶M(jìn)入保護(hù)模式后, Linux內(nèi)核繼續(xù)初始化直到建

立完整的內(nèi)存映射機(jī)制之前, 仍然需要用到頁(yè)表來映射相應(yīng)的內(nèi)存地址。 臨時(shí)頁(yè)表的初始化是在arch/i386/kernel/head.S中進(jìn)行的:swapper_pg_dir是臨時(shí)頁(yè)全局目錄表, 它是在Linux內(nèi)核編譯過程中靜態(tài)初始化的.pg0是第一個(gè)頁(yè)表開始的地方, 它也是Linux內(nèi)核編譯過程中靜態(tài)初始化的.Linux內(nèi)核通過以下代碼建立臨時(shí)頁(yè)表:ENTRY(startup_32)/* 得到開始目錄項(xiàng)的索引,從這可以看出Linux內(nèi)核是在swapper_pg_dir的768個(gè)表項(xiàng)開始進(jìn)行建立的, 其對(duì)應(yīng)的線性地址就是0xc0000000以上的地

址, 也就是Linux內(nèi)核在初始化它自己的頁(yè)表 */page_pde_offset = (__PAGE_OFFSET >> 20);/* pg0地址在Linux內(nèi)核編譯的時(shí)候, 已經(jīng)是加上0xc0000000了, 減去0xc00000000得到對(duì)應(yīng)的物理地址 */movl $(pg0 - __PAGE_OFFSET), %edi/* 將目錄表的地址傳給edx, 表明Linux內(nèi)核也要從0x00000000開始建立頁(yè)表, 這樣可以保證從以物理地址取指令到以線性地址在系統(tǒng)空間取指令

的平穩(wěn)過渡, 下面會(huì)詳細(xì)解釋 */
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
/* Linux內(nèi)核到底要建立多少頁(yè)表, 也就是要映射多少內(nèi)存空間, 取決于這個(gè)判斷條件。在Linux內(nèi)核初始化程中Linux內(nèi)核只要保證能映射到包括Linux內(nèi)核的代碼段,數(shù)據(jù)段, 初始頁(yè)表和用于存放動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu)的128k大小的空間就行 */leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)

在上述代碼中, Linux內(nèi)核為什么要把用戶空間和Linux內(nèi)核空間的前幾個(gè)目錄項(xiàng)映射到相同的頁(yè)表中去呢,雖然在head.S中Linux內(nèi)核已經(jīng)進(jìn)入保護(hù)模式,但是Linux內(nèi)核現(xiàn)在是處于保護(hù)模式的段式尋址方式下,因?yàn)長(zhǎng)inux內(nèi)核還沒有啟用分頁(yè)映射機(jī)制,現(xiàn)在都是以物理地址來取指令。

如果代碼中遇到了符號(hào)地址,只能減去0xc0000000才行, 當(dāng)開啟了映射機(jī)制后就不用了現(xiàn)在cpu中的取指令指針eip仍指向低區(qū),如果只建立Linux內(nèi)核空間中的映射, 那么當(dāng)Linux內(nèi)核開啟映射機(jī)制后, 低區(qū)中的地址就沒辦法尋址了,應(yīng)為沒有對(duì)應(yīng)的頁(yè)表, 除非遇到某個(gè)符號(hào)地址作為絕對(duì)轉(zhuǎn)移或調(diào)用子程序?yàn)橹?。因此要盡快開啟CPU的頁(yè)式映射機(jī)制.
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* cr3控制寄存器保存的是目錄表地址 */
movl %cr0,%eax /* 向cr0的最高位置1來開啟映射機(jī)制 */
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進(jìn)入了系統(tǒng)空間繼續(xù)執(zhí)行 因?yàn)開_BOOT_CS是個(gè)符號(hào)地址,地址在0xc0000000以上。在head.S完成了Linux內(nèi)核臨時(shí)頁(yè)表的建立后,它繼續(xù)進(jìn)行初始化,包括初始化INIT_TASK,也就是系統(tǒng)開啟后的第一個(gè)進(jìn)程;建立完整的中斷處理程序,然后重新加載GDT描述符,最后跳轉(zhuǎn)到init/main.c中的start_kernel函數(shù)繼續(xù)初始化.

3.3Linux內(nèi)核頁(yè)表的完整建立

Linux內(nèi)核在start_kernel()中繼續(xù)做第二階段的初始化,因?yàn)樵谶@個(gè)階段中, Linux內(nèi)核已經(jīng)處于保護(hù)模式下,前面只是簡(jiǎn)單的設(shè)置了Linux內(nèi)核頁(yè)表, 內(nèi)必須首先要建立一個(gè)完整的頁(yè)表才能繼續(xù)運(yùn)行,因?yàn)閮?nèi)存尋址是Linux內(nèi)核繼續(xù)運(yùn)行的前提。
pagetable_init()的代碼在mm/init.c中:
[start_kernel()>setup_arch()>paging_init()>pagetable_init()]
為了簡(jiǎn)單起見, 我忽略了對(duì)PAE選項(xiàng)的支持。
static void __init pagetable_init (void)
{
……
pgd_t *pgd_base = swapper_pg_dir;
……
kernel_physical_mapping_init(pgd_base);
……
}
在這個(gè)函數(shù)中pgd_base變量指向了swapper_pg_dir, 這正是Linux內(nèi)核目錄表的開始地址,pagetable_init()函數(shù)在通過

kernel_physical_mapping_init()函數(shù)完成Linux內(nèi)核頁(yè)表的完整建立。
kernel_physical_mapping_init函數(shù)同樣在mm/init.c中, 我略去了與PAE模式相關(guān)的代碼:
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));
……
}
}

通過作者的注釋, 可以了解到這個(gè)函數(shù)的作用是把整個(gè)物理內(nèi)存地址都映射到從Linux內(nèi)核空間的開始地址,即從0xc0000000的整個(gè)Linux內(nèi)核空間中,直到物理內(nèi)存映射完畢為止。這個(gè)函數(shù)比較長(zhǎng), 而且用到很多關(guān)于內(nèi)存管理方面的宏定義,理解了這個(gè)函數(shù), 就能大概理解Linux內(nèi)核是如何建立

頁(yè)表的,將這個(gè)抽象的模型完全的理解。 下面將詳細(xì)分析這個(gè)函數(shù):函數(shù)開始定義了4個(gè)變量pgd_t *pgd, pmd_t *pmd, pte_t *pte, pfn;pgd指向一個(gè)目錄項(xiàng)開始的地址,pmd指向一個(gè)中間目錄開始的地址,pte指向一個(gè)頁(yè)表開始的地址pfn是頁(yè)框號(hào)被初始為0. pgd_idx根據(jù)pgd_index宏計(jì)算結(jié)果為768,也是Linux內(nèi)核要從目錄表中第768個(gè)表項(xiàng)開始進(jìn)行設(shè)置。 從768到1024這個(gè)256個(gè)表項(xiàng)被linuxLinux內(nèi)核設(shè)置成Linux內(nèi)核目錄項(xiàng),低768個(gè)目錄項(xiàng)被用戶空間使用. pgd = pgd_base + pgd_idx; pgd便指向了第768個(gè)表項(xiàng)。

然后函數(shù)開始一個(gè)循環(huán)即開始填充從768到1024這256個(gè)目錄項(xiàng)的內(nèi)容。one_md_table_init()函數(shù)根據(jù)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;
}
可以看出, 如果Linux內(nèi)核不啟用PAE選項(xiàng), 函數(shù)將通過 pmd_offset返回pgd的地址。因?yàn)閘inux的二級(jí)映射模型,本來就是忽略pmd中間目錄表的。接著又個(gè)判斷語(yǔ)句:
>> if (pfn >= max_low_pfn)
>> continue;
這個(gè)很關(guān)鍵, max_low_pfn代表著整個(gè)物理內(nèi)存一共有多少頁(yè)框。 當(dāng)pfn大于max_low_pfn的時(shí)候,表明Linux內(nèi)核已經(jīng)把整個(gè)物理內(nèi)存都映射到了系統(tǒng)空間中, 所以剩下有沒被填充的表項(xiàng)就直接忽略了。因?yàn)長(zhǎng)inux內(nèi)核已經(jīng)可以映射整個(gè)物理空間了, 沒必要繼續(xù)填充剩下的表項(xiàng)。

緊接著的第2個(gè)for循環(huán),在linux的3級(jí)映射模型中,是要設(shè)置pmd表的, 但在2級(jí)映射中忽略, 只循環(huán)一次,直接進(jìn)行頁(yè)表pte的設(shè)置。>> address = pfn * PAGE_SIZE + PAGE_OFFSET;address是個(gè)線性地址, 根據(jù)上面的語(yǔ)句可以看出address是從0xc000000開始的,也就是從Linux內(nèi)核空間開始,后面在設(shè)置頁(yè)表項(xiàng)屬性的時(shí)候會(huì)用

到它.
>> pte = one_page_table_init(pmd);
根據(jù)pmd分配一個(gè)頁(yè)表, 代碼同樣在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分配一個(gè)4k大小的物理頁(yè)面。 然后通過set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));來設(shè)置pmd表項(xiàng)。page_table顯然屬于線性地址,先通過__pa宏轉(zhuǎn)化為物理地址,在與上_PAGE_TABLE宏,此時(shí)它們還是無符號(hào)整數(shù),在通過__pmd把無符號(hào)整數(shù)轉(zhuǎn)化為pmd類型,經(jīng)過這些轉(zhuǎn)換, 就得到了一個(gè)具有屬性的表項(xiàng), 然后通過set_pmd宏設(shè)置pmd表項(xiàng).接著又是一個(gè)循環(huán),設(shè)置1024個(gè)頁(yè)表項(xiàng)。

is_kernel_text函數(shù)根據(jù)前面提到的address來判斷address線性地址是否屬于Linux內(nèi)核代碼段,它同樣在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是個(gè)Linux內(nèi)核符號(hào), 在Linux內(nèi)核鏈接的時(shí)候生成的, 分別表示Linux內(nèi)核代碼段的開始和終止地址.如果address屬于Linux內(nèi)核代碼段, 那么在設(shè)置頁(yè)表項(xiàng)的時(shí)候就要加個(gè)PAGE_KERNEL_EXEC屬性,如果不是,則加個(gè)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));來設(shè)置頁(yè)表項(xiàng), 先通過pfn_pte宏根據(jù)頁(yè)框號(hào)和頁(yè)表項(xiàng)的屬性值合并成一個(gè)頁(yè)表項(xiàng)值,然戶在用set_pte宏把頁(yè)表項(xiàng)值寫到頁(yè)表項(xiàng)里。

當(dāng)pagetable_init()函數(shù)返回后,Linux內(nèi)核已經(jīng)設(shè)置好了Linux內(nèi)核頁(yè)表,緊著調(diào)用load_cr3(swapper_pg_dir);#define load_cr3(pgdir) \asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir)))將控制swapper_pg_dir送入控制寄存器cr3. 每當(dāng)重新設(shè)置cr3時(shí)。

CPU就會(huì)將頁(yè)面映射目錄所在的頁(yè)面裝入CPU內(nèi)部高速緩存中的TLB部分. 現(xiàn)在內(nèi)存中(實(shí)際上是高速緩存中)的映射目錄變了,就要再讓CPU裝入一次。由于頁(yè)面映射機(jī)制本來就是開啟著的, 所以從這條指令以后就擴(kuò)大了系統(tǒng)空間中有映射區(qū)域的大小, 使整個(gè)映射覆蓋到整個(gè)物理內(nèi)存(高端內(nèi)存)除外. 實(shí)際上此時(shí)swapper_pg_dir中已經(jīng)改變的目錄項(xiàng)很可能還
在高速緩存中, 所以還要通過__flush_tlb_all()將高速緩存中的內(nèi)容沖刷到內(nèi)存中,這樣才能保證內(nèi)存中映射目錄內(nèi)容的一致性。

3.4 對(duì)如何構(gòu)建頁(yè)表的總結(jié)

通過上述對(duì)pagetable_init()的剖析, 我們可以清晰的看到, 構(gòu)建Linux內(nèi)核頁(yè)表, 無非就是向相應(yīng)的表項(xiàng)寫入下一級(jí)地址和屬性。 在Linux內(nèi)核空間保留著一部分內(nèi)存專門用來存放Linux內(nèi)核頁(yè)表.當(dāng)cpu要進(jìn)行尋址的時(shí)候,無論在Linux內(nèi)核空間,還是在用戶空間, 都會(huì)通過這個(gè)頁(yè)表來進(jìn)行映射。

對(duì)于這個(gè)函數(shù), Linux內(nèi)核把整個(gè)物理內(nèi)存空間都映射完了, 當(dāng)用戶空間的進(jìn)程要使用物理內(nèi)存時(shí), 豈不是不能做相應(yīng)的映射了? 其實(shí)不會(huì)的, Linux內(nèi)核只是做了映射, 映射不代表使用, 這樣做是Linux內(nèi)核為了方便管理內(nèi)存而已。

四. 實(shí)例分析映射機(jī)制

4.1示例代碼

通過前面的理論分析,我們通過編寫一個(gè)簡(jiǎn)單的程序, 來分析Linux內(nèi)核是如何把線性地址映射到物理地址的。
[root@localhost temp]# cat test.c
#include <stdio.h>
void test(void)
{
printf("hello, world.\n");
}
int main(void)
{
test();
}
這段代碼很簡(jiǎn)單, 我們故意要main調(diào)用test函數(shù), 就是想看下test函數(shù)的虛擬地址是如何映射成物理地址的。

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
從上述結(jié)果可以看到, ld給test()函數(shù)分配的地址為0x08048368.在elf格式的可執(zhí)行文件代碼中,ld的實(shí)際位置總是從0x8000000開始安排程序的代碼段, 對(duì)每個(gè)程序都是這樣。至于程序在執(zhí)行時(shí)在物理內(nèi)存中的實(shí)際位置就要由Linux內(nèi)核在為其建立內(nèi)存映射時(shí)臨時(shí)做出安排, 具體地址則取決于當(dāng)時(shí)所分配到的物理內(nèi)存頁(yè)面。

假設(shè)該程序已經(jīng)運(yùn)行, 整個(gè)映射機(jī)制都已經(jīng)建立好, 并且CPU正在執(zhí)行main()中的call 8048368這條指令, 要轉(zhuǎn)移到虛擬地址0x08048368去運(yùn)行. 下面將詳細(xì)介紹這個(gè)虛擬地址轉(zhuǎn)換為物理地址的映射過程.

首先是段式映射階段。由于0x08048368是一個(gè)程序的入口,更重要的是在執(zhí)行的過程中是由CPU中的指令計(jì)數(shù)器EIP所指向的, 所以在代碼段中。 因此, i386CPU使用代碼段寄存器CS的當(dāng)前值作為段式映射的選擇子, 也就是用它作為在段描述表的下標(biāo).那么CS的值是多少呢?
用GDB調(diào)試下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, 我們把它分解成二進(jìn)制:
0000 0000 0111 0011
最低2位為3, 說明RPL的值為3, 應(yīng)為我們這個(gè)程序本省就是在用戶空間,RPL的值自然為3.
第3位為0表示這個(gè)下標(biāo)在GDT中。
高13位為14, 所以段描述符在GDT表的第14個(gè)表項(xiàng)中, 我們可以到Linux內(nèi)核代碼中去驗(yàn)證下:
在i386/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
可以看到段描述符的確就是GDT表的第14個(gè)表項(xiàng)中。
我們?nèi)DT表看看具體的表項(xiàng)值是什么, GDT的內(nèi)容在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 */

我們把這個(gè)值展開成二進(jìn)制:
0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
根據(jù)上述對(duì)段描述符表項(xiàng)值的描述, 可以得出如下結(jié)論:
B0-B15, B16-B31是0, 表示基地址全為0.
L0-L15, L16-L19是1, 表示段的上限全是0xffff.
G位是1 表示段長(zhǎng)度單位均為4KB。
D位是1 表示對(duì)段的訪問都是32位指令
P位是1 表示段在內(nèi)存中。
DPL是3 表示特權(quán)級(jí)是3級(jí)
S位是1 表示為代碼段或數(shù)據(jù)段
type為1010 表示代碼段, 可讀, 可執(zhí)行, 尚未收到訪問
這個(gè)描述符指示了段從0地址開始的整個(gè)4G虛存空間,邏輯地址直接轉(zhuǎn)換為線性地址。所以在經(jīng)過段式映射后就把邏輯地址轉(zhuǎn)換成了線性地址, 這也是在linux中, 為什么邏輯地址等同于線性地址的原因了。

4.3 頁(yè)式映射分析

現(xiàn)在進(jìn)入頁(yè)式映射的過程了, Linux系統(tǒng)中的每個(gè)進(jìn)程都有其自身的頁(yè)面目錄PGD, 指向這個(gè)目錄的指針保存在每個(gè)進(jìn)程的mm_struct數(shù)據(jù)結(jié)構(gòu)中。 每當(dāng)調(diào)度一個(gè)進(jìn)程進(jìn)入運(yùn)行的時(shí)候,Linux內(nèi)核都要為即將運(yùn)行的進(jìn)程設(shè)置好控制寄存器cr3, 而MMU的硬件則總是從cr3中取得指向當(dāng)前頁(yè)面目錄的指針。當(dāng)我們?cè)诔绦蛑幸D(zhuǎn)移到地址0x08048368去的時(shí)候, 進(jìn)程正在運(yùn)行,cr3早以設(shè)置好,指向我們這個(gè)進(jìn)程的頁(yè)面目錄了。 先將線性地址0x08048368展開成二進(jìn)制:

0000 1000 0000 0100 1000 0011 0110 1000對(duì)照線性地址的格式,可見最高10位為二進(jìn)制的0000 1000 00, 也就是十進(jìn)制的32,所以MMU就以32為下標(biāo)在其頁(yè)面目錄中找到其目錄項(xiàng)。這個(gè)目錄項(xiàng)的高20位指向一個(gè)頁(yè)面表,CPU在這20位后添上12個(gè)0就得到頁(yè)面表的指針。找到頁(yè)面表以后, CPU再來看線性地址中的中間10位,0001001000,即十進(jìn)制的72.于是CPU就以此為下標(biāo)在頁(yè)表中找相應(yīng)的表項(xiàng)。表項(xiàng)值的高20位指向一個(gè)物理內(nèi)存頁(yè)面,在后邊添上12個(gè)0就得到物理頁(yè)面的開始地址。假設(shè)物理地址在0x620000的,線性地址的最低12位為0x368. 那么test()函數(shù)的入口地址就為0x620000+0x368 = 0x620368

【編輯推薦】

  1. Linux內(nèi)存使用情況查看單位顯示
  2. 闡述Linux內(nèi)存管理方式
  3. 詳細(xì)講解Linux內(nèi)存管理中分段分頁(yè)機(jī)制
  4. 闡述Linux內(nèi)存管理:紅黑樹
  5. 簡(jiǎn)要概括Linux內(nèi)存管理的方式
責(zé)任編輯:佚名 來源: CSDN
相關(guān)推薦

2016-10-11 16:28:11

源代碼

2015-08-28 09:38:51

Linux源代碼分析工具

2015-08-26 17:38:47

Linux源代碼

2019-03-24 14:14:40

代碼閱讀源代碼

2013-10-31 14:15:10

2017-11-20 22:28:43

程序員源代碼編程

2010-03-02 10:44:52

Linux rpm

2016-06-23 14:19:59

DevOpsOpenStackIaaS

2021-10-01 12:17:30

Facebook開源工具Mariana Tre

2021-05-06 09:58:08

微軟AI 系統(tǒng)工具Counterfit

2010-06-03 10:15:35

2022-09-01 12:45:55

代碼

2015-11-30 08:57:07

源代碼閱讀程序員

2015-11-30 11:01:07

程序員閱讀源代碼

2009-04-03 08:28:39

2011-08-02 10:13:30

Java工具

2009-03-10 08:59:18

2010-08-03 10:16:52

Flex源代碼

2010-04-29 12:57:33

Unix源代碼

2009-06-12 19:03:41

Hadoop源代碼Yahoo
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)