Linux 内核启动分析

Linux 内核启动分析-BugMan-ChinaUnix博客

通过《Linux应用程序elf描述》,我们了解到一个应用程序编译后,最终会按照指定方式进行链接,而我们通过ld --verbose可以查看对应应用的默认链接方式。那么对于Linux内核呢?毫无疑问, Linux内核也是按照指定格式进行链接的,只是Linux的链接方式是由arch/arm64/kernel/vmlinux.lds.S指定的(gcc可以在链接的时候指定自定义链接脚本-T)。本章基于Linux内核Linux-4.19.73来作为例子说明的。

首先,我们看看vxlinux.lds.S为何物,如下图:

Linux 内核启动分析_第1张图片

上图是一个删减了很多其他暂时不关心段的vmlinux.ld.s脚本文件,vmlinux.ld.s脚本文件的语法,这里不作出介绍, 关注的可以去查看相关文档。通过vmlinux.ld.s我们可以看到,bin文件的入口被设置为ENTRY(_text),  因此,_text即为入口函数地址,那么_text又在那里呢?那么接着看这个脚本文件,我们发现在灰色框里面有一个_text = .; 这说明_text的地址就是.head.text的首地址。那么哪些数据被链接到.head.text段了呢?通过搜索发现

点击(此处)折叠或打开

  1. #define HEAD_TEXT  KEEP(*(.head.text))
  2. #define __HEAD .section ".head.text","ax"
  3. $ grep __HEAD arch/arm64/ -r
  4. include/linux/init.h:#define __HEAD             .section        ".head.text","ax"
  5. $grep __HEAD arch/arm64/ -r
  6. arch/arm64/kernel/head.S:       __HEAD

因此, 目前只有kernel/head.S有代码被放置在了.head.text段,我们进一步看看arch/arm64/kernel/head.S文件,如下:
点击(此处)折叠或打开

  1. #define __PHYS_OFFSET   (KERNEL_START - TEXT_OFFSET) // 内核物理地址起始位置
  2. __HEAD
  3. _head:
  4.     b stext // branch to kernel start, magic
  5.     .long 0 // reserved
  6.     le64sym _kernel_offset_le // Image load offset from start of RAM, little-endian
  7.     le64sym _kernel_size_le // Effective size of kernel image, little-endian
  8.     le64sym _kernel_flags_le // Informative flags, little-endian
  9.     .quad 0 // reserved
  10.     .quad 0 // reserved
  11.     .quad 0 // reserved
  12.     .ascii "ARM\x64" // Magic number
  13.     .long 0 // reserved
  14. __INIT
  15. ENTRY(stext)
  16.     bl  preserve_boot_args
  17.     bl  el2_setup           // Drop to EL1, w0=cpu_boot_mode
  18.     adrp    x23, __PHYS_OFFSET // 物理地址偏移
  19.     and x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0,一种内核安全机制,通过物理地址起始位置计算出偏移大小,偏移大小保存在X23寄存器
  20.     bl  set_cpu_boot_mode_flag
  21.     bl  __create_page_tables
  22.     bl  __cpu_setup         // initialise processor
  23.     b   __primary_switch
  24. ENDPROC(stext)
  25. $ grep __INIT include/ -r
  26. include/linux/init.h:#define __INIT             .section        ".init.text","ax"
  27. $

从上面代码可以看出,只有_head被放在了.head.text段,而下面的stext是放在.init.text段的。因此,当前版本的Linux kernel的入口函数就是_head函数, 而_head函数就只有一条跳转指令:b stext;因此内核启动后, 最终去stext函数运行。而stext主要调用了几个函数,他们的作用如下:

1、preserve_boot_args: 将uboot传入的参数 保存到bootargs[4] 全局变量里面。

2、el2_setup :判断启动的模式是el2还是el1并进行相关级别的系统配置(armv8中el2是hypervisor模式,el1是标准的内核模式,具体的参考手册),  然后返回启动模式

3、set_cpu_boot_mode_flag: 将启动模式保存到全局变量

4、__create_page_tables: 创建内存映射表,一共两张,一张存放在swapper_pg_dir(线性映射),一张存放在idmap_pg_dir(一对一映射)。

5、__cpu_setup : 初始化处理器相关的代码,配置访问权限,内存地址划分等。

6、__primary_switch :开启MMU, 准备0号进程和内核栈,然后跳转到start_kernel运行

首先,我们说说preserve_boot_args函数, 它的实现如下:

