接上一篇文章 linux内核启动流程分析 - efistub的入口函数,我们继续看efi_pe_entry这个函数。
该函数有两个参数,根据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函数。
在调用完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函数的实现。