background

上一篇 我们已经将compressed kernel解压到了指定位置 , 下面我们就接着来看看uncompressed kernel的启动过程 .

checking

首先是初始化stack , 并查看cpu是否支持64位 :

/* Set up the stack for verify_cpu(), similar to initial_stack below */
leaq	(__end_init_task - SIZEOF_PTREGS)(%rip), %rsp

/* Sanitize CPU configuration */
call verify_cpu

其实 , 在之前我们已经验证过cpu是否支持64位 , 这里再次检查是因为有可能我们是通过64位的bootloader跳转到这里 , 从这段代码的上面的comment , 我们也可以得到验证 :

* We come here either directly from a 64bit bootloader, or from
* arch/x86/boot/compressed/head_64.S.

接着计算当前位置和编译时的位置之间的差值 :

/*
 * 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
...
#define __START_KERNEL_map 0xffffffff80000000

而编译时的位置可以从 arch/x86/kernel/vmlinux.lds 中得到 :

SECTIONS
{
 . = (0xffffffff80000000 + ALIGN(0x1000000, 0x200000));
 phys_startup_64 = ABSOLUTE(startup_64 - 0xffffffff80000000);
 /* Text and read-only data */
 .text : AT(ADDR(.text) - 0xffffffff80000000) {
  _text = .;

所以 , 这里的差值为0 (0x1000000 - (0xffffffff80000000 + 0x1000000 - 0xffffffff80000000)).

接着 , 检查地址的合法性 , 包括是否2M对其 , 是否超过64位cpu支持的最大地址 :

/* Is the address not 2M aligned? */
testl	$~PMD_PAGE_MASK, %ebp
jnz	bad_address

/*
 * Is the address too large?
 */
leaq	_text(%rip), %rax
shrq	$MAX_PHYSMEM_BITS, %rax
jnz	bad_address

early page mapping

arch/x86/kernel/vmlinux.lds 可以看出编译时的线性地址从 0xffffffff80000000 开始 , 而真实的物理地址却是从0开始 , 所以 ,这里需要建立页表将其映射起来 .

下面我们就来看看这部分页表的结构 , 由于我们采用的4级页表的结构 , 首先需要计算出该线性地址在pgdt中的位置 :

(0xffffffff80000000 » 39) & 511 = 511

其次是在pudt中的位置 :

(0xffffffff80000000 » 30) & 511 = 510

接着是在pmdt中的位置 :

(0xffffffff80000000 » 21) & 511 = 0

清楚了这些位置 ,下面就是对应的页表初始化 :

NEXT_PAGE(early_level4_pgt)
	.fill	511,8,0
	.quad	level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level3_kernel_pgt)
	.fill	L3_START_KERNEL,8,0
	.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
...
#define KERNEL_IMAGE_SIZE	(512 * 1024 * 1024)
#define PMDS(START, PERM, COUNT)			\
	i = 0 ;						\
	.rept (COUNT) ;					\
	.quad	(START) + (i << PMD_SHIFT) + (PERM) ;	\
	i = i + 1 ;					\
	.endr

可以看出这里将虚拟地址空间 ( 0xffffffff80000000 ~ 0xffffffff80000000+512M ) 映射到物理地址空间 ( 0 ~ 512M ) .

由于该页表目录结构是在编译时初始化的 , 所以这里还需要将其地址调整为实际运行的地址 , 这里 , 只需要加上之前计算出的差值即可 :

/*
 * 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)

除了建立上述的地址映射 , kernel还建立一个对等映射 , 即线性地址对应和其相同的物理地址 :

/*
 * Set up the identity mapping for the switchover.  These
 * entries should *NOT* have 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	(PAGE_SIZE + _KERNPG_TABLE)(%rbx), %rdx
movq	%rdx, 0(%rbx,%rax,8)
movq	%rdx, 8(%rbx,%rax,8)
addq	$PAGE_SIZE, %rdx
movq	%rdi, %rax
shrq	$PUD_SHIFT, %rax
andl	$(PTRS_PER_PUD-1), %eax
movq	%rdx, PAGE_SIZE(%rbx,%rax,8)
incl	%eax
andl	$(PTRS_PER_PUD-1), %eax
movq	%rdx, PAGE_SIZE(%rbx,%rax,8)
addq	$PAGE_SIZE * 2, %rbx
movq	%rdi, %rax
shrq	$PMD_SHIFT, %rdi
addq	$(__PAGE_KERNEL_LARGE_EXEC & ~_PAGE_GLOBAL), %rax
leaq	(_end - 1)(%rip), %rcx
shrq	$PMD_SHIFT, %rcx
subq	%rdi, %rcx
incl	%ecx
1:
andq	$(PTRS_PER_PMD - 1), %rdi
movq	%rax, (%rbx,%rdi,8)
incq	%rdi
addq	$PMD_SIZE, %rax
decl	%ecx
jnz	1b

可以看出这里将虚拟地址空间 (_text(0x1000000) ~ _end) 映射到对等的物理地址空间 .

之后 , 根据kernel是否被relocation , 对页表中的地址做调整 :

leaq	level2_kernel_pgt(%rip), %rdi
leaq	PAGE_SIZE(%rdi), %r8
/* See if it is a valid page table entry */
1:	testb	$_PAGE_PRESENT, 0(%rdi)
jz	2f
addq	%rbp, 0(%rdi)
/* Go to the next page */
2:	addq	$8, %rdi
cmp	%r8, %rdi
jne	1b

最终 , 物理地址和虚拟地址的对应关系如下 :

        physical address           virtual address
          +-------+                 +-------+
          |       |                 |       |
          |       |     +-----------+-------+ 0xffffffff80000000+512M
     512M +-------+<----+           |       |
          |       |                 |       |
          |       |     +-----------+-------+ 0xffffffff80000000
          |       |     |           |       |
          |       |     |           |       |
          |       |     |           |       |
          |       |     |           |       |
     _end +-------+<----+-----------+-------+ _end
          | kernel|     |           |       |
          | image |     |           |       |
0x1000000 +-------+<----+-----------+-------+ 0x1000000
          |       |     |           |       |
          |       |     |           |       |
          |       |     |           |       |
          |       |     |           |       |
      0x0 +-------+<----+           +-------+

之后打开pae和pge , 并加载新的页表 :

	movq	$(early_level4_pgt - __START_KERNEL_map), %rax
	jmp 1f
...
1:
	/* 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

这样 , 我们就能调转到编译时指定的虚拟地址 :

	/* Ensure I am executing from virtual addresses */
	movq	$1f, %rax
	jmp	*%rax
1:

之后 , kernel会重新设置cr0 , 初始化stack和各个段寄存器 . 最终, 跳入到c代码 x86_64_start_kernel :

	movq	initial_code(%rip), %rax
	pushq	$__KERNEL_CS	# set correct cs
	pushq	%rax		# target address in negative space
	lretq
	...
	GLOBAL(initial_code)
	.quad	x86_64_start_kernel

FIN.