点击(此处)折叠或打开

  1. preserve_boot_args:
  2.     mov x21, x0 // 默认x0是uboot传入的第一个参数,通常是fdt的基地址,这里给x21寄存器保存
  3.     adr_l x0, boot_args //adr指令读取boot_args变量的当前地址,而不是链接地址(因为此时还没没有创建映射表,链接地址占时还不能用),boot_args是一个全局变量,默认地址是链接地址。
  4.     stp x21, x1, [x0] // 将uboot传入的第一个参数和第二个参数保存到boot_args的[0],[1]里面,表示地址和大小
  5.     stp x2, x3, [x0, #16] // 将uboot传入的第三个核第四个参数保存到boot_args的[2],[3]变量里面
  6.     dmb sy // 数据存储器栅栏,具体作用参考汇编手册
  7.     mov x1, #0x20 // boot_args有四个变量,每个变量8字节大小,因此 x1存入boot_args的长度(x0是boot_args的地址),然后调用_inval_dcache_area无效这段地址的缓存
  8.     b __inval_dcache_area // 无效x0和x1指定区域的缓存
  9. ENDPROC(preserve_boot_args)

其次是el2_setup 函数, 它的实现如下:

点击(此处)折叠或打开

  1. ENTRY(el2_setup)
  2.     msr SPsel, #1 // 设置SP的使用方式,是各用各的 还是共用一个,这里设置的是各用各的(armv8的栈使用)
  3.     mrs x0, CurrentEL // 读取当前的EL模式
  4.     cmp x0, #CurrentEL_EL2 // 判断当前的模式是不是el2,是 就跳转到el2的处理代码
  5.     b.eq 1f
  6.     mov_q x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) // 配置el1模式
  7.     msr sctlr_el1, x0
  8.     mov w0, #BOOT_CPU_MODE_EL1 // 返回值设置成 el1模式启动,注:w0是32位寄存器,通常x0/w0作为函数返回值使用
  9.     isb
  10.     ret
  11. // 下面是el2,即hypervisor模式的处理代码,这里不介绍,在hypervisor会有介绍
  12. 1: mov_q x0, (SCTLR_EL2_RES1 | ENDIAN_SET_EL2)
  13.     msr sctlr_el2, x0
  14.     .............
  15.     eret
  16. ENDPROC(el2_setup)

然后set_cpu_boot_mode_flag函数用于保存启动模式,该函数实现如下:

点击(此处)折叠或打开

  1. set_cpu_boot_mode_flag:
  2.     adr_l x1, __boot_cpu_mode //将_boot_cpu_mode的物理地址读取到x1寄存器
  3.     cmp w0, #BOOT_CPU_MODE_EL2 // w0是el2_setup返回的值,即模式
  4.     b.ne 1f
  5.     add x1, x1, #4
  6. 1: str w0, [x1] // This CPU has booted in EL1,将模式保存到_boot_cpu_mode变量
  7.     dmb sy
  8.     dc ivac, x1 // Invalidate potentially stale cache line
  9.     ret
  10. ENDPROC(set_cpu_boot_mode_flag)

对于__create_page_tables,则主要是创建内存映射表(这里只是简单的映射,只把内核代码段映射进来,用于开启MMU),后期还会做出二次映射。在映射函数中,有两种映射,一个是直接映射(即va=pa, 用于处理开启mmu那一瞬间不会出现异常),一个是线性映射(va = pa + offset)。具体函数如下:

