映射kernel地址空间
swapper_pg_dir其实就是swapper进程(pid等于0的那个,其实就是idle进程)的地址空间,用它来代表kernel地址空间。和恒等映射的区别是,这段空间映射没有扩展页表的需求,当然它也是使用section mapping。和前面的恒等映射一样,swapper_pg_dir也是需要3个页面。
映射kernel空间
adrp x0, swapper_pg_dir kernel页表基地址
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text) 代码段开始虚拟地址
add x5, x5, x23 // add KASLR displacement
create_pgd_entry x0, x5, x3, x6 建立各个中间level的table描述符
adrp x6, _end // runtime __pa(_end) 代码段结束物理地址
adrp x3, _text // runtime __pa(_text) 代码段开始物理地址
sub x6, x6, x3 // _end - _text 代码段长度
add x6, x6, x5 // runtime __va(_end) 代码段结束虚拟地址
create_block_map x0, x7, x3, x5, x6 创建PTE
adrp x0, idmap_pg_dir
ldr x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
dmb sy
bl __inval_dcache_area 再次清理cache
ret x28
ENDPROC(__create_page_tables)
上面调用的函数在前面都介绍过,不再次介绍了。至此内核地址空间被全部映射。
ffffff8008080000 T _text
ffffff8009175000 A _end
sharkl3 1h10 arm64的kernel使用了17,780,736字节,即不到20MB字节。
因为level2页表采用了block型描述符,每个entry管2MB,需要10个entry就够了。
左侧是内核虚拟地址分配图
cpu设置
stext--> __cpu_setup
这部分代码主要是在使能mmu前对cpu做一些设置。
ENTRY(__cpu_setup)
tlbi vmalle1 // Invalidate local TLB
dsb nsh
mov x0, #3 << 20
msr cpacr_el1, x0 // Enable FP/ASIMD
mov x0, #1 << 12 // Reset mdscr_el1 and disable
msr mdscr_el1, x0 // access to the DCC from EL0 用于debug的监控器
isb // Unmask debug exceptions now,
enable_dbg // since this is per-cpu
reset_pmuserenr_el0 x0 // Disable PMU access from EL0
上面的设置和内存管理部分没有关系,看看comment就好了,暂时不去调查细节
下面是内存属性的定义,一共是2类:
- Normal型:sram或者dram那样的内存空间,一般都是过cache的(当然也可不过cache,如外设访问的地址空间,标记为NC)。
这部分主要和cache相关,可参考我的另外一篇文档《ARM Linux Cache》
- device型:设备寄存器那样的io空间,都不会过cache。Device属性的内存空间还有下面三种子属性,都有打开和关闭的定义。
G(gather:对多个memory的访问可以合并) nG与之相反
R(Reordering:对内存访问指令进行重排) nR与之相反
E(Early Write Acknowledgement hint:写操作的ack可提早应答) nE与之相反
MAIR寄存器定义如下:
Linux预先定义了6种内存属性,分别存在MAIR寄存器的attr0~attr5。内存页表属性部分可以选择这个寄存器的某个index,范围(0~5)作为自己的属性。
ldr x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
MAIR(0x04, MT_DEVICE_nGnRE) | \
MAIR(0x0c, MT_DEVICE_GRE) | \
MAIR(0x44, MT_NORMAL_NC) | \
MAIR(0xff, MT_NORMAL) | \
MAIR(0xbb, MT_NORMAL_WT)
msr mair_el1, x5 写入预定义的5种内存属性值
adr x5, crval crval是已经定义好的sctlr寄存器的值
.type crval, #object
crval:
.word 0xfcffffff // clear 对应的bit为1的会被清0
.word 0x34d5d91d // set 对应的bit为1的会置1
.popsection
ldp w5, w6, [x5] 把[x5]的clear值读到w5,ser值读到w6
mrs x0, sctlr_el1 读sctlr寄存器到x0
bic x0, x0, x5 // clear bits
orr x0, x0, x6 // set bits
下面是设置TCR寄存器,设置了其中8个寄存器字段
TCR寄存器定义:
【图片加载失败】
虚拟地址VA_BITS使用39bit。
TCR_TxSZ:VA_BITS相关
TCR_CACHE_FLAGS:cacheable, inner/outer WBWA(写回/写分配)
TCR_SMP_FLAGS:共享
TCR_TG_FLAGS:页面大小为4KB
TCR_ASID16:TTBR中名字空间的bit数为16 (ASID用于OS改善tlb的刷新)
TCR_TBI0:Top Byte ignored in the address calculation. 不详,可能是查表匹配规则
TCR_A1:ASID选用TTBR1中的ASID
TCR_KASAN_FLAGS:类似TCR_TBI0的作用
ldr x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
TCR_TG_FLAGS | TCR_ASID16 | TCR_TBI0 | TCR_A1 | \
TCR_KASAN_FLAGS
tcr_set_idmap_t0sz x10, x9 将恒等映射中VA_bits的值插入到x10中
.macro tcr_set_idmap_t0sz, valreg, tmpreg
#ifndef CONFIG_ARM64_VA_BITS_48
ldr_l \tmpreg, idmap_t0sz
bfi \valreg, \tmpreg, #TCR_T0SZ_OFFSET, #TCR_TxSZ_WIDTH
#endif
.endm
mrs x9, ID_AA64MMFR0_EL1 读ID_AA64MMFR0_EL1寄存器,获得PARange值
bfi x10, x9, #32, #3 将x9寄存器的内容左移32bit,copy 3个bit到x10寄存器 设置物理地址宽度
#ifdef CONFIG_ARM64_HW_AFDBM
…略
#endif /* CONFIG_ARM64_HW_AFDBM */
msr tcr_el1, x10 将x10写入TCR寄存器
ret // return to head.S
ENDPROC(__cpu_setup)
这个函数执行完后进入__primary_switch,它会调用__enable_mmu函数
使能mmu
ENTRY(__enable_mmu)
下面4条代码判断硬件是否实际支持4K页表映射, 如果不支持, 就跳入__no_granule_support, 表明启动失败, CPU调用wfe, wfi进入idle状态
mrs x1, ID_AA64MMFR0_EL1
ubfx x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4
cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED
b.ne __no_granule_support
update_early_cpu_boot_status 0, x1, x2 存入0表明CPU状态正常
adrp x1, idmap_pg_dir
adrp x2, swapper_pg_dir
msr ttbr0_el1, x1 // load TTBR0 将恒等映射的页表放在TTBR0上
msr ttbr1_el1, x2 // load TTBR1 将kernel映射的页表放在TTRB1上
isb
msr sctlr_el1, x0 使能mmu x0为传入参数 包含打开mmu的控制信息
isb
ic iallu
dsb nsh
isb
ret 出来后跳转到__primary_switched,做最后一步的准备 进入start_kernel
ENDPROC(__enable_mmu)
上面就使能了arm的mmu,系统进入虚拟地址时代
__primary_switched
__primary_switched:
ARM64和thread_info和stack不共用, thread_info放在task_strcut里面, init_thread_union只是用于内核堆栈
adrp x4, init_thread_union
add sp, x4, #THREAD_SIZE sp = x4+内核任务栈的大小 把EL0的sp放在init_task下面
adr_l x5, init_task init线程(idle进程)的task_struct地址
msr sp_el0, x5 // Save thread_info的地址 到sp_el0,内核使用sp_el0保存current信息
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address 设置异常向量表地址
isb
stp xzr, x30, [sp, #-16]! x30是lr寄存器 注意lr在栈中的位置
mov x29, sp 这两句话不清楚具体意义
str_l x21, __fdt_pointer, x5 // Save FDT pointer 把设备树物理地址存在__fdt_pointer中
ldr_l x4, kimage_vaddr // Save the offset between
sub x4, x4, x0 // the kernel virtual and
str_l x4, kimage_voffset, x5 // physical mappings 计算物理和虚拟地址间的偏移 存入kimage_voffset
将BSS区域清0
adr_l x0, __bss_start
mov x1, xzr
adr_l x2, __bss_stop
sub x2, x2, x0
bl __pi_memset
dsb ishst // Make zero page visible to PTW
#ifdef CONFIG_KASAN
bl kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASE
…
#endif
add sp, sp, #16 更新sp,在进入c之前应该不需要作sp的出入栈操作
mov x29, #0
mov x30, #0
b start_kernel 终于进入c代码了
ENDPROC(__primary_switched)