kernel启动流程-start_kernel的执行_2.setup_arch

目录

  • 1.前言
  • 2.setup_arch(&command_line)
    • 2.1 init_mm
    • 2.2 global mapping
    • 2.3 early_fixmap_init
    • 2.4 early_ioremap_init
    • 2.5 setup_machine_fdt
    • 2.6 jump_label_init
    • 2.7 parse_early_param
    • 2.8 arm64_memblock_init
    • 2.9 paging_init
    • 2.10 acpi init
    • 2.11 bootmem_init
    • 2.12 kasan_init
    • 2.13 request_standard_resources
    • 2.14 early_ioremap_reset
    • 2.15 psci_dt_init
    • 2.16 init_bootcpu_ops
    • 2.17 smp_init_cpus
    • 2.18 smp_build_mpidr_hash
    • 2.19 kasan_init_tags
    • 2.20 CONFIG_ARM64_SW_TTBR0_PAN
    • 2.21 boot_args检查
  • 3.总结
  • 参考文档

1.前言

本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本文重点介绍start_kernel的setup_arch的主要流程.

kernel版本:5.10
平台:arm64

2.setup_arch(&command_line)

2.1 init_mm

init_mm.start_code = (unsigned long) _text;   
init_mm.end_code   = (unsigned long) _etext;   
init_mm.end_data   = (unsigned long) _edata; 

init_mm是init进程(0号进程)的内存描述符,定义在mm/init-mm.c,此处初始化它的代码段起始/结束地址,数据段的结束地址,brk的起始地址

2.2 global mapping

 arm64_use_ng_mappings = kaslr_requires_kpti();

KPTI(kernel page-table isolation)内核页表隔离
KASLR(Kernel Address Space Layout Randomization).
此处判断是否使用KPTI机制,此决定了是否创建非全局页表。

由于meladown的漏洞导致了用户空间可以间接获取到内核空间的数据,在运行user application 的时候,将kernel mapping 减少到最少,只保留必须的user到kernel的exception entry mapping. 其他的kernel mapping 在运行user application时都去掉,变成无效mapping,这样的话,如果user访问kernel data, 在MMU地址转换的时候就会被挡掉(因为无效mapping).另外为内核空间分配ASID(用户空间和内核空间ASDI分别采奇偶数),这样用户空间访问内核数据,在TLB时就会被挡住,可参考文档2

2.3 early_fixmap_init

early_fixmap_init(); 

early_fixmap_init就是为FIXADDR_START地址创建了页表项,fixmap区域pgd页表位于kernel image的init_pg_dir地址,而fixmap的pud, pmd, pte页表则位于kernel image的bss段,也就是说init_pg_dir,bm_pud, bm_pmd, bm_pte就是用来存放fixup固定映射区相应页表项的,而临时映射区的含义就是它会被用来临时与物理内存建立虚实映射,之后这个映射会被再接触,如对于swapper_pg就是在填充前建立映射,填充完毕后映射

from:https://www.cnblogs.com/LoyenWang/p/11440957.html
Uboot会将kernel image和dtb拷贝到内存中,并且将dtb物理地址告知kernel,kernel需要从该物理地址上读取到dtb文件并解析,才能得到最终的内存信息,dtb的物理地址需要映射到虚拟地址上才能访问,但是这个时候paging_init还没有调用,也就是说物理地址的映射还没有完成,那该怎么办呢?没错,Fixed map机制出现了。

kernel启动流程-start_kernel的执行_2.setup_arch_第1张图片
虚拟地址空间的fixmap区域范围为FIXADDR_START~FIXADDR_TOP,从打印来看本例为:
0xfffffdfffe5f9000-0xfffffdfffea00000
fixup区域被划分成多个区间,enum fixed_addresses 定义了每个区间的索引,每个区间的起始地址为:
FIXADDR_TOP - (区间索引 << PAGE_SHIFT)
对于FDT的映射区间为:

FIXADDR_TOP - (FIX_FDT<< PAGE_SHIFT) ~ FIXADDR_TOP - (FIX_FDT_END<< PAGE_SHIFT)