点击(此处)折叠或打开

  1. __create_page_tables:
  2.     mov x28, lr
  3.     // 无效 idmap_pg_dir和swpper_pg_end直接的数据缓存
  4.     adrp x0, idmap_pg_dir
  5.     adrp x1, swapper_pg_end
  6.     sub x1, x1, x0
  7.     bl __inval_dcache_area
  8.     // 清楚idmap和swapper映射表里的脏数据
  9.     adrp x0, idmap_pg_dir
  10.     adrp x1, swapper_pg_end
  11.     sub x1, x1, x0
  12. 1: stp xzr, xzr, [x0], #16
  13.     stp xzr, xzr, [x0], #16
  14.     stp xzr, xzr, [x0], #16
  15.     stp xzr, xzr, [x0], #16
  16.     subs x1, x1, #64
  17.     b.ne 1b
  18.     // mmu也属性标记
  19.     mov x7, SWAPPER_MM_MMUFLAGS
  20.     //创建直接映射 idmap,从idmap_text_start到idmap_text_end
  21.     adrp x0, idmap_pg_dir
  22.     adrp x3, __idmap_text_start // __pa(__idmap_text_start)
  23.     adrp x5, __idmap_text_end
  24.     clz x5, x5
  25.     cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?
  26.     b.ge 1f // .. then skip VA range extension
  27.     adr_l x6, idmap_t0sz
  28.     str x5, [x6]
  29.     dmb sy
  30.     dc ivac, x6 // Invalidate potentially stale cache line
  31.     mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
  32.     // VA_BITS = 48bit
  33.     str_l   x4, idmap_ptrs_per_pgd, x5
  34.     ldr_l   x4, idmap_ptrs_per_pgd
  35.     mov x5, x3              // __pa(__idmap_text_start)
  36.     adr_l   x6, __idmap_text_end        // __pa(__idmap_text_end)
  37.     // map_memory用于映射, 具体怎么写映射表, 参考armv8体系结构
  38.     map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
  39.     // 线性映射内核代码段, 存放到swapper_pg_dir, 从_text段开始到_end之间的数据
  40.     adrp    x0, swapper_pg_dir
  41.     mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
  42.     add x5, x5, x23         // add KASLR displacement
  43.     mov x4, PTRS_PER_PGD
  44.     adrp    x6, _end            // runtime __pa(_end)
  45.     adrp    x3, _text           // runtime __pa(_text)
  46.     sub x6, x6, x3          // _end - _text
  47.     add x6, x6, x5          // runtime __va(_end)
  48.     // map_memory用于映射, 具体怎么写映射表, 参考armv8体系结构
  49.     map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
  50.     // 无效映射表对应的缓存
  51.     adrp    x0, idmap_pg_dir
  52.     adrp    x1, swapper_pg_end
  53.     sub x1, x1, x0
  54.     dmb sy
  55.     bl  __inval_dcache_area
  56.     ret x28
  57. ENDPROC(__create_page_tables)

注:__idmap_text_start到__idmap_text_end的数据,其实就是启用mmu前后,需调用的那几个函数(因为CPU有加速指令处理的关系, 有些指令是乱序执行,防止开启mmu后,因为地址空间切换,导致的代码混乱的问题),因为有一段是va=pa因此, 之后即使还有code在用老的物理地址,也是不会出问题的。

__cpu_setup主要设置一些访问属性和内存划分等 ,具体函数如下:

点击(此处)折叠或打开

  1. ENTRY(__cpu_setup)
  2.     tlbi vmalle1   // 无效TLB
  3.     dsb nsh
  4.     mov x0, #3 << 20
  5.     msr cpacr_el1, x0 // 使能FP/ASIMD
  6.     mov x0, #1 << 12
  7.     msr mdscr_el1, x0 // 允许EL0访问DCC
  8.     isb 
  9.     reset_pmuserenr_el0 x0 // 设置EL0禁止PMU访问
  10.     /*
  11.      * LPAE内存属性:
  12.      *
  13.      * n = AttrIndx[2:0]
  14.      * n MAIR
  15.      * DEVICE_nGnRnE 000 00000000
  16.      * DEVICE_nGnRE 001 00000100
  17.      * DEVICE_GRE 010 00001100
  18.      * NORMAL_NC 011 01000100
  19.      * NORMAL 100 11111111
  20.      * NORMAL_WT 101 10111011
  21.      */
  22.     ldr x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
  23.              MAIR(0x04, MT_DEVICE_nGnRE) | \
  24.              MAIR(0x0c, MT_DEVICE_GRE) | \
  25.              MAIR(0x44, MT_NORMAL_NC) | \
  26.              MAIR(0xff, MT_NORMAL) | \
  27.              MAIR(0xbb, MT_NORMAL_WT)
  28.     msr mair_el1, x5
  29.     mov_q x0, SCTLR_EL1_SET
  30.     /*
  31.      * 设置 TCR and TTBR. 用户和内核采用512GB (39-bit) 地址
  32.      */
  33.     ldr x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
  34.             TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
  35.             TCR_TBI0 | TCR_A1
  36.     tcr_set_idmap_t0sz x10, x9
  37.     /*
  38.      * Set the IPS bits in TCR_EL1.
  39.      */
  40.     tcr_compute_pa_size x10, #TCR_IPS_SHIFT, x5, x6
  41.     msr tcr_el1, x10
  42.     ret
  43. ENDPROC(__cpu_setup)

最后__primary_switch准备好0号进程栈,然后切换到start_kernel运行,具体代码实现如下:

