UEFI介绍

初步介绍

  • UEFI的全称是Unified Extensible Firmware Interface即统一可扩展固件接口。它是用来定义操作系统与系统固件之间的软件界面,是作为BIOS的替代方案,可扩展固件接口负责加电自检、联系操作系统以及提供连接操作系统与硬件的接口。
  • UEFI最初被称为EFI,最初是Intel公司开发,英特尔已于2005年将此规范格式交由UEFI论坛来推广与发展,这样对于EFI的标准就有了一个组织进行统一,所以EFI后来被更名为UEFI
  • 固件是机器中软件与硬件的中间层面,固件其实是一个软件,这个软件一般是存储在非易失性的ROM或者Flash上。

image-20250318222015853

  • UEFI其实就相当于一个管理固件的小型操作系统,这个操作系统向上为真正的操作系统,比如WindowsLinux,这些提供服务接口,向下又与真正的系统硬件、固件等进行交互。UEFI是用来取代BIOS的,所以UEFI会具有如下与BIOS相同的功能:

    • 硬件初始化:在计算机启动时,UEFI 负责对 CPU、内存、显卡、存储设备、网络等硬件进行初始化和配置。
    • 执行电源自检(Power-On Self Test,POST)
    • 初始化 CPU 和内存控制器
    • 检测并配置 PCIe 设备、USB 控制器等
    • 直接引导操作系统,无需引导扇区。
  • 除此之外UEFI还有其他BIOS所没有的功能:

    • UEFI Shell:UEFI还提供了shell环境,我们可以通过shell命令来查看已识别的磁盘和设备
    • 提供驱动程序接口:这些驱动以.efi文件形式存在,支持热拔插、动态加载等特性
    • 具有用户界面(GUI)
    • 能更安全的进行启动
  • 接下来我们再进一步理解UEFI这个名称的具体由来,以及它取代BIOS的原因:

    • 在早期,各个硬件的厂商都有自己的BIOS来管理或者自检自己生产的主板等硬件,这就使得BIOS的类型太多,并且各个BIOS也不兼容。
    • 这时UEFI就对这些厂商的设计进行了一定的规范,各个厂商在设计主板的时候都遵循这个规范,并且让UEFI取代BIOS,作为自检的固件。并且UEFI还兼容不同的架构,即x86x86_64ARM,这就使得底层的设计变得稍微轻松一点。这就是UEFIU(统一)的由来。
    • 早期使用BIOS的时候,固件的功能已经固定了,如果我们要添加新的固件,比如新添加一个网卡,这时我们就需要更新整个BIOS,使得BIOS增加新网卡的这一固件,这样上层操作系统才能够与新的固件网卡交互。
    • UEFI的出现就使得我们添加新固件后并不需要再更新UEFI,这时我们如果新添加一个SSD,就无需更新UEFI,它可以将SSD中的.efi程序加载进来,实现灵活扩展的功能。这就是E(可扩展的由来)
    • F也就是Firmware(固件)的由来是UEFI本身就存储在ROMFlash中,并且是一个软件,所以就被称为固件。
    • I就是Interface(接口):也就是UEFI为操作系统提供了统一的可以访问底层硬件的方式。
  • 接下来我们再具体介绍一下UEFI,当我们计算机还没有开机的时候,架构是这样的,此时我们主板的FIRMWARE其实也就是UEFI,然后Loaderkernel被放入到磁盘格式为FAT32的文件中

  • 但是主板的UEFI 固件只认识efi格式的文件,加载不了exe或者elf文件(下文都使用elf文件),即我们的内核kernel就没办法加载到内存中。

image-20250322080144179

  • 这时我们就需要使用LoaderLoader这个程序起到承上启下的作用,它会解析kernel这个elf文件格式,并对其进行加载。而Loader则是一个efi格式并且会保存在特定位置,这时计算机启动的时候主板的UEFI 固件就能识别并运行Loader,然后Loader又可以将Kernal加载进内存当中。

image-20250322080904885

  • Loader所处的特定位置,要求位于启动设备的FAT32格式分区,一个U盘或者其他东西,只要是FAT32的格式,在启动的时候就会被计算机识别,并标明UEFI的字样,说明固件发现了这个磁盘格式。

