arm64内核efi格式入口函数efi_pe_entry

本文详细介绍了Linux内核启动过程中EFIstub的efi_pe_entry函数,涉及UEFI系统表、内存映射、LOADED_IMAGE_PROTOCOL的使用,以及bzImage和setup_header的处理,揭示了内核启动时如何正确计算和定位启动信息。
摘要由CSDN通过智能技术生成

内核启动过程最早的打印,这些是内核中的代码打印efi_info打印,这时候内核的打印功能还不具备,依靠uefi来打印:

EFI stub: Booting Linux Kernel...
EFI stub: EFI_RNG_PROTOCOL unavailable
EFI stub: Using DTB from configuration table
EFI stub: Exiting boot services... 

接上一篇文章 linux内核启动流程分析 - efistub的入口函数,我们继续看efi_pe_entry这个函数。


efi_status_t __efiapi efi_pe_entry(efi_handle_t handle,
				   efi_system_table_t *sys_table_arg)
{
    efi_system_table = sys_table_arg;  //保存efi_system_table,这个是uefi提供给内核唯一接口
	...
	efi_system_table = sys_table_arg;

	efi_info("Booting Linux Kernel...\n");

	si = setup_graphics();
	//重新移动内核在内存的位置
	status = handle_kernel_image(&image_addr, &image_size,
				     &reserve_addr,
				     &reserve_size,
				     image);
	...

	if (fdt_addr) {
		efi_info("Using DTB from command line\n");
	} else {
		/* Look for a device tree configuration table entry. */
		fdt_addr = (uintptr_t)get_fdt(&fdt_size);
		if (fdt_addr)
			efi_info("Using DTB from configuration table\n");
	}					 
	...		   
	status = allocate_new_fdt_and_exit_boot(handle, &fdt_addr,         //生成fdt文件
						efi_get_max_fdt_addr(image_addr),
						initrd_addr, initrd_size,
						cmdline_ptr, fdt_addr, fdt_size);	

	...
	efi_enter_kernel(image_addr, fdt_addr, fdt_totalsize((void *)fdt_addr));  //正式进入内核
	...
}

//类型定义efi_system_table_t

efi_pe_entry:uefi调用内核的入口函数,其中参数efi_system_table_t *efi_system_table:

typedef union {
	struct {
		efi_table_hdr_t hdr;
		unsigned long fw_vendor;	/* physical addr of CHAR16 vendor string */
		u32 fw_revision;
		unsigned long con_in_handle;
		efi_simple_text_input_protocol_t *con_in;
		unsigned long con_out_handle;   //uefi提供给内核早期的打印接口
		efi_simple_text_output_protocol_t *con_out;
		unsigned long stderr_handle;
		unsigned long stderr;
		efi_runtime_services_t *runtime;   //内核运行时调用uefi的接口
		efi_boot_services_t *boottime;     //内核早期启动是调用uefi的接口
		unsigned long nr_tables;
		unsigned long tables;   //uefi提供的各类表(包括acpi表)等信息。efi_config_table_t。不同的表通过guid来区分。
	};
	efi_system_table_32_t mixed_mode;
} efi_system_table_t;

该函数有两个参数,根据uefi specification中有关entry point的定义可知:

handle指向的是运行时的kernel,sys_table_arg指向的是uefi的system table(有了system table,就可以使用uefi的各种服务,比如输入输出、boot service、runtime service等)。

接下来该函数验证了system table中的signature是否等于uefi specification中定义的signature,以此来判断该次启动是否用的是uefi方式。

然后通过efi_bs_call宏,调用system table中的boot service的handle_protocol方法,该方法指定的protocol为LOADED_IMAGE_PROTOCOL_GUID,即查询handle指向的image的相关信息,并把查询结果返回在image这个参数里。

我们来具体看下uefi specification中定义的LOADED_IMAGE_PROTOCOL返回的结果是什么。

由上可见,该protocol返回结果的数据结构是EFI_LOADED_IMAGE_PROTOCOL,从该数据结构中,我们可以获取很多有关image的信息,比如ImageBase、ImageSize等。

继续看efi_pe_entry函数。

​
efi_status_t allocate_new_fdt_and_exit_boot(void *handle,
                                            unsigned long *new_fdt_addr,
                                            unsigned long max_addr,
                                            u64 initrd_addr, u64 initrd_size,
                                            char *cmdline_ptr,
                                            unsigned long fdt_addr,
                                            unsigned long fdt_size)
{

        unsigned long map_size, desc_size, buff_size;
        u32 desc_ver;
        unsigned long mmap_key;
        efi_memory_desc_t *memory_map, *runtime_map;
        efi_status_t status;
        int runtime_entry_count;
        struct efi_boot_memmap map;
        struct exit_boot_struct priv;

        map.map         = &runtime_map;
        map.map_size    = &map_size;
        map.desc_size   = &desc_size;
        map.desc_ver    = &desc_ver;
        map.key_ptr     = &mmap_key;
        map.buff_size   = &buff_size;

		status = efi_get_memory_map(&map);     //获取物理内存信息
		
		efi_info("Exiting boot services and installing virtual address map...\n");
        
		map.map = &memory_map;
        status = efi_allocate_pages(MAX_FDT_SIZE, new_fdt_addr, max_addr);
		
		status = efi_get_memory_map(&map);//将通过uefi申请空间,并将map.map指向那段内存地址,其中的内容为efi_memory_desc_t。这个地址在函数exit_boot_func填入到fdt中
		
		//更新fdt文件
        status = update_fdt((void *)fdt_addr, fdt_size,
                            (void *)*new_fdt_addr, MAX_FDT_SIZE, cmdline_ptr,
                            initrd_addr, initrd_size);
...
        runtime_entry_count             = 0;
        priv.runtime_map                = runtime_map;
        priv.runtime_entry_count        = &runtime_entry_count;
        priv.new_fdt_addr               = (void *)*new_fdt_addr;

        status = efi_exit_boot_services(handle, &map, &priv, exit_boot_func);  //exit_boot_func将fdt文件填充内容
...
}

