前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux内核启动流程分析 - efi_stub_entry

linux内核启动流程分析 - efi_stub_entry

作者头像
KINGYT
发布2020-06-29 16:07:43
2.3K0
发布2020-06-29 16:07:43
举报

接上一篇文章 linux内核启动流程分析 - efi_pe_entry,我们继续看efi_stub_entry函数。

该函数比较特殊,是用汇编写的,下面我们来逐行分析下。

第一行是通过and指令,使rsp寄存器里的值满足16字节对齐。

第二行看注释可知,是保存efi_pe_entry传过来的boot_params参数到rbx寄存器里。

也就是说,boot_params参数原本是被保存在rdx里的。

那为什么是保存在rdx里,且又是怎么保存到rdx里的呢?

这就要说到汇编语言的calling convention了。

所谓calling convention,其实就是一种约定,是说当我调用你写的汇编函数时,我会把该函数所需的参数,放到约定好的寄存器或堆栈里,你在获取这些参数时,直接到那里去拿就好了。

当你有返回值时,也要将其放到比如rax寄存器里,我也会到那里去取。

有了这种约定之后,大家就可以各自写各自的逻辑,且在最终链接到一起时,还是可以正常执行的。

对于c语言来说,我们其实可以不用关心这个,因为编译器比如gcc等会将我们写的c语言,转化成符合上述calling convention的汇编代码,一切都在gcc里帮我们处理好了。

但如果我们要直接写汇编代码,这些就是要了解清楚的。

那对于x64的linux内核来说,calling convention具体是怎么约定的呢?

这个我们可以参考下面wiki中的说明:

https://en.wikipedia.org/wiki/X86_calling_conventions

或者可以看这个表格,比较不同版本的calling convention(是的,有很多版本):

由上面截图可知,对于整型和指针型参数来说,如果参数个数小于等于6个,则其都是通过寄存器来传递的,使用的寄存器顺序分别为 RDI, RSI, RDX, RCX, R8, R9。

我们再来看下efi_pe_entry中调用efi_stub_entry的地方:

该调用传递了三个指针类型的参数,所以它们使用的寄存器分别是 rdi, rsi, rdx。

现在大家就应该明白了,为什么保存boot_params到rbx时,要从rdx里取了吧。

这里可能又有人会问,为什么要在rbx里备份一份呢,如果要用到boot_params,直接从rdx里取不就行了吗?

我们继续看efi_stub_entry中的第三行代码,它是通过call指令,调用efi_main函数,执行efi_main里的逻辑。

在efi_main函数执行时,rdx很可能会被修改掉,所以我们没法确保,在efi_main执行完毕后,rdx里存放的还是boot_params的地址。

那又有人会问,存到rbx里就不会被修改了吗?

是的。

看上面介绍calling convention时的第一个截图,当被调用函数要使用rbx时,它必须在返回之前,恢复rbx原来的值,所以rbx一定是不会被修改的。

好,efi_stub_entry函数的第二行代码就已经说明白了,我们继续看第三行。

第三行是通过call指令,调用efi_main方法:

代码语言:javascript
复制
// drivers/firmware/efi/libstub/x86-stub.c
unsigned long efi_main(efi_handle_t handle,
                             efi_system_table_t *sys_table_arg,
                             struct boot_params *boot_params)
{
        unsigned long bzimage_addr = (unsigned long)startup_32;
        ...
        return bzimage_addr;
} 

这里我们不详细展开该方法,只看一些重要的点(下篇文章再详细讲)。

首先,efi_stub_entry在调用该方法时,寄存器rdi, rsi, rdx里的值都没有改变,还是efi_pe_entry调用efi_stub_entry时传递的那些值,所以根据上述calling convention,efi_main作为efi_stub_entry的被调用函数,其参数类型及顺序也应该和efi_pe_entry的参数传递顺序是一样的。

这个可以从上面代码里得到确认。

接着,efi_main里取startup_32函数运行时的地址,并返回给efi_stub_entry。

根据calling convention可知,该地址被放到了rax里。

继续看efi_stub_entry。

在efi_main函数返回后,第四行代码把之前保存在rbx里的boot_params的地址,拷贝到了rsi里。

第五行代码将startup_64函数的编译时地址,加到了rax寄存器里,也就是加到了startup_32函数运行时的地址上,这样rax里存放的地址,就是运行时的startup_64函数的地址了。

为什么这样相加就是startup_64运行时的地址呢?

首先第五行代码中使用的startup_64是编译时(构建时)地址,而并不是运行时地址。

这个可以通过下述方法确认。

首先看下startup_64函数的声明:

上图中的 .org 0x200 是说,startup_64函数的编译后地址要求是0x200。

这个可以反汇编确认:

看上面选中的行,确实是0x200。

我们再来看下efi_stub_entry中使用到startup_64的那行代码的反汇编:

这里也是0x200,说明这行代码使用的确实是startup_64的编译时地址。

由上一篇文章中我们可以知道,startup_32的编译时地址是0,所以startup_64的编译时地址,就成了startup_32到startup_64的偏移量。

所以当我们把这偏移量加到startup_32的运行时地址时(rax寄存器里),得到的自然是startup_64的运行时地址。

第五行代码就这些内容,我们再看第六行。

第六行也超级简单,就是jmp到rax代表的函数,即startup_64,之后就是开始执行startup_64函数的逻辑了。

到这里,efi_stub_entry函数的内容就都讲完了,希望大家能有所收获。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档