RISCV linux kernel启动代码分析之三:setup_vm分析

文摘   2024-11-15 14:44   湖南  

一. 前言

前面我们介绍了riscvmmu以及kernel汇编代码中的relocate处理。这一篇继续来分享启动代码中最重要的一环setup_vm。该函数为relocate做准备,即准备实现VA-PA映射的页表,在relocate时使能MMU的satp指向这些页表,从物理地址切换到虚拟地址运行。

二. 分析过程

为了方便整个运行过程的分析,我们基于QEMUGDB来进行仿真分析,可以方便对照汇编,查看当前状态等。

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:1234end
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_addrend

gdb,链接qemu初始化,打断点运行到kernel的入口处

connectlinux_inithb *0x80200000c

然后

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.ssetup_vm函数的地址为ffffffe00000527e,转为运行地址是0x8020527e.

hb *0x8020527esetup_vm处打断点

4.c 运行到断点处

现在开始,就可以

si单步运行跟踪整个运行过程了。

函数入口出口

函数的入口调整栈栈指针分配栈空间,保存寄存器(需要使用的s寄存器和ra)

函数出口即恢复寄存器,恢复栈指针,然后ret回到ra处。racall时硬件自动保存为下一条指令地址(call返回后继续执行的地址处)

先来看函数入口

asmlinkage void __init setup_vm(uintptr_t dtb_pa){ffffffe00000527e:  7139                  addi  sp,sp,-64ffffffe000005280:  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)/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:441  uintptr_t va, pa, end_va;  uintptr_t load_pa = (uintptr_t)(&_start);ffffffe00000528a:  ffffb917            auipc  s2,0xffffbffffffe00000528e:  d7690913            addi  s2,s2,-650 # ffffffe000000000 <_start>/home/qinyunti/sdk/linux/linux-custom/output/../arch/riscv/mm/init.c:439{ffffffe000005292:  fc06                  sd  ra,56(sp)ffffffe000005294:  f426                  sd  s1,40(sp)ffffffe000005296:  e05a                  sd  s6,0(sp)

先来看sp,此时sp的值为0x81c04000

(gdb) info reg spsp             0x81c04000       0x81c04000(gdb)

然后

sp往下挪动8DWORD64字节

addi sp,sp,-64

预留8DWORD空间来存储,s0,s2,s3,s4,s5,ra,s1,s6

保存ra是因为,ra记录了返回地址,而本函数还可能继续调用子函数call时会重新更新ra

最后ret前要恢复raret根据ra返回到调用处。

sx寄存器即被调用保存寄存器,即函数内部需要使用这些寄存器所以要保存,函数返回时恢复原样。

sd s0,48(sp)sd表示store dword即存储一个8字节,源是s0寄存器,目的是sp寄存器的值表示的地址偏移48字节。

所以就是将s0寄存器的内容存储到0x81c04000+48这个位置去。

ld s0,48(sp)类似,表load dword即从sp寄存器的值标志的地址处偏移48处加载dwords0寄存器。

sdld的源头和目的是反的。sd是前面往后面storeld是从后面往前面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,64ffffffe00000543a:  8082                  ret

然后以下代码获取运行地址

uintptr_t va, pa, end_va;  uintptr_t load_pa = (uintptr_t)(&_start);ffffffe00000528a:  ffffb917            auipc  s2,0xffffbffffffe00000528e:  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

(gdb) info reg s2s2             0x8020028a       2149581450

0x8020528a + 0xffffb000 = 0x1 8020 028A 丢掉高位位0x8020028A

然后执行完

addi s2,s2,-650

s2

(gdb) info reg s2s2             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. 这里_startauipc前面,所以偏差是负数。

原理即如下

获取镜像大小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,0x1bc8ffffffe00000529e:  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 - &_startwarning: 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,0x2bsetup_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,s2best_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_pa0x80200000

计算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

#define PMD_SHIFT       21/* Size of region mapped by a page middle directory */#define PMD_SIZE        (_AC(1, UL) << PMD_SHIFT)

