0. 前言

首先我们需要了解一下就是,为何需要找个fixmap的内存映射?从前面的文章可以知道,当内核启动后首先会进入汇编的Head.S中运行,在那里启动了MMU,所以到现在这个阶段,CPU只能使用虚拟地址访问RAM。而setup_arch阶段在paging_init之前(paging_init会完成所有的内存映射)。那此时如果想要访问某些模块(例如dtb),那就需要将这些模块的虚拟地址和物理地址进行映射。这个就是fixmap映射产生的原因。

fixmap 理解为 固定映射,其虚拟地址空间是为了早期 fdtconsole外设动态映射paging_init() 使用。需要注意的是,不能完全认为 fixmap 都是固定映射,fixmap 的详细分布可以查看第 2 节。

本文重点分析三个工作:

  • early_fixmap_init()
  • early_ioremap_init()
  • setup_machine_fdt()

1. 内核的布局

按照 VA_BITS=39,PAGE_SHIFT=12(4K) 来看下ARM64内存的内存布局:

VA_BITS=39,PAGE_SHIFT=12(4K)

fixmap 的区域在编译阶段就确定好的,在代码中可以看到如下的定义:

/*
 * Size of the PCI I/O space. This must remain a power of two so that
 * IO_SPACE_LIMIT acts as a mask for the low bits of I/O addresses.
 */
#define PCI_IO_SIZE		SZ_16M

/*
 * VMEMMAP_SIZE - allows the whole linear region to be covered by
 *                a struct page array
 *
 * If we are configured with a 52-bit kernel VA then our VMEMMAP_SIZE
 * needs to cover the memory region from the beginning of the 52-bit
 * PAGE_OFFSET all the way to PAGE_END for 48-bit. This allows us to
 * keep a constant PAGE_OFFSET and "fallback" to using the higher end
 * of the VMEMMAP where 52-bit support is not available in hardware.
 */
#define VMEMMAP_SHIFT	(PAGE_SHIFT - STRUCT_PAGE_MAX_SHIFT)
#define VMEMMAP_SIZE	((_PAGE_END(VA_BITS_MIN) - PAGE_OFFSET) >> VMEMMAP_SHIFT)

/*
 * PAGE_OFFSET - the virtual address of the start of the linear map, at the
 *               start of the TTBR1 address space.
 * PAGE_END - the end of the linear map, where all other kernel mappings begin.
 * KIMAGE_VADDR - the virtual address of the start of the kernel image.
 * VA_BITS - the maximum number of bits for virtual addresses.
 */
#define VA_BITS			(CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va)	(-(UL(1) << (va)))
#define PAGE_OFFSET		(_PAGE_OFFSET(VA_BITS))
#define KIMAGE_VADDR		(MODULES_END)
#define BPF_JIT_REGION_START	(_PAGE_END(VA_BITS_MIN))
#define BPF_JIT_REGION_SIZE	(SZ_128M)
#define BPF_JIT_REGION_END	(BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
#define MODULES_END		(MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR		(BPF_JIT_REGION_END)
#define MODULES_VSIZE		(SZ_128M)
#define VMEMMAP_START		(-(UL(1) << (VA_BITS - VMEMMAP_SHIFT)))
#define VMEMMAP_END		(VMEMMAP_START + VMEMMAP_SIZE)
#define PCI_IO_END		(VMEMMAP_START - SZ_8M)
#define PCI_IO_START		(PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP		(VMEMMAP_START - SZ_32M)

#if VA_BITS > 48
#define VA_BITS_MIN		(48)
#else
#define VA_BITS_MIN		(VA_BITS)
#endif

#define _PAGE_END(va)		(-(UL(1) << ((va) - 1)))

#define KERNEL_START		_text
#define KERNEL_END		_end

本文重点是分析 fixmap 内存,所以重点关注 FIXADDR_TOP,是在 PCI_IO 下面,之间有个 2M的 hole,再来看下 fixmap 的大小:

#define FIXADDR_SIZE	(__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START	(FIXADDR_TOP - FIXADDR_SIZE)

FIXADDR_START ~ FIXADDR_TOP 之间的内存,就是留给 fixmap 的,大小为 FIXADDR_SIZE

注意:

从上面内存分布来看,其实留给 FIXMAP 的空间是 FIXADDR_SIZE,截止到的 fixed_addresses 的枚举类型是 __end_of_permanent_fixed_addresses,这里就出现了一个问题:

fixed_addresses 中 __end_of_permanent_fixed_addresses 之后的类型在fixmap 充当什么角色?在内存分布中又是处于什么地址位置呢?

2. fixmap的分布


enum fixed_addresses {
	FIX_HOLE,
 
	/*
	 * Reserve a virtual window for the FDT that is 2 MB larger than the
	 * maximum supported size, and put it at the top of the fixmap region.
	 * The additional space ensures that any FDT that does not exceed
	 * MAX_FDT_SIZE can be mapped regardless of whether it crosses any
	 * 2 MB alignment boundaries.
	 *
	 * Keep this at the top so it remains 2 MB aligned.
	 */
#define FIX_FDT_SIZE		(MAX_FDT_SIZE + SZ_2M)
	FIX_FDT_END,
	FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,
 