image-20250322081055653

  • 当我们启动时,选择了该位置则计算机就会在这里去寻找Loader,计算机会按照/EFI/Boot/BootX64.efi这个路径去寻找Loader
  • 一般这个FAT32分区,都位于磁盘最开始,大小在100M左右

image-20250322081351126

  • 总结:

    • UEFI固件会将是FAT32格式分区的磁盘(一个磁盘可能有多个格式分区),都当做启动磁盘,并将这些发现的磁盘都添加到开机菜单中
    • 接下去我们就可以选择一个启动磁盘,如果是带UEFI前缀,就去搜索固定路径。
  • 而在加载kernal中又会出现如下问题:

    • kernalFAT32分区应该如何读取?
    • kernal以何种方式载入内存,应该放入内存的什么位置?
    • kernal放入的内存是空闲的吗?
    • kernal应该采取段式内存载入还是进行内存分页呢?
    • 这些问题如果没有统一的API,就会使得Loader的开发相当复杂,所以UEFI存在的目的不仅仅是加载Loader,还为开发者创造一个统一、便捷的启动环境
    • 并且对于UEFI其提供的API,不仅仅是提供给Loader,还提供给了运行时的操作系统,例如操作系统是通过UEFI提供的API来获取物理内存的大小。

image-20250322082707983

关于开发文档

  • 我们可以去这个网站下载关于UEFI的一些开发规范:Specifications | Unified Extensible Firmware Interface Forum

  • 在访问这个网站的时候我们还会看到一个固件规范:ACPI,这个固件和UEFI一起密切协作,共同负责系统启动、硬件管理和电源控制。不过这里主要是对UEFI的开发。特别地:休眠状态与ACPI这个固件关系比较大

  • 在这个网站中有UEFI规范UEFI Shell规范UEFI平台初始化规范,但是我们只使用UEFI规范,其他两个在这边并不需要使用。这边视频教程中使用的是2.10版本的规范,这边我就也使用2.10版本的规范(还比较新,没旧到哪里去)。网址在这里:UEFI_Spec_2_10_A_Aug8.pdf (SECURED)

image-20250318230452368

image-20250318230537594

image-20250318230611045

环境与工具

  • 首先要明确一点:UEFI的制定很大程度上借鉴了Microsoft,它的编写规范似乎遵循了Microsoft的编程习惯,并且UEFI规定需要使用PE可执行文件。所以我们一般是在windows上对UEFI进行开发,或者也可以在Linux下,使用交叉编译工具对UEFI进行编译。
  • 这个内容将会使用gcc交叉编译,并且使用make来构建编译脚本,然后使用qemu模拟运行它,然后使用OVMF充当固件,之后会使用工具创建一个GPT磁盘镜像(EFI规范的镜像格式),用于存放EFI应用。之后可以使用模拟器运行EFI也可以将镜像装入U盘在真实机中运行UEFI(这一步就不做了)。

编译器安装

  • gcc或者mingw或者clang,这里gcc需要的是交叉编译的gcc即能编译出PE文件的gcc。
1
2
sudo apt update
sudo apt install gcc-mingw-w64-x86-64
  • 安装好后查看是否安装成功
1
x86_64-w64-mingw32-gcc --version

自动化构建工具

  • 使用make这一自动化构建工具,通过制作make文件,使得make调用编译器去编译。安装如下:
1
2
sudo apt update
sudo apt install make
  • 安装好后检查是否安装成功
1
make --version

image-20250319075928359

虚拟化运行工具

  • 安装qemu,使用qemu运行我们所编译好的UEFI,这样我们就可以查看我们所编写的UEFI的具体功能和效果。
1
2
sudo apt install qemu-system-x86_64
// 如果这个命令安装不成功就查看其他安装命令
  • 然后使用命令,查看qemu是否安装成功,可以使用Ctrl+Alt+加号放大终端,也可以使用Ctrl+ALT+减号缩小终端。
1
qemu-system-x86_64

image-20250319080921754

UEFI模拟文件

  • 安装ovmf,UEFI模拟文件,在虚拟机中ovmf充当UEFI/BIOS的功能,在虚拟机启动的时候完成对应的功能。这个文件存储在/usr/share/ovmf这个目录下。

