Linux内核驱动之efi-rtc

Linux内核驱动之efi-rtc

    • 1. UEFI与BIOS概述
      • 1.1. BIOS 概述
        • 1.1.1. BIOS缺点:
        • 1.1.2. BIOS的启动流程
      • 1.2 UEFI 概述
        • 1.2.1 Boot Sevices:
        • 1.2.2. Runtime Service:
        • 1.2.3. UEFI优点:
        • 1.2.4. UEFI启动过程:
      • 1.3 Legacy和UEFI
      • 1.4 GPT
      • 1.5 MBR
    • 2. UEFI到操作系统的虚拟地址转换
      • 2.1 物理地址与虚拟地址
      • 2.2 X86中的地址转换
      • 2.3 UEFI中的地址转换
    • 3. UEFI Common:UEFI 中的Runtime Services
    • 4. rtc-efi注册
      • 4.1 rtc-efi驱动模块的注册流程 (init/main.c)
      • 4.2 rtc-efi driver的注册过程(driver/rtc/rtc-efi.c)
      • 4.3 rtc-efi device的注册过程(driver/rtc/rtc-efi-platform.c)
    • 5. efi runtime services的启动
      • 5.1 efi runtime services的启动过程
      • 5.2 手动开启 efi runtime services
    • 6. 内核配置项
      • 6.1 CONFIG_RTC_SYSTOHC
      • 6.2 CONFIG_RTC_DRV_EFI
    • 7. kernel 如何调用uefi的runtime service

1. UEFI与BIOS概述

从我们按下开机键到进入到操作系统之前的系统初始化动作,即是BIOS run的过程。如今操作系统已经从枯燥的文本时代演化到丰富多彩的图形界面,而BIOS却一直延续着枯燥的过程,BIOS设置也一直是单调的蓝底白字格式。
BIOS的坚持出于两个原因:

  1. 外因是BIOS基本能够满足市场需求;
  2. 内因是BIOS的设计使得BIOS的升级和扩增变得非常困难。

随着64CPU逐渐取代32CPUBIOS越来越不能满足市场需求,这使得UEFI作为BIOS的替代者应运而生。

1.1. BIOS 概述

BIOS全称“基本输入输出系统”(Basic Input/Output System),它是存储子主板ROM里的一组代码。

其主要功能是在计算机上电时对硬件进行初始化配置,并将硬件操作封装为BIOS中断服务。这样,各种硬件间的差异便由BIOS负责维护,程序直接调用BIOS中断服务即可实现对硬件的控制。以下是BIOS的主要组成部分:

  • 加电自检程序,在开机时负责检测硬件设备是否正常工作。
  • 系统初始化程序,其中包括硬件设备的初始化以及创建BIOS中断向量等。
  • 适配外围即插即用设备。
  • CMOS设置程序,负责读写保存在CMOS中的系统设置信息。
1.1.1. BIOS缺点:
  • 开发效率低:大部分BIOS代码使用汇编开发,开发效率低;汇编开发的另一个缺点是使得代码与设备的耦合程度太高,代码受硬件变化的影响大;
  • 性能差:BIOS基本输入输出服务需要通过中断来完成,开销大,并且BIOS没有提供异步工作模式,大量时间消耗在等待时间上;
  • 功能扩展性差,升级缓慢:BIOS代码采用静态链接,增加硬件功能时,必须将16位代码放置在0xC0000~0xDFFFF区间,初始化时将其设置为约定的中断处理程序。而且BIOS没有提供动态加载设备驱动的方案;
  • 安全性差:BIOS运行过程中对可执行代码没有安全方面的考虑;
  • 不支持从硬盘2TB以上的地址引导:受限于BIOS硬盘的寻址方式,BIOS硬盘采用32位地址,因而引导扇区的最大逻辑块地址是232次方(2TB)。
1.1.2. BIOS的启动流程

在系统上电后,CPU运行于实模式工作环境中,数据位宽为16位,最大物理地址寻址范围是0~1MB,其中的物理地址0x0C0000~0x0FFFFF保留给BIOS使用。开机后,CPU首先跳转到物理地址0xFFFFFFF0处执行程序。一般情况下,这里是一条跳转指令,CPU通过执行此处的跳转指令跳转到真正的BIOS入口地址处执行,BIOS POST阶段执行完后,BIOS会将控制权交给引导程序,最终引导进入操作系统。以下是BIOS的启动流程:

  1. BIOS代码首先做的是POSTPower On Self Test,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与CMOS中的设置一致。如果发现硬件错误,则通过喇叭报警。
  2. 初始化显示设备并显示显卡信息,接着初始化其他设备。
  3. 检测CPU和内存并显示检测结果。
  4. 检测标准设备,例如硬盘、光驱、串口设备、并口设备等。
  5. 检测即插即用设备,并为这些设备分配中断号、I/O端口和DMA通道等资源。
  6. 如果硬件配置发生变化,那么这些变化的配置将更新到CMOS中。
  7. 根据配置的启动顺序引导设备启动,通过BIOS中断将设备的引导程序读入内存。
  8. 将处理器的控制权交给引导程序,最终引导进入操作系统。

1.2 UEFI 概述

UEFI全称“统一可扩展固件接口”(Unified Extensible Firmware Interface),定义了操作系统和平台固件之间的接口,它是UEFI Forum发布的一种标准。它是一种标准,没有提供实现。其实现由其他公司或开源组织提供。如Intel公司提供的开源UEFI实现TianoCorePhoenix公司提供的SecureCore Tiano

UEFI发端于20世纪90年代中期的安腾处理器。相对于当时流行的IA32Intel Architecture 32)系统,安腾是一种全新的64位系统,BIOS的限制对于这种64位系统变得不可接受。1998Intel发起了Intel Boot Initiate项目,后来更名为EFI2005年,Intel联手微软、AMD、联想等11家公司成立了Unified EFI Forum,负责制定统一的EFI标准。第一个UEFI标准–UEFI2.02006年发布。

UEFI提供给系统的的接口包括启动服务(Boot Services)和运行时服务(Runtime Services)。

UEFI规范描述了操作系统和平台固件之间的接口,其目的是为操作系统和平台固件定义一种通信方法。UEFI的前身是EFIExtensible Firmware Interface,可扩展固件接口)规范1.10。因此,一些代码和协议仍保留EFI名称。除非另行说明,否则EFI名称可视为UEFI的一部分。

UEFI规范仅提供操作系统引导过程所需的信息,旨在无需对平台或操作系统进行深入定制便可在处理器规范兼容的平台上运行操作系统。UEFI规范还允许平台引入创新的特性和功能,在无需为OS引导程序重新编程的情况下增强平台功能。UEFI规范适用于从移动系统到服务器的各种硬件平台,并允许原始设备制造商具有最大的扩展性和定制能力,以实现差异化。