对于IO的映射区间为:

FIXADDR_TOP - (FIX_BTMAP_BEGIN << PAGE_SHIFT) ~ FIXADDR_TOP - (FIX_BTMAP_END << PAGE_SHIFT)

可以看出early_fixmap_init就是为FIXADDR_START地址创建了页表项,fixmap区域pgd页表位于kernel image的init_pg_dir地址,而fixmap的pud, pmd, pte页表则位于kernel image的bss段(如下),也就是说init_pg_dir,bm_pud, bm_pmd, bm_pte就是用来存放fixup固定映射区相应页表项的

#arch/arm64/mm/mmu.c
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;

PTRS_PER_PTE只有512项,也就是同一时刻只有一个非block pmd页表项有效,而一个pmd页表项可以映射2M空间,这里需要注意的是由于fixmap区域有4M(0xfffffdfffe5f9000-0xfffffdfffea00000)空间,因此从FIXMAP的FDT区域开始为另外2M空间,需要另外一个pmd页表项管理,而fdt要求是2M block映射,因此不需要pte页表,直接指向了2M的内存block.
参考文章:Fixmap机制深入分析

1.事实上,early_fixmap_init只是建立了一个映射的框架,只填充了FIXADDR_START地址的pgd, pud,pmd页表项,具体的物理地址和虚拟地址的映射PTE页表项没有去填充,这个是由使用者具体在使用时再去填充对应的pte entry。比如像fixmap_remap_fdt()函数,就是典型的填充pte entry的过程,完成最后的一步映射,然后才能读取dtb文件。
2. FIXMAP映射区中的PGD,PUD, PMD, PTE又是作何用? 因为用于fixmap映射的静态页表也是需要访问的,需要为这些页表建立映射关系才能访问,PGD,PUD, PMD, PTE就是为映射这些页表提供的虚拟地址映射区,由于它们在访问后会解除映射,因此称为临时映射区

内核确保在上下文切换期间,对应于固定映射的页表项不会从TLB刷出,因此在访问固定映射的内存时,总是通过TLB高速缓存取得对应的物理地址

2.4 early_ioremap_init

early_ioremap_init();

early_ioremap_init将ioremap的空间为7 * 256K的区域,保存在slot_vir[]数组中
当需要进行IO操作的时候,最终会调用到__early_ioremap函数,在该函数中去填充对应的pte entry,从而完成最终的虚拟地址和物理地址的映射。

页表创建解决两个问题:一是映射区所在的区域;二是页表存放的物理位置。
从启动之初到此处我们一共有三次创建页表的动作,简单总结一下:

  1. 为kernel image的.idmap.text段创建映射,页表存放在kernel image的idmap_pg_dir开始的区域,因为是恒等映射,虚拟地址映射区为与.idmap.text段所在的物理区域相同;
  2. 为整个kernel image创建映射,页表存放在kernel image的init_pg_dir开始的区域;
  3. 为fixup区域创建映射,fixup区域的pgd页表存放在kernel image的init_pg_dir开始的区域,fixup区域的pud,pmd,pte页表存放在kernel image的bss段,注意此处只是创建了一个映射的框架,并未实际填充pte页表项

2.5 setup_machine_fdt

setup_machine_fdt(__fdt_pointer);  

我们在前面head.S的__primary_switched中分析过,FDT的物理地址会保存到__fdt_pointer中,此处通过fixmap_remap_fdt为FDT物理区域创建PTE页表项,映射到fixup映射区的FDT索引区域,这样就可以通过虚拟地址访问到FDT了
setup_machine_fdt主要做了如下工作:

  1. 通过fixmap_remap_fdt为fdt创建页表项,将映射到fixup固定映射区的FDT索引区域
  2. 通过memblock_reserve(dt_phys, size)记录FDT对应的物理空间到memblock.reserved区域,本例中通过GDB可以看到为:
    base:0x48000000, size:0x100000,2MB
  3. early_init_dt_scan将会通过memblock_add将memory节点描述的区域记录到memblock.memory区域,初始化memblock.memory下的各个memblock_region,每个memblock_region可理解为一个物理内存bank区域,比如有两个DDR芯片,则每个DDR芯片的物理区间对应一个bank本例通过GDB查看为:
    base:0x40000000, size:0x40000000,1GB
  4. 解析chosen中的bootargs命令行存放到boot_command_line

