专栏首页Linux内核及编程语言底层相关技术研究linux内核启动流程分析 - efi_stub_entry

linux内核启动流程分析 - efi_stub_entry

接上一篇文章 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方法:

// 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函数的内容就都讲完了,希望大家能有所收获。

本文分享自微信公众号 - Linux内核及JVM底层相关技术研究(ytcode),作者:wangyuntao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • linux内核启动流程分析 - efi_pe_entry

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

    KINGYT
  • linux内核启动流程分析 - efi_main

    上一篇文章 linux内核启动流程分析 - efi_stub_entry 中,为了叙述方便,我们只是粗略的讲了下efi_main函数,这里我们再具体看下。

    KINGYT
  • linux内核启动流程分析 - startup_64

    在前面的第三篇文章中我们说道,efi_stub_entry最终会调用startup_64,那这篇文章我们就来看下startup_64的具体逻辑。

    KINGYT
  • 06.内核启动流程分析之内核启动

    嵌入式与Linux那些事
  • linux内核启动过程分析

    start_kernel是内核启动阶段的入口,通过单步调试,可以发现它是linux内核执行的第一个init,我们单步进入看看它做了哪些操作:

    De4dCr0w
  • linux内核启动流程分析 - efistub的入口函数

    网上类似标题的文章很多,但大都是从start_kernel讲起,我觉得这是远远不够的。

    KINGYT
  • 05.内核启动流程分析之makefile

      分析makefile从顶层开始,顺藤摸瓜的分析下去,会涉及到所有的makefile文件。各级子目下的makefile完成的动作obj -y += obj -...

    嵌入式与Linux那些事
  • Linux 启动流程分析

    这个部分比较有意思。因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分...

    用户5807183
  • Linux 启动流程分析

    这个部分比较有意思。因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分...

    良月柒
  • Linux 启动流程分析

    这个部分比较有意思。因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分...

    用户6543014
  • 第3阶段——内核启动分析之创建si工程和分析stext启动内核函数(4)

    目标: (1)创建Source Insight 工程,方便后面分析如何启动内核的 (2)分析uboot传递参数,链接脚本如何进入stext的  (3) 分析st...

    张诺谦
  • 第3阶段——内核启动分析之创建si工程和分析stext启动内核函数(4)

    目标: (1)创建Source Insight 工程,方便后面分析如何启动内核的 (2)分析uboot传递参数,链接脚本如何进入stext的  (3) 分析st...

    张诺谦
  • Spark内核详解 (3) | Spark集群启动流程的简单分析

    本片博文主要分析的是Standalone 模式下 Spark 集群(Master, work)启动流程

    不温卜火
  • Influxdb启动流程分析

    我们启动的话通常是 influxd run -config [config file path]

    扫帚的影子
  • kubelet 启动流程分析

    本来这篇文章会继续讲述 kubelet 中的主要模块,但由于网友反馈能不能先从 kubelet 的启动流程开始,kubelet 的启动流程在很久之前基于 v1....

    田飞雨
  • kubelet 启动流程分析

    上篇文章(kubelet 架构浅析 )已经介绍过 kubelet 在整个集群架构中的功能以及自身各模块的用途,本篇文章主要介绍 kubelet 的启动流程。

    田飞雨
  • kubelet 启动流程分析

    上篇文章(kubelet 架构浅析 )已经介绍过 kubelet 在整个集群架构中的功能以及自身各模块的用途,本篇文章主要介绍 kubelet 的启动流程。

    田飞雨
  • 04.uboot分析之uboot启动内核

    首先要明确:uboot目标是从flash读出内核(nand read.jffs2 0x30007FC0 kernel;),启动它(bootm 0x30007FC...

    嵌入式与Linux那些事
  • 超详细分析Bootloader(Uboot)到内核的启动流程(万字长文!)

      Bootloader的启动过程可以分为单阶段、多阶段两种。通常多阶段的 Bootloader能提供更为复杂的功能以及更好的可移植性。从固态存储设备上启动的 ...

    嵌入式与Linux那些事

扫码关注云+社区

领取腾讯云代金券