UEFI接口的表现形式是数据表,其中包括与平台相关的信息,以及操作系统加载器和操作系统可使用的引导服务和运行时服务。它们一起为启动操作系统提供了一个标准环境。UEFI规范设计为纯接口规范。因此,UEFI规范定义了平台固件必须实现的一组接口和结构。以下是UEFI设计的基本要素:

  • 重用现有接口表。为了让操作系统和固件中的代码可以在现有设计结构中持续使用。凡是兼容UEFI规范的处理器平台都必须遵照UEFI规范进行实现。
  • 系统分区。系统分区定义了一个独立的、可共享的分区和文件系统,这个系统分区可允许多个供应商之间安全共享数据,即使这些供应商出于不同目的去访问系统分区。
  • 引导服务。引导服务提供了在启动期间可以使用的设备和系统功能的接口。设备的访问是通过句柄(Handle)和协议(Protocol)抽象出来的。UEFI通过将基础实现隔离在规范之外,以避免给设备的访问者带来负担,进而促进现有BIOS代码的重用。
  • 运行时服务。运行时服务为操作系统提供了正常运行期间可以使用的基础平台硬件资源的接口。

Linux内核驱动之efi-rtc_第1张图片
上图描绘了UEFI的整体结构,以及UEFI、平台固件、操作系统三者之间的关系。UEFI自带引导管理器,平台固件通过引导管理器可以从UEFI定义的系统分区中加载任何文件,也可以通过UEFI定义的镜像加载服务来加载文件。
UEFI规范提供了各种海量存储设备类型,包括:磁盘、CD-ROMDVD,以及通过网络进行远程引导。而且,平台固件借助扩展协议接口可以添加其他引导媒体类型。

UEFI还定义了NVRAM变量来记录加载的文件,这些变量包含传递给应用程序的数据,以及可以在菜单中显示给用户的字符串。

一旦启动,操作系统加载程序便会继续完成操作系统的引导工作。为此,他可以使用UEFI的引导服务和接口来初始化各种平台组件和系统管理软件。在引导阶段,UEFI的运行时服务也可供操作系统加载程序使用。

1.2.1 Boot Sevices:

从操作系统加载器(OS Loader)被加载到OS Loader执行ExitBootServices()的这段时间,是从UEFI环境向操作系统过渡的过程。这个过程被称为TSLTransient System Load)。

TSL阶段,系统资源通过Boot Service管理,Boot Service提供如下服务:

  • 事件服务:事件是异步操作的基础,有了事件的支持,才可以在UEFI系统内执行并发操作;
  • 内存管理:主要提供内存的分配与释放,管理系统内存映射;
  • Protocol管理:安装与卸载Protocol的服务,以及注册Protocol通知函数的服务;
  • Protocol使用类管理:Protocol的打开关闭,查找支持Protocol的控制器;例如要读写某个PCI设备的寄存器,可以通过OpenProtocol服务打开这个设备上的PciIoProtocol,用PciIo->Io.Read()服务可以读取这个设备上的寄存器;
  • 驱动管理:包括将驱动安装到控制器的connect服务,以及将驱动从控制器上卸载的disconnect服务。例如启动时,我们需要网络支持,则可以通过LoadImage将驱动加载到内存,然后通过connect服务将驱动安装到设备;
  • Image管理:包括加载、卸载、启动和退出UEFI应用程序或驱动。
  • ExitBootServices服务:用以结束启动服务;
1.2.2. Runtime Service:
  • 时间服务:读取设定时间,读取设定系统从睡眠中唤醒的时间;
  • 读写UEFI系统变量:读取设置系统变量,例如BootOrder用于指定启动顺序,通过这些系统变量可以保存系统配置;
  • 虚拟内存服务:将物理地址转换为虚拟地址;
  • 其他服务:包括启动系统的ResetSystem
1.2.3. UEFI优点:
  • 开发效率高:BIOS开发一般采用汇编语言,代码多是硬件相关。而在UEFI中,绝大部分代码采用C语言编写,UEFI应用程序和驱动甚至可以用C++编写。UEFI通过固件-操作系统接口BSRT)为OSOS加载器屏蔽了底层硬件细节,使得UEFI上层应用可以方便重用;
  • 可扩展:UEFI可扩展性体现在两个方面:一是驱动的模块化设计;二是软硬件升级的兼容性。大部分硬件的初始化通过UEFI驱动实现,每个驱动是一个独立模块,可以包含在固件中,也可以放在设备上,运行时根据需要动态加载;UEFI中每个表、每个Protocol都有版本号,这使得系统的平滑升级变得简单;
  • UEFI系统性能:相比Legacy BIOS,系统有了很大提升,从启动到进入操作系统的时间大大缩短。性能提高源于:
    • UEFI基于time的异步操作,提高了CPU的效率,减少了等待时间。
    • UEFI舍弃了中端这种比较耗时的操作系统外部设备的方式,仅仅保留了时钟中断,外部设备的操作采用“时间+异步操作”完成;
    • 可伸缩的遍历设备的方式,启动时可以仅仅遍历启动所需要的设备,从而加速系统启动;
    • 系统安全性提高,这是UEFI的一个重要突破。当系统的安全启动设置被打开后,UEFI在执行应用程序和驱动前会先检测程序和驱动的安全证书,仅当安全证书被信任时才会执行这个应用程序或驱动。UEFI应用程序和驱动采用PE/COFF格式,其签名放在签名块中。
1.2.4. UEFI启动过程:

Linux内核驱动之efi-rtc_第2张图片
Linux内核驱动之efi-rtc_第3张图片
Linux内核驱动之efi-rtc_第4张图片
Linux内核驱动之efi-rtc_第5张图片

UEFI系统启动遵循UEFI平台初始化标准,分为7个阶段:

  • SEC PhaseSecurity,安全验证)
    开机之后,系统开始执行第一条指令,此时就已经进入了SEC阶段。这时的Memory还没有被初始化,还不可用,所以这一阶段最主要的工作就是建立一些临时的Memory,它可以是处理器的Cache,或是system Static RAM(SRAM)。并且使CPU进入Protect Mode。 另外,SEC Phase可以先天知道(Prior Knowledge)这些早期的内存被映射到得位置以及BFV(Boot Firmware Volume)的位置。
  • PEI Phase(Pre-EFI Initialization,EFI前期初始化)
    PEI阶段最主要的工作就是Memory的初始化以及一些必要的CPUChipset等等的初始化。由于这些都是没有压缩的Code,所以要求越精简越好。另外,PEI Phase还要确定系统的引导路径(Boot Path),初始化和描述最小数量的包含DXE foundationDXE Architecture ProtocolsSystem RAMfirmware volume
  • DXE PhaseDriver Execution Environment,驱动执行环境)
    DXE阶段是实现EFI的最重要的阶段,大部分的工作都是在这个阶段实现的。
  • BDS PhaseBoot Device Select,启动设备选择)
    BDS阶段的主要工作是:
    • 初始化基于环境变量ConInConOutStrErr的控制台设备。
    • 尝试去加载列在环境变量Driver####DriverOrder上的Driver
  • TSL PhaseTransient System Load,操作系统加载前期)
    Shell
  • RT PhaseRuntime,运行时)
    OS呼叫了Boot Service ExitBootService()之后,系统就进入了RT阶段。此时,DXE FoundationBoot Service都已经终止了,只有EFI Runtime ServiceEFI System Table还可以继续被使用。
  • AL( After Life)(系统灾难恢复期)。
    OS呼叫了EFI Runtime Service ResetSystem()或者是呼叫了ACPI Sleep State,系统就进入了AL阶段。 异步Event(比如SMINMI)的触发也可使系统进入AL阶段,这在ServerWorkstation上比较常见。