2.6 jump_label_init

2.7 parse_early_param

   /* 
         * Initialise the static keys early as they may be enabled by the 
         * cpufeature code and early parameters. 
         */  
        jump_label_init();  
        parse_early_param();
  • jump_label_init:初始化jump-label子系统(TODO)
  • parse_early_param:内核中会通过early_param宏定义一些参数,定义的时候会指定这些参数的回调函数,并以结构体的方式放入.init.setup段,此处将遍历.init.setup段,与command line进行匹配,如果command line有参数与.init.setup中预定义的参数匹配,则会执行此参数对应的回调。
    需要注意的是__setup宏也可以预定义参数,并保存到.init.setup段,但是由于parse_early_param->parse_early_options->parse_args->do_early_param
    会查看early标记,如果early标记没置位,则不会执行相应的回调。
    详细可参考 __setup,early_param宏
/* 
 * Unmask asynchronous aborts and fiq after bringing up possible 
 * earlycon. (Report possible System Errors once we can report this
 * occurred). 
 */ 
local_daif_restore(DAIF_PROCCTX_NOIRQ);

unmask asynchronous aborts and fiq以尽可能早的报告错误

/* 
 * TTBR0 is only used for the identity mapping at this stage. Make it  
 * point to zero page to avoid speculatively fetching new entries. 
 */
cpu_uninstall_idmap();

从TTBR0移除idmap,让TTBR0指向zero页面。在此过程中需要清除idmap的残留,因此需要清空TLB操作。

xen_early_init();
efi_init(); 

TODO

2.8 arm64_memblock_init

arm64_memblock_init();

在它之前会通过setup_machine_fdt(__fdt_pointer)解析fdt中的memory节点,为之初始化memblock.memory下的各个memblock_region,每个memblock_region可理解为一个物理内存bank区域。
arm64_memblock_init主要是通过memblock_remove将某些memblock_region区域从memblock.memory中移除,这些区域包含了DDR物理地址所不包含的区域,以及内核线性映射区所不能涵盖的区域;同时将某些物理区间添加到memblock.reserved中,这些区间包含dts中预留区域,命令行中通过参数预留的CMA区域,内核的代码段、initrd、页表、数据段等所在区域,crash kernel保留区域以及elf相关区域。这个过程中也会初始化一些全局变量,如物理内存起始地址memstart_addr
kernel启动流程-start_kernel的执行_2.setup_arch_第2张图片
memblock是系统启动早期使用的内存管理分配器,它将内存看成一大块连续的集合。
memblock分为三种类型:
memory类型:memory类型用于描述可用的物理内存区域
reserved类型:reserved类型用于描述正在使用的或即将被使用的物理内存块
physmap类型:用于描述硬件探查到的真实物理内存区间,适用于某些架构

arm64_memblock_init实际就是将该保留的区间保留到memblock.reserved区域,将不支持的区间从memblock.memory区域删除,需要预留的区域主要包括

1.dts中reserved-memory节点所描述的区域,其中包含了cma区域,如果cma区域未定义,默认也会给cma保留32M的默认区域
2.其它预留区域还包含了fdt和kernel image区域

前面已经介绍过,添加memory区域主要是通过如下完成memory的添加:

setup_arch->setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_memory->early_init_dt_add_memory_arch->memblock_add

至此在本例中memblock.memory区域包含的区间为:

{
    base = 0x40000000,
    size = 0x40000000,
    flags = 0x0,
    nid = 0x10
  },

memoblock.reserved区域包含的区间为:

  {
    base = 0x40200000,//for kernel image(31M)
    size = 0x1f30000,
    flags = 0x0,
    nid = 0x10
  },
  {
    base = 0x48000000, //for fdt(2M)
    size = 0x100000,
    flags = 0x0,
    nid = 0x10
  },
  {
    base = 0x7e000000,// for cma(32M)
    size = 0x2000000,
    flags = 0x0,
    nid = 0x10
  },