image-20250319082516863

image-20250319082826695

  • 下载后解压文件夹就会看到这个.fd文件,然后将这个文件复制到/usr/share/ovmf这个文件夹中。

image-20250319082931780

  • 我们使用qemu先运行一下这个fd文件看看真实效果,这时我们会发现我们进入了UEFI Shell
1
qemu-system-x86_64 -bios /usr/share/ovmf/OVMF-pure-efi.fd -net none 

image-20250319083327576

  • 之后我们要将这个OVMF-pure-efi.fd移动到这个目录下,并重新命名为bios64.bin
1
/home/myheart/program/my_uefi
  • 之后我们制作好了BOOTX64.EFI ,使用qemu启动时,bios64.bin使用的就是OVMF-pure-efi.fd
1
2
3
4
5
6
7
8
9
#!/bin/sh
qemu-system-x86_64 \
-drive format=raw,unit=0,file=test.hdd \
-bios ../bios64.bin \
-m 256M \
-vga std \
-name TESTOS \
-machine q35 \
-net none

UEFI_GPT镜像生成器

1
git clone https://github.com/queso-fuego/UEFI-GPT-image-creator.git
  • 拉取到本地文件夹后,就可以使用cd命令进入该文件夹,在该文件夹中会看到.sh文件,这是自动编译该镜像生成器的命令,在windos上可以使用.bat文件实现自动编译。编译后就会出现write_gpt这个文件

image-20250319084345386

U盘(不详细说明)

  • U盘(可选):用于测试真实硬件环境是否可以启动UEFI

Hello_world

  • 这里简单介绍一下EFI应用的一个例子,初步编写一个在终端上显示Hello_worldUEFI固件。我们只需要两个文件分别为efi.cefi.h
  • 然后我们要使用make对这两个C语言文件进行交叉编译。
  • 可以将这些代码敲一遍,也可以直接复制,建议还是敲一遍。

efi.h编写

  • 我们先来看efi.h的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include <uchar.h>
#include <stdint.h>

typedef uint16_t UINT16;
typedef uint32_t UINT32;
typedef uint64_t UINT64;
typedef uint64_t UINTN;
typedef char16_t CHAR16;
typedef void VOID;

typedef UINTN EFI_STATUS;
typedef VOID* EFI_HANDLE;

#define IN
#define OUT
#define OPTIONAL
#define CONST const

#define EFIAPI __attribute__((ms_abi))

#define EFI_SUCCESS 0

typedef struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL
EFI_SIMPLE_TEXT_INPUT_PROTOCOL;

typedef struct{
UINT16 ScanCode;
CHAR16 UnicodeChar;
} EFI_INPUT_KEY;

typedef
EFI_STATUS
(EFIAPI *EFI_INPUT_READ_KEY) (
IN EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
OUT EFI_INPUT_KEY *Key
);

typedef struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL {
void* Reset;
EFI_INPUT_READ_KEY ReadKeyStroke;
void* WaitForKey;
} EFI_SIMPLE_TEXT_INPUT_PROTOCOL;

typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

#define EFI_BLACK 0x00
#define EFI_BLUE 0x01
#define EFI_GREEN 0x02
#define EFI_CYAN 0x03
#define EFI_RED 0x04
#define EFI_YELLOW 0x0E
#define EFI_WHITE 0x0F

#define EFI_TEXT_ATTR(Foreground,Background) \
((Foreground) | ((Background) << 4))

typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_STRING) (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
IN CHAR16 *String
);


typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_SET_ATTRIBUTE) (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
IN UINTN Attribute
);

typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_CLEAR_SCREEN) (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This
);

typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
void * Reset;
EFI_TEXT_STRING OutputString;
void * TestStringl;
void * QueryMode;
void * SetMode;
EFI_TEXT_SET_ATTRIBUTE SetAttribute;
EFI_TEXT_CLEAR_SCREEN ClearScreen;
void * SetCursorPosition;
void * EnableCursor;
void * Mode;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef enum {
EfiResetColde,
EfiResetWarm,
EfiResetShutdown,
EfiResetPlatformSpecific
} EFI_RESET_TYPE;