点击(此处)折叠或打开

  1. __primary_switch:
  2. #ifdef CONFIG_RANDOMIZE_BASE
  3.     mov x19, x0 // preserve new SCTLR_EL1 value
  4.     mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value
  5. #endif
  6.     bl __enable_mmu   // 开启mmu ,就是只是配置一些MMU寄存器
  7. #ifdef CONFIG_RELOCATABLE
  8.     ......  // 这里省略掉 内核代码重定位代码,这个主要用于gdb调试驱动
  9. #endif
  10.     ldr x8, =__primary_switched  // 将内核的物理地址起始地址作为参数1,调用_primary_switched函数
  11.     adrp x0, __PHYS_OFFSET
  12.     br x8
  13. ENDPROC(__primary_switch)

  1. union thread_union {
  2.      unsigned long stack[THREAD_SIZE/sizeof(long)];
  3. } init_thread_union;

  1. __primary_switched:
  2.     adrp    x4, init_thread_union  // 读取0号进程的thread_union地址
  3.     add sp, x4, #THREAD_SIZE       // 将init_thread_union  +THREAD_SIZE作为内核线程的栈顶地址
  4.     adr_l   x5, init_task          // 读取0号进程的task_struct结构
  5.     msr sp_el0, x5                 // 在内核空间中,将当前task_sturct给sp_el0保存
  6.     adr_l   x8, vectors         // 设置中断向量表,vector在中断章节说明
  7.     msr vbar_el1, x8            // 系统寄存器vector table address
  8.     isb
  9.     str_l   x21, __fdt_pointer, x5      // X21存放的是fdt指针, 这里将fdt保存到__fdt_pointer
  10.     // 下面是保存虚拟地址和物理地址之差到kimg_voffset变量
  11.     ldr_l   x4, kimage_vaddr  // 获取到内核虚拟起始地址
  12.     sub x4, x4, x0    // x0是传参传入的 内核物理起始地址
  13.     str_l   x4, kimage_voffset, x5
  14.     // 将内核BSS段 请0
  15.     adr_l   x0, __bss_start
  16.     mov x1, xzr
  17.     adr_l   x2, __bss_stop
  18.     sub x2, x2, x0
  19.     bl  __pi_memset
  20.     dsb ishst               // Make zero page visible to PTW
  21. #ifdef CONFIG_KASAN
  22.     bl  kasan_early_init  // 一种内存调试手段初始化
  23. #endif
  24.     mov x30, #0  // x30是Lr寄存器, 这里赋值成NULL,不需要返回,返回即异常
  25.     b   start_kernel // 跳转到start_kernel运行
  26. ENDPROC(__primary_switched)

最后Linux内核进入C代码空间,start_kernel。

注: PAGE_OFFSET是内核虚拟地址起始地址, PAGE_SHIFT是页大小位数, TEXT_OFFSET是内核代码起始位置到内核起始地址的偏移。
注:在32位CPU中, 内核通常会保留开始的32k(0x8000)地址,前16k(0x4000)保存bootargs参数,后16k用于保存pgd,因此可以看到内核的代码地址基本都是0x8000开始,如0xC0008000.
注:vectors向量表位于:"arch/arm64/kernel/entry.S"文件中,实现如下:
点击(此处)折叠或打开

  1. ENTRY(vectors)
  2.     kernel_ventry 1, sync_invalid // Synchronous EL1t
  3.     kernel_ventry 1, irq_invalid // IRQ EL1t
  4.     kernel_ventry 1, fiq_invalid // FIQ EL1t
  5.     kernel_ventry 1, error_invalid // Error EL1t
  6.     kernel_ventry 1, sync // Synchronous EL1h
  7.     kernel_ventry 1, irq // IRQ EL1h
  8.     kernel_ventry 1, fiq_invalid // FIQ EL1h
  9.     kernel_ventry 1, error // Error EL1h
  10.     kernel_ventry 0, sync // Synchronous 64-bit EL0
  11.     kernel_ventry 0, irq // IRQ 64-bit EL0
  12.     kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
  13.     kernel_ventry 0, error // Error 64-bit EL0
  14. #ifdef CONFIG_COMPAT
  15.     kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
  16.     kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
  17.     kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
  18.     kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
  19. #else
  20.     kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
  21.     kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
  22.     kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
  23.     kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
  24. #endif
  25. END(vectors)

注:armv8中,每个异常的 向量地址不再是4字节,而是0x80字节,可以放更多的代码在向量表里面。

你可能感兴趣的:(ARM64_V8V9,linux,运维,服务器)