版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/48707579
更多内容可关注微信公众号
##一些关键的宏
下面是启动阶段用到的一些宏在测试平台上的值,当前linux使用的是三级页表,4KB小页。
#define __pa(x) __virt_to_phys((unsigned long)(x))
#define __va(x) ((void *)__phys_to_virt((phys_addr_t)(x)))
PTRS_PER_PGD = 0x200 //PGD表项为512项
PTRS_PER_PMD = 0x200 //PMD表项为512项
PTRS_PER_PTE = 0x200 //PTE表项为512项
//PGD中一个页表项代表的地址范围是1GB
PGDIR_SHIFT = 30
PGDIR_SIZE = 0x40000000 //(2^PGDIR_SHIFT)
PGDIR_MASK = 0x3FFFFFFF
//PMD中一个表项代表的地址范围是2MB
PMD_SHIFT = 21
PMD_SIZE = 0x200000 //(2^PMD_SHIFT)
PMD_MASK = 0x1FFFFF
//物理页的大小为4KB,同时也是PTE中一个页表项代表的地址范围
PAGE_SHIFT = 12
PAGE_SIZE = 0x1000 //(2^PAGE_SHIFT)
PAGE_MASK = 0xFFFFFFFFFFFFF000
SECTION_SHIFT = 21
BLOCK_SHIFT = 21
KERNEL_RAM_VADDR = 0xFFFFFFC000800000
PAGE_OFFSET: 内核态的起始地址。
PAGE_OFFSET + TEXT_OFFSET: 内核代码(stext)的起始地址。
PHYS_OFFSET: 是PAGE_OFFSET这个虚拟地址对应的物理地址
总结之:
整个64位地址空间中有两段有效地址空间:
1. 0x0000000000000000 - 0x0000007FFFFFFFFF(512GB),通过TTBR0寻址,从level1开始做地址转换。
2. 0xFFFFFF8000000000 - 0xFFFFFFFFFFFFFFFF(512GB),通过TTBR1寻址,从level1开始做地址转换。
####pgd
##linux kernel启动过程
###ENTRY(stext)
//arm64内核的入口
ENTRY(stext)
mov x21, x0 //x21=FDT,bootloader传过来的设备树地址
//获取物理地址和虚拟地址之差 -> x28, PHYS_OFFSET -> x24
bl __calc_phys_offset
//不知道干啥的,先过......
bl el2_setup // Drop to EL1
//读取cpuid -> x22,cpuid是记录在midr_el1寄存器中的
mrs x22, midr_el1 //x22=cpuid
//lookup_processor_type根据传入的w0(x0)查找体系结构相关的cpu_table,找到后返回地址给x0
mov x0, x22
bl lookup_processor_type
//x23存当前体系结构的cpu_table地址
mov x23, x0 //x23=current cpu_table
//如果没找到,直接error
cbz x23, __error_p //invalid processor (x23=0)?
//没看,先过......
bl __vet_fdt
/*
运行到这里之前,x0 = cpu_table, x21 = FDT, x24 = PHYS_OFFSET(0x40000000), 这个函数内部做了两张表,idmap_pg_dir 和 swapper_pg_dir。idmap_pg_dir负责恒等映射,__turn_mmu_on函数所在的2MB空间映射到这张表了,swapper_pg_dir负责正常内核的映射,整个kernel映射在这张表上。
*/
bl __create_page_tables // x25=TTBR0, x26=TTBR1
//这个是__turn_mmu_on执行后的跳转地址,也是最后调用的函数,这个函数内部调用start_kernel.
ldr x27, __switch_data
//__enable_mmu内部跳转到__turn_mmu_on,这是最后一句br x12返回的地址
adr lr, __enable_mmu // return (PIC) address
//找到cpu对应的CPU_INFO_SETUP函数,并调用之
ldr x12, [x23, #CPU_INFO_SETUP]
add x12, x12, x28 // __virt_to_phys
//跳转到体系结构相关的CPU_INFO_SETUP函数
br x12 // initialise processor
//这里的实际调用流程是: CPU_INFO_SETUP -> __enable_mmu -> __turn_mmu_on -> __switch_data.__mmap_switched -> start_kernel
ENDPROC(stext)
###__create_page_tables
__create_page_tables:
/*
pgtbl定义如下:
.macro pgtbl, ttb0, ttb1, phys
add \ttb1, \phys, #TEXT_OFFSET - SWAPPER_DIR_SIZE
sub \ttb0, \ttb1, #IDMAP_DIR_SIZE
.endm
在内核初始化的时候,kernel是放在PAGE_OFFSET + TEXT_OFFSET的位置,而idmap_pg_dir,swapper_pg_dir这两张表是固定紧挨着kernel,在kernel前面的。这句宏是根据内核载入的物理基地址PHYS_OFFSET(x24),计算idmap_pg_dir,swapper_pg_dir的物理地址分别 -> x25, x26 (在__create_page_tables返回后,开启mmu之前,x25的值会赋值给ttb0,x26的值会赋值给ttb1)。
*/
// idmap_pg_dir and swapper_pg_dir addresses
pgtbl x25, x26, x24
/*
将[idmap_pg_dir, idmap_pg_dir + SWAPPER_DIR_SIZE + IDMAP_DIR_SIZE] 全部清空,这里面不止包含idmap_pg_dir, swapper_pg_dir两张表,具体的结构如下:
PA=idmap_pg_dir:------> idmap_pg_dir
PA+=0x000001000:------> idmap_pg_dir[x]'s tbl(idm_tbl)
PA+=0x000001000:------> swapper_pg_dir
PA+=0x000001000:------> swapper_pg_dir[x]'s tbl(swap_tbl)
PA+=0x000001000:------> kernel(PA=PHYS_OFFSET + TEXT_OFFSET):
idmap_pg_dir是一张恒等映射表,对应arm的一级页表,idm_tbl是一个二级页表。
swapper_pg_dir是系统正常运行时的一份内核页表,所有进程页表的kernel部分都是从这里复制出来的,swap_tbl是其的一个二级页表
最后空的1MB不知道是干嘛的.
arm64的一个页表项为8byte,idmap_pg_dir大小4KB, 4KB/8byte = 512(0x200)个页表项。每个页表代表的地址空间就是PGDIR_SIZE(1GB),一个idmap_pg_dir理论上能代表512G地址空间(刚好是三级寻址空间大小,上一篇提到过)。
在系统初始化的时候只为idmap_pg_dir预留了一个二级页表,即swap_tbl,可代表1GB空间(其余的idmap_pg_dir作为一级页表项,英应该可以用block的方式填充)。
swap_tbl同理 4KB/8byte=512(0x200)个页表项,每隔页表项代表PMD_SIZE(2MB)的地址空间,512*2MB=1GB空间,由于没有预留pte三级页表,所以在开始阶段swap_tbl想要初始化,估计也只能初始化为block。
swapper_pg_dir与idmap_pg_dir类似。
*/
//x0 = idmap_pg_dir
mov x0, x25
//x6 = 内核起始位置
add x6, x26, #SWAPPER_DIR_SIZE
//循环将idmap_pg_dir到内核开始位置都清零。
1: stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
cmp x0, x6
//如果x0 < x6,则jmp 1
b.lo 1b
// x7 = 0x711 (0,1,2级页描述符的访问属性是通用的,0x711最后一位1表示用的是段映射(SECT/block),没有下一级页表了,初始化的时候这是默认属性。
ldr x7, =MM_MMUFLAGS
/*
以idmap_pg_dir为基址,创建恒等映射表, 在恒等映射表中,虚拟地址就等于物理地址。恒等映射表是为开启mmu的代码准备的,在当前内核中相关的代码有: __turn_mmu_on(), cpu_resume_mmu() and cpu_reset()。以__enable_mmu为例,其代码如下:
__turn_mmu_on:
msr sctlr_el1, x0 //开启mmu
isb //指令同步屏障
br x27
ENDPROC(__turn_mmu_on)
这段代码所在的页,在恒等映射表中必须要有。因为在msr指令开启mmu之后,isb指令打断了指令流水,下一条指令br执行的时候会先根据pc+4查找指令地址,此时的pc是一个物理地址,pc+4是br x27这条指令的物理地址。内核在开启mmu之前会将恒等映射表存入TTBR0, swapper_pg_dir表存入TTBR1,br x27指令最终会到恒等映射表上寻找物理地址,恒等映射表物理地址等于虚拟地址,从而保证这条指令的正常执行。
在内核中__turn_mmu_on(), cpu_resume_mmu() and cpu_reset()的代码一般都在同一个2MB页上,所以这里实际上映射__turn_mmu_on一个函数就够了,如果内核中有其他enable mmu的函数,则也需要加到恒等映射表中。
*/
//x0 = idm_tlb,二级页表地址。
add x0, x25, #PAGE_SIZE
//获取__turn_mmu_on函数的物理地址(adr是基于pc寻址,由于当前没有开启mmu,pc就是物理地址,所以adr寻址后的函数也是物理地址)
adr x3, __turn_mmu_on
//pgd = x25, tbl = x0, virt = x3。这个宏用来修改pgd的页表项,具体哪个页表项根据virt算出来,修改为tbl & 3(代表table)。这里的x3传入的是__turn_mmu_on的物理地址。
create_pgd_entry x25, x0, x3, x5, x6
/*
idmap=1表示这里做恒等映射,x0=idm_tbl地址,x7为附加属性,x3为映射的起始物理地址(这个不一定是对齐的),x5这里实际上没用上,恒等映射不用start。end和start一个寄存器。这里实际上只是对x3(__turn_mmu_on)所在的2MB做了恒等映射
*/
create_block_map x0, x7, x3, x5, x5, idmap=1
/*
对内核做非恒等映射,内核起始虚拟地址是PAGE_OFFSET,映射到物理地址PHYS_OFFSET,大小为整个内核大小,用的pgd为swapper_pg_dir。
*/
//x0为swap_tbl的地址
add x0, x26, #PAGE_SIZE // section table address
//映射的起始虚拟地址
mov x5, #PAGE_OFFSET
//pgd = x26, tbl = x0, virt = x5,修改swapper_pg_dir中某个pgd页表项的值,为 tbl &3, 哪个pgd页是由virt来决定的。
create_pgd_entry x26, x0, x5, x3, x6
//映射的结束位置
ldr x6, =KERNEL_END - 1
//映射的起始物理地址PHYS_OFFSET
mov x3, x24 // phys offset
//循环将内核整个映射到swapper_pg_dir
create_block_map x0, x7, x3, x5, x6
//对FDT的映射,先pass
mov x3, x21 // FDT phys address
and x3, x3, #~((1 << 21) - 1) // 2MB aligned
mov x6, #PAGE_OFFSET
sub x5, x3, x24 // subtract PHYS_OFFSET
tst x5, #~((1 << 29) - 1) // within 512MB?
csel x21, xzr, x21, ne // zero the FDT pointer
b.ne 1f
add x5, x5, x6 // __va(FDT blob)
add x6, x5, #1 << 21 // 2MB for the FDT blob
sub x6, x6, #1 // inclusive range
create_block_map x0, x7, x3, x5, x6
ret
ENDPROC(__create_page_tables)
###create_pgd_entry
/*
pgd 是要在哪个pgd中创建页表项
tbl 是要写入的内容(最终&3写入)
virt 是一个虚拟地址,往哪个页表项中写入,取决于这个虚拟地址
tmp1/tmp2是临时寄存器,没用。
create\_pgd\_entry的作用是修改pgd的页表项,具体哪个页表项根据virt算出来,修改为tbl & 3(代表table)。
*/
.macro create_pgd_entry, pgd, tbl, virt, tmp1, tmp2
//获取pgd的index, tmp1 = virt >> PGDIR_SHIFT 右移30位
lsr \tmp1, \virt, #PGDIR_SHIFT
//tmp1 = tmp1 & 1FF
//这两句实际上是tmp1 = virt[38,30],取的是pgd的index
and \tmp1, \tmp1, #PTRS_PER_PGD - 1 // PGD index
//tbl = tbl |3 这个3是代表TABLE, 表示还有下一级分页,pgd默认是table
orr \tmp2, \tbl, #3 // PGD entry table type
//pgd的一个表项8byte,将tlb & 3 写入pgd[index]
str \tmp2, [\pgd, \tmp1, lsl #3]
.endm
###create_block_map
/*
tbl: 一个二级页表(pmd)页表的地址,这里是向二级页表项写入内容(pud为空)
flags: 每个映射页的附加属性
phys: 被映射到的起始物理地址
start: 映射的起始虚拟地址
end: 映射的结束虚拟地址
idmap: 是否为恒等映射
create_block_map是将虚拟地址[start,end]映射到物理地址phys开始的内存,映射大小必须在一个二级页表范围内(1GB),每个二级页表项附加属性flags,如果是恒等映射(idmap=1)则忽略start,直接映射[phys,end]。
*/
.macro create_block_map, tbl, flags, phys, start, end, idmap=0
//phys = phys >> BLOCK_SHIFT(21)
lsr \phys, \phys, #BLOCK_SHIFT
//这里的目的是获取虚拟地址start 对应的pmd的index,正常情况下应该是(start >> 21) & 0x1ff (只取start[29,21]),如果是恒等映射(idmap = 1),则start应该=phys,所以直接取phys[29,21]就行
.if \idmap
//PTRS_PER_PTE = 0x200, start = phys & 0x1ff
and \start, \phys, #PTRS_PER_PTE - 1 // table index
.else
//非恒等映射,取start[29,21]
lsr \start, \start, #BLOCK_SHIFT
and \start, \start, #PTRS_PER_PTE - 1 // table index
.endif
//phys = phys << 21 | flags (这个物理地址 & 属性标志,作为页表项内容)
orr \phys, \flags, \phys, lsl #BLOCK_SHIFT // table entry
//这里实际上是判断start和end是否是同一个寄存器,除非同一个寄存器,否则start都移位了,不可能等于end。
.ifnc \start,\end
//end = end >> 21
lsr \end, \end, #BLOCK_SHIFT
//end & = 0x1ff ,就是end = end[29,21];
and \end, \end, #PTRS_PER_PTE - 1 // table end index
.endif
//tbl[start * 8] = phys (这时候的start已经作为index了)
9999: str \phys, [\tbl, \start, lsl #3]
//如果start != end
.ifnc \start,\end
//start ++; (index++)
add \start, \start, #1 // next entry
//phys += 2MB;
add \phys, \phys, #BLOCK_SIZE // next block
cmp \start, \end
//映射下一个pmd
b.ls 9999b
.endif
.endm