	FIX_EARLYCON_MEM_BASE,
	FIX_TEXT_POKE0,
 
#ifdef CONFIG_ACPI_APEI_GHES
	/* Used for GHES mapping from assorted contexts */
	FIX_APEI_GHES_IRQ,
	FIX_APEI_GHES_SEA,
#ifdef CONFIG_ARM_SDE_INTERFACE
	FIX_APEI_GHES_SDEI_NORMAL,
	FIX_APEI_GHES_SDEI_CRITICAL,
#endif
#endif /* CONFIG_ACPI_APEI_GHES */
 
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
	FIX_ENTRY_TRAMP_TEXT3,
	FIX_ENTRY_TRAMP_TEXT2,
	FIX_ENTRY_TRAMP_TEXT1,
	FIX_ENTRY_TRAMP_DATA,
#define TRAMP_VALIAS		(__fix_to_virt(FIX_ENTRY_TRAMP_TEXT1))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
	__end_of_permanent_fixed_addresses,
 
	/*
	 * Temporary boot-time mappings, used by early_ioremap(),
	 * before ioremap() is functional.
	 */
#define NR_FIX_BTMAPS		(SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS	7
#define TOTAL_FIX_BTMAPS	(NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
 
	FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
	FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
 
	/*
	 * Used for kernel page table creation, so unmapped memory may be used
	 * for tables.
	 */
	FIX_PTE,
	FIX_PMD,
	FIX_PUD,
	FIX_PGD,
 
	__end_of_fixed_addresses
};

通过代码可以将 fixmap 分成三个部分:

  • 固定映射;
  • 动态映射;
  • 映射查找;

固定映射,即在内核启动过程中,用于分配给指定物理地址设定的映射关系,持续在系统启动到关机的整个生命周期;

动态映射,即在内核启动过程中,或内核启动完成后,动态地给模块分配虚拟内存,模块退出后释放该虚拟内存,该过程中动态建立映射关系;

映射查找,用于paging_init() ,通过 pgd_set_fixmap() 给页目录表的全局表项 swapper_pg_dir 做虚拟内存映射,映射到具体的 PTE 的表项,然后后续就可以根据这个页表项所映射的内存空间建立模块的映射关系;

在上面将 fixmap 分成三个部分:固定映射、动态映射、映射查找,其中,动态映射和映射查找可以合并为临时启动映射。代码也可以简略成:

enum fixed_addresses {
	FIX_HOLE,
 
	...
 
	__end_of_permanent_fixed_addresses,
 
	...
 
	FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
	...
 
	__end_of_fixed_addresses
};

这样可以将 fixmap 空间分成三个部分:

  • FIX_HOLE,留了4k 的空洞;
  • 固定映射,从 FDT__end_of_permanent_fixed_addresses
  • 临时启动映射,从 __end_of_permanent_fixed_addresses__end_of_fixed_addresses

对此,上面的内存分布图,对 fixmap 进行细分:

fixmap分布

注意:本图中的fixmap分布去除了enum fixed_addresses中所有的ifdef框定的定义,只标注默认的定义。

fixmap 的虚拟地址计算是根据上面 fixed_address 这个枚举:

#define __fix_to_virt(x)	(FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x)	((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

即通过 fixed_address 的枚举值偏移 PAGE_SHIFT 来计算。通过 __fix_to_virt(x), 不难看出参数 x 为页数,即距离 FIXADDR_TOP 之间的页数。

2.1 hole 在fixmap中的分布

例如 FIX_HOLE,枚举值为0,即 0 个 pages。带入 0 之后,得到 FIX_HOLE 的虚拟地址就是 FIXADDR_TOP。

其实,FIX_HOLE 的虚拟地址空间应该是 FIXADDR_TOP ~ FIXADDR_TOP - 4k;

为什么呢?

FIX_HOLE 下面是 FIX_FDT_END,枚举值为1,即表示距离 FIXADDR_TOP 之间 1 个 pages,即FIX_FDT_END 的虚拟地址为 FIXADDR_TOP - 4k。这也就解释为什么 FIX_HOLE 占用了 4k 的空间。

2.2 dtb 在fixmap中的分布

FIX_FDT_END 的枚举值为1,通过 __fix_to_virt() 可以出该枚举值对应的虚拟地址为 FIXADDR_TOP - 4k;

FIX_FDT 的枚举值为 1 + 4MB / 4K - 1 = 1024,即距离 FIXADDR_TOP 之间 1024 个pages,即 FIX_FDT 的虚拟地址为 FIXADDR_TOP - 1024 * 4k = FIXADDR_TOP - 4M, 即 FIX_FDT 的虚拟地址为 FIXADDR_TOP - 4M,即 FDT 占用空间为:

FIXADDR_TOP - 4M ~ FIXADDR_TOP - 4k,即 FDT 占用4M 空间(实际留给 FDT 只会是2M)。

注意:

FDT 给的空间是 MAX_FDT_SIZE,即 2 M。而系统在这个基础上又增加了 SZ_2M,只是为了保证不超过 MAX_FDT_SIZE 的 FDT 都能够映射成功,而不管是否映射跨过了 2M 对齐的边界。

Uboot 启动后会将 kernel image 和 dtb 拷贝到内存中,并且将 dtb 物理地址告知 kernel,kernel 需要从该物理地址上读取 dtb 文件进行解析,因此需要将 dtb 的物理地址映射到虚拟地址上。所以,最终会通过 fixmap 区域进行映射。

2.3 earlycon 在fixmap中的分布

枚举值在 FIX_FDT 之后,也就是earlycon 只占了一个 page,其实对于earlycon只需要映射一些串口相关的IO寄存器即可,故所占的空间很小,一个页的空间已经足够。

2.4 text poke0 在fixmap中的分布

它也只占用 1 个 page。

它可以用于修改代码段指令的操作。其实现位于arch/arm64/kernel/insn.c中,在通过fixmap映射该地址时会将代码段的flag映射为可读写的,然后通过构造所需的指令,并将其覆盖掉原先的内容,从而实现代码段的修改。
这一机制在调试跟踪中非常有用,如kgdb可以利用这一机制插入和移除断点,若需要在某处插入断点,则可将该地址通过fixmap映射,然后用断点指令替换原始指令,当需要继续执行时,则把原始指令再替换回去。还有像kprobe之类的动态跟踪工具也可以通过该机制向被跟踪程序插入一些跟踪点等

