linux中执行uefi runtime service call的内存上下文切换

发布时间 2023-10-25 12:37:15作者: 半山随笔

当linux kernel从UEFI启动之后尽管boot service退出了但是仍然可以使用runtime service。这就引发了一个问题:存在于uefi内存空间的code如何被kernel调用。

首先找一个调用efi runtime service的例子:

static void efi_call_rts(struct work_struct *work)
{
    ...
    switch (efi_rts_work.efi_rts_id) {
    case EFI_GET_TIME:
        status = efi_call_virt(get_time, (efi_time_t *)arg1,
                       (efi_time_cap_t *)arg2);
    ...

这个函数算是调用runtime service的一个入口。在里面有所有可用的runtime service。调用入口是一个宏:

/*
 * Wrap around the new efi_call_virt_generic() macros so that the
 * code doesn't get too cluttered:
 */
#define efi_call_virt(f, args...)   \
	efi_call_virt_pointer(efi.runtime, f, args)

 efi.runtime是一个包含efi runtime service相关的全局变量,

#define efi_call_virt_pointer(p, f, args...)				\
({									\
	efi_status_t __s;						\
	unsigned long __flags;						\
									\
	arch_efi_call_virt_setup();					\
									\
	__flags = efi_call_virt_save_flags();				\
	__s = arch_efi_call_virt(p, f, args);				\
	efi_call_virt_check_flags(__flags, __stringify(f));		\
									\
	arch_efi_call_virt_teardown();					\
									\
	__s;								\
})

  arch_efi_call_virt_setup() 负责efi 内存上下文的切换。

#define arch_efi_call_virt_setup()					\
({									\
	efi_virtmap_load();						\
	__efi_fpsimd_begin();						\
	raw_spin_lock(&efi_rt_lock);					\
})

 efi_virtmap_load最重要的操作就是switch_mm

void efi_virtmap_load(void)
{
	preempt_disable();
	efi_set_pgd(&efi_mm);
}

static inline void efi_set_pgd(struct mm_struct *mm)
{
	__switch_mm(mm);

  __switch_mm最重要的事就是切换页表也就是设置pgd,一旦pgd切换成功,当前的内存执行环境就切换成功了。那么页表创建是在什么时候完成的呢?

通过搜索efi_mm.pgd找到efi_virtmap_init。

static bool __init efi_virtmap_init(void)
{
	efi_memory_desc_t *md;

	efi_mm.pgd = pgd_alloc(&efi_mm);
	。。。

	for_each_efi_memory_desc(md) {
		。。。
		ret = efi_create_mapping(&efi_mm, md);
		if (ret) {

  这里分配了pgd的内存。看看efi_create_mapping做了什么:

int __init efi_create_mapping(struct mm_struct *mm, efi_memory_desc_t *md)
{
	。。。
	create_pgd_mapping(mm, md->phys_addr, md->virt_addr,
			   md->num_pages << EFI_PAGE_SHIFT,
			   __pgprot(prot_val | PTE_NG), page_mappings_only);

 可以看到,页表的创建依赖于从for_each_efi_memory_desc得到的md。md是efi内存映射的描述符:

typedef struct {
	u32 type;
	u32 pad;
	u64 phys_addr;
	u64 virt_addr;
	u64 num_pages;
	u64 attribute;
} efi_memory_desc_t;

它描述了一个从虚拟内存到物理内存的连续映射的内存段。

看看md是如何赋值的:

#define for_each_efi_memory_desc(md) \
	for_each_efi_memory_desc_in_map(&efi.memmap, md)

#define for_each_efi_memory_desc_in_map(m, md)				   \
	for ((md) = (m)->map;						   \
	     (md) && ((void *)(md) + (m)->desc_size) <= (m)->map_end;	   \
	     (md) = (void *)(md) + (m)->desc_size)

 可知,efi.memmap是md的来源。继续探索它是如何设置的。通过搜索我们找到这里:

static int __init arm_enable_runtime_services(void)
{
	...

	if (efi_memmap_init_late(efi.memmap.phys_map, mapsize)) {  //phys_map保存efi 内存描述符表的物理地址
		pr_err("Failed to remap EFI memory map\n");
		return 0;
	}
...
}

int __init efi_memmap_init_late(phys_addr_t addr, unsigned long size)
{
	struct efi_memory_map_data data = {
		.phys_map = addr,
		.size = size,
		.flags = EFI_MEMMAP_LATE,
	};
...
	return __efi_memmap_init(&data);
}

int __init __efi_memmap_init(struct efi_memory_map_data *data)
{
	...
	phys_map = data->phys_map;

	if (data->flags & EFI_MEMMAP_LATE)
		map.map = memremap(phys_map, data->size, MEMREMAP_WB);
	else
		map.map = early_memremap(phys_map, data->size);
...
	set_bit(EFI_MEMMAP, &efi.flags);

	efi.memmap = map;

  这里稍微复杂一点,__efi_memmap_init函数负责赋值efi.memmap,这个map从phys_map起始的一段内存,这就是所有efi内存描述符表的存储地址。那么这个地址的来源又是哪里?

从source的搜索中发现,初始化efi.memmap的地方只有__efi_memmap_init一处。找到调用__efi_memmap_init的另一函数:efi_memmap_init_early

int __init efi_memmap_init_early(struct efi_memory_map_data *data)
{
	/* Cannot go backwards */
	WARN_ON(efi.memmap.flags & EFI_MEMMAP_LATE);

	data->flags = 0;
	return __efi_memmap_init(data);
}

  引用该函数的只有一处:

void __init efi_init(void)
{
	struct efi_memory_map_data data;
	u64 efi_system_table;

	/* Grab UEFI information placed in FDT by stub */
	efi_system_table = efi_get_fdt_params(&data);
	if (!efi_system_table)
		return;

	if (efi_memmap_init_early(&data) < 0) {
	。。。

  现在接近真相了,这些原始数据是从fdt里面获取的。似乎非常合理,但是等等,我好像知道一点fdt的秘密,那就是传给kernel的fdt也许根本就是空的,如果是这样,后续的操作还有什么意义?

其实fdt还有另一种途径生成,那就是在真正的kernel代码执行前的efi初始化阶段。

static
efi_status_t allocate_new_fdt_and_exit_boot(void *handle,
					    efi_loaded_image_t *image,
					    unsigned long *new_fdt_addr,
					    char *cmdline_ptr)
{
。。。
status = efi_exit_boot_services(handle, &priv, exit_boot_func);
。。
}

efi_status_t efi_exit_boot_services(void *handle, void *priv,
				    efi_exit_boot_map_processing priv_func)
{
	struct efi_boot_memmap *map;
	efi_status_t status;

	if (efi_disable_pci_dma)
		efi_pci_disable_bridge_busmaster();

	status = efi_get_memory_map(&map, true);
	if (status != EFI_SUCCESS)
		return status;

	status = priv_func(map, priv);
。。
}


static efi_status_t exit_boot_func(struct efi_boot_memmap *map, void *priv)
{
。。。
	efi_get_virtmap(map->map, map->map_size, map->desc_size,
			p->runtime_map, &p->runtime_entry_count);

	return update_fdt_memmap(p->new_fdt_addr, map);
}

  可以看到得到内存映射的关键函数是efi_get_memory_map

efi_status_t efi_get_memory_map(struct efi_boot_memmap **map,
				bool install_cfg_tbl)
{
	...
	status = efi_bs_call(get_memory_map, &tmp.map_size, NULL, &tmp.map_key,
			     &tmp.desc_size, &tmp.desc_ver);

  该函数通过get_memory_map这个efi的boot time service去获取内存映射的描述符表。至此我们算是从kernel层面清楚了efi runtime service调用所需内存上下文的设置。如果想了解uefi是怎么生成memory map的可以去看firmware的源码。

总结一下:

efi runtime service内存页表的切换流程:

1. 在efi初始化阶段通过调用get_memory_map boot time service将efi memory map信息放到fdt中;

2. efi_init中从fdt获取efi memory map其实地址信息,并存储到efi.memmap中;

3. arm_enable_runtime_services->efi_memmap_init_late->__efi_memmap_init将memory map信息remap并设置到efi.memmap中;

4. 在efi_virtmap_init中创建页表;

4. 通过efi_call_virt调用efi runtime service的时候切换页表执行service call;

 

总体来看,流程有点绕但是并不复杂,记录下来,留作以后回看。