引导程序加载完成后,会执行 arch/arm64/kernel/head.s 中的汇编代码。

😁_head 段

  1. /*
  2. * 内核启动入口点.
  3. * ---------------------------
  4. *
  5. * 要求:
  6. * MMU = off, D-cache = off, I-cache = on or off,
  7. * x0 = physical address to the FDT blob.
  8. *
  9. * 这段代码的位置是独立的, 所以你可以通过下面的语句进行调用
  10. * __pa(PAGE_OFFSET + TEXT_OFFSET).
  11. */
  12. __HEAD
  13. _head:
  14. #ifdef CONFIG_EFI
  15. /*
  16. * 这个 add 指令除了生成 UEFI 需要的 MZ 标志位外没有其他作用.
  17. */
  18. add x13, x18, #0x16
  19. b stext
  20. #else
  21. b stext // branch to kernel start, magic
  22. .long 0 // reserved
  23. #endif

这段代码主要用于开启对 UEFI 的支持;UEFI(Unified Extensible Firmware Interface)是统一的可扩展固件接口,用于取代BIOS。

😂_stext 段

  1. /*
  2. * 下面的被调用者保存的通用寄存器用于主低级别引导:
  3. *
  4. * Register Scope Purpose
  5. * x21 stext() .. start_kernel() 在 x0 引导中传递的 FDT 指针
  6. * x23 stext() .. start_kernel() 物理偏移/KASLR offset
  7. * x28 __create_page_tables() 被调用者保存的临时寄存器
  8. * x19/x20 __primary_switch() 被调用者保存的临时寄存器
  9. */
  10. ENTRY(stext)
  11. bl preserve_boot_args // 把引导程序传递的 4 个参数保存在全局数组 boot_args 中
  12. bl el2_setup // 判断是否需要将异常级别降低到 EL1
  13. adrp x23, __PHYS_OFFSET // 保存物理内核物理偏移
  14. and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR 偏移, 默认是 0
  15. bl set_cpu_boot_mode_flag // 更具异常级别设置 CPU 运行模式
  16. bl __create_page_tables // 创建页表映射
  17. /*
  18. * 下面的 CPU 启动代码的细节可以查看 arch/arm64/mm/proc.S
  19. * 执行完成时, CPU 将会开启 MMU和 TCR.
  20. */
  21. bl __cpu_setup // 初始化处理器
  22. /*
  23. * 为主处理器开启内存管理单元, 搭建 C 语言执行环境, 进入 C 语言部分的入口函数 start_kernel.
  24. */
  25. b __primary_switch
  26. ENDPROC(stext)

🍕el2_setup 函数