2.5 ACPI_APEI_GHES

ACPI_APEI_GHES一般在服务器领域用于RAS功能,其作用是通过firmware向内核报告硬件错误。如在armv8中这些硬件错误会通过事件路由机制被路由到运行于EL3的bl31中,然后由bl31处理后再通过sdei机制将事件发送给内核的ghes模块处理。
其优势就是bl31的异常等级比内核更高,能够获取到更多硬件相关信息(如可以访问EL3的系统寄存器,访问secure内存等)。同时若将中断路由到EL3,则即使内核处于关中断或死锁等无法响应中断的状态时,bl31依然可以接收到该种类型的中断。

2.6 FIX_ENTRY_TRAMP_DATA和FIX_ENTRY_TRAMP_TEXT

被用于内核security增强机制。其原理是在内核退出进入用户态前将自身的页表替换为一个只映射了包含系统调用等很少部分代码的页表,而隐藏内核的实际页表,在通过系统调用进入内核后再恢复实际的页表。其作用是减少了内核的攻击面。

2.7 BITMAP

从定义可见以上部分是属于permanent fixmap,而从bitmap映射开始是非permanent fixmap的。

bitmap 空间会被用于early ioremap,而ioremap的映射在使用完以后即可通过iounmap操作解除映射,因此其映射关系不是永久的。该区域一共占7 * 256k(1792k)空间。

2.8 FIX_PGD ~ FIX_PTE

用于paging_init() 作为映射内存空间存放临时页表使用。每个页表占用 1 个 page;

另外,这里解释下第 1 节提出的问题:

fixed_addresses 中 __end_of_permanent_fixed_addresses 之后的类型在fixmap 充当什么角色?在内存分布中又是处于什么地址位置呢?

通过 __fix_to_virt(x) 得知,fixed_addresses 中的枚举值代表距离 FIXADDR_TOP 的页数。

而在 __end_of_permanent_fixed_addresses 之后的类型,仍然是 FIXADDR_TOP 往低地址偏移枚举值所代表的页数,得到该枚举值在 fixmap 中对应的虚拟地址。

所以,这些类型的区域,仍然处于 fixmap 所指定的内存空间中。

但,需要特别注意的是,这些枚举值所代表的内存空间, 只是在启动过程中的临时映射时使用,例如在 paging_init() 中大量使用 FIX_PGD、FIX_PMD、FIX_PTE 等空间,但每一次循环的映射之后,必须将这片区域 clear。

3. early_fixmap_init()

在bootloader 做好初始化工作后,将 kernel image 加载到内存后,就会跳到kernel 部分继续执行,跑的先是汇编部分的代码,进行各种设置和环境初始化后,就会跳到 kernel 的第一个函数 start_kernel(), start_kernel() 完成内核系统的所有配置和初始化,其中 setup_arch() 是早期系统的配置工作:

// arch/arm64/kernel/setup.c

void __init setup_arch(char **cmdline_p)
{
	...

	early_fixmap_init();
	early_ioremap_init();
 
	setup_machine_fdt(__fdt_pointer);
 
	...
 
    parse_early_param();
 
    ...
 
	arm64_memblock_init();
 
	paging_init();
 
	...
 
	if (acpi_disabled)
		unflatten_device_tree();
 
	bootmem_init();
    ...
}

memblock 初始化 之前,我们看到需要有三个重要的事情:

  • early_fixmap_init()
  • early_ioremap_init()
  • setup_machine_fdt()

其中最开始的地方就是 fixmap 的初始化:

void __init early_fixmap_init(void)
{
	pgd_t *pgdp, pgd;
	pud_t *pudp;
	pmd_t *pmdp;
	unsigned long addr = FIXADDR_START;  //fixmap 虚拟地址从FIXADDR_START 开始
 
    //先获取 fixmap地址中PGD 页表位置,在swapper_pg_dir 中某个特定位置
    //下面第3.1节会详细说明
	pgdp = pgd_offset_k(addr);
 
    //读取该位置的值,该值就是存放的是下一个页表的基地址
	pgd = READ_ONCE(*pgdp);
 
    //确认是否有PUD 项,如果4级页表,则通过 pgdp获取PUD项的位置
    //如果是3级页表,则 pudp 就是pgdp 的值,认为PGD为 PUD项
	if (CONFIG_PGTABLE_LEVELS > 3 &&
	    !(pgd_none(pgd) || pgd_page_paddr(pgd) == __pa_symbol(bm_pud))) {
		/*
		 * We only end up here if the kernel mapping and the fixmap
		 * share the top level pgd entry, which should only happen on
		 * 16k/4 levels configurations.
		 */
		BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
		pudp = pud_offset_kimg(pgdp, addr);
	} else {
		if (pgd_none(pgd))
			__pgd_populate(pgdp, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
		pudp = fixmap_pud(addr);
	}
 
    //读取该PUD页表项的值,如果不存在,则通过__pud_populate()将bm_pmd的物理地址写入pudp中
	if (pud_none(READ_ONCE(*pudp)))
		__pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);
 
    //同上,获取fixmap addr中的PMD 页表项地址
	pmdp = fixmap_pmd(addr);
 
    //将bm_pte的物理地址写到PMD页表项中
	__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE);
 
	...
}

本函数为 fixmap 的初始化函数,其根本目的就是将 fixmap 的起始地址 FIXADDR_START 与全局的页表 bm_pud、bm_pmd、bm_pte 进行映射。