2.9 paging_init

paging_init();
    |--pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir)) // 获取swapper_pg_dir对应的pgd项
    |--map_kernel(pgdp) // 内核细粒度映射
    |--map_mem(pgdp)  //内核线性区映射,对于标记为no map的不会映射,如dts中的reserved memory中标记为no map 的区域
    |--pgd_clear_fixmap() //清空fixmap临时映射
    |--cpu_replace_ttbr1(lm_alias(swapper_pg_dir))  //设置新的内核页表基址
    |--init_mm.pgd = swapper_pg_dir 
    |--memblock_free(__pa_symbol(init_pg_dir)..) //释放init_pg

paging_init主要是为kernel image,swapper_pg页表本身,memblock.memory区域创建映射。经过paging_init将内核空间页表真正从init_pg_dir转换到swapper_pg_dir。这里主要利用了fixmap区域的PGD区域为swapper_pg页表本身创建临时映射,这样可以向swapper_pg填充页表项,填充完毕后解除临时映射。之后将会用swapper_pg_dir地址来填充ttbr1_el1,来替换之前填充的init_pg_dir。

1.我们看到fixmap区域的PGD,PUD,PMD,PTE用于对页表项的物理地址做临时映射,所以被称为临时映射区
2. 为何要用swapper_pg页表来替换init_pg页表?由于之前创建的init_pg页表是以2M块大小对kernel image所做的的粗粒度映射,只为能访问到内核的某些函数,此处需要对kernel image以及线性映射区做细粒度的映射,因此需要切换为swapper_pg

到此我们可以总结一下,我们创建过哪些映射及这些映射存放在哪些位置:

  1. head.S开启MMU之前,我们访问的都是物理地址,此时指令代码都是位置无关码;
  2. head.S的create_page_tables为kernel image的.idmap.text段创建了一致性映射,这是为了使能mmu而创建,虚拟地址映射区与.idmap.text段物理区间相同,相关页表存放在物理内存的kernel image的idmap_pg_dir开始的区域(物理地址),但是在
    setup_arch->cpu_uninstall_idmap函数进行了移除;
  3. head.S的create_page_tables为整个kernel image的物理区间创建映射,此为2M块大小的粗粒度映射,映射区为vmalloc_start开始的一段区域,页表存放在kernel image的init_pg_dir开始的区域,自此使能了MMU后,CPU访问时可以不再与物理地址直接关联;
  4. 在setup_arch->early_fixmap_init和early_ioremap_init中为fdt和io memory物理区间创建映射,映射区为fixup区域的fdt和bitmap索引区域,pgd页表存放在kernel image的init_pg_dir开始的区域(与kernel image的pgd页表区域为同一区域),pud,pmd,pte页表在编译时定义,存放在kernel image的bss段
    注:此处只是为FIXMAP_START地址创建了PGD,PUD,PMD页表项,即创建了映射框架,未填充pte页表项,需要在实际映射某个物理地址时填充
  5. 在setup_arch->paging_init中为swapper_pg pgd页表本身的物理地址创建pte页表项,映射区为fixup固定映射区的FIX_PGD索引区域(位于临时映射区),pgd页表项保存在swapper_pg_dir页表区域;
    (1)通过setup_arch->paging_init->map_kernel多次调用map_kernel_segment,为kennel image的text, data, rodata, __inittext,__initdata等段创建细粒度映射关系,映射区与init_pg_dir中记录的一致,页表项保存在swapper_pg页表区域
    (2)通过setup_arch->paging_init->map_mem为memblock.memory区域创建映射,同样页表项保存在swapper_pg页表区域,映射区为内核线性区起始地址PAGE_OFFSET
    将swapper_pg页表设置给TTBR1。从此之后的页表项将有swapper_pg页表来接管,由于swapper_pg只有4k大小,如果需要分配pud,pmd,pte页表则需要通过early_pgtable_alloc来分配

