转载

Linux内存初始化(汇编部分)

之前有几篇博客详细介绍了Xen的内存初始化,确实感觉这部分内容蛮复杂的。这两天在看Linux内核启动中内存的初始化,也是看的云里雾里的,想尝试下边看边写,在写博客的过程中慢慢思考,最后也能把自己的思考分享给其它人。

这个系列主要分为两个部分,汇编部分和C语言部分。

这篇博文主要介绍的是汇编部分。

内核解压缩过程

这个过程就不详述了,整个Linux内核是作为一个压缩过的镜像提供的,在执行内核代码之前,首先需要bootloader对其进行一个解压缩,对这部分有兴趣可以参看 这篇博客 。

最初的页表什么样?

解压结束后,会进行一个对elf格式的parse,然后对内核进行加载,最后进入 arch/x86/kernel/head_64.S 中的 startup_64

startup_64 主要完成分页功能启用,最后跳入C代码x86_64_start_kernel。在开始分析代码之前,我们要先来看看在内核的数据段中,初始化页表是长怎么样的?

  __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_PAGEPMDS 代入上面的数据结构:

#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 

我们就可以很轻易地画出下面这张图:

Linux内存初始化(汇编部分)

后面的初始化过程,就是建立在这个早期的页表结构中的。

正式进入startup_64

我们一段段来分析:

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 

这里的这段代码非常奇怪:

  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 是什么呢?我们来看下面的定义:

#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_map0xffffffff80000000 ,即内核代码和数据段在64位的虚拟地址空间中的最低地址段( 0xffffffff800000000xffffffffa0000000 这512MB的虚拟机之空间映射了内核段)。而 _text 表示的是 __START_KERNEL_map 加上了一段编译过程中指定的地址,在我机器内核的 .config 文件中为 0x1000000 。也就是说,如果 __START_KERNEL_map 映射的是物理地址为 0 的内存的话,那么在编译中我们期望的真正的物理地址就为 0x1000000 ,也就是说, $_text - __START_KERNEL_map 表示的是我们在编译过程中期望的内核段被装载到内存的起始地址,因此 subq $_text - __START_KERNEL_map, %rbp 表示将当前内核段真实被装载到内存中的地址和编译过程中期望被装载到内存中的地址的差值赋值给 %rbx ,在我们的例子中即为 0x20000000x3000000 - 0x1000000 )。

之后我们就对这个真实被装载到内存中的地址做一些检查,包括是否2M对齐,以及有没有超过最大大小等等,这里就不详述了。

然后做的一件事就是调整初始化页表中的物理地址映射:

  /*    * 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_pgtlevel3_kernel_pgtlevel2_fixmap_pgt ,而真正映射内核段的 level2_kernel_pgt 会在之后进行fixup。

之后又进入了一段诡异的代码,来建立 identity mapping for the switchover ,我也不懂这里的 switchover 是什么,我们先来看下这段代码做了什么吧:

  /*    * 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  (4096 + _KERNPG_TABLE)(%rbx), %rdx   movq  %rdx, 0(%rbx,%rax,8)   movq  %rdx, 8(%rbx,%rax,8)    addq  $4096, %rdx   movq  %rdi, %rax   shrq  $PUD_SHIFT, %rax   andl  $(PTRS_PER_PUD-1), %eax   movq  %rdx, 4096(%rbx,%rax,8)   incl  %eax   andl  $(PTRS_PER_PUD-1), %eax   movq  %rdx, 4096(%rbx,%rax,8)    addq  $8192, %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 

我们可以稍微进行一个计算,首先 %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 文件中找到这几个新添加的页表页的定义:

  __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的准备。

至此,页表变成了这个样子。

Linux内存初始化(汇编部分)

startup_64 最后一步就是fixup内核段真正的物理页对应的页表项了,代码如下所示:

  /*    * 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(%rdi)   jz  2f   addq  %rbp, 0(%rdi)   /* Go to the next page */ 2:  addq  $8, %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

这部分代码的主要功能是一些模式的开启,以及相关数据结构的加载,我们同样逐段进行分析:

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 。至此,分页模式开启!

  /* 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  $MSR_EFER, %ecx   rdmsr   btsl  $_EFER_SCE, %eax  /* Enable System Call */   btl $20,%edi    /* No Execute supported? */   jnc     1f   btsl  $_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:

/*    * We must switch to a new descriptor 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) 

初始化段寄存器:

  /* 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的跳转:

  /* 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 定义为:

  GLOBAL(initial_code)   .quad x86_64_start_kernel 

因此,最后进入了 x86_64_start_kernel 函数,这是一个C语言写的函数,所以,会在下一篇博客中进行介绍。

原文  http://ytliu.info/blog/2016/03/14/linuxnei-cun-chu-shi-hua-assembly/
正文到此结束
Loading...