3个阶段是UEFI初始化阶段,DXE阶段结束后,UEFI环境已经准备好。
BDSTSL是操作系统加载器作为UEFI应用程序运行阶段,操作系统调用ExitBootServices()服务后进入RT阶段。

其中我们关注的位SECPEIDXEBDS四个阶段。

1.3 Legacy和UEFI

  • LegacyUEFI指的是系统引导方式(Legacy为传统BIOSUEFI为新式BIOS),MBRGPT指的是磁盘分区表类型。

  • 一般情况下都是Legacy+MBRUEFI+GPT这两种组合。

  • Legacy用的是8086汇编,UEFI 99%以上用CUEFIAPPDrives可以用C/C++

  • 64位的UEFI固件是64位的操作系统(少数二合一平板用32UEFI固件的可以忽略不计),Legacy16位的。

  • Legacy是直接针对底层硬件细节,UEFI通过Firmware-OS InterfaceBoot ServicesRuntime Services为操作系统和引导器屏蔽了底层硬件的细节。

  • UEFI可以扩展,大多数硬件加载UEFI的驱动模块就可以完成初始化,驱动模块可以放在固件中,也可以放在设备上,比如显卡的GOP,系统启动就自动加载。UEFI中的每个TableProtocol都有版本号,可以平滑升级。开发者可以自己根据规范开发UEFI应用程序和驱动程序。

  • UEFI基于time的异步操作,提高了CPU的效率,减少了等待时间。

  • UEFI舍弃了中断这种外部设备操作方式,仅保留了时钟中断,操作外部设备采用事件+异步操作,启动的时候按需加载外部设备。

  • UEFI有个安全启动功能,只有当程序的证书被信任才会被执行。

  • UEFI模式下启动,启动的是EFI驱动和应用程序,而且只要系统一启动,就直接是64位的了。(少数二合一平板32位的UEFI固件忽略不计)

  • 启动过程的区别:UEFI启动不需要BIOS
    Linux内核驱动之efi-rtc_第6张图片
    那么如果选择UEFI模式启动,所有的16位的MS-DOS实用程序,DOS工具包和其它的维护工具以及32位的应用程序都是无法加载和启动的。UEFI必须安装使用64位系统!
    所以在UEFI模式下,我们不能引导32位的系统。

但是呢,在Legacy模式下呢,16位的DOS工具包、32位的程序和系统、64位的都可以OK
本文只讨论原生UEFI和原生BIOS
至于带有CSM兼容模块的UEFI本身就是UEFI+BIOS的结合体,自然全兼容没话说。

1.4 GPT

GPT的意思是GUID Partition Table,即“全局唯一标识磁盘分区表”。他是另外一种更加先进新颖的磁盘组织方式,一种使用UEFI启动的磁盘组织方式。最开始是为了更好的兼容性,后来因为其更大的支持内存(mbr分区最多支持2T的磁盘),更多的兼容而被广泛使用,特别是苹果的MAC系统全部使用gpt分区。gtp不在有分区的概念,所有CDEF盘都在一段信息中存储。可以简单的理解为更先进但是使用不够广泛的技术。

GUID分区表(简称GPT。使用GUID分区表的磁盘称为GPT磁盘)与普遍使用的主引导记录(MBR)分区方案相比,GPT提供了更加灵活的磁盘分区机制。

优点是支持2TB以上的大硬盘;每个磁盘的分区个数几乎没有限制,分区大小也几乎没有限制。

1.5 MBR

MBR的意思是“主引导记录”,是IBM公司早年间提出的。它是存在于磁盘驱动器开始部分的一个特殊的启动扇区。这个扇区包含了已安装的操作系统系统信息,并用一小段代码来启动系统。如果你安装了Windows,其启动信息就放在这一段代码中——如果MBR的信息损坏或误删就不能正常启动Windows,这时候你就需要找一个引导修复软件工具来修复它就可以了。Linux系统中MBR通常会是GRUB加载器。MBR。当一台电脑启动时,它会先启动主板自带的BIOS系统,bios加载MBR,MBR再启动Windows,这就是mbr的启动过程。

硬盘一个逻辑扇区有512个字节,硬盘的第一个扇区,也就是0磁道0柱面1扇区,也就是逻辑扇区0,这个扇区就叫做主引导记录,叫MBRmaster boot record)翻译成中文就叫(明(M)白(B)人(R)),就是你得弄明白了。

