您好,歡迎來電子發(fā)燒友網(wǎng)! ,新用戶?[免費(fèi)注冊]

您的位置:電子發(fā)燒友網(wǎng)>源碼下載>匯編編程>

Linux內(nèi)存初始化

大?。?/span>0.4 MB 人氣: 2017-10-12 需要積分:1
之前有幾篇博客詳細(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%

      發(fā)表評論

      用戶評論
      評價(jià):好評中評差評

      發(fā)表評論,獲取積分! 請遵守相關(guān)規(guī)定!

      ?