一. 前言
前面我们介绍了riscv的mmu以及kernel汇编代码中的relocate处理。这一篇继续来分享启动代码中最重要的一环setup_vm。该函数为relocate做准备,即准备实现VA-PA映射的页表,在relocate时使能MMU的satp指向这些页表,从物理地址切换到虚拟地址运行。
二. 分析过程
为了方便整个运行过程的分析,我们基于QEMU和GDB来进行仿真分析,可以方便对照汇编,查看当前状态等。
2.1准备工作
先根据vmlinux导出汇编代码
riscv64-unknown-linux-gnu-objdump -l -S output/vmlinux > a.s
开一个终端运行qemu
qemu-system-riscv64 -M virt -nographic -S -s
然后再开一个终端运行gdb
riscv64-unknown-linux-gnu-gdb -f output/vmlinux -x linux/qemu_linux_gdbinit.txt
qemu_linux_gdbinit.txt内容如下
define connect
target extended-remote localhost:1234
end
define linux_init
set $opensbi_addr = 0x80000000
set $vmlinux_addr = $opensbi_addr + 0x00200000
set $rootfs_addr = $opensbi_addr + 0x04000000
set $dtb_addr = $rootfs_addr - 0x00100000
set $dyn_info_addr = $rootfs_addr - 0x40
set *(unsigned long *)($dyn_info_addr) = 0x4942534f
set *(unsigned long *)($dyn_info_addr + 8) = 0x1
set *(unsigned long *)($dyn_info_addr + 16) = $vmlinux_addr
set *(unsigned long *)($dyn_info_addr + 24) = 0x1
set *(unsigned long *)($dyn_info_addr + 32) = 0x0
set *(unsigned long *)($dyn_info_addr + 48) = 0x0
set $a0 = 0
set $a1 = $dtb_addr
set $a2 = $dyn_info_addr
set $pc = $opensbi_addr
#导入kernel镜像
restore output/arch/riscv/boot/Image binary $vmlinux_addr
#导入设备树
restore opensbi/dump_qemu.dtb binary $dtb_addr
#导入opensbi
restore opensbi/build/platform/generic/firmware/fw_dynamic.bin binary $opensbi_addr
#导入根文件系统
restore buildroot/images/rootfs.cpio.gz binary $rootfs_addr
end
gdb中,链接qemu初始化,打断点运行到kernel的入口处
connect
linux_init
hb *0x80200000
c
然后
layout asm打开汇编界面,即可和上面根据vmlinux生成的a.s汇编对应。
汇编中是链接地址ffffffe000000000,对应的是GDB中的运行地址0x80200000
2.2 代码分析
下面就逐步分析setup_vm函数的执行过程:
如下位置调用setup_vm
head.S中
Initialize page tables and relocate to virtual addresses */
la sp, init_thread_union + THREAD_SIZE
mv a0, s1
call setup_vm
setup_vm见
arch/riscv/mm/init.c
上述汇编代码我们前面已经分析过了,现在重点来看setup_vm.
我们先删除原来的hb硬件断点,新创建一个hb断点,运行到setup_vm.
1.info b 简写i b查看当前有一个断点1
2.delete 1 简写d 1删除该断点
3.查找到a.s中setup_vm函数的地址为ffffffe00000527e,转为运行地址是0x8020527e.
hb *0x8020527e在setup_vm处打断点
4.c 运行到断点处
现在开始,就可以
si单步运行跟踪整个运行过程了。
函数入口出口
函数的入口调整栈栈指针分配栈空间,保存寄存器(需要使用的s寄存器和ra),
函数出口即恢复寄存器,恢复栈指针,然后ret回到ra处。ra在call时硬件自动保存为下一条指令地址(即call返回后继续执行的地址处)。
先来看函数入口
asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
ffffffe00000527e: 7139 addi sp,sp,-64
ffffffe000005280: f822 sd s0,48(sp)
ffffffe000005282: f04a sd s2,32(sp)
ffffffe000005284: ec4e sd s3,24(sp)
ffffffe000005286: e852 sd s4,16(sp)
ffffffe000005288: e456 sd s5,8(sp)
441 :
uintptr_t va, pa, end_va;
uintptr_t load_pa = (uintptr_t)(&_start);
ffffffe00000528a: ffffb917 auipc s2,0xffffb
ffffffe00000528e: d7690913 addi s2,s2,-650 # ffffffe000000000 <_start>
439 :
{
ffffffe000005292: fc06 sd ra,56(sp)
ffffffe000005294: f426 sd s1,40(sp)
ffffffe000005296: e05a sd s6,0(sp)
先来看sp,此时sp的值为0x81c04000
info reg sp
sp 0x81c04000 0x81c04000
(gdb)
然后
sp往下挪动8个DWORD即64字节
addi sp,sp,-64
预留8个DWORD空间来存储,s0,s2,s3,s4,s5,ra,s1,s6
保存ra是因为,ra记录了返回地址,而本函数还可能继续调用子函数call时会重新更新ra。
最后ret前要恢复ra,ret根据ra返回到调用处。
sx寄存器即被调用保存寄存器,即函数内部需要使用这些寄存器所以要保存,函数返回时恢复原样。
sd s0,48(sp)的sd表示store dword即存储一个8字节,源是s0寄存器,目的是sp寄存器的值表示的地址偏移48字节。
所以就是将s0寄存器的内容存储到0x81c04000+48这个位置去。
ld s0,48(sp)类似,表load dword即从sp寄存器的值标志的地址处偏移48处加载dword到s0寄存器。
sd和ld的源头和目的是反的。sd是前面往后面store,ld是从后面往前面load。
对应函数退出时
ffffffe000005428: 70e2 ld ra,56(sp)
ffffffe00000542a: 7442 ld s0,48(sp)
ffffffe00000542c: 74a2 ld s1,40(sp)
ffffffe00000542e: 7902 ld s2,32(sp)
ffffffe000005430: 69e2 ld s3,24(sp)
ffffffe000005432: 6a42 ld s4,16(sp)
ffffffe000005434: 6aa2 ld s5,8(sp)
ffffffe000005436: 6b02 ld s6,0(sp)
ffffffe000005438: 6121 addi sp,sp,64
ffffffe00000543a: 8082 ret
然后以下代码获取运行地址
uintptr_t va, pa, end_va;
uintptr_t load_pa = (uintptr_t)(&_start);
ffffffe00000528a: ffffb917 auipc s2,0xffffb
ffffffe00000528e: d7690913 addi s2,s2,-650 # ffffffe000000000 <_start>
上述代码用于获取镜像的运行基地址到s2寄存器。
auipc s2, 0xffffb表示PC加立即数到高位。
s2 = pc + (0xffffb << 12) 其中(0xffffb << 12)按照符号扩展到32位。
addi s2,s2,-650表示,立即数加
s2 = s2 + (-650)
所以执行完
auipc s2,0xffffb 后
info reg s2
s2 0x8020028a 2149581450
即
0x8020528a + 0xffffb000 = 0x1 8020 028A 丢掉高位位0x8020028A。
然后执行完
addi s2,s2,-650
s2为
info reg s2
s2 0x80200000 2149580800
(gdb)
正好是我们加载到0x80200000运行的地址。
我们直接查看_start符号的地址是链接地址即记录在elf文件中的信息,
(gdb) p &_start
$1 = (char (*)[]) 0xffffffe000000000 <_start>
(gdb)
对应的是我们要计算这里的加载地址0x80200000
那么编译器是怎么去做这件事的呢,
实际我们使用了-PIC位置无关编译,那么编译器会使用相对于PC的相对寻址。
auipc s2,0xffffb 这条指令的链接地址是
ffffffe00000528a
而符号_start对应链接基地址
ffffffe000000000 <_start>:
两者的偏差是ffffffe00000528a-ffffffe000000000=0x528a=21130。
不管加载在哪里运行,这个差是不变的(因为镜像是整体搬运),这是一个常数,编译器是知道的。比如加载到0x8020000运行则是
8020528a-80200000=0x528a=21130。
编译器将auipc指令处的pc值减去这个偏差,就知道最开始处_start处加载的地址了。
所以这个偏差就可以编码在指令码中,实际是通过两条指令来实现(一条指令无法编码这么偏移立即数),编译器是根据0xFFFFB000 - 650得到的这个偏差,分为了两步:
0xFFFFB000对应-(0xFFFFFFFF-0xFFFFB000)=-20480
-20480-650=-21130. 这里_start在auipc前面,所以偏差是负数。
原理即如下
获取镜像大小load_sz
函数入口保存完寄存器后继续
ffffffe000005298: 0080 addi s0,sp,64
之前sp -64,现在
s0=sp+64即变为函数进来时调整sp之前的sp值。
然后
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:442
uintptr_t load_sz = (uintptr_t)(&_end) - load_pa;
ffffffe00000529a: 01bc8a17 auipc s4,0x1bc8
ffffffe00000529e: d66a0a13 addi s4,s4,-666 # ffffffe001bcd000 <__efistub__end>
上面两条汇编和计算&_start一样,计算(uintptr_t)(&_end) 即镜像运行地址的最后面。
load_pa为前面获取的镜像运行基地址,两者相减即得到了镜像的大小(这个减的操作对应的汇编在后面,不是在这里计算,编译器做了优化)。
执行完auipc s4,0x1bc8后
0x8020529A+0x1bc8000=0x81DCD29A
然后0x81DCD29A-666=0x81DCD000
- load_pa,对应如下汇编,计算的镜像大小存放在了s4
所以可以计算出镜像大小为
0x81DCD000-0x80200000=29151232
也可以根据链接地址算出这个偏差
(gdb) p &_end
$1 = (char (*)[]) 0xffffffe001bcd000
(gdb) p &_start
$2 = (char (*)[]) 0xffffffe000000000 <_start>
(gdb) p &_end - &_start
warning: Type size unknown, assuming 1. Try casting to a known type, or void *.
$3 = 29151232
(gdb)
接下来
best_map_size():
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:414
if ((base & (PMD_SIZE - 1)) || (size & (PMD_SIZE - 1)))
ffffffe0000052a2: 02b91793 slli a5,s2,0x2b
setup_vm():
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:439
{
ffffffe0000052a6: 89aa mv s3,a0
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:442
uintptr_t load_sz = (uintptr_t)(&_end) - load_pa;
ffffffe0000052a8: 412a0a33 sub s4,s4,s2
best_map_size():
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:415
return PAGE_SIZE;
ffffffe0000052ac: 6a85 lui s5,0x1
/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:414
if ((base & (PMD_SIZE - 1)) || (size & (PMD_SIZE - 1)))
ffffffe0000052ae: e399 bnez a5,ffffffe0000052b4 <setup_vm+0x36>
ffffffe0000052b0: 00200ab7 lui s5,0x200
s2前面记录的是load_pa即0x80200000
计算map_size
uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE);
其中
arch/riscv/mm/init.c中
#define MAX_EARLY_MAPPING_SIZE SZ_128M
include/linux/sizes.h中
#define SZ_128M 0x08000000
arch/riscv/mm/init.c中内联函数
static uintptr_t __init best_map_size(phys_addr_t base, phys_addr_t size)
{
/* Upgrade to PMD_SIZE mappings whenever possible */
if ((base & (PMD_SIZE - 1)) || (size & (PMD_SIZE - 1)))
return PAGE_SIZE;
return PMD_SIZE;
}
其中
arch/riscv/include/asm/pgtable-64.h中
/* Size of region mapped by a page middle directory */
所以PMD_SIZE为2M
其中
arch/riscv/include/asm/page.h中
PAGE_SIZE为4K。
所以
uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE);
如果load_pa(0x80200000)和MAX_EARLY_MAPPING_SIZE(128M)任意一个不能被PDM_SIZE整除则返回PAGE_SIZE否则返回PMD_SIZE。
这里都能被2M整除,所以map_size为2M。
__PAGETABLE_PMD_FOLDED未定义
所以以下忽略
pmd_t fix_bmap_spmd, fix_bmap_epmd;
计算va-pa偏差
va_pa_offset = PAGE_OFFSET - load_pa;
其中
arch/riscv/include/asm/page.h
#define PAGE_OFFSET _AC(CONFIG_PAGE_OFFSET, UL)
即链接地址,前面汇编代码中介绍过了,这里是
0xffffffe000000000
所以va_pa_offset = 0xffffffe000000000 - 0x80200000
计算pfn_base
pfn_base = PFN_DOWN(load_pa);
include/linux/pfn.h中
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
arch/riscv/include/asm/page.h中
#define PAGE_SHIFT (12)
所以pfn_base表示load_pa>>12
这里是0x80200000>>12=0x80200
即页号。
参数检查
/*
* Enforce boot alignment requirements of RV32 and
* RV64 by only allowing PMD or PGD mappings.
*/
BUG_ON(map_size == PAGE_SIZE);
/* Sanity check alignment and size */
BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);
BUG_ON((load_pa % map_size) != 0);
BUG_ON(load_sz > MAX_EARLY_MAPPING_SIZE);
map_size前面计算得到的是2M,
PAGE_SIZE为4K,所以BUG_ON条件不满足,没问题。
这里要求是map_size必须按照PMD大页进行,即以2M为单位。
BUG_ON在
include/asm-generic/bug.h中定义
如果定义了CONFIG_BUG
printk( , __FILE__, __LINE__, __func__); \
barrier_before_unreachable(); \
panic( ); \
} while (0)
没有定义则
同时后面检查
PAGE_OFFSET % PGDIR_SIZE要为0
这里PAGE_OFFSET为0xffffffe000000000
arch/riscv/include/asm/pgtable-64.h中
/* Size of region mapped by a page global directory */
PGDIR_SIZE为1GB。
也就是PAGE_OFFSET要1GB对齐。
继续检查
load_pa % map_size要为0
这里load_pa为0x80200000
map_size为2M,
也就是load_pa要为2M对齐。
继续检查
load_sz 要<= MAX_EARLY_MAPPING_SIZE
load_sz是前面计算的镜像大小29151232大概28M
arch/riscv/mm/init.c中
#define MAX_EARLY_MAPPING_SIZE SZ_128M
所以镜像大小是小于128M,没问题。
总结下以上的参数检查就是
虚拟地址即链接地址要1GB对齐
加载地址即物理地址要按照PDM大小2MB对齐
按照PMD 2MB大小做MMU映射。
镜像大小不要超过128MB。
设置pte,pmd接口
pt_ops.alloc_pte = alloc_pte_early;
pt_ops.get_pte_virt = get_pte_virt_early;
pt_ops.alloc_pmd = alloc_pmd_early;
pt_ops.get_pmd_virt = get_pmd_virt_early;
pt_ops是全局变量
static struct pt_alloc_ops pt_ops;
struct pt_alloc_ops {
pte_t *(*get_pte_virt)(phys_addr_t pa);
phys_addr_t (*alloc_pte)(uintptr_t va);
pmd_t *(*get_pmd_virt)(phys_addr_t pa);
phys_addr_t (*alloc_pmd)(uintptr_t va);
};
分别设置对应的接口函数
这里__PAGETABLE_PMD_FOLDED没有定义。
create_pgd_mapping
前面都是该函数的准备工作,下面就是关键步骤了,即调用create_pgd_mapping
和create_pmd_mapping创建pgd和pmd页表。所以重点就来介绍这两个函数。需要用到前面riscv的mmu相关的内容。
参数
先以第一个
/* Setup early PGD for fixmap */
create_pgd_mapping(early_pg_dir, FIXADDR_START,
(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
为例
void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
第一个参数pgdp是根页表,这里传入early_pg_dir是全局变量
pgd_t early_pg_dir[PTRS_PER_PGD] __initdata __aligned(PAGE_SIZE);
其中
/* Page Global Directory entry */
typedef struct {
unsigned long pgd;
} pgd_t;
由于现在mem还没初始化不能使用动态内存分配,所以这里都是使用静态的变量。
且页表都是设置为只有一个页大小即4k/8=512个条目。
arch/riscv/include/asm/pgtable.h
#define PTRS_PER_PGD (PAGE_SIZE / sizeof(pgd_t))
PTRS_PER_PGD表示一个页中可以有多少条pgd表项。
对于64位,一个表项8字节,所以4K/8=512条。
include/linux/init.h中,表示将这个变量放在.init.data段中
#define __initdata __section(".init.data")
__aligned(PAGE_SIZE)表示这个变量按照4K对齐。
GDB查看链接地址
(gdb) p &early_pg_dir
$18 = (pgd_t (*)[512]) 0xffffffe00087b000 <early_pg_dir>
(gdb)
对应的运行地址是
0x80200000+0x87b000=0x80a7b000
第二个参数VA,
即需要映射的虚拟地址,这里是FIXADDR_START
arch/riscv/include/asm/pgtable.h中
#define KERN_VIRT_SIZE (-PAGE_OFFSET)
0xffffffe000000000 -> 0x2000000000
#define VMALLOC_SIZE (KERN_VIRT_SIZE >> 1)
0x1000000000
#define VMALLOC_END (PAGE_OFFSET - 1)
0xffffffe000000000-1
#define VMALLOC_START (PAGE_OFFSET - VMALLOC_SIZE)
0xffffffe000000000-0x1000000000
然后,
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
其中
#define FIXADDR_TOP PCI_IO_START
这里是64位
#ifdef CONFIG_64BIT
#define FIXADDR_SIZE PMD_SIZE
#else
所以FIXADDR_START = PCI_IO_START - 2M
#define PCI_IO_SIZE SZ_16M
#define PCI_IO_END VMEMMAP_START
#define PCI_IO_START (PCI_IO_END - PCI_IO_SIZE)
FIXADDR_START = VMEMMAP_START -16M - 2M
#define VMEMMAP_START (VMALLOC_START - VMEMMAP_SIZE)
所以
FIXADDR_START = VMALLOC_START - VMEMMAP_SIZE -16M - 2M
FIXADDR_START = 0xffffffe000000000-0x1000000000 - VMEMMAP_SIZE -16M - 2M
而
#define VMEMMAP_SHIFT \
(CONFIG_VA_BITS - PAGE_SHIFT - 1 + STRUCT_PAGE_MAX_SHIFT)
#define VMEMMAP_SIZE BIT(VMEMMAP_SHIFT)
其中
#define BIT(nr) (UL(1) << (nr))
即 VMEMMAP_SIZE = 1<<(CONFIG_VA_BITS - PAGE_SHIFT - 1 + STRUCT_PAGE_MAX_SHIFT)
其中CONFIG_VA_BITS来自于.config
CONFIG_VA_BITS=39
得到autoconf.h中
#define CONFIG_VA_BITS 39
其中
arch/riscv/include/asm/page.h中
#define PAGE_SHIFT (12)
include/linux/mm_types.h中
#define STRUCT_PAGE_MAX_SHIFT (order_base_2(sizeof(struct page)))
其中
(gdb) p sizeof(struct page)
$19 = 56
其中
linux/linux-custom/include/linux/log2.h中实现order_base_2表示2的多少指数倍,向上圆整。
2^5=32
2^6=64
所以STRUCT_PAGE_MAX_SHIFT为6
所以VMEMMAP_SIZE=1<<(39-12-1+6)=1<<32=4GB
所以
FIXADDR_START = 0xffffffe000000000-0x1000000000 - 4GB -16M - 2M
= 0xffffffe000000000-0x1000000000 - 0x100000000 -0x1000000- 0x200000
= 0xFFFF FFCE FEE0 0000
第三个参数pa,
表示VA对应映射的物理地址或者下一级页表的物理i地址
这里是fixmap_pgd_next
__PAGETABLE_PMD_FOLDED未定义时
#define fixmap_pgd_next fixmap_pmd
pmd_t fixmap_pmd[PTRS_PER_PMD] __page_aligned_bss;
即一个全局变量
其中arch/riscv/include/asm/pgtable-64.h
/* Page Middle Directory entry */
typedef struct {
unsigned long pmd;
} pmd_t;
include/linux/linkage.h中
#define __page_aligned_bss __section(".bss..page_aligned") __aligned(PAGE_SIZE)
即放在.bss..page_aligned段,4KB对齐。
GDB查看链接地址
(gdb) p &fixmap_pmd
$17 = (pmd_t (*)[512]) 0xffffffe001b7a000 <fixmap_pmd>
(gdb)
实际对应的运行地址应该是
0x80200000+1b7a000 =0x81d7a000
第三个参数size
即从va地址开始映射多大的空间
这里是PGDIR_SIZE为1GB=0x40000000
第四个参数prot
即对应的属性
这里传入的参数是PAGE_TABLE代表pa是下一级页表的物理地址
在arch/riscv/include/asm/pgtable.h中
#define PAGE_TABLE __pgprot(_PAGE_TABLE)
arch/riscv/include/asm/pgtable-bits.h中
#define _PAGE_PRESENT (1 << 0)
#define _PAGE_TABLE _PAGE_PRESENT
arch/riscv/include/asm/page.h中
typedef struct {
unsigned long pgprot;
} pgprot_t;
#define __pgprot(x) ((pgprot_t) { (x) })
所以
PAGE_TABLE 即 ((pgprot_t) { (1) })
其他可能的属性见
arch/riscv/include/asm/pgtable.h,和前面介绍的pte的内容对应
#define PAGE_KERNEL __pgprot(_PAGE_KERNEL)
#define PAGE_KERNEL_EXEC __pgprot(_PAGE_KERNEL | _PAGE_EXEC)
#define PAGE_KERNEL_READ __pgprot(_PAGE_KERNEL & ~_PAGE_WRITE)
#define PAGE_KERNEL_EXEC __pgprot(_PAGE_KERNEL | _PAGE_EXEC)
#define PAGE_KERNEL_READ_EXEC __pgprot((_PAGE_KERNEL & ~_PAGE_WRITE) \
| _PAGE_EXEC)
比如
PAGE_KERNEL_EXEC
PAGE_KERNEL
所以这里的参数
create_pgd_mapping(early_pg_dir, FIXADDR_START,
(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
为
create_pgd_mapping(0x80a7b000, 0xffffffcefee00000,
0x81d7a000, 0x40000000,1);
GDB查看参数可以看到和分析的是一样的
代码分析
void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pgd_next_t *nextp;
phys_addr_t next_phys;
uintptr_t pgd_idx = pgd_index(va);
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_idx]) == 0)
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
if (pgd_val(pgdp[pgd_idx]) == 0) {
next_phys = alloc_pgd_next(va);
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
nextp = get_pgd_next_virt(next_phys);
memset(nextp, 0, PAGE_SIZE);
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));
nextp = get_pgd_next_virt(next_phys);
}
create_pgd_next_mapping(nextp, va, pa, sz, prot);
}
首先获取pgd索引
uintptr_t pgd_idx = pgd_index(va);
其中include/linux/pgtable.h中PGDIR_SHIFT为30(对应1GB), PTRS_PER_PGD为512
#ifndef pgd_index
/* Must be a compile-time constant, so implement it as a macro */
#define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#endif
即PGD对应映射颗粒度是1GB。
create_pgd_mapping(0x80a7b000, 0xffffffcefee00000,
0x81d7a000, 0x40000000,1);
va为
0xffffffcefee00000则
pgd_idx为0x13B
(0xffffffcefee00000>>30)&511
然后
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_idx]) == 0)
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
sz==PGDIR_SIZE sz为1GB是满足的。
然后计算pgd_val(pgdp[pgd_idx])
其中pgdp[pgd_idx]即pgdp第pgd_idx个条目,一个条目8字节,pgd_idx个条目即对应偏移
pgd_idx*8, 即对应slli s3,s3,0x3
然后add s3,s3,a0即
pgdp地址基础上偏移pgd_idx*8,a0即参数pgdp,即early_pg_dir
最后pgd_val(pgdp[pgd_idx])
即获取该条目的内容
arch/riscv/include/asm/page.h中
#define pgd_val(x) ((x).pgd)
刚开始条目的内容肯定是0,所以以下语句满足
if (pgd_val(pgdp[pgd_idx]) == 0)
执行
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
其中prot为1
include/linux/pfn.h中
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
pa为0x81d7a000所以结果是0x81d7a
其中
arch/riscv/include/asm/pgtable.h
static inline pgd_t pfn_pgd(unsigned long pfn, pgprot_t prot)
{
return __pgd((pfn << _PAGE_PFN_SHIFT) | pgprot_val(prot));
}
arch/riscv/include/asm/pgtable-bits.h中
#define _PAGE_PFN_SHIFT 10
arch/riscv/include/asm/page.h中
#define pgprot_val(x) ((x).pgprot)
arch/riscv/include/asm/page.h中
#define __pgd(x) ((pgd_t) { (x) })
即返回pgt_t条目,内容为
(0x81d7a<<10) | 1 = 0x2075E801
将其写入到
pgdp[pgd_idx]
即early_pg_dir[0x13b]中。
即early_pg_dir[0x13b]表项指向了
fixmap_pmd。
此时查看
(gdb) p *(pgd_t (*)[512])0x80a7b000
$4 = {{pgd = 0} <repeats 315 times>, {pgd = 544598017}, {
pgd = 0} <repeats 196 times>}
(gdb)
索引315即0x13B处值为 544598017即0x2075 E801,和前面手动计算的对应。
上面
/* Setup early PGD for fixmap */
create_pgd_mapping(early_pg_dir, FIXADDR_START,
(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
sz为PGDIR_SIZE所以
执行到如下直接退出
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_idx]) == 0)
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
即只设置了pgd页表的一个条目,映射一级。
如果sz不为PGDIR_SIZE,
不会进入上述if而是往后继续
if (pgd_val(pgdp[pgd_idx]) == 0) {
next_phys = alloc_pgd_next(va);
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
nextp = get_pgd_next_virt(next_phys);
memset(nextp, 0, PAGE_SIZE);
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));
nextp = get_pgd_next_virt(next_phys);
}
create_pgd_next_mapping(nextp, va, pa, sz, prot);
首先if (pgd_val(pgdp[pgd_idx]) == 0)
如果pgd页表中为0说明pgd中还没有这个va对应的条目,即没有指向下一级pmd
则这里 next_phys = alloc_pgd_next(va);先分配一个pmd页表,分配pmd页表实现如下
#define alloc_pgd_next(__va) pt_ops.alloc_pmd(__va)
pt_ops.alloc_pmd = alloc_pmd_early;
实际是从静态变量early_pmd这个页表中分出
static phys_addr_t __init alloc_pmd_early(uintptr_t va)
{
uintptr_t pmd_num;
pmd_num = (va - PAGE_OFFSET) >> PGDIR_SHIFT;
BUG_ON(pmd_num >= NUM_EARLY_PMDS);
return (uintptr_t)&early_pmd[pmd_num * PTRS_PER_PMD];
}
然后设置pgd页表中条目,指向刚才分配的pmd
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
然后 nextp = get_pgd_next_virt(next_phys);
实现如下
#define get_pgd_next_virt(__pa) pt_ops.get_pmd_virt(__pa)获取pmd页表虚拟地址
pt_ops.get_pte_virt = get_pte_virt_early;
static inline pte_t *__init get_pte_virt_early(phys_addr_t pa)
{
return (pte_t *)((uintptr_t)pa);
}
然后将该pmd页表清零
memset(nextp, 0, PAGE_SIZE);
如果之前条目已经存在则early_pg_dir[x]不为0,会走
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));
nextp = get_pgd_next_virt(next_phys);
}
next_phys会索引到之前创建的early_pg_dir[x]指向的pmd
然后获取到其虚拟地址nextp
然后继续创建下一级
create_pgd_next_mapping(nextp, va, pa, sz, prot);
即继承开始的参数
xxx_pgd, FIXADDR_START,
(uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABL
除了xxx_pgd变为了nexpt即xxx_pmd
create_pgd_next_mapping实现如下,即继续调用create_pmd_mapping创建下一级pmd到pte的映射
#define create_pgd_next_mapping(__nextp, __va, __pa, __sz, __prot) \
create_pmd_mapping(__nextp, __va, __pa, __sz, __prot)
create_pmd_mapping
和create_pgd_mapping的实现完全一样,只是他会调用下一级创建pte页表,而
create_pgd_mapping下一级调用的是create_pmd_mapping
以如下为例
#ifndef __PAGETABLE_PMD_FOLDED
/* Setup fixmap PMD */
create_pmd_mapping(fixmap_pmd, FIXADDR_START,
(uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE);
类似create_pgd_mapping分析的过程
pte_t *ptep;
phys_addr_t pte_phys;
uintptr_t pmd_idx = pmd_index(va);
if (sz == PMD_SIZE) {
if (pmd_none(pmdp[pmd_idx]))
pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pa), prot);
return;
}
此时5个参数的值分别为
pmd_index
create_pte_mapping
实现创建叶子pte,即最后一级
static void __init create_pte_mapping(pte_t *ptep,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
uintptr_t pte_idx = pte_index(va);
BUG_ON(sz != PAGE_SIZE);
if (pte_none(ptep[pte_idx]))
ptep[pte_idx] = pfn_pte(PFN_DOWN(pa), prot);
}
include/linux/pgtable.h中
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
PAGE_SHIFT为12,PTRS_PER_PTE为512,
所以pte_index获取的是pte页表的条目索引。
然后检查
BUG_ON(sz != PAGE_SIZE);
sz必须为4K大小
然后检查如果当前页表项目为0才设置,即该条目指向最终的物理页pa
if (pte_none(ptep[pte_idx]))
ptep[pte_idx] = pfn_pte(PFN_DOWN(pa), prot);
三.总结
主要是调用以下配置页表的接口实现va-pa的映射,该函数后面还有一些特殊处理后面再分析
create_pgd_mapping
create_pmd_mapping
create_pte_mapping
三者的实现模式是相通的,前两者可能还有后级,会调用后一级的映射处理,而pte是最后一级所以找到条目直接会指向对应的物理地址。
setup_vm创建了两个页表relocate时从trampoline_pg_dir切换到early_pg_dir
1.trampoline_pg_dir->trampoline_pmd
2.early_pg_dir->fixmap_pmd->fixmap_pte
->early_pmd
->early_dtb_pmd->