// arch/arm64/mm/mmu.c
 
static pte_t bm_pte[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;

注意:

在内核的全局变量中为 fixmap 页表静态定了了一段页表项内存,它会被编译进 kernel img 的 bss 段,因此其在镜像映射流程中作为kernel img 的一部分被 init_pg_dir 映射。当然,在 paging_init() 中 map_kernel() 和 map_mem() 之后会将这部分内存释放出来。

针对 VA_BITS 为39,在之前博文 《页表查询过程简述》 中的第 8.3 节中已经说明:

虚拟地址组成

所以,上述数组中的:

  • PTRS_PER_PTE 值为 1<<9;
  • PTRS_PER_PMD 值为 1<<9;
  • PTRS_PER_PUD 值为 1<<9;
  • 当然,PTRS_PER_PGD 值也为 1<<9;

3.1 pgd_offset_k()获取pgd的位置

arch/arm64/include/asm/pgtable.h

#define pgd_offset_k(addr)	pgd_offset(&init_mm, addr)
#define pgd_offset(mm, addr)	(pgd_offset_raw((mm)->pgd, (addr)))
#define pgd_offset_raw(pgd, addr)	((pgd) + pgd_index(addr))
#define pgd_index(addr)		(((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

extern pgd_t swapper_pg_dir[PTRS_PER_PGD];

这里需要注意三个细节:

  • 全局数组变量 swapper_pg_dir,用以存放 PGD 的页表;
  • 全局结构体变量 init_mm,见下文;
  • 最终通过 pgd_index() 确认 swapper_pg_dir 的 index,在通过 init_mm->pgd 进行偏移;

注意上面的全局结构体变量 init_mm:

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,  ///启动后的pgd
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.write_protect_seq = SEQCNT_ZERO(init_mm.write_protect_seq),
	MMAP_LOCK_INITIALIZER(init_mm)
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.arg_lock	=  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	.cpu_bitmap	= CPU_BITS_NONE,
	INIT_MM_CONTEXT(init_mm)   ///初始化过程的地址
};

pgd 指的就是全局数组变量 swapper_pg_dir,见上面代码。

3.2 fixmap_pud()获取pud的位置

arch/arm64/mm/mmu.c

static inline pud_t * fixmap_pud(unsigned long addr)
{
	pgd_t *pgdp = pgd_offset_k(addr);
	pgd_t pgd = READ_ONCE(*pgdp);

	BUG_ON(pgd_none(pgd) || pgd_bad(pgd));

	return pud_offset_kimg(pgdp, addr);
}

#define pud_offset_kimg(dir,addr)	((pud_t *)dir)

本文针对的是 VA_BITS 为39,所以 pud 的位置与pgd 是相同的。

3.3 fixmap_pmd()获取pmd的位置

arch/arm64/mm/mmu.c

static inline pmd_t * fixmap_pmd(unsigned long addr)
{
	pud_t *pudp = fixmap_pud(addr);
	pud_t pud = READ_ONCE(*pudp);

	BUG_ON(pud_none(pud) || pud_bad(pud));

	return pmd_offset_kimg(pudp, addr);
}
arch/arm64/include/asm/pgtable.h

#define pmd_offset_kimg(dir,addr)	((pmd_t *)__phys_to_kimg(pmd_offset_phys((dir), (addr))))
#define pmd_offset_phys(dir, addr)	(pud_page_paddr(READ_ONCE(*(dir))) + pmd_index(addr) * sizeof(pmd_t))
#define pmd_index(addr)		(((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))

static inline phys_addr_t pud_page_paddr(pud_t pud)
{
	return __pud_to_phys(pud);
}

重点是最后的 pmd_offset_kimg(),读取 PUD中的页表项中PMD的物理地址,最终通过通过 __phys_to_kimg() 加上物理地址与线性地址的偏移量,获得 PMD的位置。

4. early_ioremap_init()

一般传统驱动模块都是使用 ioremap() 函数来完成地址映射的,但是该函数必须依赖 buddy 系统来创建某个 level 的转换表。一些硬件需要在内存管理系统运行起来之前就需要工作,内核在启动初期采用 early_ioremap() 机制来映射内存给这些硬件驱动使用,并且这些硬件驱动在使用完 early_ioremap() 的地址后需要尽快的释放掉这些内存,这样才能保证其他硬件模块继续使用。因此 early_ioremap() 采用的是 fixed map 的 temporary fixmap 段虚拟地址。

arch/arm64/mm/ioremap.c

void __init early_ioremap_init(void)
{
	early_ioremap_setup();
}
mm/early_ioremap.c

static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;