kernel启动流程-start_kernel的执行_2.setup_arch_第3张图片如上是通过临时映射区来映射swapper_pg并填充swapper_pg,当swapper_pg填充完毕,页表将切换到swapper_pg

如下内存地址空间分布参考自深入剖析Linux页表课程:
kernel启动流程-start_kernel的执行_2.setup_arch_第4张图片

到目前关于物理地址与虚拟地址空间映射关系如上图,到目前为止已经为物理地址空间创建了映射关系,包括memblock.memory区域(实际对应线性映射区),kernel image, DTB,swapper_pg页表区域

2.10 acpi init

acpi_table_upgrade();

/* Parse the ACPI tables for possible boot-time configuration */
acpi_boot_table_init();

if (acpi_disabled)
        unflatten_device_tree();

2.11 bootmem_init

bootmem_init
    |--min = PFN_UP(memblock_start_of_DRAM());
    |--max = PFN_DOWN(memblock_end_of_DRAM());
    |--early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT)
    |--arm64_numa_init()
    |--arm64_hugetlb_cma_reserve
    |--dma_pernuma_cma_reserve
    |--sparse_init
    |--zone_sizes_init
    |--memblock_dump_all
  1. bootmem_init
    通过early_memtest对整个memblock执行memory test测试,以确定是否有坏的内存区域,如果有则通过memblock_reserve进行预留;同时bootmem_init分配出的numa_distance数组,并根据各个numa的distance初始化(关于numa distance TODO),通过of_numa_init来设定memblock.memory每段区间的nid
  2. sparse_init
    kernel启动流程-start_kernel的执行_2.setup_arch_第5张图片

遍历memblock.memory的每个range,并划分为section(本例大小为1G),保存在全局mem_section数组,标记mem_section为present;sparse_init遍历每个标记为present的section, 为每个section创建page结构体数组,并为这些struct page结构体创建页表,映射到vmemmap虚拟映射区,同时对每个在线的mem_section进行初始化,最主要的是初始化ms->section_mem_map指向page数组

  1. zone_sizes_init
    初始化每个node, node下的zone, 以及node下的每一个pfn对应的page

2.12 kasan_init

kasan_init();

ksan主要利用shadow memory来检测内存是否可以访问,此处主要做一些初始化,以后再详细研究

参考网址:http://www.wowotech.net/memory_management/424.html

2.13 request_standard_resources

  request_standard_resources();

为membolock.memory的每段区域执行request_resource,也就是插入到resource tree的正确位置,如果某个区域与kernel_code, kernel_data重叠,则划分为更小的区域插入到resource tree。

参考https://www.cnblogs.com/ronnydm/p/5736813.html

2.14 early_ioremap_reset

 early_ioremap_reset();

作为paging_init的最后一步,after_paging_init = 1

2.15 psci_dt_init

        if (acpi_disabled)
                psci_dt_init();
        else
                psci_acpi_init();

psci_dt_init()会去读取并解析Device Tree中的内容,从而选择版本(psci_0_1_init/psci_0_2_init),选择指令(hvc/smc)等;psci_0_1_init()函数完成的主要内容其实是填充对应的函数指针,以及psci_function_id[]数组;

以Suspend为例,在用户输入echo mem > /sys/power/state,调用流程如下:
cpu_psci_ops.cpu_suspend()(arm64为psci_cpu_suspend_enter())->
psci_ops.cpu_suspend()(arm64为psci_system_suspend())->
invoke_psci_fn(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND)…)

psci_ops中会根据实际的Function ID找到对应的函数,从而通过hvc/smc指令调用Firmware接口;
其中PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND)就是psci_function_id数组元素,通过它就可以调用到firmware对应的函数

参考:https://www.cnblogs.com/LoyenWang/p/11370557.html

2.16 init_bootcpu_ops