MBR记录了整块磁盘的重要信息,是计算机开机后访问磁盘时所必须要读取的首个扇区。主要有三个部分:

  1. 主引导分区(Master Boot RecordMBR):主要作用是检查分区表是否正确,并且在系统硬件完成自检以后将控制权交给磁盘上的引导程序(如GNUGRUB
  2. 分区表(partition table):占据64个字节,可以对四个分区的信息进行描述,其中每个分区的信息占据16个字节
  3. 结束标志字:0x55AA,最后两个字节,是检验主引导记录是否有效的标志

下图来自Inside the Linux boot process,较为清晰的画出了MBR中各个部分的结构
Linux内核驱动之efi-rtc_第7张图片

2. UEFI到操作系统的虚拟地址转换

大家一定对计算机系统的地址空间概念有所了解。那么,UEFI的地址空间是什么样的呢?UEFI加载并把控制权交给操作系统时,地址空间又发生了什么变化呢?在这篇文章中,我们来一起探讨一下。

2.1 物理地址与虚拟地址

首先,我们来复习一下物理地址和虚拟地址的概念。

所谓物理地址,就是从CPU 发出的读写请求所引用的地址。请注意,物理地址并不一定指向内存,它有可能指向一个芯片组的功能块,也可能指向外设,甚至什么也没有指到。一个读写请求,从CPU出发,经由芯片组,桥接设备,及各种地址解码硬件配合工作,被最终“引导”到目标设备,从而完成一次读写操作。
Linux内核驱动之efi-rtc_第8张图片
所谓虚拟地址,则是程序的指令所使用的地址 ( 包括指令本身所在的地址,已及指令所读写的目标的地址 ) 。每当CPU遇到一个虚拟地址(可能是CPU从内存读入指令时,也可能是CPU按指令要求读写操作数时),CPU都会进行从虚拟地址到物理地址的转换 ( 简单起见这里只画出来页表 ) ,然后使用最终得到的物理地址发出读写请求。
Linux内核驱动之efi-rtc_第9张图片

为什么计算机设计者要引入两种不同的地址概念呢?是为了显示自己很聪明,故意把事情搞复杂吗?当然不是(我倒希望是的,这样我们就可以理直气壮地少费些脑细胞了)。事实上,对虚拟地址和物理地址加以区分,在操作系统中十分有用。因为这样可以使每段程序都有自己“固定”的虚拟地址空间,不随内存分配的调整(比如把程序在内存中移动或剔除)而变动,这个特性在一段程序被多段程序调用时显得格外重要。

此外页表所提供的保护机制,可以使操作系统更安全地管理整个内存,也为在内存紧张时将部分内存换出到磁盘提供了可能。

2.2 X86中的地址转换

X86体系结构从诞生到大放光彩,其卓越的向后兼容性起了很大作用。成也萧何,败也萧何。保证了用户一致性体验的同时,也背上了沉重的历史包袱。

X86体系架构对虚拟地址的支持有一个发展过程,最早在8086中并没有虚拟地址的概念,只有物理地址。为了在16ALU16位代码和寄存器上寻址20位地址总线(2^20=1MB)Intel想到了一个折中的办法:把内存分段,并设计了4个段寄存器,CSDSESSS,分别用于指令、数据、其它和堆栈。这样,一个完整的物理内存地址就由两部分组成,高16位的段基址和低16位的段内偏移量,当然它们有12位是重叠的,它们两部分相加在一起,才构成完整的物理地址。

计划赶不上变化,198280286诞生了,24位的地址线催生了保护模式的产生,线性地址概念和段描述符等等被启用。线性地址通过MMU转化成物理地址,即:

线性地址=>物理地址

同时实模式可以完全兼容8086的代码,保护了现有的投资。

1985年收到现代计算机体系结构的影响,80386又引入了页机制,从而引入了虚拟地址,从而地址转换又添加了一层

逻辑地址(虚拟地址)=>线性地址=>物理地址

如图
Linux内核驱动之efi-rtc_第10张图片
而地址开启页模式也要经历开启段模式的过程,过程十分繁复。幸运的是,从此以后,X86在没有加入更多的层次,只是在现有的基础上小修小补,譬如页表变大些(4k->2M->1G),加入更多保护,页表分层更多等等,这里不再细表,感兴趣的同学可以参看IA32硬件参考手册。

2.3 UEFI中的地址转换

好了,有了对虚拟地址和物理地址的了解,我们来看看UEFI的地址空间吧。UEFI并不是一个操作系统,不需要先进的进程管理,不会把已装载入内存的程序进一步移动,所以UEFI采取了最简单的地址映射机制:虚拟地址==物理地址

注:UEFI固件在resetvector后是实模式,在SEC开始就进入保护模式并打开了段,进入Flat mode。后期如果是64位的UEFI固件的话,会在PEI末期Dxeipl打开页模式。但虚拟地址==物理地址总是有效的。

这样一来,各个UEFI模块(驱动程序或者应用程序)被逐一装入内存的不同地址,共存于内存中。在每个模块装载过程中,根据装载的首地址,装载程序把这个模块内部各处对函数和数据的访问所使用的绝对地址重新改写一遍,这个过程叫做重定位(relocation)。这样,模块在自己的装载地址上就能够正确运行了。请注意,装载程序对模块中绝对地址的改写是有依据的,它的依据是模块中的一张重定位表(relocation table),这张表是模块生成时连接器自动生成的,这张表指出了这个模块中哪些地方编译器生成了对绝对地址的引用。

Linux内核驱动之efi-rtc_第11张图片
这个系统运行得很好,直到它装载了操作系统,这时操作系统就会接管机器,建立自己一整套全新的虚拟地址系统 ( 这时虚拟地址在绝大部分情况下不再等于物理地址 ),即OS会换上一套自己的页表 。这看上没有瑕疵。但是我们要考虑到,有一部分UEFI代码在操作系统运行时仍然发挥作用, 这就是UEFI Runtime Services所用到的代码(及数据)。前面已经说过,这些代码已经在UEFI环境下被重定位过,那么这些代码在操作系统环境下能否被直接调用呢?答案是否定的,因为UEFI环境下的虚拟地址很可能和操作系统环境下的虚拟地址冲突,比如,RuntimeServiceA()UEFI虚拟地址可能是0xCDEF0123,而操作系统很可能已经把0xCDEF0123映射在了另一端代码或数据上了。

如何解决这个问题呢?聪明的你一定想到了,那就是让操作系统的引导程序为UEFI Runtime Services专门指定一段虚拟地址空间,然后请UEFIUEFI Runtime Services所用到的代码根据新的地址空间再次重定位到指定的虚拟地址上。于是,这些再次重定位后的Runtime Services代码,就可以在操作系统的虚拟地址环境中继续发光发热了!请注意重定位后的代码在物理内存中的位置是不需要变的,只是虚拟地址变了。( 详情请参阅UEFI规范中SetVirtualAddressMap() )
Linux内核驱动之efi-rtc_第12张图片

我们的问题好像终于解决了:) 只是我们如果不够仔细的话,我们很可能会忽视一个细节,那就是,一个Runtime模块为操作系统所做的重定位,是否也是如它初始为UEFI所做的重定位一样,完全依赖于连接器生成的重定位表呢 ( Relocation Table )?在继续往下看之前,请先思考下 ……

好了,答案出来了,是否定的。为操作系统做重定位时,这个模块已经运行了一段时间,它的状态已经发生了一定变化,具体来说,就是各个全局变量(包括局部静态变量)的内容可能已经包含了一些新的绝对地址,比如指向了UEFI其他数据结或或新分配的内存。这些变化的内容,都是编译器和链接器无法预测的,当然也就不可能反映在重定位表中。同时,有些绝对地址在UEFI中是一个常数(如Memory Mapped IO地址,也就是设备的物理地址),对这个常数地址的访问也不会出现在重定位表中,而在操作系统环境下这个地址仍然要被访问。如果存在这些情况,为了让这些绝对地址在新的虚拟地中空间中也能指向正确的目标,UEFI Runtime Services驱动程序需要额外写一些代码,把这些指针根据操作系统的要求加以修正。(当然,基于重定位表的重定位仍然是需要的,这项工作UEFI Core会替我们做,我们的额外代码做的是重定位表做不了的事。)请参考UEFI规范和开源UEFI代码中关于VIRTUAL_ADDRESS_CHANGE eventConvertPointer()的解释和使用。

这里尤其要注意的是:

  1. 如果一个新地址指向了结构体,而结构体内还有指针成员又指向了一个新地址,那指向结构体的指针和成员指针都要修正;
  2. 不要重复修正同一个指针。比如,一个动态分配的结构体里包含了指针,而这个结构体被多个UEFI Runtime驱动共享,那么每个驱动需要修正它指向该结构体的指针,但必须且只能有一个驱动修正该结构体内部的指针。为了防止出错,建议尽量避免这样的UEFI Runtime数据共享方式。
  3. 每段被修正的地址都必须具备Runtime属性。这对于使用EfiRuntimeServicesCode, EfiRuntimeServicesData类型分配获取的内存是自然成立的。但对于常数地址,比如Memory Mapped IO 地址,需要采取特殊方法。这可以先用 gDS->GetMemorySpaceDescriptor (BaseAddress, &MemorySpaceDescriptor); 取得这段地址的属性(Attributes),然后 Attributes = MemorySpaceDescriptor.Attributes | EFI_MEMORY_RUNTIME; 最后用 gDS->SetMemorySpaceAttributes (..); 把Runtime属性设置回去。请注意考察MemorySpaceDescriptorBaseAddressLength是否覆盖了所需要的地址范围,如果不是,需要对多段地址进行Runtime属性设置,直到所需地址范围被覆盖。相关函数的详细说明,请参阅UEFI Platform Initialization Specification

地址转换的时间点如下图:
Linux内核驱动之efi-rtc_第13张图片

3. UEFI Common:UEFI 中的Runtime Services