void __init early_ioremap_setup(void)
{
	int i;

	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		if (WARN_ON(prev_map[i]))
			break;

	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

FIX_BTMAP_BEGIN ~ FIX_BTMAP_END 的虚拟内存做初始化,依序将各个 slot 的地址填入 slot_virt 数组中,目的是为后续使用这部分虚拟内存的时候,只需要调用这个数组对应的数组项即可,这段虚拟内存,既被称为临时映射虚拟内存区域。同时,也可以通过数组元素是否被使用,判断这段临时映射的虚拟内存是否空闲。总的来说,通过有 7 个成员的数组 slot_virt 管理这段被平均分为 7 份的 7*256K 大小的虚拟内存,用于做临时映射。

在 boot 早期如果需要操作 IO 设备,那么 ioremap 就派上用场,系统提供了两个函数:

  • early_ioremap()
  • early_iounmap()

该两个函数用于从 IO 物理地址到虚拟地址的映射和解除映射,这两个函数依赖 CONFIG_MMU,如果没有定义宏,就直接返回物理地址,如果定义了宏,early_ioremap() 就会调用 __early_ioremap(), 详细可以查看 mm/early_ioremap.c

5. setup_machine_fdt()

当剖析 setup_machine_fdt() 时,首先需要确认 __fdt_pointer 是在哪里初始化的。这里暂时不去过多的剖析了,知道在 start_kernel() 之前还有汇编代码的运行,而 __fdt_pointer 就是在那里初始化的:

arch/arm64/kernel/head.S

这个 __fdt_pointer 非常重要,指向 fdb,也就是dtb被加载到内存的首地址(uboot启动完会将dtb的物理地址传入kernel),这里是物理地址。

arch/arm64/kernel/setup.c

phys_addr_t __fdt_pointer __initdata;


include/linux/init.h
#define __initdata	__section(.init.data)

了解完 __fdt_pointer 之后,还是回过头来看下 setup_machine_fdt():

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
	int size;
	///完成fdt的pte页表填写,返回fdt虚拟地址,这里虚拟地址事先定义预留
	///映射了2MB
	void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
	const char *name;

	if (dt_virt)
		///把dtb所占内存添加到memblock管理的reserve模块,后续内存分配不会使用这段内存
		//使用完后,会使用memblock_free()释放
		memblock_reserve(dt_phys, size);

	///扫描解析dtb,将内存布局信息填入memblock系统
	if (!dt_virt || !early_init_dt_scan(dt_virt)) {
		pr_crit("\n"
			"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
			"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
			"\nPlease check your bootloader.",
			&dt_phys, dt_virt);

		while (true)
			cpu_relax();
	}

	/* Early fixups are done, map the FDT as read-only now */
	fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);

	name = of_flat_dt_get_machine_name();
	if (!name)
		return;

	pr_info("Machine model: %s\n", name);
	dump_stack_set_arch_desc("%s (DT)", name);
}
  • 拿到 dtb 的物理地址后,会通过 fixmap_remap_fdt() 进行 mapping,其中包括 pgd、pud、pte等 mapping,当 mapping 成功后会返回 dt_virt,详细可以查看第 5.1 节;
  • 通过memblock_reserve()添加到memblock.reserved 中,详细可以查看第 5.2 节;
  • 接着是early_init_dt_scan()在 early_init_dt_verify() 会检查dtb 数据完整,映射之后代码就可以直接访问这段内存。然后是调用early_init_dt_scan_nodes() ,详细可以查看第 5.3 节;
  • 再次调用fixmap_remap_fdt()进行mapping,将FDT 映射为只读,prot 从最开始的 PAGE_KERNEL 修改为PAGE_KERNEL_RO

5.1 fixmap_remap_fdt()

arch/arm64/mm/mmu.c
 
void *__init fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)
{
    //找到为 dtb提供的fixmap中的base虚拟地址
	const u64 dt_virt_base = __fix_to_virt(FIX_FDT);
	int offset;
	void *dt_virt;
 
	//检查fdt_pointer,确定dtb的物理地址是按照MIN_FDT_ALIGN对齐
	BUILD_BUG_ON(MIN_FDT_ALIGN < 8);
	if (!dt_phys || dt_phys % MIN_FDT_ALIGN)
		return NULL;
 
	//检查为dtb预留的虚拟地址SZ_2M对齐
	BUILD_BUG_ON(dt_virt_base % SZ_2M);
 
    //保证FDT所在的虚拟地址范围在early_fixmap_init()建立的PMD范围内,因为在
    //early_fixmap_init()已经建立了PUD和PMD,不能让其额外浪费PMD内存
	BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=
		     __fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT);
 
    //获取fdt物理地址的偏移,对于VA_BITS为39,获取末尾21位
    //这里就是去fdt物理地址的末尾21位,在加上FDT的base虚拟地址,组成一个新的虚拟地址
	offset = dt_phys % SWAPPER_BLOCK_SIZE;
    //偏移之后的实际虚拟地址
	dt_virt = (void *)dt_virt_base + offset;
 
    //根据提供的物理地址和虚拟地址创建页表的entry,映射的空间大小为SWAPPER_BLOCK_SIZE
	//注意了,这里先映射一块,这样就可以读取dtb的头,用以确认是否符合条件
    //因此,先映射了SWAPPER_BLOCK_SIZE大小,如果后续dtb符合要求,超过的空间会再做一次映射
	create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),
			dt_virt_base, SWAPPER_BLOCK_SIZE, prot);
 
    //根据实际的虚拟地址访问物理地址的空间内容,即FDT文件内容,此处检测DTB文件首部内容是否是FDT_MAGIC
	if (fdt_magic(dt_virt) != FDT_MAGIC)
		return NULL;
 
    //获取dtb文件大小,且确保FDT大小不超过MAX_FDT_SIZE,即2M
	*size = fdt_totalsize(dt_virt);
	if (*size > MAX_FDT_SIZE)
		return NULL;
 
    //当然,dtb的size可能没有MAX_FDT_SIZE,但加上offset之后,总的空间大小如果超过了之前
    //  映射的SWAPPER_BLOCK_SIZE,那么超过的部分也要进行映射
	if (offset + *size > SWAPPER_BLOCK_SIZE)
		create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE), dt_virt_base,
			       round_up(offset + *size, SWAPPER_BLOCK_SIZE), prot);
 
	return dt_virt;
}

注意:

	offset = dt_phys % SWAPPER_BLOCK_SIZE;
	dt_virt = (void *)dt_virt_base + offset;

	/* map the first chunk so we can read the size from the header */
	create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),
			dt_virt_base, SWAPPER_BLOCK_SIZE, prot);