typedef
VOID
(EFIAPI *EFI_RESET_SYSTEM) (
IN EFI_RESET_TYPE ResetType,
IN EFI_STATUS ResetStatus,
IN UINTN DataSize,
IN VOID *ResetData OPTIONAL
);


typedef struct {
UINT64 Signature;
UINT32 Revision;
UINT32 HeaderSize;
UINT32 CRC32;
UINT32 Reserved;
} EFI_TABLE_HEADER;

typedef struct {
EFI_TABLE_HEADER Hdr;
void* GetTime;
void* SetTime;
void* GetWakeupTime;
void* SetWakeupTime;

void* SetVirtualAddressMap;
void* ConvertPointer;

void* GetVariable;
void* GetNextVariableName;
void* SetVariable;

void* GetNextHighMonotonicCount;
EFI_RESET_SYSTEM ResetSystem;

void* UpdateCapsule;
void* QueryCapsuleCapabilities;

void* QueryVariableInfo;
} EFI_RUNTIME_SERVICES;

typedef struct {
EFI_TABLE_HEADER Hdr;

void* FirmwareVendor;
UINT32 FirmwareRevision;
void* ConsoleInHandle;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn;
void* ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
void* StandardErrorHandle;
void* StdErr;
EFI_RUNTIME_SERVICES *RuntimeServices;
void* BootServices;
UINTN NumberOfTableEntries;
void* ConfigurationTable;
} EFI_SYSTEM_TABLE;

efi.c编写

  • 然后再查看efi.c这个代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "efi.h"
EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
(void)ImageHandle;

SystemTable->ConOut->SetAttribute(SystemTable->ConOut,
EFI_TEXT_ATTR(EFI_YELLOW,EFI_GREEN));

SystemTable->ConOut->ClearScreen(SystemTable->ConOut);

SystemTable->ConOut->OutputString(SystemTable->ConOut, u"Hello, World!\r\n\r\n");

SystemTable->ConOut->SetAttribute(SystemTable->ConOut,
EFI_TEXT_ATTR(EFI_RED,EFI_BLACK));
SystemTable->ConOut->OutputString(SystemTable->ConOut,
u"Press any key to shutdown...");
EFI_INPUT_KEY key;
while (SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn,&key) != EFI_SUCCESS)
;
SystemTable->RuntimeServices->ResetSystem(EfiResetShutdown, EFI_SUCCESS, 0, NULL);

return EFI_SUCCESS;
}

makefile编写

  • 编写好着两个代码后,我们就可以创建一个makefile,向该文件写入如下内容,其中--subsystem,10这个参数需要在MinGW 链接器版本在v2.36版本及以上才能使用:
1
2
3
4
5
6
7
8
9
10
11
12
gcc:
x86_64-w64-mingw32-gcc efi.c \
-std=c17 \
-Wall \
-Wextra \
-Wpedantic \
-mno-red-zone \
-ffreestanding \
-nostdlib \
-Wl,--subsystem,10 \
-e efi_main \
-o BOOTX64.EFI

其他步骤

  • 编译好后我们就会生成一个BOOTX64.EFI这个PE文件

image-20250319231748420

  • 然后我们将使用cp命令,将BOOTX64.EFI这个文件复制一份到UEFI-GPT-image-creator这个目录下
1
cp ./BOOTX64.EFI ./UEFI-GPT-image-creator/
  • 然后我们进入到UEFI-GPT-image-creator文件下,使用./write_gpt,就可以将BOOTX64.EFI写入到GPT格式的磁盘中。

image-20250319232020910

  • 之后我们就使用sh脚本,运行qemu来模拟UEFI的运行
1
2
3
4
5
6
7
8
9
#!/bin/sh
qemu-system-x86_64 \
-drive format=raw,unit=0,file=test.hdd \
-bios ../bios64.bin \
-m 256M \
-vga std \
-name TESTOS \
-machine q35 \
-net none
  • 运行后具体效果如下:

image-20250319232130065

efi.h和efi.c详解

  • 按顺序我们应该先搞清楚efi.h这个文件中的内容,然后再搞清楚efi.c中的内容。