EFI System Table 里面有两个ServicesRuntime ServicesBoot Services,其中Runtime Services 是在UEFI 兼容系统上面几乎全时可用的Services,区别于Boot Services只能在EFI_BOOT_SERVICES.ExitBootServices()之前可用的特性。
Runtime Services提供了几组有限的Services

  • Variable Services
  • Time Services
  • Virtual Memory Services
  • Miscellaneous Runtime Services

Variable Services提供了用于读写Variable的函数,具体用法比较简单,所以就不再提了。我们只看一下这些Services是怎么运作的好了。在VariableServiceInitialize这个函数里面有下面的定义:

  SystemTable->RuntimeServices->GetVariable         = VariableServiceGetVariable;
  SystemTable->RuntimeServices->GetNextVariableName = VariableServiceGetNextVariableName;
  SystemTable->RuntimeServices->SetVariable         = VariableServiceSetVariable;
  SystemTable->RuntimeServices->QueryVariableInfo   = VariableServiceQueryVariableInfo;

可以看出来GetVariableSetVariableinstance是在函数VariableServiceInitialize里面定义的。EDK默认是从0: Volatile, 1: HOB, 2: Non-Volatile.这几个区域里面搜索。当然,根据平台或者其他原因的要求,也可以在后面用自己的instanceoverride VariableServiceInitialize里面的定义。

Time Services的功能就比较单一,提供了GetTime()/SetTime()/GetWakeupTime()/SetWakeupTime()这四个Services。它们的Instance如下:

  gRT->GetTime       = PcRtcEfiGetTime;
  gRT->SetTime       = PcRtcEfiSetTime;
  gRT->GetWakeupTime = PcRtcEfiGetWakeupTime;
  gRT->SetWakeupTime = PcRtcEfiSetWakeupTime;

看一下Code就知道,这几个函数只是在操作CMOS的几个RTC 寄存器而已,所以实际中用处不是太大。或许我们可以自己加一个外部定时器,可以取代CMOSRTC,这样或者这四个Services更加有意义一点。

Virtual Memory Services这个提供下面两个Service,可以看出是给OS loader使用的,具体用法我们稍后研究:
Linux内核驱动之efi-rtc_第14张图片

4. rtc-efi注册

4.1 rtc-efi驱动模块的注册流程 (init/main.c)

[   18.273077] Call trace:
[   18.277454]  dump_backtrace+0x0/0x190
[   18.282906]  show_stack+0x14/0x20
[   18.288061]  dump_stack+0xa8/0xcc
[   18.293153]  rtc_device_register+0x164/0x1b8
[   18.299116]  devm_rtc_device_register+0x5c/0xd0
[   18.305362]  efi_rtc_probe+0x74/0xa8
[   18.310668]  platform_drv_probe+0x50/0xa0
[   18.316394]  really_probe+0x23c/0x3c8
[   18.321807]  driver_probe_device+0x64/0x130
[   18.327759]  __driver_attach+0x128/0x150
[   18.333425]  bus_for_each_dev+0x60/0x98
[   18.339045]  driver_attach+0x20/0x28
[   18.344388]  bus_add_driver+0x1d4/0x2c0
[   18.350045]  driver_register+0xc0/0x110
[   18.355681]  __platform_driver_register+0x68/0x74
[   18.362228]  __platform_driver_probe+0x88/0x128
[   18.368552]  efi_rtc_driver_init+0x20/0x28
[   18.374452]  do_one_initcall+0x30/0x19c
[   18.380120]  kernel_init_freeable+0x2bc/0x360
[   18.386307]  kernel_init+0x10/0x100
[   18.391671]  ret_from_fork+0x10/0x1

其中

kernel_init_freeable+0x1a8/0x24c 
-> do_pre_smp_initcalls 
--> do_one_initcall

看一下函数do_pre_smp_initcalls()

/*./init/main.c*/

static void __init do_pre_smp_initcalls(void)
{
	initcall_entry_t *fn;

	trace_initcall_level("early");
	for (fn = __initcall_start; fn < __initcall0_start; fn++)
		do_one_initcall(initcall_from_entry(fn));
}

其中

static initcall_entry_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

/* Keep these in sync with initcalls in include/linux/init.h */
static const char *initcall_level_names[] __initdata = {
	"pure",
	"core",
	"postcore",
	"arch",
	"subsys",
	"fs",
	"device",
	"late",
};

也就是说,内核会一次初始化这个8level的模块,其中rtc-efi模块在 __initcall6_start,也就是device这个分类中。
之所以是6是因为

#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn);
#define device_initcall(fn)  __define_initcall(fn, 6);

正常流程是:

do_one_initcall()
->efi_rtc_driver_init() 
--> platform_driver_probe()
---> device_register() 
----> driver_attach()

driver_attach中,会调用driver_match_device -- platform_match查找platform总线上的同名device,本例是rtc-efi,如果可以找到,会继续调用driver_probe_device直到efi_rtc_probe -- rtc_device_register,最终看到内核log中会打印

[   18.397103] rtc-efi rtc-efi: rtc core: registered rtc-efi as rtc0

4.2 rtc-efi driver的注册过程(driver/rtc/rtc-efi.c)

在该文件中,使用module_platform_driver_probe(efi_rtc_driver, efi_rtc_probe); efi_rtc_driver链接到initcall这个section,同时注册该驱动的probe函数,在本例中是efi_rtc_probe

include/linux/platform_device.h

/* module_platform_driver_probe() - Helper macro for drivers that don't do
 * anything special in module init/exit.  This eliminates a lot of
 * boilerplate.  Each module may only use this macro once, and
 * calling it replaces module_init() and module_exit()
 */