创建映射关系的物理地址是 round_down(dt_phys, SWAPPER_BLOCK_SIZE),即按照物理地址 末尾21位值 进行向下对齐,假设向下对齐后的物理地址为 dt_phys_align。则 dt_phys_align 将与 dt_virt_base 创建映射,映射的空间为 dt_virt_base 开始的 SWAPPER_BLOCK_SIZE。

那么,实际的 dtb 的虚拟地址,应该是从 dt_phys_align 所对应的虚拟地址 dt_virt_base 进行偏移,偏移大小就是末尾 21 位值,即 dt_virt 为 dt_virt_base + offset。

关于这个offset我一开始是很难理解的,既然dtb的物理地址已经固定映射到FIX_FDT了,那为什么还要在函数中去计算offset?

{% tip success %}

fixmap_remap_fdt 中,虽然设备树(DTB)的物理地址确实通过 FIX_FDT 映射到了一个固定的虚拟地址区域,但计算 offset 是必要的,这是因为设备树的物理地址可能并不对齐到映射块(SWAPPER_BLOCK_SIZE,通常为 2MB)的边界。

设备树的物理地址(dt_phys)可能由引导加载程序(bootloader)提供,而不是内核决定。这些地址可能不是对齐到内核映射块大小(如 2MB)的边界。

假设:

  • SWAPPER_BLOCK_SIZE = 0x200000(2MB)。
  • 设备树的物理地址为 0x12345678
  • 固定映射的虚拟地址基址为 0xffffffc000000000

计算偏移量

offset = dt_phys % SWAPPER_BLOCK_SIZE; // 0x12345678 % 0x200000 = 0x45678

计算虚拟地址

dt_virt = (void *)dt_virt_base + offset; // 0xffffffc000000000 + 0x45678

最终得到的虚拟地址 dt_virt 指向了设备树内容的正确位置。

假设不考虑 offset,直接使用 FIX_FDT 对应的虚拟地址 dt_virt_base,那么映射的起始虚拟地址指向的是物理地址对齐到块大小后的基地址(例如 0x12340000),这会导致以下问题:

  1. 数据错位:映射后的虚拟地址不会直接指向设备树的实际起始地址,而是对齐后的起始地址。
  2. 访问异常:如果直接通过固定基址访问设备树内容,解析设备树时会读取到错误的数据。

通过加入 offset,确保虚拟地址能够正确映射到设备树的物理地址。

{% endtip %}

5.1.1 create_mapping_noalloc()

arcch/arm64/mm/mmu.c

static void __init create_mapping_noalloc(phys_addr_t phys, unsigned long virt,
				  phys_addr_t size, pgprot_t prot)
{
	if ((virt >= PAGE_END) && (virt < VMALLOC_START)) {
		pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n",
			&phys, virt);
		return;
	}
	__create_pgd_mapping(init_mm.pgd, phys, virt, size, prot, NULL,
			     NO_CONT_MAPPINGS);
}

这里有个疑问??

经过上面的操作,fdt的虚拟地址已经有了,那为何还要去使用__create_pdg_mapping来填充一条pte呢?

{% tip success %}

FIX_FDT 是一个逻辑定义

  • FIX_FDT固定映射表(fixmap table) 中的一个条目,对应一个预留的虚拟地址。
  • 它是通过宏定义映射到某个虚拟地址空间,但这个空间在初始化时并没有被实际映射到物理地址。

没有 PTE 就无法访问

  • 如果没有填充页表项(PTE),CPU 无法将 FIX_FDT 对应的虚拟地址转换为物理地址。访问这个地址会触发 页表缺失异常(page fault)
  • 因此,我们必须在页表中设置对应的 PTE,告诉 CPU 这个虚拟地址指向哪个物理地址。

{% endtip %}

最终调用 __create_pgd_mapping() 函数创建 dtb 物理内存地址 dt_phys 到 FDT 虚拟内存的映射页表。详细的代码剖析,可以查看 paging_init 一文,大致流程如下图。

__create_pgd_mapping依次创建页表项

总体来说就是逐级页表建立映射关系,同时中间会进行权限的控制等。

5.2 memblock_reserve(dt_phys, size)

这里传入的是 FDT 的物理地址和dtb的大小。

mm/memblock.c

int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
	phys_addr_t end = base + size - 1;

	memblock_dbg("memblock_reserve: [%pa-%pa] %pS\n",
		     &base, &end, (void *)_RET_IP_);

	return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0);
}

主要目的是将 FDT 这块内存添加到 reserved 部分,核心函数在 memblock_add_range()

5.3 early_init_dt_scan()

drivers/of/fdt.c

bool __init early_init_dt_scan(void *params)
{
	bool status;

	status = early_init_dt_verify(params);    //----step1
	if (!status)
		return false;

	early_init_dt_scan_nodes();               //----step2
	return true;
}

会检查dtb 数据完整,映射之后代码就可以直接访问这段内存,需要注意的是在 early_init_dt_verify() 中会把传入的 fdt 的虚拟地址赋值给 initial_boot_params,在下面的函数 early_init_dt_scan_nodes() 中会使用。

5.3.1 early_init_dt_verify()

drivers/of/fdt.c

bool __init early_init_dt_verify(void *params)
{
	if (!params)
		return false;

	//fdt 头部的check,一堆条件需要check
	if (fdt_check_header(params))
		return false;

	//将fdt 的虚拟地址存放到全局变量中,之后的系统中都是通过该变量解析dtb
	initial_boot_params = params;

	of_fdt_crc32 = crc32_be(~0, initial_boot_params,
				fdt_totalsize(initial_boot_params));
	return true;
}

