Linux內存初始化
大小:0.4 MB 人氣: 2017-10-12 需要積分:1
這個系列主要分為兩個部分,匯編部分和C語言部分。
這篇博文主要介紹的是匯編部分。
內核解壓縮過程
這個過程就不詳述了,整個Linux內核是作為一個壓縮過的鏡像提供的,在執行內核代碼之前,首先需要bootloader對其進行一個解壓縮,對這部分有興趣可以參看這篇博客。
最初的頁表什么樣?
解壓結束后,會進行一個對elf格式的parse,然后對內核進行加載,最后進入arch/x86/kernel/head_64.S中的startup_64。
startup_64主要完成分頁功能啟用,最后跳入C代碼x86_64_start_kernel。在開始分析代碼之前,我們要先來看看在內核的數據段中,初始化頁表是長怎么樣的?
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
這段數據結構還是比較清楚的,你把下面這兩個宏NEXT_PAGE和PMDS代入上面的數據結構:
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
后面的初始化過程,就是建立在這個早期的頁表結構中的。
正式進入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
我想了好久,現在終于在Liangpig的指導下有了點眉目。(不確定的)解釋如下:
首先leaq _text(%rip), %rbp是一個相對尋址的指令,其并不是直接將_text的地址和當前%rip的值相加,而是%rip加上一個_text和它的相對地址,其實就是$-7(因為該地址的長度為7,而當前的%rip就是_text地址加上7),這個相對值是在link的時候計算出來的,可以參看這個問題和這個問題。
這里另外需要注意的一點是,在當前這個時候,計算機還是通過實模式進行尋址的,所以內核的代碼應該是被load到了一個低地址(而不是大于0xffffffff8000000的地址),因此,%rbp存儲的也是一個低地址,表示的是內核的代碼段被實際裝載到內存到的地址,讓我們假設是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,即內核代碼和數據段在64位的虛擬地址空間中的最低地址段(0xffffffff80000000到0xffffffffa0000000這512MB的虛擬機之空間映射了內核段)。而_text表示的是__START_KERNEL_map加上了一段編譯過程中指定的地址,在我機器內核的.config文件中為0x1000000。也就是說,如果__START_KERNEL_map映射的是物理地址為0的內存的話,那么在編譯中我們期望的真正的物理地址就為0x1000000,也就是說,_text - __START_KERNEL_map表示的是我們在編譯過程中期望的內核段被裝載到內存的起始地址,因此subq_text - __START_KERNEL_map, %rbp表示將當前內核段真實被裝載到內存中的地址和編譯過程中期望被裝載到內存中的地址的差值賦值給%rbx,在我們的例子中即為0x2000000(0x3000000 - 0x1000000)。
之后我們就對這個真實被裝載到內存中的地址做一些檢查,包括是否2M對齊,以及有沒有超過最大大小等等,這里就不詳述了。
然后做的一件事就是調整初始化頁表中的物理地址映射:
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)
這又是一段相對尋址,由于頁表處于數據段,所以需要根據其和%rip中的相對地址來定位到頁表,然后將頁表中的表項加上之前計算的相對偏移量。當然這里只處理了early_level4_pgt、level3_kernel_pgt和level2_fixmap_pgt,而真正映射內核段的level2_kernel_pgt會在之后進行fixup。
之后又進入了一段詭異的代碼,來建立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
我們可以稍微進行一個計算,首先%rdi保存了當前內核代碼段的首地址,%rbx保存了early_level4_pgt的地址,%rax是內核代碼首地址對于level4頁表的index,在當前即為0。所以leaq (4096 + _KERNPG_TABLE)(%rbx), %rdx表示的是將early_level4_pgt所在的地址加上一個頁的地址,作為第3級頁表頁,再加上相應的權限位,保存在%rdx中,然后通過movq %rdx, 0(%rbx,%rax,8)和movq %rdx, 8(%rbx,%rax,8)指令把%rdx作為一個表項,存在early_level4_pgt的第0和第1項中。
然后將%rdx再加上一個頁的大小,作為第2級頁表頁,找到內核代碼段對于level3頁表的index,然后將第2級頁表頁加上對應的權限作為一個頁表項存在剛剛建立的level3頁表的第0項和第1項。
然后將%rbx加上兩個頁的大小,即第2級頁表的位置,找到從_text到_end所有內核代碼段對于level2頁表的索引,然后將對應的地址+權限作為頁表項逐個填到這個第2級頁表中。
我們可以在arch/x86/kernel/head_64.S文件中找到這幾個新添加的頁表頁的定義:
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。這個就是所謂的identity mapping for the switchover,表示在之后的一小段頁表轉換過程中會被用到的identity mapping。因為在頁表中虛擬地址從低地址到高地址轉換的過程中不可避免的會通過低位的虛擬地址進行索引,所以需要預先做個identity mapping的準備。
至此,頁表變成了這個樣子。
early page table 2
startup_64最后一步就是fixup內核段真正的物理頁對應的頁表項了,代碼如下所示:
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
這個過程的前半部分就是將level2_kernel_pgt中的表項進行一個個的檢查,如果不是0(即為一個可能存在的頁表項),則將其加上之前計算的真實地址和被期待地址的偏移量(%rbp)。
當這個fixup結束之后,將%rbp保存在phys_base這個地址中,然后再將early_level4_pgt - __START_KERNEL_map保存在%rax中。
接下來就進入secondary_startup_64。
secondary_startup_64
這部分代碼的主要功能是一些模式的開啟,以及相關數據結構的加載,我們同樣逐段進行分析:
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中,同時將初始頁表的第四級頁表地址寫入了%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
上面的代碼進行了一系列的初始化,包括檢查nx(non-execution)是否開啟,創建EFER,創建cr0,以及設置一個啟動時會用到的棧,并且將所有eflags清零。這里就不細講了。
然后是加載早期的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變量相關,是一個比較關鍵的段寄存器。不過由于這個系列主要是和內存相關,所以這里就不詳述了。
最后就是一個通過far jump的跳轉:
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
因此,最后進入了x86_64_start_kernel函數,這是一個C語言寫的函數,所以,會在下一篇博客中進行介紹。
?
非常好我支持^.^
(0) 0%
不好我反對
(0) 0%