#define module_platform_driver_probe(__platform_driver, __platform_probe) \
static int __init __platform_driver##_init(void) \
{ \
	return platform_driver_probe(&(__platform_driver), \
				     __platform_probe);    \
} \
module_init(__platform_driver##_init); \

其中,efi_drc_driver定义如下:

/*drivers/rtc/rtc-efi.c*/
static struct platform_driver efi_rtc_driver = {
	.driver = {
		.name = "rtc-efi",
	},
};

module_platform_driver_probe(efi_rtc_driver, efi_rtc_probe);

4.3 rtc-efi device的注册过程(driver/rtc/rtc-efi-platform.c)

4.19.90版本的内核中是

static struct platform_device rtc_efi_dev = {
        .name = "rtc-efi",
        .id = -1,
};

static int __init rtc_init(void)
{
        if (efi_enabled(EFI_RUNTIME_SERVICES))
                if (platform_device_register(&rtc_efi_dev) < 0)
                        pr_err("unable to register rtc device...\n");

        /* not necessarily an error */
        return 0;
}
module_init(rtc_init);

5.10版本中则是

/*drivers/rtc/rtc-efi.c*/

static int __init efi_rtc_probe(struct platform_device *dev)
{
	struct rtc_device *rtc;
	efi_time_t eft;
	efi_time_cap_t cap;

	/* First check if the RTC is usable */
	if (efi.get_time(&eft, &cap) != EFI_SUCCESS)
		return -ENODEV;

	rtc = devm_rtc_device_register(&dev->dev, "rtc-efi", &efi_rtc_ops,
					THIS_MODULE);
	if (IS_ERR(rtc))
		return PTR_ERR(rtc);

	rtc->uie_unsupported = 1;
	platform_set_drvdata(dev, rtc);

	return 0;
}

static struct platform_driver efi_rtc_driver = {
	.driver = {
		.name = "rtc-efi",
	},
};

module_platform_driver_probe(efi_rtc_driver, efi_rtc_probe);

通过module_platform_driver_probe()函数将名为rtc-efi的设备注册到系统device链表中。

5. efi runtime services的启动

5.1 efi runtime services的启动过程

代码drivers/firmware/efi/arm-runtime.c的函数arm_enable_runtime_services在下面的代码返回:

/*
 * Enable the UEFI Runtime Services if all prerequisites are in place, i.e.,
 * non-early mapping of the UEFI system table and virtual mappings for all
 * EFI_MEMORY_RUNTIME regions.
 */
static int __init arm_enable_runtime_services(void)
{
	u64 mapsize;

	if (!efi_enabled(EFI_BOOT)) {
		pr_info("EFI services will not be available.\n");
		return 0;
	}

	efi_memmap_unmap();

	mapsize = efi.memmap.desc_size * efi.memmap.nr_map;

	if (efi_memmap_init_late(efi.memmap.phys_map, mapsize)) {
		pr_err("Failed to remap EFI memory map\n");
		return 0;
	}

	if (efi_soft_reserve_enabled()) {
		efi_memory_desc_t *md;

		for_each_efi_memory_desc(md) {
			int md_size = md->num_pages << EFI_PAGE_SHIFT;
			struct resource *res;

			if (!(md->attribute & EFI_MEMORY_SP))
				continue;

			res = kzalloc(sizeof(*res), GFP_KERNEL);
			if (WARN_ON(!res))
				break;

			res->start	= md->phys_addr;
			res->end	= md->phys_addr + md_size - 1;
			res->name	= "Soft Reserved";
			res->flags	= IORESOURCE_MEM;
			res->desc	= IORES_DESC_SOFT_RESERVED;

			insert_resource(&iomem_resource, res);
		}
	}

	if (efi_runtime_disabled()) {
		pr_info("EFI runtime services will be disabled.\n");
		return 0;
	}

	if (efi_enabled(EFI_RUNTIME_SERVICES)) {
		pr_info("EFI runtime services access via paravirt.\n");
		return 0;
	}

	pr_info("Remapping and enabling EFI services.\n");

	if (!efi_virtmap_init()) {
		pr_err("UEFI virtual mapping missing or invalid -- runtime services will not be available\n");
		return -ENOMEM;
	}

	/* Set up runtime services function pointers */
	efi_native_runtime_setup();
	set_bit(EFI_RUNTIME_SERVICES, &efi.flags);

	return 0;
}
early_initcall(arm_enable_runtime_services);

5.2 手动开启 efi runtime services

cmdline中增加efi=runtime选项,启动efi runtime services.

6. 内核配置项

6.1 CONFIG_RTC_SYSTOHC

kernel中如果定义CONFIG_RTC_SYSTOHC,则会编译systohc.c

obj-$(CONFIG_RTC_SYSTOHC)    += systohc.o

systohc.c 中只有一个函数,会在kernel 初始化的后期自动执行late_initcall(rtc_hctosys);

/*./drivers/rtc/class.c*/

#ifdef CONFIG_RTC_HCTOSYS_DEVICE
/* Result of the last RTC to system clock attempt. */
int rtc_hctosys_ret = -ENODEV;

/* IMPORTANT: the RTC only stores whole seconds. It is arbitrary
 * whether it stores the most close value or the value with partial
 * seconds truncated. However, it is important that we use it to store
 * the truncated value. This is because otherwise it is necessary,
 * in an rtc sync function, to read both xtime.tv_sec and
 * xtime.tv_nsec. On some processors (i.e. ARM), an atomic read
 * of >32bits is not possible. So storing the most close value would
 * slow down the sync API. So here we have the truncated value and
 * the best guess is to add 0.5s.
 */

static void rtc_hctosys(struct rtc_device *rtc)
{
	int err;
	struct rtc_time tm;
	struct timespec64 tv64 = {
		.tv_nsec = NSEC_PER_SEC >> 1,
	};

	err = rtc_read_time(rtc, &tm);
	if (err) {
		dev_err(rtc->dev.parent,
			"hctosys: unable to read the hardware clock\n");
		goto err_read;
	}

	tv64.tv_sec = rtc_tm_to_time64(&tm);

#if BITS_PER_LONG == 32
	if (tv64.tv_sec > INT_MAX) {
		err = -ERANGE;
		goto err_read;
	}
#endif

	err = do_settimeofday64(&tv64);

	dev_info(rtc->dev.parent, "setting system clock to %ptR UTC (%lld)\n",
		 &tm, (long long)tv64.tv_sec);

err_read:
	rtc_hctosys_ret = err;
}
#endif

这个函数最主要的作用是打开rtc driverrtc_class_open(CONFIG_RTC_HCTOSYS_DEVICE);
读取时间
rtc_read_time(rtc, &tm);
将时间写到kernel
err = do_settimeofday64(&tv64);
最后会输出log

[   22.276019] calling  rtc_hctosys+0x0/0xd8 @ 1
[   22.296125] rtc-efi rtc-efi: setting system clock to 2022-11-10 10:46:17 UTC (1668077177)
[   22.307961] initcall rtc_hctosys+0x0/0xd8 returned 0 after 23388 usecs

6.2 CONFIG_RTC_DRV_EFI

4.19.90版本内核中定义如下
如果定义CONFIG_RTC_DRV_EFI的话

./drivers/rtc/Makefile

ifdef CONFIG_RTC_DRV_EFI
rtc-core-y                      += rtc-efi-platform.o
endif

obj-$(CONFIG_RTC_DRV_EFI)       += rtc-efi.o

就会新增rtc-efi-platform.crtc-efi.c
rtc-efi-platform.c 中注册了一个platform_device_register

static struct platform_device rtc_efi_dev = {
	.name = "rtc-efi",
	.id = -1,
};

static int __init rtc_init(void)
{
	if (efi_enabled(EFI_RUNTIME_SERVICES))
		if (platform_device_register(&rtc_efi_dev) < 0)
			pr_err("unable to register rtc device...\n");

	/* not necessarily an error */
	return 0;
}
module_init(rtc_init);

rtc-efi.c 中有注册一个platform_drivername都是rtc-efi,正好匹配调用efi_rtc_probe函数

static struct platform_driver efi_rtc_driver = {
	.driver = {
		.name = "rtc-efi",
	},
};

module_platform_driver_probe(efi_rtc_driver, efi_rtc_probe);

efi_rtc_probe 中最重要是注册一个efi_rtc_ops

static int __init efi_rtc_probe(struct platform_device *dev)
{
	struct rtc_device *rtc;
	efi_time_t eft;
	efi_time_cap_t cap;

	/* First check if the RTC is usable */
	if (efi.get_time(&eft, &cap) != EFI_SUCCESS)
		return -ENODEV;

	rtc = devm_rtc_device_register(&dev->dev, "rtc-efi", &efi_rtc_ops,
					THIS_MODULE);
	if (IS_ERR(rtc))
		return PTR_ERR(rtc);

	rtc->uie_unsupported = 1;
	platform_set_drvdata(dev, rtc);

	return 0;
}

efi_rtc_ops 提供了如下四个函数

static const struct rtc_class_ops efi_rtc_ops = {
	.read_time	= efi_read_time,
	.set_time	= efi_set_time,
	.read_alarm	= efi_read_alarm,
	.set_alarm	= efi_set_alarm,
	.proc		= efi_procfs,
};

我们以efi_read_time为例

static int efi_read_time(struct device *dev, struct rtc_time *tm)
{
	efi_status_t status;
	efi_time_t eft;
	efi_time_cap_t cap;

	status = efi.get_time(&eft, &cap);

	if (status != EFI_SUCCESS) {
		/* should never happen */
		dev_err(dev, "can't read time\n");
		return -EINVAL;
	}

	if (!convert_from_efi_time(&eft, tm))
		return -EIO;

	return 0;
}

原来是调用efi.get_time
efi实在drivers/firmware/efi/runtime-wrappers.c中赋值的

void efi_native_runtime_setup(void)
{
	efi.get_time = virt_efi_get_time;
	efi.set_time = virt_efi_set_time;
	efi.get_wakeup_time = virt_efi_get_wakeup_time;
	efi.set_wakeup_time = virt_efi_set_wakeup_time;
	efi.get_variable = virt_efi_get_variable;
	efi.get_next_variable = virt_efi_get_next_variable;
	efi.set_variable = virt_efi_set_variable;
	efi.set_variable_nonblocking = virt_efi_set_variable_nonblocking;
	efi.get_next_high_mono_count = virt_efi_get_next_high_mono_count;
	efi.reset_system = virt_efi_reset_system;
	efi.query_variable_info = virt_efi_query_variable_info;
	efi.query_variable_info_nonblocking = virt_efi_query_variable_info_nonblocking;
	efi.update_capsule = virt_efi_update_capsule;
	efi.query_capsule_caps = virt_efi_query_capsule_caps;
}

这样就调用到uefiruntime service里面去了

static efi_status_t virt_efi_get_time(efi_time_t *tm, efi_time_cap_t *tc)
{
	efi_status_t status;

	if (down_interruptible(&efi_runtime_lock))
		return EFI_ABORTED;
	status = efi_queue_work(GET_TIME, tm, tc, NULL, NULL, NULL);
	up(&efi_runtime_lock);
	return status;
}

7. kernel 如何调用uefi的runtime service

目前上游已经通过patch:3eb420e70d879ce0e6bf752accf5cdedb0a59de8

From 3eb420e70d879ce0e6bf752accf5cdedb0a59de8 Mon Sep 17 00:00:00 2001
From: Sai Praneeth <sai.praneeth.prakhya@intel.com>
Date: Wed, 11 Jul 2018 11:40:35 +0200
Subject: [PATCH] efi: Use a work queue to invoke EFI Runtime Services

Presently, when a user process requests the kernel to execute any
UEFI runtime service, the kernel temporarily switches to a separate
set of page tables that describe the virtual mapping of the UEFI
runtime services regions in memory. Since UEFI runtime services are
typically invoked with interrupts enabled, any code that may be called
during this time, will have an incorrect view of the process's address
space. Although it is unusual for code running in interrupt context to
make assumptions about the process context it runs in, there are cases
(such as the perf subsystem taking samples) where this causes problems.

So let's set up a work queue for calling UEFI runtime services, so that
the actual calls are made when the work queue items are dispatched by a
work queue worker running in a separate kernel thread. Such threads are
not expected to have userland mappings in the first place, and so the
additional mappings created for the UEFI runtime services can never
clash with any.

The ResetSystem() runtime service is not covered by the work queue
handling, since it is not expected to return, and may be called at a
time when the kernel is torn down to the point where we cannot expect
work queues to still be operational.

The non-blocking variants of SetVariable() and QueryVariableInfo()
are also excluded: these are intended to be used from atomic context,
which obviously rules out waiting for a completion to be signalled by
another thread. Note that these variants are currently only used for
UEFI runtime services calls that occur very early in the boot, and
for ones that occur in critical conditions, e.g., to flush kernel logs
to UEFI variables via efi-pstore.

因此后期通过为UEFI运行时服务设置一个工作队列,从而内核调用这个工作队列调用UEFI运行时服务。
drivers/firmware/efi/runtime-wrappers.c 中会建立kernel调用uefiruntime service

/*
 * Enable the UEFI Runtime Services if all prerequisites are in place, i.e.,
 * non-early mapping of the UEFI system table and virtual mappings for all
 * EFI_MEMORY_RUNTIME regions.
 */
static int __init arm_enable_runtime_services(void)
{
	u64 mapsize;

	if (!efi_enabled(EFI_BOOT)) {
		pr_info("EFI services will not be available.\n");
		return 0;
	}

	efi_memmap_unmap();

	mapsize = efi.memmap.desc_size * efi.memmap.nr_map;

	if (efi_memmap_init_late(efi.memmap.phys_map, mapsize)) {
		pr_err("Failed to remap EFI memory map\n");
		return 0;
	}

	if (efi_soft_reserve_enabled()) {
		efi_memory_desc_t *md;

		for_each_efi_memory_desc(md) {
			int md_size = md->num_pages << EFI_PAGE_SHIFT;
			struct resource *res;

			if (!(md->attribute & EFI_MEMORY_SP))
				continue;

			res = kzalloc(sizeof(*res), GFP_KERNEL);
			if (WARN_ON(!res))
				break;

			res->start	= md->phys_addr;
			res->end	= md->phys_addr + md_size - 1;
			res->name	= "Soft Reserved";
			res->flags	= IORESOURCE_MEM;
			res->desc	= IORES_DESC_SOFT_RESERVED;

			insert_resource(&iomem_resource, res);
		}
	}

	if (efi_runtime_disabled()) {
		pr_info("EFI runtime services will be disabled.\n");
		return 0;
	}

	if (efi_enabled(EFI_RUNTIME_SERVICES)) {
		pr_info("EFI runtime services access via paravirt.\n");
		return 0;
	}

	pr_info("Remapping and enabling EFI services.\n");

	if (!efi_virtmap_init()) {
		pr_err("UEFI virtual mapping missing or invalid -- runtime services will not be available\n");
		return -ENOMEM;
	}

	/* Set up runtime services function pointers */
	efi_native_runtime_setup();
	set_bit(EFI_RUNTIME_SERVICES, &efi.flags);

	return 0;
}
early_initcall(arm_enable_runtime_services);

可见是通过early_initcall自动调用arm_enable_runtime_services

void efi_native_runtime_setup(void)
{
	efi.get_time = virt_efi_get_time;
	efi.set_time = virt_efi_set_time;
	efi.get_wakeup_time = virt_efi_get_wakeup_time;
	efi.set_wakeup_time = virt_efi_set_wakeup_time;
	efi.get_variable = virt_efi_get_variable;
	efi.get_next_variable = virt_efi_get_next_variable;
	efi.set_variable = virt_efi_set_variable;
	efi.set_variable_nonblocking = virt_efi_set_variable_nonblocking;
	efi.get_next_high_mono_count = virt_efi_get_next_high_mono_count;
	efi.reset_system = virt_efi_reset_system;
	efi.query_variable_info = virt_efi_query_variable_info;
	efi.query_variable_info_nonblocking = virt_efi_query_variable_info_nonblocking;
	efi.update_capsule = virt_efi_update_capsule;
	efi.query_capsule_caps = virt_efi_query_capsule_caps;
}

我们以virt_efi_get_time为例

/*
 * Calls the appropriate efi_runtime_service() with the appropriate
 * arguments.
 *
 * Semantics followed by efi_call_rts() to understand efi_runtime_work:
 * 1. If argument was a pointer, recast it from void pointer to original
 * pointer type.
 * 2. If argument was a value, recast it from void pointer to original
 * pointer type and dereference it.
 */
static void efi_call_rts(struct work_struct *work)
{
	void *arg1, *arg2, *arg3, *arg4, *arg5;
	efi_status_t status = EFI_NOT_FOUND;

	arg1 = efi_rts_work.arg1;
	arg2 = efi_rts_work.arg2;
	arg3 = efi_rts_work.arg3;
	arg4 = efi_rts_work.arg4;
	arg5 = efi_rts_work.arg5;

	switch (efi_rts_work.efi_rts_id) {
	case EFI_GET_TIME:
		status = efi_call_virt(get_time, (efi_time_t *)arg1,
				       (efi_time_cap_t *)arg2);
		break;
	case EFI_SET_TIME:
		status = efi_call_virt(set_time, (efi_time_t *)arg1);
		break;
	case EFI_GET_WAKEUP_TIME:
		status = efi_call_virt(get_wakeup_time, (efi_bool_t *)arg1,
				       (efi_bool_t *)arg2, (efi_time_t *)arg3);
		break;
	case EFI_SET_WAKEUP_TIME:
		status = efi_call_virt(set_wakeup_time, *(efi_bool_t *)arg1,
				       (efi_time_t *)arg2);
		break;
	case EFI_GET_VARIABLE:
		status = efi_call_virt(get_variable, (efi_char16_t *)arg1,
				       (efi_guid_t *)arg2, (u32 *)arg3,
				       (unsigned long *)arg4, (void *)arg5);
		break;
	case EFI_GET_NEXT_VARIABLE:
		status = efi_call_virt(get_next_variable, (unsigned long *)arg1,
				       (efi_char16_t *)arg2,
				       (efi_guid_t *)arg3);
		break;
	case EFI_SET_VARIABLE:
		status = efi_call_virt(set_variable, (efi_char16_t *)arg1,
				       (efi_guid_t *)arg2, *(u32 *)arg3,
				       *(unsigned long *)arg4, (void *)arg5);
		break;
	case EFI_QUERY_VARIABLE_INFO:
		status = efi_call_virt(query_variable_info, *(u32 *)arg1,
				       (u64 *)arg2, (u64 *)arg3, (u64 *)arg4);
		break;
	case EFI_GET_NEXT_HIGH_MONO_COUNT:
		status = efi_call_virt(get_next_high_mono_count, (u32 *)arg1);
		break;
	case EFI_UPDATE_CAPSULE:
		status = efi_call_virt(update_capsule,
				       (efi_capsule_header_t **)arg1,
				       *(unsigned long *)arg2,
				       *(unsigned long *)arg3);
		break;
	case EFI_QUERY_CAPSULE_CAPS:
		status = efi_call_virt(query_capsule_caps,
				       (efi_capsule_header_t **)arg1,
				       *(unsigned long *)arg2, (u64 *)arg3,
				       (int *)arg4);
		break;
	default:
		/*
		 * Ideally, we should never reach here because a caller of this
		 * function should have put the right efi_runtime_service()
		 * function identifier into efi_rts_work->efi_rts_id
		 */
		pr_err("Requested executing invalid EFI Runtime Service.\n");
	}
	efi_rts_work.status = status;
	complete(&efi_rts_work.efi_rts_comp);
}


static efi_status_t virt_efi_get_time(efi_time_t *tm, efi_time_cap_t *tc)
{
	efi_status_t status;

	if (down_interruptible(&efi_runtime_lock))
		return EFI_ABORTED;
	status = efi_queue_work(GET_TIME, tm, tc, NULL, NULL, NULL);
	up(&efi_runtime_lock);
	return status;
}

efi_call_virt定义如下:

#define arch_efi_call_virt(p, f, args...)				\
({									\
	efi_##f##_t *__f;						\
	__f = p->f;							\
	__f(args);							\
})


/*
 * Arch code can implement the following three template macros, avoiding
 * reptition for the void/non-void return cases of {__,}efi_call_virt():
 *
 *  * arch_efi_call_virt_setup()
 *
 *    Sets up the environment for the call (e.g. switching page tables,
 *    allowing kernel-mode use of floating point, if required).
 *
 *  * arch_efi_call_virt()
 *
 *    Performs the call. The last expression in the macro must be the call
 *    itself, allowing the logic to be shared by the void and non-void
 *    cases.
 *
 *  * arch_efi_call_virt_teardown()
 *
 *    Restores the usual kernel environment once the call has returned.
 */

#define efi_call_virt_pointer(p, f, args...)				\
({									\
	efi_status_t __s;						\
	unsigned long __flags;						\
									\
	arch_efi_call_virt_setup();					\
									\
	__flags = efi_call_virt_save_flags();				\
	__s = arch_efi_call_virt(p, f, args);				\
	efi_call_virt_check_flags(__flags, __stringify(f));		\
									\
	arch_efi_call_virt_teardown();					\
									\
	__s;								\
})


#define efi_call_virt(f, args...)   \
	efi_call_virt_pointer(efi.runtime, f, args)
[    0.002085] Remapping and enabling EFI services.
[    0.002367] lyl drivers/firmware/efi/arm-runtime.c:arm_enable_runtime_services:150 runtime_setup start 
[    0.002369] lyl drivers/firmware/efi/arm-runtime.c:arm_enable_runtime_services:152 runtime_setup end 
[    0.002372] initcall arm_enable_runtime_services+0x0/0x2c8 returned 0 after 0 usecs

替换掉宏定义最终还是调用efi.runtime->get_time(tm,tc)

refer to:

  1. rtc问题引起的内核驱动初始化的探究
  2. CONFIG_RTC_SYSTOHC
  3. CONFIG_RTC_DRV_EFI
  4. kernel 如何调用uefi的runtime service
  5. UEFI Common:UEFI 中的Runtime Services
  6. UEFI Common:Event 简单介绍和用法
  7. BIOS接班人EFI基础知识介绍
  8. UEFI开发探索102 – ACPI探究01(UEFI配置表)
  9. UEFI Boot Flow
  10. Legacy和UEFI,MBR和GPT的区别
  11. UEFI Boot Flow系列之 概述
  12. UEFI Boot Flow 系列之 SEC Phase
  13. UEFI Boot Flow 系列之 PEI Phase
  14. UEFI Boot Flow 系列之 DXE Phase
  15. UEFI Boot Flow 系列之 BDS Phase
  16. UEFI简介
  17. UEFI与BIOS概述
  18. UEFI 固件启动 Gentoo EFI Stub Kernel
  19. 使用UEFI启动的计算机,其整个开机流程是怎么样的?
  20. Linux Kernel initcall机制
  21. 驱动加载的本质

你可能感兴趣的:(Linux,kernel,linux)