init_bootcpu_ops();
  1. 对ARM64平台来说,kernel使用struct cpu_operations来抽象cpu ops,该接口提供了一些CPU操作相关的回调函数,由底层代码(可以称作cpu ops driver)根据实际情况实现
  2. 针对ARM64,kernel提供了两种可选的方法,smp spin table和psci,具体使用哪一个operation,是通过DTS指定的。即在每一个cpu子节点中,使用“enable-method”指定是使用“spin-table”还是“psci”。
  3. 系统初始化的时候,会根据DTS信息,获取使用的operations(setup_arch–>cpu_read_bootcpu_ops–>cpu_read_ops),最终保存在一个operation数组(每个CPU一个)中,供SMP(arch/arm64/kernel/smp.c)
    参考: http://www.wowotech.net/pm_subsystem/cpu_ops.html

2.17 smp_init_cpus

 smp_init_cpus();

从DTS中解析所有CPU的HWID(通过‘reg’关键字),并保存在__cpu_logical_map数组中;
通过early_map_cpu_to_node设置每个cpu关联的numa node id到cpu_to_node_map[cpu]数组;
对所有__cpu_logical_map数组中的CPU,执行cpu_init操作,然后执行set_cpu_possible操作,将它们设置为possible状态

2.18 smp_build_mpidr_hash

smp_build_mpidr_hash();

2.19 kasan_init_tags

/* Init percpu seeds for random tags after cpus are set up. */
kasan_init_tags();

2.20 CONFIG_ARM64_SW_TTBR0_PAN

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Make sure init_thread_info.ttbr0 always generates translation
         * faults in case uaccess_enable() is inadvertently called by the init
         * thread.
         */
        init_task.thread_info.ttbr0 = __pa_symbol(empty_zero_page);
#endif

CONFIG_ARM64_SW_TTBR0_PAN的含义是Emulate Privileged Access Never using TTBR0_EL1 switching
本例没有开启CONFIG_ARM64_SW_TTBR0_PAN宏

armv8 PAN指的是内核态不能访问用户态的数据,如果内核态想访问用户态的数据,需要copy_from_user,copy_to_user。 通过CONFIG_ARM64_SW_TTBR0_PAN来配置是否开启PAN,原理是将ttbr0_el1寄存器置为0,ttbr0_el1实际上保存的是用户态一级页表的地址,所以ttbr0_el1被置零以后,内存页寻址失败,PAN生效。
实际在setup_arch->cpu_uninstall_idmap时就已经将TTBR0指向了zero页面?
参考:https://cloud.tencent.com/developer/article/1413360

2.21 boot_args检查

                                                                                                                                  
 if (boot_args[1] || boot_args[2] || boot_args[3]) {
         pr_err("WARNING: x1-x3 nonzero in violation of boot protocol:\n"
                 "\tx1: %016llx\n\tx2: %016llx\n\tx3: %016llx\n"
                 "This indicates a broken bootloader or old kernel\n",
                 boot_args[1], boot_args[2], boot_args[3]);
 }                       

ARM64 boot protocol对启动时候的x0~x3这四个寄存器有严格的限制:x0是dtb的物理地址,x1~x3必须是0(非零值是保留将来使用)。在setup_arch函数执行的时候会访问boot_args并进行校验
参考:Documentation/arm64/booting.rst

3.总结

至此总结一下setup_arch主要完成了哪些工作:

  1. 初始化init进程的init_mm结构体;
  2. 为DTB创建固定映射, 解析command line存放到boot_command_line;
  3. 为memblock.memory添加区间并为之创建映射,同时将保留区域添加到memblock.reserve
  4. 关于arch(本例为arm64)的其它一些初始化,包括psci_opts,cpu opts等

参考文档

  1. https://blog.csdn.net/jus3ve/article/details/79544927
    KPTI补丁分析
  2. https://www.jianshu.com/p/ce6df090924b
    Arm64 Linux Kernel KPTI (Meltdown防御)方案解释
  3. https://blog.csdn.net/forevertingting/article/details/79656824
    meltdown分析
  4. https://www.cnblogs.com/LoyenWang/p/11440957.html
    【原创】(二)Linux物理内存初始化
  5. Fixmap机制深入分析

你可能感兴趣的:(#,Kernel,Start,kernel,start)