el2_setup 函数的主要工作如下:

  • 进入内核的时候,ARM64 处理器的异常级别可能是 1 或者 2
  • 如果异常级别是 1,那么在异常级别 1 执行内核
  • 如果异常级别是 2,那么根据处理器是否支持 虚拟化宿主扩展(Virtualization Host Extensions, VHE),决定是否需要降级到异常级别1

    • 如果处理器支持虚拟化宿主扩展,那么在异常级别 2 执行内核
    • 如果处理器不支持虚拟化宿主扩展,那么降级到异常级别 1,在异常级别 1 执行内核

      🍔 __create_page_tables 函数

      ```c /*

      • 设置初始化页表. 我们仅仅设置提供内核运行的最基本的页表数量.
      • 需要完成下面的工作:
        • 创建恒等映射, 开启 MMU (低地址, TTBR0)
        • 启动 MMU 之后直接跳转到内核开始的数个 MB */ __create_page_tables: mov x28, lr

      /*

      • 使 idmap 和 swapper 页表无效,以避免收回潜在的脏缓存. */ adrp x0, idmap_pg_dir adrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE bl __inval_cache_range

      /*

      • 清空 idmap 和 swapper 页表. */ adrp x0, idmap_pg_dir adrp x6, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE 1: stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 cmp x0, x6 b.lo 1b

      mov x7, SWAPPER_MM_MMUFLAGS

      /*

      • 创建恒等映射. */ adrp x0, idmap_pg_dir adrp x3, idmap_text_start // pa(__idmap_text_start)

ifndef CONFIG_ARM64_VA_BITS_48

define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)

define EXTRA_PTRS (1 << (48 - EXTRA_SHIFT))

  1. /*
  2. * If VA_BITS < 48, it may be too small to allow for an ID mapping to be
  3. * created that covers system RAM if that is located sufficiently high
  4. * in the physical address space. So for the ID map, use an extended
  5. * virtual range in that case, by configuring an additional translation
  6. * level.
  7. * First, we have to verify our assumption that the current value of
  8. * VA_BITS was chosen such that all translation levels are fully
  9. * utilised, and that lowering T0SZ will always result in an additional
  10. * translation level to be configured.
  11. */

if VA_BITS != EXTRA_SHIFT

error “Mismatch between VA_BITS and page size/number of translation levels”

endif

  1. /*
  2. * 计算 TCR_EL1.T0SZ 的最大允许值, 以便可以映射整个ID映射区域.
  3. * 由于 T0SZ == (64 - #bits used), 这个数字恰好等于
  4. * __idmap_text_end 的物理地址中前导零的数量.
  5. */
  6. adrp x5, __idmap_text_end
  7. clz x5, x5
  8. cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?
  9. b.ge 1f // .. then skip additional level
  10. adr_l x6, idmap_t0sz
  11. str x5, [x6]
  12. dmb sy
  13. dc ivac, x6 // Invalidate potentially stale cache line
  14. create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6

1:

endif

  1. create_pgd_entry x0, x3, x5, x6
  2. mov x5, x3 // __pa(__idmap_text_start)
  3. adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
  4. create_block_map x0, x7, x3, x5, x6
  5. /*
  6. * 映射内核镜像 (从 PHYS_OFFSET 开始).
  7. */
  8. adrp x0, swapper_pg_dir
  9. mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
  10. add x5, x5, x23 // add KASLR displacement
  11. create_pgd_entry x0, x5, x3, x6
  12. adrp x6, _end // runtime __pa(_end)
  13. adrp x3, _text // runtime __pa(_text)
  14. sub x6, x6, x3 // _end - _text
  15. add x6, x6, x5 // runtime __va(_end)
  16. create_block_map x0, x7, x3, x5, x6
  17. /*
  18. * 由于页表中填充了不可缓存的访问 (禁用了MMU),
  19. * 再次使 idmap 和 swapper 页表无效,以删除任何加载的缓存线.
  20. */
  21. adrp x0, idmap_pg_dir
  22. adrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE
  23. dmb sy
  24. bl __inval_cache_range
  25. ret x28

ENDPROC(__create_page_tables)

  1. __create_page_tables 函数的主要工作如下:
  2. 1. 创建恒等映射(identity mapping
  3. 1. 为内核镜像创建映射
  4. 恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。<br />函数 __enable_mmu 负责开启内存管理单元,内核把函数 __enable_mmu 附近的代码放在恒等映射代码节(.idmap.text)里面,恒等映射代码节的起始地址存放在全局变量 __idmap_text_start 中,结束地址存放在全局变量 __idmap_text_end 中。<br />恒等映射是为恒等映射代码节创建的映射,idmap_pg_dir 是恒等映射的页全局目录(即第一级页表)的起始地址。<br />在内核的页表中为内核镜像创建映射,内核镜像的起始地址是 _text,结束地址是 _endswapper_pg_dir 是内核的页全局目录的起始地址。
  5. <a name="7kEF5"></a>
  6. ## 🍟__primary_switch 函数
  7. __primary_switch 函数的主要执行流程如下:
  8. 1. 调用函数 __enable_mmu 以开启内存管理单元
  9. 1. 调用函数 __primary_switched
  10. <a name="VnZSc"></a>
  11. ### 🚗__enable_mmu 函数
  12. __enable_mmu 函数的主要执行流程如下:
  13. 1. 把转换表基准寄存器 0TTBR0_EL1)设置为恒等映射的页全局目录的起始物理地址
  14. 1. 把转换表基准寄存器 1TTBR1_EL1)设置为内核的页全局目录的起始物理地址
  15. 1. 设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址
  16. <a name="zQj0k"></a>
  17. ### 🚓__primary_switched 函数
  18. ```c
  19. /*
  20. * 下面的代码需要在 MMU 开启后执行.
  21. *
  22. * x0 = __PHYS_OFFSET
  23. */
  24. __primary_switched:
  25. adrp x4, init_thread_union
  26. add sp, x4, #THREAD_SIZE // 将栈指针指向 0 号线程顶部
  27. adr_l x5, init_task
  28. msr sp_el0, x5 // 保存线程信息
  29. adr_l x8, vectors // 将 VBAR_EL1 设置为虚拟
  30. msr vbar_el1, x8 // 向量表的初始地址
  31. isb
  32. stp xzr, x30, [sp, #-16]!
  33. mov x29, sp
  34. str_l x21, __fdt_pointer, x5 // 保存 FDT 指针
  35. ldr_l x4, kimage_vaddr // 保存内核虚拟地址
  36. sub x4, x4, x0 // 与实际地址之间
  37. str_l x4, kimage_voffset, x5 // 的差值
  38. // Clear BSS
  39. adr_l x0, __bss_start
  40. mov x1, xzr
  41. adr_l x2, __bss_stop
  42. sub x2, x2, x0
  43. bl __pi_memset
  44. dsb ishst // 用 0 初始化内核的未初始化数据段
  45. /*
  46. * 下面的这些宏配置不是特别懂
  47. */
  48. #ifdef CONFIG_KASAN
  49. bl kasan_early_init
  50. #endif
  51. #ifdef CONFIG_RANDOMIZE_BASE
  52. tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?
  53. b.ne 0f
  54. mov x0, x21 // pass FDT address in x0
  55. mov x1, x23 // pass modulo offset in x1
  56. bl kaslr_early_init // parse FDT for KASLR options
  57. cbz x0, 0f // KASLR disabled? just proceed
  58. orr x23, x23, x0 // record KASLR offset
  59. ldp x29, x30, [sp], #16 // we must enable KASLR, return
  60. ret // to __primary_switch()
  61. 0:
  62. #endif
  63. b start_kernel // 调用 C 语言函数 start_kernel
  64. ENDPROC(__primary_switched)

__primary_switched 函数的执行流程如下

  1. 把当前异常级别的栈指针寄存器设置为 0 号线程内核栈的顶部(init_thread_union +THREAD_SIZE)
  2. 把异常级别 0 的栈指针寄存器(SP_EL0)设置为 0 号线程的结构体 thread_info 的地址(init_task.thread_info)
  3. 把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址(vectors)
  4. 计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量 kimage_voffset 中
  5. 用 0 初始化内核的未初始化数据段
  6. 调用 C 语言函数 start_kernel