​

在调用完handle_protocol获取了image信息后,该函数紧接着使用了efi_table_attr宏,从image中获取image_base的值,即运行时的kernel在内存中的起始地址。

接着根据startup_32函数地址和image_base的值,算出image_offset,该offset指的是bzImage中的compressed部分在整个bzImage中的偏移量(startup_32是compressed部分的第一个方法)。

接下来调用efi_allocate_pages函数,创建了一个boot_params实例,并将各字段初始化为0。

boot_params又被称为zeropage,该结构体用来存放各种启动参数,供后续启动kernel使用,其具体结构如下:

接下来pe_efi_entry又调用memcpy,将加载到内存的bzImage的第二个sector的内容,拷贝到boot_params里的setup_header里,拷贝的起始位置为setup_header里的jump字段。

那bzImage的第二个sector是在哪里,且又是什么内容呢?

其实就是在上一篇文章中讲到的 arch/x86/boot/header.S 文件里。

header.S里不仅有pecoff header,还有kernel的setup header,其主要用途是使kernel和bootloader之间可以进行数据交换。

有关setup header更详细的介绍,请看下面的链接:

https://www.kernel.org/doc/html/latest/x86/boot.html

header.S里的第512字节正是jump指令,且该指令之后的各种setup header字段和boot_params中setup header字段是一一对应的。

也就是说,该拷贝操作是把bzImage中的setup_header里的内容拷贝到boot_params里的setup_header里。

继续efi_pe_entry函数。

在拷贝完setup header之后,该函数又设置了boot_params里的setup header的root_flags、boot_flag、cmdline等字段。

在boot_params里的setup_header都初始化完毕之后,该函数最终调用了efi_stub_entry函数,并将参数image handle,system table,和boot params传递给了它。

至此,efi_pe_entry函数就结束了。

如果熟读过uefi specification,该函数的大部分逻辑理解起来都非常简单,所以很多细节我就不再赘述了。

这里我们只再重点说下image_offset的计算,这个当时花了我不少时间才理解清楚。

由上一篇文章我们知道,bzImage是由build.c这个工具,将setup部分和compressed部分顺序拼接在一起的,并没有做什么特殊处理。

startup_32作为compressed部分中的一个函数,我们可以通过下面的方法获取其编译后的地址:

由上可见,startup_32函数的地址是0。

既然build.c只是将setup和compressed部分顺序拼接,并没有做地址的转换处理,那理应efi_pe_entry函数里使用的startup_32函数的地址就是0。

如果此想法成立,那在image_base大于0的情况下,image_offset岂不是负数了?

这种推断肯定是有问题的,那问题出在哪呢?

后来经过一番苦苦查找,终于通过反汇编找到了答案,我们来看下在反汇编情况下,image_offset具体是怎么计算的。

由上可见,startup_32的地址,是根据rip的当前值减去0x895b8c得来的,而0x895b8c这个值正好是startup_32函数地址到上图选中指令的下一条指令的偏移量。

又由于rip存放的是下一条指令的地址,所以上面rip减去0x895b8c正好就是startup_32运行时的函数地址。

所以最开始我认为efi_pe_entry中使用的是startup_32的绝对地址,即上面输出的0,这种想法是错误的,其实它是根据当前rip中的地址,以及startup_32到下一条指令的偏移量,计算出真正的运行时中的startup_32的地址,这样不管compressed部分被加载到了内存的什么位置,startup_32的地址都是正确的,即位置无关。

这个结论我们从compressed/Makefile中加了-pie参数,可以进一步得到验证:

好,今天就讲这么多,下篇文章我们接着看最后的efi_stub_entry函数的实现。

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在了解 `efi_main` 函数之前,先简单介绍一下 EFI(Extensible Firmware Interface)。 EFI 是一种固件接口,是替代传统 BIOS 的新一代系统启动接口。EFI 提供了一个标准的、可扩展的、可定制的接口,可以在启动时加载硬件驱动程序和操作系统。相比于传统 BIOS,EFI 更加灵活、安全和可靠。 在 Linux 中,EFI 主要用于系统引导和启动。当计算机开机时,UEFI 固件会自动启动 EFI 程序,这个程序就是 `efi_main` 函数。`efi_main` 函数EFI 程序的入口点,它的原型如下: ``` EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) ``` 其中,`ImageHandle` 是该 EFI 程序的句柄,`SystemTable` 是 EFI 系统表,包含了各种 EFI 服务和数据结构。 `efi_main` 函数的作用是初始化 EFI 环境,加载 Linux 内核,并跳转到内核入口点开始执行。具体的流程如下: 1. 获取 EFI 系统表中的 Boot Services,这些服务包含了各种操作系统启动所需的函数。 2. 使用 Boot Services 中的函数加载 Linux 内核到内存中。 3. 构造 Linux 内核启动参数结构体 `struct boot_params`,这个结构体包含了 Linux 内核启动所需的各种参数。 4. 使用 Boot Services 中的函数将 `struct boot_params` 结构体复制到内存中,然后将控制权转移到内核入口点。 5. 内核启动后,会解析 `struct boot_params` 结构体,获取各种启动参数,并开始执行内核初始化流程。 总之,`efi_main` 函数Linux EFI 启动的关键,它负责初始化 EFI 环境和启动 Linux 内核
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古井无波 2024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值