所以PMD_SIZE2M

其中

arch/riscv/include/asm/page.h

#define PAGE_SHIFT  (12)#define PAGE_SIZE   (_AC(1, UL) << PAGE_SHIFT)

PAGE_SIZE4K

所以

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_size2M

__PAGETABLE_PMD_FOLDED未定义

所以以下忽略

#ifndef __PAGETABLE_PMD_FOLDED    pmd_t fix_bmap_spmd, fix_bmap_epmd;#endif

计算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_SIZE4K,所以BUG_ON条件不满足,没问题。

这里要求是map_size必须按照PMD大页进行,即以2M为单位。

BUG_ON

include/asm-generic/bug.h中定义

如果定义了CONFIG_BUG

#ifndef HAVE_ARCH_BUG#define BUG() do { \    printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \    barrier_before_unreachable(); \    panic("BUG!"); \} while (0)#endif
#ifndef HAVE_ARCH_BUG_ON#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)#endif

没有定义则

#ifndef HAVE_ARCH_BUG#define BUG() do {} while (1)#endif
#ifndef HAVE_ARCH_BUG_ON#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)#endif

同时后面检查

PAGE_OFFSET % PGDIR_SIZE要为0

这里PAGE_OFFSET0xffffffe000000000

arch/riscv/include/asm/pgtable-64.h

#define PGDIR_SHIFT     30/* Size of region mapped by a page global directory */#define PGDIR_SIZE      (_AC(1, UL) << PGDIR_SHIFT)

PGDIR_SIZE1GB

也就是PAGE_OFFSET1GB对齐。

继续检查

load_pa % map_size要为0

这里load_pa0x80200000

map_size2M

也就是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

设置ptepmd接口

    pt_ops.alloc_pte = alloc_pte_early;    pt_ops.get_pte_virt = get_pte_virt_early;#ifndef __PAGETABLE_PMD_FOLDED    pt_ops.alloc_pmd = alloc_pmd_early;    pt_ops.get_pmd_virt = get_pmd_virt_early;#endif

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);#ifndef __PAGETABLE_PMD_FOLDED    pmd_t *(*get_pmd_virt)(phys_addr_t pa);    phys_addr_t (*alloc_pmd)(uintptr_t va);#endif};

分别设置对应的接口函数

这里__PAGETABLE_PMD_FOLDED没有定义。

create_pgd_mapping

前面都是该函数的准备工作,下面就是关键步骤了,即调用create_pgd_mapping

create_pmd_mapping创建pgdpmd页表。所以重点就来介绍这两个函数。需要用到前面riscvmmu相关的内容。

参数

先以第一个

    /* 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_SHIFT6

所以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_SIZE1GB=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.hPGDIR_SHIFT30(对应1GB), PTRS_PER_PGD512

#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_idx0x13B

(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 sz1GB是满足的。

然后计算pgd_val(pgdp[pgd_idx])

其中pgdp[pgd_idx]pgdppgd_idx个条目,一个条目8字节,pgd_idx个条目即对应偏移

pgd_idx*8, 即对应slli s3,s3,0x3

然后add s3,s3,a0

pgdp地址基础上偏移pgd_idx*8a0即参数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);

其中prot1

include/linux/pfn.h

#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)

pa0x81d7a000所以结果是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)

索引3150x13B处值为 5445980170x2075 E801,和前面手动计算的对应。

上面

  /* Setup early PGD for fixmap */

  create_pgd_mapping(early_pg_dir, FIXADDR_START,

             (uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);

szPGDIR_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变为了nexptxxx_pmd

create_pgd_next_mapping实现如下,即继续调用create_pmd_mapping创建下一级pmdpte的映射

#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_SHIFT12PTRS_PER_PTE512

所以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->










嵌入式Lee
嵌入式软硬件技术:RTOS,GUI,FS,协议栈,ARM,总线,嵌入式C,开发环境 and blablaba....多年经验分享,非硬货不发,带你扒开每一个技术背后的根本原理。
 最新文章