fdt_check_header() 对 dtb 内容进行一堆check,如果符合要求,则会将 fdt 的虚拟地址存放到全局变量 initial_boot_params 中,之后系统中都会通过该地址解析 dtb,例如:

5.3.2 early_init_dt_scan_nodes()

drivers/of/fdt.c

void __init early_init_dt_scan_nodes(void)
{
	int rc = 0;

    //扫描chosen节点,并把bootargs 属性值拷贝到boot_command_line数组中
    //如果定义了CONFIG_CMDLINE这个宏,即通过config文件配置命令行,也会将其
    //配置的参数拷贝到boot_command_line
	rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
	if (!rc)
		pr_warn("No chosen node found, continuing without\n");

	/* Initialize {size,address}-cells info */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);

	/* Setup memory, calling early_init_dt_add_memory_arch */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

of_scan_flat_dt()对dtb里面的所有节点进行扫描,用提供的回调函数循环处理节点信息,回调函数返回0继续扫描,返回非0结束扫描,当扫描到最后一个节点也会结束扫描。

  • of_scan_flat_dt( early_init_dt_scan_chosen, boot_command_line) 就是处理chosen 节点,将节点中 bootargs 的值拷贝到 boot_command_line 字符数组中;
  • of_scan_flat_dt( early_init_dt_scan_root, NULL) 处理根节点信息,包括 #size-cells 属性和 #address-cells 属性,这两个属性会被存储到全局变量 dt_root_size_cellsdt_root_addr_cells 中;
  • of_scan_flat_dt( early_init_dt_scan_memory, NULL);就是处理 memory 节点,获取系统memory的信息;
5.3.2.1 early_init_dt_scan_chosen()

先来看下early_init_dt_scan_chosen,来确认是否有 chosen 节点,从 /proc/device-tree 中是有这个节点,其中不但存入了 bootargs 还指定了 linux ,initrd-endlinux,initrd-start

shift:/proc/device-tree/chosen # ls -la
total 0
drwxr-xr-x  2 root root    0 2023-09-08 17:45 .
drwxr-xr-x 16 root root    0 2023-09-08 02:44 ..
-r--r--r--  1 root root 1397 2023-09-08 17:48 bootargs
-r--r--r--  1 root root    8 2023-09-08 17:48 linux,initrd-end
-r--r--r--  1 root root    8 2023-09-08 17:48 linux,initrd-start
-r--r--r--  1 root root    7 2023-09-08 17:48 name

下面来看下该函数:

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
				     int depth, void *data)
{
	int l;
	const char *p;
	const void *rng_seed;

	pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);

	if (depth != 1 || !data ||
	    (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
		return 0;

	early_init_dt_check_for_initrd(node);
	early_init_dt_check_for_elfcorehdr(node);
	early_init_dt_check_for_usable_mem_range(node);

	/* Retrieve command line */
	p = of_get_flat_dt_prop(node, "bootargs", &l);
	if (p != NULL && l > 0)
		strlcpy(data, p, min(l, COMMAND_LINE_SIZE));

	/*
	 * CONFIG_CMDLINE is meant to be a default in case nothing else
	 * managed to set the command line, unless CONFIG_CMDLINE_FORCE
	 * is set in which case we override whatever was found earlier.
	 */
#ifdef CONFIG_CMDLINE
#if defined(CONFIG_CMDLINE_EXTEND)
	strlcat(data, " ", COMMAND_LINE_SIZE);
	strlcat(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#elif defined(CONFIG_CMDLINE_FORCE)
	strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#else
	/* No arguments from boot loader, use kernel's  cmdl*/
	if (!((char *)data)[0])
		strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif
#endif /* CONFIG_CMDLINE */

	pr_debug("Command line is: %s\n", (char *)data);

	rng_seed = of_get_flat_dt_prop(node, "rng-seed", &l);
	if (rng_seed && l > 0) {
		add_bootloader_randomness(rng_seed, l);

		/* try to clear seed so it won't be found. */
		fdt_nop_property(initial_boot_params, node, "rng-seed");

		/* update CRC check value */
		of_fdt_crc32 = crc32_be(~0, initial_boot_params,
				fdt_totalsize(initial_boot_params));
	}

	/* break now */
	return 1;
}

函数主要流程如下:

  • initrd 需要使能 CONFIG_BLK_DEV_INITRD
  • 解析属性 linux,initrd-start 获取起始地址,并存放在全局变量 phys_initrd_start
  • 解析属性 linux,initrd-end 获取结束地址,并根据 phys_initrd_start 计算出size,并存放在全局变量 phys_initrd_size

phys_initrd_startphys_initrd_size 会在后面 memblock 初始化 中使用。

5.3.2.2 early_init_dt_scan_root()
int __init early_init_dt_scan_root(unsigned long node, const char *uname,
				   int depth, void *data)
{
	const __be32 *prop;

	if (depth != 0)
		return 0;

	dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
	dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;

	prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
	if (prop)
		dt_root_size_cells = be32_to_cpup(prop);
	pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells);

	prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
	if (prop)
		dt_root_addr_cells = be32_to_cpup(prop);
	pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells);

	/* break now */
	return 1;
}

这个函数没什么好说的,解析全局的 #size-cells 节点和 #address-cells 节点。

5.3.2.3 early_init_dt_scan_memory()

下面着重来看下 early_init_dt_scan_memory

/*
 * early_init_dt_scan_memory - Look for and parse memory nodes
 */
