内核启动过程最早的打印,这些是内核中的代码打印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函数的实现。