Linux內(nèi)存初始化
大?。?/span>0.4 MB 人氣: 2017-10-12 需要積分:1
推薦 + 挑錯(cuò) + 收藏(0) + 用戶評論(0)
標(biāo)簽:Linux(205935)
之前有幾篇博客詳細(xì)介紹了Xen的內(nèi)存初始化,確實(shí)感覺這部分內(nèi)容蠻復(fù)雜的。這兩天在看Linux內(nèi)核啟動(dòng)中內(nèi)存的初始化,也是看的云里霧里的,想嘗試下邊看邊寫,在寫博客的過程中慢慢思考,最后也能把自己的思考分享給其它人。這個(gè)系列主要分為兩個(gè)部分,匯編部分和C語言部分。
這篇博文主要介紹的是匯編部分。
內(nèi)核解壓縮過程
這個(gè)過程就不詳述了,整個(gè)Linux內(nèi)核是作為一個(gè)壓縮過的鏡像提供的,在執(zhí)行內(nèi)核代碼之前,首先需要bootloader對其進(jìn)行一個(gè)解壓縮,對這部分有興趣可以參看這篇博客。
最初的頁表什么樣?
解壓結(jié)束后,會(huì)進(jìn)行一個(gè)對elf格式的parse,然后對內(nèi)核進(jìn)行加載,最后進(jìn)入arch/x86/kernel/head_64.S中的startup_64。
startup_64主要完成分頁功能啟用,最后跳入C代碼x86_64_start_kernel。在開始分析代碼之前,我們要先來看看在內(nèi)核的數(shù)據(jù)段中,初始化頁表是長怎么樣的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__INITDATA
NEXT_PAGE(early_level4_pgt)
.fill 511,8,0
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(early_dynamic_pgts)
.fill 512*EARLY_DYNAMIC_PAGE_TABLES,8,0
.data
NEXT_PAGE(init_level4_pgt)
.fill 512,8,0
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
/* (2^48-(2*1024*1024*1024)-((2^39)511))/(2^30) = 510 /
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level2_kernel_pgt)
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE)
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
.fill 5,8,0
NEXT_PAGE(level1_fixmap_pgt)
.fill 512,8,0
這段數(shù)據(jù)結(jié)構(gòu)還是比較清楚的,你把下面這兩個(gè)宏NEXT_PAGE和PMDS代入上面的數(shù)據(jù)結(jié)構(gòu):
1
2
3
4
5
6
7
8
9
10
11
define NEXT_PAGE(name) \
.balign PAGE_SIZE; \
GLOBAL(name)
/* Automate the creation of 1 to 1 mapping pmd entries */
define PMDS(START, PERM, COUNT) \
i = 0 ; \
.rept (COUNT) ; \
.quad (START) + (i 《《 PMD_SHIFT) + (PERM) ; \
i = i + 1 ; \
.endr
我們就可以很輕易地畫出下面這張圖:
early page table
后面的初始化過程,就是建立在這個(gè)早期的頁表結(jié)構(gòu)中的。
正式進(jìn)入startup_64
我們一段段來分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
startup_64:
/*
* Compute the delta between the address I am compiled to run at and the
* address I am actually running at.
*/
leaq _text(%rip), %rbp
subq $_text - __START_KERNEL_map, %rbp
/* Is the address not 2M aligned? */
movq %rbp, %rax
andl $~PMD_PAGE_MASK, %eax
testl %eax, %eax
jnz bad_address
/*
* Is the address too large?
*/
leaq _text(%rip), %rax
shrq $MAX_PHYSMEM_BITS, %rax
jnz bad_address
這里的這段代碼非常奇怪:
1
2
leaq _text(%rip), %rbp
subq $_text - __START_KERNEL_map, %rbp
我想了好久,現(xiàn)在終于在Liangpig的指導(dǎo)下有了點(diǎn)眉目。(不確定的)解釋如下:
首先leaq _text(%rip), %rbp是一個(gè)相對尋址的指令,其并不是直接將_text的地址和當(dāng)前%rip的值相加,而是%rip加上一個(gè)_text和它的相對地址,其實(shí)就是$-7(因?yàn)樵摰刂返拈L度為7,而當(dāng)前的%rip就是_text地址加上7),這個(gè)相對值是在link的時(shí)候計(jì)算出來的,可以參看這個(gè)問題和這個(gè)問題。
這里另外需要注意的一點(diǎn)是,在當(dāng)前這個(gè)時(shí)候,計(jì)算機(jī)還是通過實(shí)模式進(jìn)行尋址的,所以內(nèi)核的代碼應(yīng)該是被load到了一個(gè)低地址(而不是大于0xffffffff8000000的地址),因此,%rbp存儲(chǔ)的也是一個(gè)低地址,表示的是內(nèi)核的代碼段被實(shí)際裝載到內(nèi)存到的地址,讓我們假設(shè)是0x3000000。
那么$_text - __START_KERNEL_map是什么呢?我們來看下面的定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define __START_KERNEL_map _AC(0xffffffff80000000, UL)
define __PHYSICAL_START ALIGN(CONFIG_PHYSICAL_START, \
CONFIG_PHYSICAL_ALIGN)
define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
SECTIONS
{
。 = __START_KERNEL;
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = 。;
}
}
define
首先,__START_KERNEL_map是0xffffffff80000000,即內(nèi)核代碼和數(shù)據(jù)段在64位的虛擬地址空間中的最低地址段(0xffffffff80000000到0xffffffffa0000000這512MB的虛擬機(jī)之空間映射了內(nèi)核段)。而_text表示的是__START_KERNEL_map加上了一段編譯過程中指定的地址,在我機(jī)器內(nèi)核的.config文件中為0x1000000。也就是說,如果__START_KERNEL_map映射的是物理地址為0的內(nèi)存的話,那么在編譯中我們期望的真正的物理地址就為0x1000000,也就是說,_text - __START_KERNEL_map表示的是我們在編譯過程中期望的內(nèi)核段被裝載到內(nèi)存的起始地址,因此subq_text - __START_KERNEL_map, %rbp表示將當(dāng)前內(nèi)核段真實(shí)被裝載到內(nèi)存中的地址和編譯過程中期望被裝載到內(nèi)存中的地址的差值賦值給%rbx,在我們的例子中即為0x2000000(0x3000000 - 0x1000000)。
之后我們就對這個(gè)真實(shí)被裝載到內(nèi)存中的地址做一些檢查,包括是否2M對齊,以及有沒有超過最大大小等等,這里就不詳述了。
然后做的一件事就是調(diào)整初始化頁表中的物理地址映射:
1
2
3
4
5
6
7
8
9
/*
* Fixup the physical addresses in the page table
*/
addq %rbp, early_level4_pgt + (L4_START_KERNEL*8)(%rip)
addq %rbp, level3_kernel_pgt + (510*8)(%rip)
addq %rbp, level3_kernel_pgt + (511*8)(%rip)
addq %rbp, level2_fixmap_pgt + (506*8)(%rip)
這又是一段相對尋址,由于頁表處于數(shù)據(jù)段,所以需要根據(jù)其和%rip中的相對地址來定位到頁表,然后將頁表中的表項(xiàng)加上之前計(jì)算的相對偏移量。當(dāng)然這里只處理了early_level4_pgt、level3_kernel_pgt和level2_fixmap_pgt,而真正映射內(nèi)核段的level2_kernel_pgt會(huì)在之后進(jìn)行fixup。
之后又進(jìn)入了一段詭異的代碼,來建立identity mapping for the switchover,我也不懂這里的switchover是什么,我們先來看下這段代碼做了什么吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* Set up the identity mapping for the switchover. These
* entries should NOThave the global bit set! This also
* creates a bunch of nonsense entries but that is fine –
* it avoids problems around wraparound.
*/
leaq _text(%rip), %rdi
leaq early_level4_pgt(%rip), %rbx
movq %rdi, %rax
shrq $PGDIR_SHIFT, %rax
leaq (4096 + _KERNPG_TABLE)(%rbx), %rdx
movq %rdx, 0(%rbx,%rax,8)
movq %rdx, 8(%rbx,%rax,8)
addq 4096,movqshrqPUD_SHIFT, %rax
andl (PTRSPERPUD?1),movqinclandl(PTRS_PER_PUD-1), %eax
movq %rdx, 4096(%rbx,%rax,8)
addq 8192,movqshrqPMD_SHIFT, %rdi
addq (__PAGE_KERNEL_LARGE_EXEC & ~_PAGE_GLOBAL), %rax
leaq (_end - 1)(%rip), %rcx
shrqPMD_SHIFT, %rcx
subq %rdi, %rcx
incl %ecx
1:
andq (PTRSPERPMD?1),movqincqaddqPMD_SIZE, %rax
decl %ecx
jnz 1b
我們可以稍微進(jìn)行一個(gè)計(jì)算,首先%rdi保存了當(dāng)前內(nèi)核代碼段的首地址,%rbx保存了early_level4_pgt的地址,%rax是內(nèi)核代碼首地址對于level4頁表的index,在當(dāng)前即為0。所以leaq (4096 + _KERNPG_TABLE)(%rbx), %rdx表示的是將early_level4_pgt所在的地址加上一個(gè)頁的地址,作為第3級頁表頁,再加上相應(yīng)的權(quán)限位,保存在%rdx中,然后通過movq %rdx, 0(%rbx,%rax,8)和movq %rdx, 8(%rbx,%rax,8)指令把%rdx作為一個(gè)表項(xiàng),存在early_level4_pgt的第0和第1項(xiàng)中。
然后將%rdx再加上一個(gè)頁的大小,作為第2級頁表頁,找到內(nèi)核代碼段對于level3頁表的index,然后將第2級頁表頁加上對應(yīng)的權(quán)限作為一個(gè)頁表項(xiàng)存在剛剛建立的level3頁表的第0項(xiàng)和第1項(xiàng)。
然后將%rbx加上兩個(gè)頁的大小,即第2級頁表的位置,找到從_text到_end所有內(nèi)核代碼段對于level2頁表的索引,然后將對應(yīng)的地址+權(quán)限作為頁表項(xiàng)逐個(gè)填到這個(gè)第2級頁表中。
我們可以在arch/x86/kernel/head_64.S文件中找到這幾個(gè)新添加的頁表頁的定義:
1
2
3
4
5
6
7
__INITDATA
NEXT_PAGE(early_level4_pgt)
.fill 511,8,0
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(early_dynamic_pgts)
.fill 512*EARLY_DYNAMIC_PAGE_TABLES,8,0
即緊接著early_level4_pgt,被稱為early_dynamic_pgts。這個(gè)就是所謂的identity mapping for the switchover,表示在之后的一小段頁表轉(zhuǎn)換過程中會(huì)被用到的identity mapping。因?yàn)樵陧摫碇刑摂M地址從低地址到高地址轉(zhuǎn)換的過程中不可避免的會(huì)通過低位的虛擬地址進(jìn)行索引,所以需要預(yù)先做個(gè)identity mapping的準(zhǔn)備。
至此,頁表變成了這個(gè)樣子。
early page table 2
startup_64最后一步就是fixup內(nèi)核段真正的物理頁對應(yīng)的頁表項(xiàng)了,代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* Fixup the kernel text+data virtual addresses. Note that
* we might write invalid pmds, when the kernel is relocated
* cleanup_highmap() fixes this up along with the mappings
* beyond _end.
*/
leaq level2_kernel_pgt(%rip), %rdi
leaq 4096(%rdi), %r8
/* See if it is a valid page table entry */
1: testq 1,0(jz2faddq/?Gotothenextpage?/2:addq8, %rdi
cmp %r8, %rdi
jne 1b
/* Fixup phys_base */
addq %rbp, phys_base(%rip)
movq $(early_level4_pgt - __START_KERNEL_map), %rax
jmp 1f
這個(gè)過程的前半部分就是將level2_kernel_pgt中的表項(xiàng)進(jìn)行一個(gè)個(gè)的檢查,如果不是0(即為一個(gè)可能存在的頁表項(xiàng)),則將其加上之前計(jì)算的真實(shí)地址和被期待地址的偏移量(%rbp)。
當(dāng)這個(gè)fixup結(jié)束之后,將%rbp保存在phys_base這個(gè)地址中,然后再將early_level4_pgt - __START_KERNEL_map保存在%rax中。
接下來就進(jìn)入secondary_startup_64。
secondary_startup_64
這部分代碼的主要功能是一些模式的開啟,以及相關(guān)數(shù)據(jù)結(jié)構(gòu)的加載,我們同樣逐段進(jìn)行分析:
1
2
3
4
5
6
7
8
ENTRY(secondary_startup_64)
/* Enable PAE mode and PGE */
movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
movq %rcx, %cr4
/* Setup early boot stage 4 level pagetables. */
addq phys_base(%rip), %rax
movq %rax, %cr3
這里開啟了PAE和PGE模式,并將其寫到%cr4中,同時(shí)將初始頁表的第四級頁表地址寫入了%cr3。至此,分頁模式開啟!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* Ensure I am executing from virtual addresses */
movq $1f, %rax
jmp *%rax
1:
/* Check if nx is implemented */
movl $0x80000001, %eax
cpuid
movl %edx,%edi
/* Setup EFER (Extended Feature Enable Register) */
movl MSREFER,rdmsrbtsl_EFER_SCE, %eax /* Enable System Call */
btl 20,jnc1fbtsl_EFER_NX, %eax
btsq $_PAGE_BIT_NX,early_pmd_flags(%rip)
1: wrmsr /* Make changes effective */
/* Setup cr0 */
define CR0_STATE (X86_CR0_PE | X86_CR0_MP | X86_CR0_ET | \
X86_CR0_NE | X86_CR0_WP | X86_CR0_AM | \ X86_CR0_PG)
movl $CR0_STATE, %eax
/* Make changes effective */
movq %rax, %cr0
/* Setup a boot time stack */
movq stack_start(%rip), %rsp
/* zero EFLAGS after setting rsp */
pushq $0
popfq
上面的代碼進(jìn)行了一系列的初始化,包括檢查nx(non-execution)是否開啟,創(chuàng)建EFER,創(chuàng)建cr0,以及設(shè)置一個(gè)啟動(dòng)時(shí)會(huì)用到的棧,并且將所有eflags清零。這里就不細(xì)講了。
然后是加載早期的GDT:
1
2
3
4
5
6
7
/*
* We must switch to a new deor in kernel space for the GDT
* because soon the kernel won’t have access anymore to the userspace
* addresses where we’re currently running on. We have to do that here
* because in 32bit we couldn’t load a 64bit linear address.
*/
lgdt early_gdt_descr(%rip)
初始化段寄存器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* set up data segments */
xorl %eax,%eax
movl %eax,%ds
movl %eax,%ss
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
/* Set up %gs.
*
* The base of %gs always points to the bottom of the irqstack
* union. If the stack protector canary is enabled, it is
* located at %gs:40. Note that, on SMP, the boot cpu uses
* init data section till per cpu areas are set up.
*/
movl $MSR_GS_BASE,%ecx
movl initial_gs(%rip),%eax
movl initial_gs+4(%rip),%edx
wrmsr
這里需要注意的是%gs的建立,它和per cpu變量相關(guān),是一個(gè)比較關(guān)鍵的段寄存器。不過由于這個(gè)系列主要是和內(nèi)存相關(guān),所以這里就不詳述了。
最后就是一個(gè)通過far jump的跳轉(zhuǎn):
1
2
3
4
5
6
7
8
9
10
11
/* Finally jump to run C code and to be on real kernel address
* Since we are running on identity-mapped space we have to jump
* to the full 64bit address, this is only possible as indirect
* jump. In addition we need to ensure %cs is set so we make this
* a far return.
*/
movq initial_code(%rip),%rax
pushq 0 # fake return address to stop unwinder
pushq__KERNEL_CS # set correct cs
pushq %rax # target address in negative space
lretq
其中initial_code定義為:
1
2
GLOBAL(initial_code)
.quad x86_64_start_kernel
因此,最后進(jìn)入了x86_64_start_kernel函數(shù),這是一個(gè)C語言寫的函數(shù),所以,會(huì)在下一篇博客中進(jìn)行介紹。
?
非常好我支持^.^
(0) 0%
不好我反對
(0) 0%
下載地址
Linux內(nèi)存初始化下載
相關(guān)電子資料下載
- uboot的基本概念和啟動(dòng)流程分析 49
- Windows11上Linux安裝教程 22
- 多路徑和iSCSI SAN存儲(chǔ)技術(shù)介紹 56
- 探討嵌入式系統(tǒng)的軟硬件框架 29
- 浩辰CAD Linux版 2024全球發(fā)布 274
- 如何使用pthread_barrier_xxx系列函數(shù)來實(shí)現(xiàn)多線程之間的同步? 29
- 馬斯克曾說特斯拉堅(jiān)不可摧,卻敗在Kali Linux手下 559
- 為T507-H開發(fā)板配置Samba服務(wù),高效實(shí)現(xiàn)跨系統(tǒng)的文件共享 250
- DNS的各種玩法:程序編譯到進(jìn)程的過程解析 23
- 【凡億瘋狂星期五】LINUX基礎(chǔ)講解和操作演示 58