1.前言
本文基于高通8996平台,kernel版本为3.18.31。
本文主要介绍head.S的__create_page_tables执行流程
2. 页表基础知识
PGD(Page Global Directory)对应Level 0 translation table
PUD (Page Upper Directory) 对应Level 1 translation table
PMD (Page Middle Directory) 对应Level 2 translation table
PTE (Page Table Entry) 对应Level 3 translation table。
- 4k,48bit虚拟地址的划分
48bit的地址被分成9 + 9 + 9 + 9 + 12 = 48
PGD(Level 0)、PUD(Level 1)、PMD(Level 2)、PTE(Level 3)的translation table中的entry都是512项,每个entry是8byte,所以这些translation table都是4KB,刚好是一页。
TTBR0存储User Space所在的页表,TTBR1存储Kernel Space的页表。
注:PGD index 、PUD index、 PMD index、PTE index ,实际存储的都是对应各级page table entry的offset,各级page table的地址存放在上级page table 的相应页表项entry 中
-
4k,39bit虚拟地址的划分
-
MSM8996平台
在MSM8996 Linux3.18中采用3级页表,4K的page size,39位虚拟地址
#.config
CONFIG_PGTABLE_LEVELS=3
arch/arm64/include/asm/page.h
/*
* The idmap and swapper page tables need some space reserved in the kernel
* image. Both require pgd, pud (4 levels only) and pmd tables to (section)
* map the kernel. With the 64K page configuration, swapper and idmap need to
* map to pte level. The swapper also maps the FDT (see __create_page_tables
* for more information).
*/
#ifdef CONFIG_ARM64_64K_PAGES
#define SWAPPER_PGTABLE_LEVELS (CONFIG_PGTABLE_LEVELS)
#else
#define SWAPPER_PGTABLE_LEVELS (CONFIG_PGTABLE_LEVELS - 1)
#endif
在本例中,采用的是三级页表,即PGD PUD PTE,不包括PMD,其中:
虚拟地址中PGD,PUD,PTE分别表示PGD页表索引,PUD页表索引,2M BLOCK块内偏移;
PGD页表项,PUD页表项分别指向PUD页表地址,2M BLOCK块地址,因此PTE不需要专门的页表
所以由上面的宏定义可以知道SWAPPER_PGTABLE_LEVELS为2
2. 重要的宏说明
.macro pgtbl, ttb0, ttb1, virt_to_phys
- 含义
将idmap_pg_dir(存放用户空间页表地址)转换为物理地址存放到ttb0;
将swapper_pg_dir(存放内核空间页表地址)转换为物理地址保存在ttb1 - 参数
virt_to_phys:物理地址与虚拟地址的偏移
idmap_pg_dir :用于存放用户空间的页表,在进程切换的时候,其地址空间的切换实际就是修改TTBR0的值;
swapper_pg_dir :用于存放kernel space页表,所有的内核线程都是共享一个空间。
.macro pgtbl, ttb0, ttb1, virt_to_phys
ldr \ttb1, =swapper_pg_dir
ldr \ttb0, =idmap_pg_dir
add \ttb1, \ttb1, \virt_to_phys//将swapper_pg_dir转换为物理地址并保存到ttb1
add \ttb0, \ttb0, \virt_to_phys//将idmap_pg_dir转换为物理地址并保存到ttb0
.endm
idmap_pg_dir 和swapper_pg_dir分别定义如下:
#arch/arm64/kernel/vmlinux.lds.S
SECTIONS
{
.....
BSS_SECTION(0, 0, 0)
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
......
}
idmap_pg_dir 和 swapper_pg_dir定义在vmlinux.ld.S,位于bss段之后,idmap_pg_dir页表和swapper_pg_dir页表相邻。
#arch/arm64/include/asm/page.h
#define SWAPPER_DIR_SIZE (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE (SWAPPER_DIR_SIZE)
由于每个页表占用一个page,本例中由于是3级页表,SWAPPER_PGTABLE_LEVELS为2,因此分别为idmap_pg_dir和swapper_pg_dir准备了2个page,用于存放pgd页表项和pud页表项,由于是2M block 因此pte不需要页表(或者说没有pte?)
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
- 含义
是在物理基地址为tbl的页表上为虚拟地址virt创建页表项,此页表项的内容指向下一级页表
注:以下以pgd页表项为例,具体是在物理基地址为tbl的pgd页表上,索引为(virt>>shift)&(ptrs-1)的位置创建pgd页表项,页表项内容为tbl+PAGE_SIZE
- 参数
tbl:页表物理基地址
virt:要创建页表项的虚拟地址
/*
* Macro to create a table entry to the next page.
*
* tbl: page table address
* virt: virtual address
* shift: #imm page table shift
* ptrs: #imm pointers per table page
*
* Preserves: virt
* Corrupts: tmp1, tmp2
* Returns: tbl -> next level table page address
*/
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
// tmp1 = virt >> 30
lsr \tmp1, \virt, #\shift
// table index
// tmp1 = tmp1 & 0x1ff
// 至此tmp1的值为virt的[38:30]bit
and \tmp1, \tmp1, #\ptrs - 1
// tmp2 = tbl + 0x1000
add \tmp2, \tbl, #PAGE_SIZE
// address of next table and entry type
// tmp2 = tmp2 | 0x3
orr \tmp2, \tmp2, #PMD_TYPE_TABLE
// 将tmp2的值存入地址tbl + tmp1 * 8的内存中
str \tmp2, [\tbl, \tmp1, lsl #3]
// next level table page
// tbl = tbl + 0x1000
add \tbl, \tbl, #PAGE_SIZE
.endm
(1)此处假设虚拟地址virt为39位,shift假设为PGDIR_SHIFT(30),ptrs为PTRS_PER_PGD(0x200),假设创建的是pgd页表及其页表项,由于是三级页表,不包含PMD
(2) tmp1存放的是virt的bit30~bit38,表示页表(pgd页表)的索引
共9个bit,对应的pgd页表为2^9=512项,每个为8个字节,所以正好一个page
(3)tmp2 组成了pgd页表的页表项
tmp2为tbl页表基地址加4k,正好是一个pud的第一个页表的基地址,pgd页表项的bit0~bit1存放下一级页表类型,因此tmp2组成一个pgd页表项,指向下一级pud页表的基地址
(4)将pgd页表项保存到pgd索引(tmp1)指向的pgd页表偏移位置
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
- 含义
在物理基地址为tbl的pgd页表上为虚拟地址virt创建pgd页表项,此页表项的内容指向下一级页表PUD
/*
* Macro to populate the PGD (and possibily PUD) for the corresponding
* block entry in the next level (tbl) for the given virtual address.
*
* Preserves: tbl, next, virt
* Corrupts: tmp1, tmp2
*/
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS == 3
create_table_entry \tbl, \virt, TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
.endm
#define PGDIR_SHIFT ((PAGE_SHIFT - 3) * CONFIG_PGTABLE_LEVELS + 3) = \
(12 - 3) * 3 + 3 = 30
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT)) = (1 << (39 - 30)) = 0x200
#define SWAPPER_PGTABLE_LEVELS (CONFIG_PGTABLE_LEVELS - 1) = (3 - 1) = 2
.macro create_block_map, tbl, flags, phys, start, end
- 含义
在物理基地址为tbl的pud页表上,以phys作为起始物理地址,以(end-start)为大小创建PUD页表项,并将相应的物理地址phys+x*BLOCK_SIZE保存到tbl+(start+x)*8指向的PUD页表项,PUD页表项指向2M BLOCK块地址。
注:由于是三级页表,pud是最后一级页表,pte不需要页表,x表示第几个物理块
- 参数
tbl为当前级table的地址, 即PUD table的物理基地址
phys为起始物理地址
start为起始的虚拟地址
end为结束的虚拟地址
注:由于采用2M的block,因此虚拟地址的分配为PGD(bit38bit30),PUD(bit29bit21),
PTE(bit20~bit0),其中PGD需要专门的table来存放下级table的地址,PUD需要专门的table来存放block address,PTE只是单纯作为offset来使用,通过PUD table定位到block address再通过PTE就可以定位到某个byte,因此PTE不需要专门的table
/*
* Macro to populate block entries in the page table for the start..end
* virtual range (inclusive).
*
* Preserves: tbl, flags
* Corrupts: phys, start, end, pstate
*/
.macro create_block_map, tbl, flags, phys, start, end
lsr \phys, \phys, #BLOCK_SHIFT //#define BLOCK_SHIFT SECTION_SHIFT
//#define SECTION_SHIFT 21
//phys=phys>>21
lsr \start, \start, #BLOCK_SHIFT //start=start>>21
and \start, \start, #PTRS_PER_PTE - 1 // table index
//#define PTRS_PER_PTE (1 << (PAGE_SHIFT - 3))
//start=start & 0x1ff
//至此start保存了其初始值的bit21~bit29
orr \phys, \flags, \phys, lsl #BLOCK_SHIFT // table entry
//phys=(phys<<21)|flags
lsr \end, \end, #BLOCK_SHIFT //end=end>>21
and \end, \end, #PTRS_PER_PTE - 1 // table end index
//end=end & 0x1ff
//至此end保存了其初始值的bit21~bit29
9999:
str \phys, [\tbl, \start, lsl #3] // store the entry
// 将phys的值存入地址tbl + start * 8的内存中
//即以start的[29:21]bit为索引的以8byte为单位的页表中
add \start, \start, #1 // next entry,下一个PUD页表项索引
add \phys, \phys, #BLOCK_SIZE // next block,下一个PUD页表项内容
cmp \start, \end
b.ls 9999b
.endm
3. __inval_cache_range
如下引用自网上博文:
为什么要调用__inval_cache_range来invalidate idmap_pg_dir和swapper_pg_dir对应页表空间的cache呢?根据boot protocol,代码执行到此,对于cache的要求是kernel image对应的那段空间的cache line是clean到PoC的,不过idmap_pg_dir和swapper_pg_dir对应页表空间不属于kernel image的一部分,因此其对应的cacheline很可能有一些旧的,无效的数据,必须要清理掉。
顺便再提一句,将idmap和swapper页表内容设定为0是有意义的。实际上这些translation table中的大部分entry都是没有使用的,PGD和PUD都是只有一个entry是有用的,而PMD中有效的entry数目是和mapping的地址size有关。将页表内容清零也就是意味着将页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效,等于0表示无效描述符)
#arch/arm64/mm/cache.S
/*
* __inval_cache_range(start, end)
* - start - start address of region
* - end - end address of region
*/
ENTRY(__inval_cache_range)
/* FALLTHROUGH */
/*
* __dma_inv_range(start, end)
* - start - virtual start address of region
* - end - virtual end address of region
*/
ENTRY(__dma_inv_range)
dcache_line_size x2, x3
sub x3, x2, #1
tst x1, x3 // end cache line aligned?
bic x1, x1, x3
b.eq 1f
dc civac, x1 // clean & invalidate D / U line
1: tst x0, x3 // start cache line aligned?
bic x0, x0, x3
b.eq 2f
dc civac, x0 // clean & invalidate D / U line
b 3f
2: dc ivac, x0 // invalidate D / U line
3: add x0, x0, x2
cmp x0, x1
b.lo 2b
dsb sy
ret
ENDPIPROC(__inval_cache_range)
ENDPROC(__dma_inv_range)
4. __create_page_tables
/*
* Setup the initial page tables. We only setup the barest amount which is
* required to get the kernel running. The following sections are required:
* - identity mapping to enable the MMU (low address, TTBR0)
* - first few MB of the kernel linear mapping to jump to once the MMU has
* been enabled, including the FDT blob (TTBR1)
* - pgd entry for fixed mappings (TTBR1)
*/
__create_page_tables:
pgtbl x25, x26, x28 // idmap_pg_dir and swapper_pg_dir addresses
// pgtbl是一个宏,用将idmap_pg_dir和swapper_pg_dir的物理地址分别赋给x25和x26
mov x27, lr // 保存lr
/*
* Invalidate the idmap and swapper page tables to avoid potential
* dirty cache lines being evicted.
*/
mov x0, x25 // x0保存invalid cache的起始地址,即idmap_pg_dir
add x1, x26, #SWAPPER_DIR_SIZE // x1保存invalid cache的结束地址,即swapper_pg_dir+SWAPPER_DIR_SIZE
bl __inval_cache_range // 将idmap和swapper对应的cacheline设为无效
/*
* Clear the idmap and swapper page tables.
*/
mov x0, x25 //x25为idmap_pg_dir地址
add x6, x26, #SWAPPER_DIR_SIZE //x26为swapper_pg_dir地址
1: stp xzr, xzr, [x0], #16 //stp: 入栈指令(str 的变种指令,可以同时操作两个寄存器)入栈后x0+16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
cmp x0, x6
b.lo 1b // 循环将idmap和swapper内容清0
ldr x7, =MM_MMUFLAGS
/*
* Create the identity mapping.
* 创建kernel user mapping ,identity mapping实际上就是建立整个内核
* 为地址范围KERNEL_START~KERNEL_END创建一致性mapping,即将物理地址等于虚拟地址
* 为起始物理地址idmap_pg_dir范围KERNEL_START~KERNEL_END创建PGD页表项及PUD页表项
*/
mov x0, x25 // idmap_pg_dir// x0保存idmap_pg_dir的物理地址
ldr x3, =KERNEL_START //KERNEL_START是kernel text段的虚拟起始地址,
// KERNEL_START = PAGE_OFFSET + TEXT_OFFSET,
//PAGE_OFFSET = 0xffffffc000000000, TEXT_OFFSET = 0x00080000
add x3, x3, x28 // __pa(KERNEL_START)// x3保存kernel text段的起始地址(物理地址)
create_pgd_entry x0, x3, x5, x6 // 创建pgd页表,x0是pgd的基地址,x3是需要创建pgd页表的内存虚拟地址,x5和x6是临时变量
//x0保存idmap_pg_dir的物理地址,调用完毕x0+=0x100,指向下一个页表项指向的页表,也就是pud表基址
//x3保存kernel text段的起始地址(物理地址)(此处比较特殊,按宏约定应为虚拟地址)
//返回时x5保存了kernel text段的起始地址的bit30~bit38
//返回时x6保存了kernel text段的起始地址的pgd索引对应的pgd页表项
//至此创建PGD页表,只有一个页表项
ldr x6, =KERNEL_END // #define KERNEL_END _end
//_end定义在vmlinux.lds.S,顾名思义是x6保存kernel的结束地址(虚拟地址)
mov x5, x3 // __pa(KERNEL_START)
// x5保存kernel的起始地址(物理地址)
add x6, x6, x28 // __pa(KERNEL_END)
// x6保存kernel的结束地址(物理地址)
create_block_map x0, x7, x3, x5, x6 // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的内存物理地址
//x5是起始物理地址,x6是结束物理地址(此处比较特殊,按宏约定应为虚拟地址)
// 2MB block
//至此为kernel start ~ kernel end创建了PUD页表
/*
* Map the kernel image (starting with PHYS_OFFSET).
* 创建kernel space mapping
* 为起始物理地址PHYS_OFFSET即phys地址范围PAGE_OFFSET~KERNEL_IMAGE_END创建PGD页表项及PUD页表项
*/
mov x0, x26 // swapper_pg_dir
// swapper进程也就是idle进程的地址空间
mov x5, #PAGE_OFFSET //PAGE_OFFSET等于0xffffffc000000000
//PAGE_OFFSET定义了将kernel image安放在虚拟地址空间的哪个位置上
create_pgd_entry x0, x5, x3, x6 // 创建pgd页表,x0是pgd的基地址,x5是需要创建pgd页表的内存地址,x3和x6是临时变量
ldr x6, =KERNEL_END // #define KERNEL_END _end,_end定义在vmlinux.lds.S
//顾名思义是kernel的结束地址(虚拟地址)
mov x3, x24 // phys offset//x3保存kernel image起始物理地址
create_block_map x0, x7, x3, x5, x6 // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的物理内存地址
//x5是起始虚拟地址,x6是结束虚拟地址
// 2MB block
/*
* Map the FDT blob (maximum 2MB; must be within 512MB of
* PHYS_OFFSET).
*为起始物理地址FDT地址范围2M创建PUD页表项
*/
mov x3, x21 // FDT phys address
// x21保存的是device tree的物理地址
and x3, x3, #~((1 << 21) - 1) // 2MB aligned
// x3= x3 & 0xffffffffffe00000 // 2MB对齐
mov x6, #PAGE_OFFSET //PAGE_OFFSET为kernel image的虚拟地址
// kernel image真正内容前有TEXT_OFFSET的偏移
// PAGE_OFFSET等于0xffffffc000000000
sub x5, x3, x24 // subtract PHYS_OFFSET
// x5等于device tree相对于kernel image起始的物理内存地址的偏移
tst x5, #~((1 << 29) - 1) // within 512MB?// 是否小于512MB
csel x21, xzr, x21, ne // zero the FDT pointer
// 如果x5大于512MB,则将x21清0
b.ne 1f // 如果x5大于512MB,跳转到标号1处
add x5, x5, x6 // __va(FDT blob)
// x5等于device tree的虚拟内存地址
add x6, x5, #1 << 21 // 2MB for the FDT blob
//2MB,一般device tree编译生成的dtb只有几百KB
sub x6, x6, #1 // inclusive range
create_block_map x0, x7, x3, x5, x6 // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的内存物理地址
//x5是起始虚拟地址,x6是结束虚拟地址
// 2MB block
1:
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate the idmap and swapper page
* tables again to remove any speculatively loaded cache lines.
*/
mov x0, x25
add x1, x26, #SWAPPER_DIR_SIZE
bl __inval_cache_range // 再次将idmap和swapper对应的cacheline设为无效
mov lr, x27 // 恢复lr
ret
ENDPROC(__create_page_tables)
__create_page_tables主要通过调用create_pgd_entry ,create_block_map 做了如下的工作:
- 将idmap_pg_dir(存放用户空间页表地址)和swapper_pg_dir(存放内核空间页表地址)对应的cache line无效;
- 将idmap_pg_dir(存放用户空间页表地址)和swapper_pg_dir(存放内核空间页表地址)的page table清零;
- 创建kernel 的一致性映射页表(物理地址与虚拟地址一致)
在idmap_pg_dir处创建pgd table entry,存放的pud table地址为idmap_pg_dir+PAGE_SIZE;
在idmap_pg_dir+PAGE_SIZE处创建pud table entry; - 创建kernel的mapping映射页表
在swapper_pg_dir处创建pgd table entry,存放的pud table地址为swapper_pg_dir+PAGE_SIZE;
在swapper_pg_dir+PAGE_SIZE处创建pud table entry; - 创建FDT的mapping映射页表
在swapper_pg_dir+PAGE_SIZE处创建pud table entry,并对FDT地址和大小的合法性进行检查;
注意在Linux4.x版本之后FDT映射页表的创建已经改为在
setup_arch->early_fixmap_init
注:由于FDT在kernel image的512M范围以内,因此kernel image和fdt的虚拟地址的bit[38:30]相同,因此在(4)中已经创建了pud 地址为swapper_pg_dir+PAGE_SIZE的pgd table entry,在本步骤中不需要再创建pgd entry,而只是创建pud table entry即可
注:
(1)上文中kernel image表示kernel镜像,包含TEXT_OFFSET的内容,kernel表示直接从text段开始,不包含TEXT_OFFSET内容
(2)create_block_map或create_pgd_entry宏都是为虚拟地址创建页表项(kernel identity mapping除外),页表项中保存的是下级页表或块或页的物理基地址
(3)为什么还要为kernel物理地址进行一致性映射呢?
解释1:
CPU流水线取指令,在开MMU后,PC地址应该全部是虚拟地址,但是预取指令功能导致开MMU的时候既有虚拟地址,又有物理地址,如果我们只建立虚拟地址的映射,那么PC物理地址送到MMU之后就会找不到相应的页表,会出错。
解释2:
linux设置好页表之后,最终有一条指令是启用MMU的,假设该指令的PA是0x0800810c,根据我们要做的映射关系,它的VA应该是0xc000 810c,没有启用MMU之前CPU核发出的都是物理地址,从0x0800 810c地址取这条指令来执行,然而该指令执行之后,CPU核发出的地址都要被MMU拦截,CPU核就必须用虚拟地址来取指令了,因此下一条指令应该从0xc000 8110处取得,然而这时pc寄存器(也就是r15寄存器)的值并没有变,CPU核取下一条指令仍然要从0x0800 8110处取得,此时0x0800 8110已经成了非法地址了。为了解决这个问题,要求启用MMU的那条指令及其附近的指令虚拟地址跟物理地址相同,这样在启用MMU前后,附近指令的地址不会发生变化,从而实现平稳过渡。因此需要将物理地址从0x0800 0000开始的1M再映射到虚拟地址从0x0800 0000开始的1M,也就是做一个等价映射
解释3:
identity mapping实际上就是建立了整个内核(从KERNEL_START到KERNEL_END)的一致性mapping,就是将物理地址所在的虚拟地址段mapping到物理地址上去。为什么这么做呢?ARM ARM文档中有一段话:
If the PA of the software that enables or disables a particular stage of address translation differs from its VA, speculative instruction fetching can cause complications. ARM strongly recommends that the PA and VA of any software that enables or disables a stage of address translation are identical if that stage of translation controls translations that apply to the software currently being executed.
由于打开MMU操作的时候,内核代码欢快的执行,这时候有一个地址映射ON/OFF的切换过程,这种一致性映射可以保证在在打开MMU那一点附近的程序代码可以平滑切换
补充知识
1.stp: 入栈指令(str 的变种指令,可以同时操作两个寄存器),如: stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置
参考文档
1.https://blog.csdn.net/xichangbao/article/details/51568782
2.https://www.cnblogs.com/smartjourneys/diary/2017/04/27/6774121.html
3.http://www.wowotech.net/armv8a_arch/arm64_initialize_1.html ARM64的启动过程之(一):内核第一个脚印
3.http://www.wowotech.net/armv8a_arch/create_page_tables.html ARM64的启动过程之(二):创建启动阶段的页表
4.http://www.wowotech.net/armv8a_arch/__cpu_setup.html ARM64的启动过程之(三):为打开MMU而进行的CPU初始化
5.http://www.wowotech.net/armv8a_arch/turn-on-mmu.html ARM64的启动过程之(四):打开MMU
6.https://blog.csdn.net/xichangbao/article/details/51568782 Kernel启动流程源码解析 1 head.S
7.https://blog.csdn.net/xichangbao/article/details/51605462 Kernel启动流程源码解析 2 head.S