///choosen node,该节点保存bootargs属性
//memory node,定义系统的物理内存布局,
//DTBheader 的memreserve域,
//reserved-memorynode, 定义系统保留的内存地址区域。
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
				     int depth, void *data)
{
	// 查找节点中属性为device_type,并获取值
	const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
	const __be32 *reg, *endp;
	int l;
	bool hotpluggable;

	/* We are scanning "memory" nodes only */
	// 只查找device_type的值为memory的节点
	if (type == NULL || strcmp(type, "memory") != 0)
		return 0;

	// 确认节点中是否由linux,usable-memory属性
	reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
	if (reg == NULL)
		reg = of_get_flat_dt_prop(node, "reg", &l);//如果没有,则查找reg属性
	if (reg == NULL)
		return 0;

	endp = reg + (l / sizeof(__be32));

	// 确认该memory是否为hotplug
	hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);

	pr_debug("memory scan node %s, reg size %d,\n", uname, l);

	while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
		u64 base, size;

		base = dt_mem_next_cell(dt_root_addr_cells, &reg);
		size = dt_mem_next_cell(dt_root_size_cells, &reg);

		if (size == 0)	// 如果reg属性中的size为0,则跳过
			continue;
		pr_debug(" - %llx, %llx\n", base, size);

		///向系统注册该memory node内存区域, 通过memblock_add完成添加
		early_init_dt_add_memory_arch(base, size);

		if (!hotpluggable)
			continue;

		if (memblock_mark_hotplug(base, size))
			pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",
				base, base + size);
	}

	return 0;
}

这里就是解析 memory 描述的信息从而得到内存的base 和size 信息,最后通过 early_init_dt_add_memory_arch()-> memblock_add()添加到memblock 子系统中。

在 dts 中memory 节点描述大致如下:

注意:

如上图parrot项目中reg的size为0,所以直接返回

5.4 小结

至此, setup_machine_fdt() 就剖析完成,主要是通过 fixmap_remap_fdt() 对dtb 的物理内存进行映射,映射区域在 fixmap 中 FDT 区域,并获取到映射之后的虚拟地址。接着会将 dtb 这块区域添加到 memblock . reserved 中,然后通过 early_init_dt_scan() 对dts 中的节点进行早期的扫描工作。接着再通过 fixmap_remap_fdt() 再次映射,将 FDT 映射属性修改为 read-only。最后再通过 of_flat_dt_get_machine_name() 函数获取设备的名称,一般配置在 dts 根节点 model 或 compatible 属性中。

5.5 FDT调试

  • /sys/firmware/fdt : 查看原始的dtb 文件;
  • /sys/firmware/devicetree : 以目录结构形式呈现 dtb 文件,根节点对应 base 目录,每一个节点对应一个目录,每一个属性对应一个文件。
  • /proc/device-tree : /sys/firmware/devicetree/base 的链接目录;

获取到平台的 /sys/firmware/fdt 可以使用 dtc 工具将其解析成文本文件:

dtc -I dtb -O dts fdt > dtb.txt

其中 -I 表示输入文件格式, -O 表示输出的文件格式,详细可以查看 dtc 的使用:


Usage: dtc [options] <input file>
 
Options: -[qI:O:o:V:d:R:S:p:a:fb:i:H:sW:E:@Ahv]
  -q, --quiet
        Quiet: -q suppress warnings, -qq errors, -qqq all
  -I, --in-format <arg>
        Input formats are:
                dts - device tree source text
                dtb - device tree blob
                fs  - /proc/device-tree style directory
  -o, --out <arg>
        Output file
  -O, --out-format <arg>
        Output formats are:
                dts - device tree source text
                dtb - device tree blob
                asm - assembler source
  -V, --out-version <arg>
        Blob version to produce, defaults to 17 (for dtb and asm output)
  -d, --out-dependency <arg>
        Output dependency file
  -R, --reserve <arg>
        Make space for <number> reserve map entries (for dtb and asm output)
  -S, --space <arg>
        Make the blob at least <bytes> long (extra space)
  -p, --pad <arg>
        Add padding to the blob of <bytes> long (extra space)
  -a, --align <arg>
        Make the blob align to the <bytes> (extra space)
  -b, --boot-cpu <arg>
        Set the physical boot cpu
  -f, --force
        Try to produce output even if the input tree has errors
  -i, --include <arg>
        Add a path to search for include files
  -s, --sort
        Sort nodes and properties before outputting (useful for comparing trees)
  -H, --phandle <arg>
        Valid phandle formats are:
                legacy - "linux,phandle" properties only
                epapr  - "phandle" properties only
                both   - Both "linux,phandle" and "phandle" properties
  -W, --warning <arg>
        Enable/disable warnings (prefix with "no-")
  -E, --error <arg>
        Enable/disable errors (prefix with "no-")
  -@, --symbols
        Enable generation of symbols
  -A, --auto-alias
        Enable auto-alias of labels
  -h, --help
        Print this help and exit
  -v, --version
        Print version and exit

dtc 工具,可以使用系统自带的 /usr/bin/dtc,或者使用 linux 中 scripts/dtc/ 编译出来的 dtc 工具。

当然也可以使用我开发的OneMore中的dtb2dts工具

6. 小结

至此,fixmap 大致分析完成,这里做个小结。

fixmap 会在内核系统中预留一段虚拟内存用以固定映射,并且通过接口 __fix_to_virt() 根据 FIXADDR_TOP 的偏移量获取相应的虚拟内存作为分配策略,然后调用 __set_fixmap() 填充 pte,完成页表映射。

其实, early_fixmap_init() 只是建立了一个映射的框架,具体的物理地址和虚拟地址的映射没有去填充,这个是由使用者具体在使用时再去填充对应的 pte entry。比如像上面 fixmap_remap_fdt() 函数,就是典型的填充 pte entry 的过程,完成最后异步映射,然后才能读取 dtb 文件。

附上此阶段流程图:
FIXMAP流程图