本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本文重点介绍start_kernel的setup_arch的主要流程.
kernel版本:5.10
平台:arm64
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的起始地址
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
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机制出现了。
虚拟地址空间的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高速缓存取得对应的物理地址
early_ioremap_init();
early_ioremap_init将ioremap的空间为7 * 256K的区域,保存在slot_vir[]数组中
当需要进行IO操作的时候,最终会调用到__early_ioremap函数,在该函数中去填充对应的pte entry,从而完成最终的虚拟地址和物理地址的映射。
页表创建解决两个问题:一是映射区所在的区域;二是页表存放的物理位置。
从启动之初到此处我们一共有三次创建页表的动作,简单总结一下:
- 为kernel image的.idmap.text段创建映射,页表存放在kernel image的idmap_pg_dir开始的区域,因为是恒等映射,虚拟地址映射区为与.idmap.text段所在的物理区域相同;
- 为整个kernel image创建映射,页表存放在kernel image的init_pg_dir开始的区域;
- 为fixup区域创建映射,fixup区域的pgd页表存放在kernel image的init_pg_dir开始的区域,fixup区域的pud,pmd,pte页表存放在kernel image的bss段,注意此处只是创建了一个映射的框架,并未实际填充pte页表项
setup_machine_fdt(__fdt_pointer);
我们在前面head.S的__primary_switched中分析过,FDT的物理地址会保存到__fdt_pointer中,此处通过fixmap_remap_fdt为FDT物理区域创建PTE页表项,映射到fixup映射区的FDT索引区域,这样就可以通过虚拟地址访问到FDT了
setup_machine_fdt主要做了如下工作:
/*
* Initialise the static keys early as they may be enabled by the
* cpufeature code and early parameters.
*/
jump_label_init();
parse_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
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
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
},
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
到此我们可以总结一下,我们创建过哪些映射及这些映射存放在哪些位置:
- head.S开启MMU之前,我们访问的都是物理地址,此时指令代码都是位置无关码;
- head.S的create_page_tables为kernel image的.idmap.text段创建了一致性映射,这是为了使能mmu而创建,虚拟地址映射区与.idmap.text段物理区间相同,相关页表存放在物理内存的kernel image的idmap_pg_dir开始的区域(物理地址),但是在
setup_arch->cpu_uninstall_idmap函数进行了移除;- head.S的create_page_tables为整个kernel image的物理区间创建映射,此为2M块大小的粗粒度映射,映射区为vmalloc_start开始的一段区域,页表存放在kernel image的init_pg_dir开始的区域,自此使能了MMU后,CPU访问时可以不再与物理地址直接关联;
- 在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页表项,需要在实际映射某个物理地址时填充- 在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来分配
如上是通过临时映射区来映射swapper_pg并填充swapper_pg,当swapper_pg填充完毕,页表将切换到swapper_pg
到目前关于物理地址与虚拟地址空间映射关系如上图,到目前为止已经为物理地址空间创建了映射关系,包括memblock.memory区域(实际对应线性映射区),kernel image, DTB,swapper_pg页表区域
acpi_table_upgrade();
/* Parse the ACPI tables for possible boot-time configuration */
acpi_boot_table_init();
if (acpi_disabled)
unflatten_device_tree();
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
遍历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数组
kasan_init();
ksan主要利用shadow memory来检测内存是否可以访问,此处主要做一些初始化,以后再详细研究
参考网址:http://www.wowotech.net/memory_management/424.html
request_standard_resources();
为membolock.memory的每段区域执行request_resource,也就是插入到resource tree的正确位置,如果某个区域与kernel_code, kernel_data重叠,则划分为更小的区域插入到resource tree。
参考https://www.cnblogs.com/ronnydm/p/5736813.html
early_ioremap_reset();
作为paging_init的最后一步,after_paging_init = 1
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
init_bootcpu_ops();
- 对ARM64平台来说,kernel使用struct cpu_operations来抽象cpu ops,该接口提供了一些CPU操作相关的回调函数,由底层代码(可以称作cpu ops driver)根据实际情况实现
- 针对ARM64,kernel提供了两种可选的方法,smp spin table和psci,具体使用哪一个operation,是通过DTS指定的。即在每一个cpu子节点中,使用“enable-method”指定是使用“spin-table”还是“psci”。
- 系统初始化的时候,会根据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
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状态
smp_build_mpidr_hash();
/* Init percpu seeds for random tags after cpus are set up. */
kasan_init_tags();
#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
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
至此总结一下setup_arch主要完成了哪些工作: