高通平台UEFI学习笔记
文章目录
前言
作为一个嵌入式软件开发工程师,如果不懂UEFI实在说不过去,因此决定接下来一段时间把这块骨头啃了。
一、UEFI概述
UEFI是一种规范,定义了操作系统与平台固件之间衔接的软件接口。
UEFI启动流程
按照调用栈的形式,UEFI有如下几个启动阶段
//安全启动阶段,主要以汇编代码为主
SEC
//EFI前期初始化
//加载PEIM模块,主要包括CPU初始化,芯片初始化,主板初始化
PEI
//驱动执行环境
//遍历固件中所有的Driver,执行所有的Driver,主要包括设备、总线、驱动与服务
//通常我们的UEFI开发工作,也就是在这一阶段执行
DXE
//加载启动项
BDS
//交接系统控制权权给操作系统加载器
TSL
//这一阶段UEFI只剩下run time service,大部分控制权被操作系统接管
RT
//操作系统挂了,就有可能进入这一阶段,用于灾难恢复
AL
Module与Package基本概念
UEFI根目录下,会有很多以*Pkg
命名的文件夹。通常会以这样一个文件夹作为一个Package
。Package
的一般目录形式如下
Package/
declaration.dec
description.dsc
flashDescriptionFiles.fdf
Module/
source.c
head.h
information.inf
.inf
文件类似于makefile
, 描述了要编译的源文件
.dsc
文件,描述了要添加的.inf
文件
.dec
文件,作为声明文件,可以供其他的模块使用。
使用build
命令编译一个Package
,可以生成*.efi
文件
使用GenFW
命令,可以制作Rom Image
值得注意的是,不管是哪个阶段的驱动或者应用,都是使用上面的目录结构进行编写的。只不过在模块的.inf文件中,需要在MODULE_TYPE字段中,指明这是一个什么模块。
组成
在高通平台下,UEFI由两部分组成:
XBL Core
,包含与芯片相关的protocols, drivers, applications. 比如charging应用. XBL的代码位于non-HLOS boot_imagesABL
,包含与芯片无关的applications, 比如fastboot. ABL是Android开源代码树的一部分。ABL的代码位于Android boottable bootloader.
二、Handle&Protocol模型
1.总线、设备、驱动模型
在linux操作系统中,采用了总线、设备、驱动模型,设备挂载在总线上。
总线遍历每一个设备时,会遍历所有已安装的驱动,如果设备与驱动匹配,则进行驱动程序的运行。
系统安装驱动时,会遍历总线上所有的已存在的设备,如果驱动与设备匹配,则进行驱动程序的运行。
2.Protocol
在UEFI中,Protocol
是一种与UEFI驱动通信的接口,每一个Protocol
都有一个唯一的GUID编号,每一个协议使用一个struct
结构体表示,其中成员函数(c语言的函数指针)表明了协议拥有的方法。
在UEFI使用handle
表示一个设备的Control
,熟悉linux开发环境的同学应该很容易理解,通过这个handle就可以操作具体的设备。
Protocol的经典使用流程如下:
/* 根据protocol GUID找到所有使用该协议的handle */
gBS->LocateHandleBuffer();
/* 对于某一个handle,获取该handle的protocol实例 */
gBS->OpenProtocol();
/* 和上面的函数一样,不过是一个简化了某些常用入参的函数封装,底层也是OpenProtocol */
gBS->HandleProtocol();
/* 通过获取到的protocol实例,调用protocol中的方法,与设备通信 */
protocol->method();
可以将protocol看作UEFI中的系统调用。
UEFI驱动
UEFI中的驱动和linux不太一样,它并没有抽象为一个具体的数据结构。
通常,编写一个设备驱动需要实现三部分。
EFI_DRIVER_BINDING_PROTOCOL
,这是一个UEFI内置的结构体,我们需要实现它的supported, start, stop函数。在模块入口函数中,把这个数据结构进行注册,系统就可以使用这一个Protocol了。EFI_COMPONENT_NAME_PROTOCOL
,这也是一个UEFI内置的结构体,与EFI_DRIVER_BINDING_PROTOCOL
在模块入口函数中同时注册。这样系统就可以使用获取设备名称的Protocol了。- 在
EFI_DRIVER_BINDING_PROTOCOL
的start函数中,实现私有的Protocol
的注册,这个Protocol
是我们自定义的驱动内容,具体驱动自定义功能就在这里实现。
可以这样认为,一个UEFI驱动,至少包含以上三个protocol。当然也可以提供更多的protocol,所以说驱动是一个抽象概念,在UEFI中并没有实际的数据结构。
UEFI驱动与服务型驱动的对比。
- 服务型驱动直接在Image入口函数中注册,长期存在于内存当中,只有当模块卸载之后,才会将驱动卸载。
- UEFI驱动则是在Image入口函数中先注册一个
EFI_DRIVER_BINDING_PROTOCOL
。在它的start函数中进行具体Protocol的注册,这个注册其实就是一个服务型驱动。同时在EFI_DRIVER_BINDING_PROTOCOL
的supported与stop中提供了支持与卸载功能。因此,从代码的角度来看,服务型驱动实际上是UEFI驱动的一部分。
三、系统表
在linux系统中,用户空间通过系统调用来使用内核空间提供的系统服务。
在UEFI中,UEFI应用程序则通过系统表来使用内核空间提供的服务。
上面用到的gBS
就是启动服务表。另外一张系统表则是运行时服务表。
系统表的使用
系统表是在DXE阶段被初始化的,因此在这个阶段之后才能够使用。
在模块入口函数,可以使用入口函数的入参SystemTable
访问系统表,入参ImageHandle
则可以访问系统固件镜像。在其他函数中,则可以使用gST
, gBS
, 来访问系统表,gImageHandle
则可以访问ImageHandle
,这是通过镜像构造函数来赋值实现的。
UEFI中,由于所有程序都运行在RING0
优先级,因此用户空间和内核空间,实际上是一个地址空间,可以通过指针地址直接访问。
四、操作系统加载器
通常,每个UEFI系统至少有一个ESP(EFI System Partition)分区,在这个分区上,存放了启动文件。
UEFI不仅具有读写硬盘的能力,同时也具有读写文件的能力。
操作系统加载器是以文件的形式存放在ESP分区内。
总结
UEFI其实有点像单片机编程,但是UEFI将整个系统执行过程分为了固定的几个阶段,用户自定义的需要,则需要根据实际情况放在固定的阶段中。在单片机编程中,也可以仿照这种编程风格来作为系统框架。