从我们按下开机键到进入到操作系统之前的系统初始化动作,即是BIOS run
的过程。如今操作系统已经从枯燥的文本时代演化到丰富多彩的图形界面,而BIOS
却一直延续着枯燥的过程,BIOS
设置也一直是单调的蓝底白字格式。
BIOS
的坚持出于两个原因:
BIOS
基本能够满足市场需求;BIOS
的设计使得BIOS
的升级和扩增变得非常困难。随着64
位CPU
逐渐取代32
位CPU
,BIOS
越来越不能满足市场需求,这使得UEFI
作为BIOS
的替代者应运而生。
BIOS
全称“基本输入输出系统”(Basic Input/Output System
),它是存储子主板ROM
里的一组代码。
其主要功能是在计算机上电时对硬件进行初始化配置,并将硬件操作封装为BIOS
中断服务。这样,各种硬件间的差异便由BIOS
负责维护,程序直接调用BIOS
中断服务即可实现对硬件的控制。以下是BIOS
的主要组成部分:
BIOS
中断向量等。CMOS
设置程序,负责读写保存在CMOS
中的系统设置信息。BIOS
代码使用汇编开发,开发效率低;汇编开发的另一个缺点是使得代码与设备的耦合程度太高,代码受硬件变化的影响大;BIOS
基本输入输出服务需要通过中断来完成,开销大,并且BIOS
没有提供异步工作模式,大量时间消耗在等待时间上;BIOS
代码采用静态链接,增加硬件功能时,必须将16
位代码放置在0xC0000~0xDFFFF
区间,初始化时将其设置为约定的中断处理程序。而且BIOS
没有提供动态加载设备驱动的方案;BIOS
运行过程中对可执行代码没有安全方面的考虑;2TB
以上的地址引导:受限于BIOS
硬盘的寻址方式,BIOS
硬盘采用32
位地址,因而引导扇区的最大逻辑块地址是2
的32
次方(2TB
)。在系统上电后,CPU
运行于实模式工作环境中,数据位宽为16
位,最大物理地址寻址范围是0~1MB
,其中的物理地址0x0C0000~0x0FFFFF
保留给BIOS
使用。开机后,CPU
首先跳转到物理地址0xFFFFFFF0
处执行程序。一般情况下,这里是一条跳转指令,CPU
通过执行此处的跳转指令跳转到真正的BIOS
入口地址处执行,BIOS POST
阶段执行完后,BIOS
会将控制权交给引导程序,最终引导进入操作系统。以下是BIOS
的启动流程:
BIOS
代码首先做的是POST
(Power On Self Test
,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与CMOS
中的设置一致。如果发现硬件错误,则通过喇叭报警。CPU
和内存并显示检测结果。I/O
端口和DMA
通道等资源。CMOS
中。BIOS
中断将设备的引导程序读入内存。UEFI
全称“统一可扩展固件接口”(Unified Extensible Firmware Interface
),定义了操作系统和平台固件之间的接口,它是UEFI Forum
发布的一种标准。它是一种标准,没有提供实现。其实现由其他公司或开源组织提供。如Intel
公司提供的开源UEFI
实现TianoCore
和Phoenix
公司提供的SecureCore Tiano
。
UEFI
发端于20
世纪90
年代中期的安腾处理器。相对于当时流行的IA32
(Intel Architecture 32
)系统,安腾是一种全新的64
位系统,BIOS
的限制对于这种64
位系统变得不可接受。1998
年Intel
发起了Intel Boot Initiate
项目,后来更名为EFI
。2005
年,Intel
联手微软、AMD
、联想等11
家公司成立了Unified EFI Forum
,负责制定统一的EFI
标准。第一个UEFI
标准–UEFI2.0
在2006
年发布。
UEFI
提供给系统的的接口包括启动服务(Boot Services
)和运行时服务(Runtime Services
)。
UEFI
规范描述了操作系统和平台固件之间的接口,其目的是为操作系统和平台固件定义一种通信方法。UEFI
的前身是EFI
(Extensible Firmware Interface
,可扩展固件接口)规范1.10
。因此,一些代码和协议仍保留EFI
名称。除非另行说明,否则EFI
名称可视为UEFI
的一部分。
UEFI
规范仅提供操作系统引导过程所需的信息,旨在无需对平台或操作系统进行深入定制便可在处理器规范兼容的平台上运行操作系统。UEFI
规范还允许平台引入创新的特性和功能,在无需为OS
引导程序重新编程的情况下增强平台功能。UEFI
规范适用于从移动系统到服务器的各种硬件平台,并允许原始设备制造商具有最大的扩展性和定制能力,以实现差异化。
UEFI
接口的表现形式是数据表,其中包括与平台相关的信息,以及操作系统加载器和操作系统可使用的引导服务和运行时服务。它们一起为启动操作系统提供了一个标准环境。UEFI
规范设计为纯接口规范。因此,UEFI
规范定义了平台固件必须实现的一组接口和结构。以下是UEFI
设计的基本要素:
UEFI
规范的处理器平台都必须遵照UEFI
规范进行实现。Handle
)和协议(Protocol
)抽象出来的。UEFI
通过将基础实现隔离在规范之外,以避免给设备的访问者带来负担,进而促进现有BIOS
代码的重用。
上图描绘了UEFI
的整体结构,以及UEFI
、平台固件、操作系统三者之间的关系。UEFI
自带引导管理器,平台固件通过引导管理器可以从UEFI
定义的系统分区中加载任何文件,也可以通过UEFI
定义的镜像加载服务来加载文件。
UEFI
规范提供了各种海量存储设备类型,包括:磁盘、CD-ROM
和DVD
,以及通过网络进行远程引导。而且,平台固件借助扩展协议接口可以添加其他引导媒体类型。
UEFI
还定义了NVRAM
变量来记录加载的文件,这些变量包含传递给应用程序的数据,以及可以在菜单中显示给用户的字符串。
一旦启动,操作系统加载程序便会继续完成操作系统的引导工作。为此,他可以使用UEFI
的引导服务和接口来初始化各种平台组件和系统管理软件。在引导阶段,UEFI
的运行时服务也可供操作系统加载程序使用。
从操作系统加载器(OS Loader
)被加载到OS Loader
执行ExitBootServices()
的这段时间,是从UEFI
环境向操作系统过渡的过程。这个过程被称为TSL
(Transient System Load
)。
在TSL
阶段,系统资源通过Boot Service
管理,Boot Service
提供如下服务:
Protocol
管理:安装与卸载Protocol
的服务,以及注册Protocol
通知函数的服务;Protocol
使用类管理:Protocol
的打开关闭,查找支持Protocol
的控制器;例如要读写某个PCI
设备的寄存器,可以通过OpenProtocol
服务打开这个设备上的PciIoProtocol
,用PciIo->Io.Read()
服务可以读取这个设备上的寄存器;connect
服务,以及将驱动从控制器上卸载的disconnect
服务。例如启动时,我们需要网络支持,则可以通过LoadImage
将驱动加载到内存,然后通过connect
服务将驱动安装到设备;Image
管理:包括加载、卸载、启动和退出UEFI
应用程序或驱动。ExitBootServices
服务:用以结束启动服务;UEFI
系统变量:读取设置系统变量,例如BootOrder
用于指定启动顺序,通过这些系统变量可以保存系统配置;ResetSystem
;BIOS
开发一般采用汇编语言,代码多是硬件相关。而在UEFI
中,绝大部分代码采用C
语言编写,UEFI
应用程序和驱动甚至可以用C++
编写。UEFI
通过固件-操作系统接口
(BS
、RT
)为OS
和OS
加载器屏蔽了底层硬件细节,使得UEFI
上层应用可以方便重用;UEFI
系统性能:相比Legacy BIOS
,系统有了很大提升,从启动到进入操作系统的时间大大缩短。性能提高源于:
UEFI
基于time
的异步操作,提高了CPU
的效率,减少了等待时间。UEFI
舍弃了中端这种比较耗时的操作系统外部设备的方式,仅仅保留了时钟中断,外部设备的操作采用“时间+异步操作”完成;UEFI
的一个重要突破。当系统的安全启动设置被打开后,UEFI
在执行应用程序和驱动前会先检测程序和驱动的安全证书,仅当安全证书被信任时才会执行这个应用程序或驱动。UEFI
应用程序和驱动采用PE/COFF
格式,其签名放在签名块中。UEFI
系统启动遵循UEFI
平台初始化标准,分为7
个阶段:
SEC Phase
(Security
,安全验证)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
的初始化以及一些必要的CPU
、Chipset
等等的初始化。由于这些都是没有压缩的Code
,所以要求越精简越好。另外,PEI Phase
还要确定系统的引导路径(Boot Path
),初始化和描述最小数量的包含DXE foundation
和DXE Architecture Protocols
的System RAM
及firmware volume
。DXE Phase
(Driver Execution Environment
,驱动执行环境)DXE
阶段是实现EFI
的最重要的阶段,大部分的工作都是在这个阶段实现的。BDS Phase
(Boot Device Select
,启动设备选择)BDS
阶段的主要工作是:
ConIn
、ConOut
、StrErr
的控制台设备。Driver####
和DriverOrder
上的Driver
。TSL Phase
(Transient System Load
,操作系统加载前期)Shell
RT Phase
(Runtime
,运行时)OS
呼叫了Boot Service ExitBootService()
之后,系统就进入了RT
阶段。此时,DXE Foundation
和Boot Service
都已经终止了,只有EFI Runtime Service
和EFI System Table
还可以继续被使用。AL( After Life)
(系统灾难恢复期)。OS
呼叫了EFI Runtime Service ResetSystem()
或者是呼叫了ACPI Sleep State
,系统就进入了AL
阶段。 异步Event
(比如SMI
、NMI
)的触发也可使系统进入AL
阶段,这在Server
和Workstation
上比较常见。前3
个阶段是UEFI
初始化阶段,DXE
阶段结束后,UEFI
环境已经准备好。
BDS
和TSL
是操作系统加载器作为UEFI
应用程序运行阶段,操作系统调用ExitBootServices()
服务后进入RT
阶段。
其中我们关注的位SEC
、PEI
、DXE
、BDS
四个阶段。
Legacy
和UEFI
指的是系统引导方式(Legacy
为传统BIOS
,UEFI
为新式BIOS
),MBR
和GPT
指的是磁盘分区表类型。
一般情况下都是Legacy+MBR
, UEFI+GPT
这两种组合。
Legacy
用的是8086
汇编,UEFI
99%
以上用C
,UEFI
的APP
和Drives
可以用C/C++
。
64
位的UEFI
固件是64
位的操作系统(少数二合一平板用32
位UEFI
固件的可以忽略不计),Legacy
是16
位的。
Legacy
是直接针对底层硬件细节,UEFI
通过Firmware-OS Interface
、Boot Services
、Runtime Services
为操作系统和引导器屏蔽了底层硬件的细节。
UEFI
可以扩展,大多数硬件加载UEFI
的驱动模块就可以完成初始化,驱动模块可以放在固件中,也可以放在设备上,比如显卡的GOP
,系统启动就自动加载。UEFI
中的每个Table
和Protocol
都有版本号,可以平滑升级。开发者可以自己根据规范开发UEFI
应用程序和驱动程序。
UEFI
基于time
的异步操作,提高了CPU
的效率,减少了等待时间。
UEFI
舍弃了中断这种外部设备操作方式,仅保留了时钟中断,操作外部设备采用事件+异步
操作,启动的时候按需加载外部设备。
UEFI
有个安全启动功能,只有当程序的证书被信任才会被执行。
在UEFI
模式下启动,启动的是EFI
驱动和应用程序,而且只要系统一启动,就直接是64
位的了。(少数二合一平板32
位的UEFI
固件忽略不计)
启动过程的区别:UEFI
启动不需要BIOS
那么如果选择UEFI
模式启动,所有的16
位的MS-DOS
实用程序,DOS
工具包和其它的维护工具以及32
位的应用程序都是无法加载和启动的。UEFI
必须安装使用64
位系统!
所以在UEFI
模式下,我们不能引导32
位的系统。
但是呢,在Legacy
模式下呢,16
位的DOS
工具包、32
位的程序和系统、64
位的都可以OK
。
本文只讨论原生UEFI
和原生BIOS
。
至于带有CSM
兼容模块的UEFI
本身就是UEFI+BIOS
的结合体,自然全兼容没话说。
GPT
的意思是GUID Partition Table
,即“全局唯一标识磁盘分区表”。他是另外一种更加先进新颖的磁盘组织方式,一种使用UEFI
启动的磁盘组织方式。最开始是为了更好的兼容性,后来因为其更大的支持内存(mbr
分区最多支持2T的
磁盘),更多的兼容而被广泛使用,特别是苹果的MAC
系统全部使用gpt
分区。gtp
不在有分区的概念,所有CDEF
盘都在一段信息中存储。可以简单的理解为更先进但是使用不够广泛的技术。
GUID
分区表(简称GPT
。使用GUID
分区表的磁盘称为GPT
磁盘)与普遍使用的主引导记录(MBR
)分区方案相比,GPT
提供了更加灵活的磁盘分区机制。
优点是支持2TB
以上的大硬盘;每个磁盘的分区个数几乎没有限制,分区大小也几乎没有限制。
MBR
的意思是“主引导记录”,是IBM
公司早年间提出的。它是存在于磁盘驱动器开始部分的一个特殊的启动扇区。这个扇区包含了已安装的操作系统系统信息,并用一小段代码来启动系统。如果你安装了Windows
,其启动信息就放在这一段代码中——如果MBR
的信息损坏或误删就不能正常启动Windows
,这时候你就需要找一个引导修复软件工具来修复它就可以了。Linux
系统中MBR
通常会是GRUB
加载器。MBR
。当一台电脑启动时,它会先启动主板自带的BIOS
系统,bios
加载MB
R,MBR
再启动Windows
,这就是mbr
的启动过程。
硬盘一个逻辑扇区有512
个字节,硬盘的第一个扇区,也就是0
磁道0
柱面1
扇区,也就是逻辑扇区0
,这个扇区就叫做主引导记录,叫MBR
(master boot record
)翻译成中文就叫(明(M
)白(B
)人(R
)),就是你得弄明白了。
MBR
记录了整块磁盘的重要信息,是计算机开机后访问磁盘时所必须要读取的首个扇区。主要有三个部分:
Master Boot Record
,MBR
):主要作用是检查分区表是否正确,并且在系统硬件完成自检以后将控制权交给磁盘上的引导程序(如GNU
,GRUB
)partition table
):占据64
个字节,可以对四个分区的信息进行描述,其中每个分区的信息占据16
个字节0x55AA
,最后两个字节,是检验主引导记录是否有效的标志下图来自Inside the Linux boot process,较为清晰的画出了MBR
中各个部分的结构
大家一定对计算机系统的地址空间概念有所了解。那么,UEFI
的地址空间是什么样的呢?UEFI
加载并把控制权交给操作系统时,地址空间又发生了什么变化呢?在这篇文章中,我们来一起探讨一下。
首先,我们来复习一下物理地址和虚拟地址的概念。
所谓物理地址,就是从CPU
发出的读写请求所引用的地址。请注意,物理地址并不一定指向内存,它有可能指向一个芯片组的功能块,也可能指向外设,甚至什么也没有指到。一个读写请求,从CPU
出发,经由芯片组,桥接设备,及各种地址解码硬件配合工作,被最终“引导”到目标设备,从而完成一次读写操作。
所谓虚拟地址,则是程序的指令所使用的地址 ( 包括指令本身所在的地址,已及指令所读写的目标的地址 ) 。每当CPU
遇到一个虚拟地址(可能是CPU
从内存读入指令时,也可能是CPU
按指令要求读写操作数时),CPU
都会进行从虚拟地址到物理地址的转换 ( 简单起见这里只画出来页表 ) ,然后使用最终得到的物理地址发出读写请求。
为什么计算机设计者要引入两种不同的地址概念呢?是为了显示自己很聪明,故意把事情搞复杂吗?当然不是(我倒希望是的,这样我们就可以理直气壮地少费些脑细胞了)。事实上,对虚拟地址和物理地址加以区分,在操作系统中十分有用。因为这样可以使每段程序都有自己“固定”的虚拟地址空间,不随内存分配的调整(比如把程序在内存中移动或剔除)而变动,这个特性在一段程序被多段程序调用时显得格外重要。
此外页表所提供的保护机制,可以使操作系统更安全地管理整个内存,也为在内存紧张时将部分内存换出到磁盘提供了可能。
X86体系结构从诞生到大放光彩,其卓越的向后兼容性起了很大作用。成也萧何,败也萧何。保证了用户一致性体验的同时,也背上了沉重的历史包袱。
X86
体系架构对虚拟地址的支持有一个发展过程,最早在8086
中并没有虚拟地址的概念,只有物理地址。为了在16
位ALU
和16
位代码和寄存器上寻址20
位地址总线(2^20=1MB)
,Intel
想到了一个折中的办法:把内存分段,并设计了4
个段寄存器,CS
,DS
,ES
和SS
,分别用于指令、数据、其它和堆栈。这样,一个完整的物理内存地址就由两部分组成,高16
位的段基址和低16
位的段内偏移量,当然它们有12
位是重叠的,它们两部分相加在一起,才构成完整的物理地址。
计划赶不上变化,1982
年80286
诞生了,24
位的地址线催生了保护模式的产生,线性地址概念和段描述符等等被启用。线性地址通过MMU
转化成物理地址,即:
线性地址=>物理地址
同时实模式可以完全兼容8086
的代码,保护了现有的投资。
1985
年收到现代计算机体系结构的影响,80386
又引入了页机制,从而引入了虚拟地址,从而地址转换又添加了一层
逻辑地址(虚拟地址)=>线性地址=>物理地址
如图
而地址开启页模式也要经历开启段模式的过程,过程十分繁复。幸运的是,从此以后,X86
在没有加入更多的层次,只是在现有的基础上小修小补,譬如页表变大些(4k->2M->1G
),加入更多保护,页表分层更多等等,这里不再细表,感兴趣的同学可以参看IA32
硬件参考手册。
好了,有了对虚拟地址和物理地址的了解,我们来看看UEFI
的地址空间吧。UEFI
并不是一个操作系统,不需要先进的进程管理,不会把已装载入内存的程序进一步移动,所以UEFI
采取了最简单的地址映射机制:虚拟地址==物理地址
。
注:
UEFI
固件在resetvector
后是实模式,在SEC
开始就进入保护模式并打开了段,进入Flat mode
。后期如果是64
位的UEFI
固件的话,会在PEI
末期Dxeipl
打开页模式。但虚拟地址==物理地址总是有效的。
这样一来,各个UEFI
模块(驱动程序或者应用程序)被逐一装入内存的不同地址,共存于内存中。在每个模块装载过程中,根据装载的首地址,装载程序把这个模块内部各处对函数和数据的访问所使用的绝对地址重新改写一遍,这个过程叫做重定位(relocation
)。这样,模块在自己的装载地址上就能够正确运行了。请注意,装载程序对模块中绝对地址的改写是有依据的,它的依据是模块中的一张重定位表(relocation table
),这张表是模块生成时连接器自动生成的,这张表指出了这个模块中哪些地方编译器生成了对绝对地址的引用。
这个系统运行得很好,直到它装载了操作系统,这时操作系统就会接管机器,建立自己一整套全新的虚拟地址系统 ( 这时虚拟地址在绝大部分情况下不再等于物理地址 ),即OS
会换上一套自己的页表 。这看上没有瑕疵。但是我们要考虑到,有一部分UEFI
代码在操作系统运行时仍然发挥作用, 这就是UEFI Runtime Services
所用到的代码(及数据)。前面已经说过,这些代码已经在UEFI
环境下被重定位过,那么这些代码在操作系统环境下能否被直接调用呢?答案是否定的,因为UEFI
环境下的虚拟地址很可能和操作系统环境下的虚拟地址冲突,比如,RuntimeServiceA()
的UEFI
虚拟地址可能是0xCDEF0123
,而操作系统很可能已经把0xCDEF0123
映射在了另一端代码或数据上了。
如何解决这个问题呢?聪明的你一定想到了,那就是让操作系统的引导程序为UEFI Runtime Services
专门指定一段虚拟地址空间,然后请UEFI
把UEFI Runtime Services
所用到的代码根据新的地址空间再次重定位到指定的虚拟地址上。于是,这些再次重定位后的Runtime Services
代码,就可以在操作系统的虚拟地址环境中继续发光发热了!请注意重定位后的代码在物理内存中的位置是不需要变的,只是虚拟地址变了。( 详情请参阅UEFI
规范中SetVirtualAddressMap()
)
我们的问题好像终于解决了:) 只是我们如果不够仔细的话,我们很可能会忽视一个细节,那就是,一个Runtime
模块为操作系统所做的重定位,是否也是如它初始为UEFI
所做的重定位一样,完全依赖于连接器生成的重定位表呢 ( Relocation Table
)?在继续往下看之前,请先思考下 ……
好了,答案出来了,是否定的。为操作系统做重定位时,这个模块已经运行了一段时间,它的状态已经发生了一定变化,具体来说,就是各个全局变量(包括局部静态变量)的内容可能已经包含了一些新的绝对地址,比如指向了UEFI
其他数据结或或新分配的内存。这些变化的内容,都是编译器和链接器无法预测的,当然也就不可能反映在重定位表中。同时,有些绝对地址在UEFI
中是一个常数(如Memory Mapped IO
地址,也就是设备的物理地址),对这个常数地址的访问也不会出现在重定位表中,而在操作系统环境下这个地址仍然要被访问。如果存在这些情况,为了让这些绝对地址在新的虚拟地中空间中也能指向正确的目标,UEFI Runtime Services
驱动程序需要额外写一些代码,把这些指针根据操作系统的要求加以修正。(当然,基于重定位表的重定位仍然是需要的,这项工作UEFI Core
会替我们做,我们的额外代码做的是重定位表做不了的事。)请参考UEFI
规范和开源UEFI
代码中关于VIRTUAL_ADDRESS_CHANGE event
和ConvertPointer()
的解释和使用。
这里尤其要注意的是:
UEFI Runtime
驱动共享,那么每个驱动需要修正它指向该结构体的指针,但必须且只能有一个驱动修正该结构体内部的指针。为了防止出错,建议尽量避免这样的UEFI Runtime
数据共享方式。Runtime
属性。这对于使用EfiRuntimeServicesCode
, EfiRuntimeServicesData
类型分配获取的内存是自然成立的。但对于常数地址,比如Memory Mapped IO
地址,需要采取特殊方法。这可以先用 gDS->GetMemorySpaceDescriptor (BaseAddress, &MemorySpaceDescriptor)
; 取得这段地址的属性(Attributes
),然后 Attributes = MemorySpaceDescriptor.Attributes | EFI_MEMORY_RUNTIME
; 最后用 gDS->SetMemorySpaceAttributes (..)
; 把Runtime
属性设置回去。请注意考察MemorySpaceDescriptor
的BaseAddress
和Length
是否覆盖了所需要的地址范围,如果不是,需要对多段地址进行Runtime
属性设置,直到所需地址范围被覆盖。相关函数的详细说明,请参阅UEFI Platform Initialization Specification
。EFI System Table
里面有两个Services
:Runtime Services
和Boot 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;
可以看出来GetVariable
和SetVariable
的instance
是在函数VariableServiceInitialize
里面定义的。EDK
默认是从0: Volatile
, 1: HOB
, 2: Non-Volatile.
这几个区域里面搜索。当然,根据平台或者其他原因的要求,也可以在后面用自己的instance
去override VariableServiceInitialize
里面的定义。
Time Services
的功能就比较单一,提供了GetTime()/SetTime()/GetWakeupTime()/SetWakeupTime()
这四个Services
。它们的Instance
如下:
gRT->GetTime = PcRtcEfiGetTime;
gRT->SetTime = PcRtcEfiSetTime;
gRT->GetWakeupTime = PcRtcEfiGetWakeupTime;
gRT->SetWakeupTime = PcRtcEfiSetWakeupTime;
看一下Code
就知道,这几个函数只是在操作CMOS
的几个RTC
寄存器而已,所以实际中用处不是太大。或许我们可以自己加一个外部定时器,可以取代CMOS
的RTC
,这样或者这四个Services
更加有意义一点。
Virtual Memory Services
这个提供下面两个Service
,可以看出是给OS loader
使用的,具体用法我们稍后研究:
[ 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",
};
也就是说,内核会一次初始化这个8
个level
的模块,其中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
在该文件中,使用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.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
链表中。
代码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);
在cmdline
中增加efi=runtime
选项,启动efi runtime services
.
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 driver
,rtc_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
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.c
和 rtc-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_driver
。name
都是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;
}
这样就调用到uefi
的runtime 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;
}
目前上游已经通过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
调用uefi
的runtime 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: