在Linux 的boot 阶段,Devie tree 地址是通过bootloader 传给kernel, kernel 在函数preserve_boot_args 保存在 x21这个寄存器中。
但是内存相关的信息是在Device Tree里面描述的,所以Device Tree的使用是在内存管理准备好之前。 所以Device Tree的映射过程相对来说有些特殊。
在Arm64平台,它需要经历三次映射:
- 1:1 映射
- 读写的fixmap
- 只读的fixmap
1:1 映射
因为在boot阶段的早期,系统需要读取device tree里面的bootargs 来判断一些架构的feature是否被强制disable了,比如如下架构相关的feature是可以在bootargs 里面配置的
static const struct {
char alias[FTR_ALIAS_NAME_LEN];
char feature[FTR_ALIAS_OPTION_LEN];
} aliases[] __initconst = {
{ "kvm_arm.mode=nvhe", "id_aa64mmfr1.vh=0" },
{ "kvm_arm.mode=protected", "id_aa64mmfr1.vh=0" },
{ "arm64.nosve", "id_aa64pfr0.sve=0" },
{ "arm64.nosme", "id_aa64pfr1.sme=0" },
{ "arm64.nobti", "id_aa64pfr1.bt=0" },
{ "arm64.nopauth",
"id_aa64isar1.gpi=0 id_aa64isar1.gpa=0 "
"id_aa64isar1.api=0 id_aa64isar1.apa=0 "
"id_aa64isar2.gpa3=0 id_aa64isar2.apa3=0" },
{ "arm64.nomops", "id_aa64isar2.mops=0" },
{ "arm64.nomte", "id_aa64pfr1.mte=0" },
{ "nokaslr", "arm64_sw.nokaslr=1" },
{ "rodata=off", "arm64_sw.rodataoff=1" },
{ "arm64.nolva", "id_aa64mmfr2.varange=0" },
};
所以在__primary_switch -> early_map_kernel 把kernel切换的非1:1 VA运行之前,就需要在页表init_idmap_pg_dir 中1:1映射device tree:
static void __init map_fdt(u64 fdt)
{
static u8 ptes[INIT_IDMAP_FDT_SIZE] __initdata __aligned(PAGE_SIZE);
u64 efdt = fdt + MAX_FDT_SIZE;
u64 ptep = (u64)ptes;
/*
* Map up to MAX_FDT_SIZE bytes, but avoid overlap with
* the kernel image.
*/
map_range(&ptep, fdt, (u64)_text > fdt ? min((u64)_text, efdt) : efdt,
fdt, PAGE_KERNEL, IDMAP_ROOT_LEVEL,
(pte_t *)init_idmap_pg_dir, false, 0);
dsb(ishst);
}
这里可以发现,在v6.9的kernel中,Device Tree 的VA 地址不再跟kernel image 的地址挂钩,而是真正1:1 映射。 只是确保不要跟kernel image的地址有重叠即可,即efdt不要跟_text重叠。
这里MMU的页表 ptes 也是分配了每一级都有,不在依赖于之前映射kernel image的那些页表。之前依赖kernel image这个页表是因为想省下一些MMU的页表。 在v6.9里面,其实不关心使用多大的内存,因为这不部分最后都会被回收利用。
在Arm 的FVP平台,1:1映射好之后,有:
>mmu memory-map
Virtual Range | Physical Range | Type | AP | C | S | X
-------------------------------------------------------------------------------------------------------------------------
EL1N:0x0000000000000000-0x000000008020FFFF | <unmapped> | | | | |
EL1N:0x0000000080210000-0x0000000081E6FFFF | NP:0x0000000080210000-0x0000000081E6FFFF | Normal | RO | True | True | True
EL1N:0x0000000081E70000-0x0000000082C9FFFF | NP:0x0000000081E70000-0x0000000082C9FFFF | Normal | RW | True | True | False
EL1N:0x0000000082CA0000-0x00000000FD6BDFFF | <unmapped> | | | | |
EL1N:0x00000000FD6BE000-0x00000000FD8BDFFF | NP:0x00000000FD6BE000-0x00000000FD8BDFFF | Normal | RW | True | True | False
EL1N:0x00000000FD8BE000-0x0000FFFFFFFFFFFF | <unmapped> | | | | |
EL1N:0xFFFF000000000000-0xFFFFFFFFFFFFFFFF | <unmapped> | | | | |
Hit any key to stop autoboot: 0
Moving Image from 0x80080000 to 0x80200000, end=82ca0000
## Flattened Device Tree blob at 83000000
Booting using the fdt blob at 0x83000000
Working FDT set to 83000000
Loading Device Tree to 00000000fd6be000, end 00000000fd6c3d78 ... OK
Working FDT set to fd6be000
根据以上信息,完全是1:1 mapping。
这个映射在运行完early_map_kernel 之后就不再使用。后面接下来就转到fixmap阶段。
fixmap 映射
在__primary_switched 阶段, 这里会把x21寄存器保存在变量__fdt_pointer 中,参考函数__primary_switched:
SYM_FUNC_START_LOCAL(__primary_switched)
//....
str_l x21, __fdt_pointer, x5 // Save FDT pointer
在setup_arch的时候,会把这个PA传给setup_machine_fdt,而setup_machine_fdt 会对device tree 进行两次映射。
这是为什么呢? 可能在有些平台上需要对device tree 做一些动态的修改,所以第一次mapping的时候是读写的。 但是在Arm FVP平台上,其实第一次直接把它mapping只读也是可以的。
PA已经有了,但是虚拟地址的管理还没有ready,这个时候就用到fixmap了,这里可以通过 FIX_FDT 来获取它的虚拟地址,即device tree 是处在 FIX_FDT slot 中:
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)
在读取device tree 之前,这里并不知道系统有多少内存可以使用,也就是不能是有任何的动态内存分配。 那么这里要给device tree 映射也需要用到页表,这些页表的内存怎么分配?
在函数setup_arch -> early_fixmap_init 已经针对所有fix map 的VA,建立好了页表,只是没有把他们给对应的PA给mapping上。 这些页表全都是静态分配的,参考:
static pte_t bm_pte[NR_BM_PTE_TABLES][PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;
early_fixmap_init 里面就是把页表之间的关系的映射好,后面就可以直接调用create_mapping_noalloc 这样的函数,只要VA地址是在这些fix map address 里面,都不需要重新分配页表。
在执行完early_fixmap_init, 可以看到页表的关系已经建立好:
+ 0xFFFFFF8000000000 | Level 0 Table | NP:0x0000000082BDF000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFF8000000000 | Invalid (x511) | | |
+ 0xFFFFFFFFC0000000 | Level 1 Table | NP:0x0000000082BDF000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFC0000000 | Invalid (x506) | | |
+ 0xFFFFFFFFFF400000 | Level 2 Table | NP:0x0000000082BE1000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFFF400000 | Invalid (x512) | | |
+ 0xFFFFFFFFFF600000 | Level 2 Table | NP:0x0000000082BE2000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFFF600000 | Invalid (x512) | | |
- 0xFFFFFFFFFF800000 | Invalid (x4) | | |
这些0x0000000082BDF000 0x0000000082BDF000 对应的就是bm_pud bm_pmd bm_pte的地址。
在执行完 setup_arch->setup_machine_fdt -> fixmap_remap_fdt 之后,这个时候FDT 的VA 就和PA给对应上了:
+ 0xFFFFFF8000000000 | Level 0 Table | NP:0x0000000082BDF000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFF8000000000 | Invalid (x511) | | |
+ 0xFFFFFFFFC0000000 | Level 1 Table | NP:0x0000000082BE0000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFC0000000 | Invalid (x506) | | |
+ 0xFFFFFFFFFF400000 | Level 2 Table | NP:0x0000000082BE1000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFFF400000 | Invalid (x510) | | |
- 0xFFFFFFFFFF5FE000 | Level 3 Page | | NP:0x00000000FD6BE000 | UXN=1, PXN=1, Contiguous=0, DBM=0, GP=0, nG=0, AF=1, SH=0x3, AP=0x2, AttrIndx=0x0
- 0xFFFFFFFFFF5FF000 | Level 3 Page | | NP:0x00000000FD6BF000 | UXN=1, PXN=1, Contiguous=0, DBM=0, GP=0, nG=0, AF=1, SH=0x3, AP=0x2, AttrIndx=0x0
+ 0xFFFFFFFFFF600000 | Level 2 Table | NP:0x0000000082BE2000 | | APTable=0x0, UXNTable=0, PXNTable=0
- 0xFFFFFFFFFF600000 | Level 3 Page | | NP:0x00000000FD6C0000 | UXN=1, PXN=1, Contiguous=0, DBM=0, GP=0, nG=0, AF=1, SH=0x3, AP=0x2, AttrIndx=0x0
- 0xFFFFFFFFFF601000 | Invalid (x511) | | |
- 0xFFFFFFFFFF800000 | Invalid (x4) | | |
>mmu memory-map
Virtual Range | Physical Range | Type | AP | C | S | X
-------------------------------------------------------------------------------------------------------------------------
//........
EL2N:0xFFFF800082AA0000-0xFFFFFFFFFF5FDFFF | <unmapped> | | | | |
EL2N:0xFFFFFFFFFF5FE000-0xFFFFFFFFFF600FFF | NP:0x00000000FD6BE000-0x00000000FD6C0FFF | Normal | RO | True | True | False
EL2N:0xFFFFFFFFFF601000-0xFFFFFFFFFFFFFFFF | <unmapped> | | | | |
这样正式的映射就建立好了,可以用VA地址读取device tree了。
Comments !