在上一篇文章中我们简单介绍了UEFI的基本概念在本章中我们介绍uefi-rs库的内存管理和文件系统使用
文章目录
-
基本结构
-
UEFI的HelloWorld!
-
使用QEMU启动
-
基本的数据结构
-
Result
-
改造Result
- 内存管理
-
内存分配的注意事项
* 内存分配的关键点
* 基本数据结构 -
MemoryType
* AllocateType
* MemoryAttribute1
2
31 * AllocatePool基本使用
2 * AllocatePages基本使用
3- Protocol
-
SimpleFileSystem Protocol
-
基本数据结构
* 遍历文件夹内容
* 读取文件内容 -
下一步要做什么
基本结构
uefi-rs中基本的结构已经画成脑图的形式
uefi-rs中主要分为以下内容
-
信息类: 固件的信息,UEFI信息,uefi配置表
-
服务类: 在uefi-rs中主要包含运行时服务,启动服务,退出启动服务等
-
Protocol(协议): 在uefi-rs中支持以下协议,所有的Protocol需要使用BootServer.local_protocol::<ProtocolName>();来获取(ProtocolName表示Protocol名称,例如BootServer.local_protocol::<Input>())(以上都是伪代码)
-
标准输入Input
- 标准输出Output
- 图形输出协议GOP(Grahpics Output Protocol)
- 串行IO设备访问Serial
- 调试服务DebugSupport
- 镜像加载LoadImage
- 文件系统SimpleFileSystem
- 多处理器服务MPService
-
其他服务: 内存分配等
UEFI的HelloWorld!
我们创建好项目后再Cargo.toml中添加如下内容
1
2
3
4
5
6
7
8
9
10
11
12 1[dependencies.uefi]
2version="0.4.0"
3features = ['exts']
4
5[dependencies.uefi-services]
6version = "0.2.0"
7features = ["qemu"]
8
9[dependencies.log]
10version = "0.4.8"
11
12
然后我们在main.rs中添加几个feature
1
2
3
4
5
6
7
8
9 1// main.rs
2#![no_std]
3#![no_main]
4#![feature(asm)]
5#![feature(abi_efiapi)] // uefi-rs使用的是efi调用约定
6#![feature(never_type)]
7extern crate uefi;
8
9
然后定义UEFI的入口函数
1
2
3
4 1#[entry]
2fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{}
3
4
其中#[entry]在uefi-marcos中定义主要功能是将我们定义的函数转化为pub extern "efiapi" fn eif_main这样的形式
程序进入main函数后的第一件事情就是uefi服务进入DXE(Driver Execution Environment)阶段
1
2
3
4
5
6
7
8
9
10
11
12 1#[entry]
2fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{
3 if let Err(e) = uefi_services::init(&st).log_warning(){
4 info!("{:?}",e);
5 // 如果服务初始化失败后则不能继续执行
6 return e.status();
7 }
8 info!("Hello World!");
9 Status::SUCCESS
10}
11
12
我们编写完毕代码后再项目的根目录(与src目录同级)创建.cargo文件夹,随后创建config文件并填写一下内容(rust已经支持了x86_64-unknown-uefi编译目标)
1
2
3
4
5 1# build settings
2[build]
3target = "x86_64-unknown-uefi"
4
5
这样我们在运行时不需要指定–target参数,否则每次运行时需要添加,例如
1
2
3 1cargo xbuild --target x86_64-unknown-uefi
2
3
Cargo代理
如果连接到cargo.io比较慢可以添加国内代理
1
2
3
4
5
6
7
8
9 1//in .cargo/config
2[source.crates-io]
3registry = "https://github.com/rust-lang/crates.io-index"
4replace-with = 'ustc'
5
6[source.ustc]
7registry = "http://mirrors.ustc.edu.cn/crates.io-index"
8
9
这样当我们运行cargo xbuild后会在target/debug目录中找到xxx.efi文件(xxx表示起的项目名称)
使用QEMU启动
当编译完毕后我们需要创建一个目录esp/EFI/Boot然后将我们编译的xxx.efi命名成BootX64.efi
目录结构如下
1
2
3
4
5
6
7
8
9
10
11
12 1project
2 ├── .cargo
3 ├── src
4 ├── target
5 ├── Cargo.toml
6 ├── Cargo.lock
7 └──esp
8 └── EFI
9 └── Boot
10 └──BootX64.efi
11
12
esp目录是我们需要挂载的文件目录
因为我们使用的是虚拟机,我们需要使用OVMF(开放虚拟机固件Open Virtual Machine Firmware)OVMF的制作请参考使用Rust开发操作系统(UEFI基本介绍)的OVMF制作章节,然后为QEMU指定OVMF_CODE.fd路径,OVMF_VARS.fd的路径,以及我们创建的esp文件夹路径
例如(为了方便阅读对命令进行了换行)
1
2
3
4
5
6 1qemu-system-x86_64
2-drive if=pflash,format=raw,file=<OVMF_CODE.fd的路径>,readonly=on
3-drive if=pflash,format=raw,file=<OVMF_VARS.fd的路径>,readonly=on
4-drive format=raw,file=fat:rw:<esp文件路径>
5
6
以下是参考命令(windows)
1
2
3
4
5
6 1qemu-system-x86_64
2-drive if=pflash,format=raw,file=C:\Users\VenmoSnake\Desktop\ReboxOS\OVMF_CODE.fd,readonly=on
3-drive if=pflash,format=raw,file=C:\Users\VenmoSnake\Desktop\OS\OVMF_VARS.fd,readonly=on
4-drive format=raw,file=fat:rw:C:\Users\VenmoSnake\Desktop\OS\esp
5
6
这样我们可以在QEMU上运行我们编写的efi
基本的数据结构
Result
1
2
3 1pub type Result<Output = (), ErrData = ()> = core::result::Result<Completion<Output>, Error<ErrData>>;
2
3
在uefi-rs中Result的定义有些许不同而且使用方式跟通常用法也不同,因此我们要具体说明一下
我们可以看到一个名叫Completion的数据结构其定义如下
1
2
3
4
5
6
7
8 1#[must_use]
2#[derive(Clone, Copy, Debug, PartialEq)]
3pub struct Completion<T> {
4 status: Status,
5 result: T,
6}
7
8
Completion用于表示UEFI服务操作完成后的结果,但在此过程中可能会遇到一些非致命问题,其中的Status表示执行的结果状态,定义如下
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 1newtype_enum! {
2 #[must_use]
3 pub enum Status: usize => {
4 /// 操作执行成果
5 SUCCESS = 0,
6
7 /// 该字符串包含无法呈显示的字符,并被跳过。 UEFI使用的是UCS-2编码
8 WARN_UNKNOWN_GLYPH = 1,
9 /// handle已关闭,但未删除文件。
10 WARN_DELETE_FAILURE = 2,
11 /// handle已关闭,但文件内容没有正确刷新。
12 WARN_WRITE_FAILURE = 3,
13 /// 指定的缓冲区太小,数据被截断。
14 WARN_BUFFER_TOO_SMALL = 4,
15 /// The data has not been updated within the timeframe set by local policy.
16 /// 数据尚没有在指定的时间范围内更新。(时间范围在本地策略中设置)
17 WARN_STALE_DATA = 5,
18 /// 结果缓冲区包含符合UEFI的文件系统。
19 WARN_FILE_SYSTEM = 6,
20 /// 系统重置后将处理该操作。
21 WARN_RESET_REQUIRED = 7,
22
23 /// 镜像读取失败
24 LOAD_ERROR = ERROR_BIT | 1,
25 /// 参数不正确
26 INVALID_PARAMETER = ERROR_BIT | 2,
27 /// 不支持的操作
28 UNSUPPORTED = ERROR_BIT | 3,
29 /// 缓冲区的大小不符合要求。
30 BAD_BUFFER_SIZE = ERROR_BIT | 4,
31 /// 缓冲区的大小不足以容纳请求的数据。或者表示在参数中应该返回所需的缓冲区大小。
32 BUFFER_TOO_SMALL = ERROR_BIT | 5,
33 /// 没有数据返回。
34 NOT_READY = ERROR_BIT | 6,
35 /// 物理设备在尝试操作时出错。
36 DEVICE_ERROR = ERROR_BIT | 7,
37 /// 该设备不能执行写操作。
38 WRITE_PROTECTED = ERROR_BIT | 8,
39 /// 该资源已用完。
40 OUT_OF_RESOURCES = ERROR_BIT | 9,
41 /// 在文件系统上检测到不一致。
42 VOLUME_CORRUPTED = ERROR_BIT | 10,
43 /// 文件系统上没有足够的空间。
44 VOLUME_FULL = ERROR_BIT | 11,
45 /// 设备不包含任何执行操作的介质.
46 NO_MEDIA = ERROR_BIT | 12,
47 /// 自上次访问以来,设备中的介质已更改.
48 MEDIA_CHANGED = ERROR_BIT | 13,
49 /// 找不到该项。
50 NOT_FOUND = ERROR_BIT | 14,
51 /// 访问被拒绝.
52 ACCESS_DENIED = ERROR_BIT | 15,
53 /// 找不到服务器或未响应请求.
54 NO_RESPONSE = ERROR_BIT | 16,
55 /// 设备的映射不存在.
56 NO_MAPPING = ERROR_BIT | 17,
57 /// 超时.
58 TIMEOUT = ERROR_BIT | 18,
59 /// Protocol尚未启动.
60 NOT_STARTED = ERROR_BIT | 19,
61 /// Protocol已经启动.
62 ALREADY_STARTED = ERROR_BIT | 20,
63 /// 该操作被中止。
64 ABORTED = ERROR_BIT | 21,
65 /// 网络通信期间发生了ICMP错误。
66 ICMP_ERROR = ERROR_BIT | 22,
67 /// 网络通信期间发生了TFTP 错误
68 TFTP_ERROR = ERROR_BIT | 23,
69 /// 网络通信期间发生了protocol 错误
70 PROTOCOL_ERROR = ERROR_BIT | 24,
71 /// 该函数遇到的内部版本与调用者请求的版本不兼容。
72 INCOMPATIBLE_VERSION = ERROR_BIT | 25,
73 /// 由于违反安全性,未执行该功能。
74 SECURITY_VIOLATION = ERROR_BIT | 26,
75 /// 检测到 CRC 错误 (CRC循环冗余校验)
76 CRC_ERROR = ERROR_BIT | 27,
77 /// Beginning or end of media was reached
78 END_OF_MEDIA = ERROR_BIT | 28,
79 /// 文件结束 EOF
80 END_OF_FILE = ERROR_BIT | 31,
81 /// 指定的语言无效。
82 INVALID_LANGUAGE = ERROR_BIT | 32,
83 /// 数据的安全状态未知或受到破坏,必须更新或替换数据以便恢复。
84 COMPROMISED_DATA = ERROR_BIT | 33,
85 /// 地址冲突地址分配
86 IP_ADDRESS_CONFLICT = ERROR_BIT | 34,
87 /// 网络通信期间发生HTTP错误。
88 HTTP_ERROR = ERROR_BIT | 35,
89 }
90}
91
92
Completion提供了4种便利的函数分别是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1impl<T> From<T> for Completion<T> {
2 fn from(result: T) -> Self {
3 Completion::new(Status::SUCCESS, result)
4 }
5}
6
7impl From<Status> for Completion<()> {
8 fn from(status: Status) -> Self {
9 Completion::new(status, ())
10 }
11}
12
13pub fn status(&self) -> Status {
14 self.status
15}
16
17pub fn split(self) -> (Status, T) {
18 (self.status, self.result)
19}
20
21
fn from(status: Status) -> Self用于封装指定的状态,用于快速返回执行状态(只需要执行状态不要求其结果的情况),例如
1
2
3 1Completion::from(Status::SUCCESS)
2
3
fn from(result: T) -> Self用于封装内容通常用于操作成功需要返回执行内容,例如
1
2
3 1Completion::from("执行成功")
2
3
pub fn status(&self) -> Status用于返回状态,用于判断执行操作的结果状态
pub fn split(self) -> (Status, T)将状态和结果分离
改造Result
了解到这些基本结构后我们可以吧一些常用的函数签名定义出来,uefi-rs不支持自定义异常,uefi::Result表示UEFI执行的结果,Result分为uefi服务执行结果与uefi应用(我们自己编写的程序)的执行结果,因此我们的函数如果要使用uefi的服务则可以抽象为以下形式
1
2
3
4
5
6
7
8
9
10
11
12
13 1// 该函数用于打印当前内存布局
2fn alloc_memory(bt: &BootServices)-> uefi::Result<()>{
3 let size = bt.memory_map_size();
4 let buffer = bt.allocate_pool(MemoryType::BOOT_SERVICES_DATA,size).log_warning()?;
5 let buffer = unsafe{core::slice::from_raw_parts_mut(buffer,size)};
6 let (map,mut iter) = bt.memory_map(buffer).log_warning()?;
7 while let Some(desc) = iter.next(){
8 info!("{:?}",desc);
9 }
10 Ok(Completion::from(()))
11}
12
13
这样我们的代码简化不少否则会充斥大量的if let.match等代码,如果函数需要返回处理的结果则我们的函数可以设计为
1
2
3
4
5
6
7
8
9
10
11 1// 该函数用于分配指定大小的内存并转换为切片的形式
2fn alloc_slice(bt: &BootServices,size:usize)-> uefi::Result<Option<&mut [u8]>>{
3 if size <= 0 {
4 Ok(Completion::from(None))
5 }
6 let ptr = bt.allocate_pool(MemoryType::LOADER_DATA,size).log_warning()?;
7 let slice:&mut [u8] = unsafe{ core::slice::from_raw_parts_mut(ptr,size)};
8 Ok(Completion::from(Some(slice)))
9}
10
11
我们需要注意的是虽然Completion提供了fn from(status: Status) -> Self这样的函数,该函数的使用时机是使用UEFI服务发生错误时返回的状态(uefi服务层面),如果出现了应用层面的错误(应用层面)则不能使用uefi::Result来做返回结果,需要进行封装例如uefi::Result<core::result::Result<Data>>相应的返回成功结果为Ok(Completion::from(core::result::Result::Ok(data))),错误结果为Ok(Completion::from(core::result::Result::Err(err))),当然我们可以使用函数来简化
首先我们定义2种Result,分别表示UEFI和应用的
1
2
3
4
5
6 1// 表示UEFI执行的结果
2pub type UefiResult<T> = uefi::Result<T>;
3// 表示应用程序的结果
4pub type Result<T> = core::result::Result<T, Error>;
5
6
然后我们定义应用程序使用的Error
1
2
3
4
5
6 1#[derive(Debug)]
2pub enum Error {
3 Io,
4}
5
6
然后我们提供ok和err函数来简化
1
2
3
4
5
6
7
8
9 1pub fn ok<T>(t: T) -> UefiResult<Result<T>> {
2 Ok(Completion::from(core::result::Result::Ok(t)))
3}
4
5pub fn err<T>(e: Error) -> UefiResult<Result<T>> {
6 Ok(Completion::from(core::result::Result::Err(e)))
7}
8
9
然后我们的函数就会变成如下形式
1
2
3
4
5
6
7
8
9
10 1fn alloc_slice(bt: &BootServices, size: usize) -> UefiResult<Result<&mut [u8]>> {
2 if size <= 0 {
3 return err::<&mut [u8]>(Error::Io);
4 }
5 let ptr = bt.allocate_pool(MemoryType::LOADER_DATA, size).log_warning()?;
6 let slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(ptr, size) };
7 ok(slice)
8}
9
10
这样就简化了编写的内容
内存管理
uefi中提供了基础的内存管理方式主要分为两种
- AllocatePool: UEFI驱动程序使用allocate_pool()和free_pool()引导服务来分配和释放保证8字节边界对齐的小缓冲区
- AllocatePages: UEFI驱动程序使用allocate_pages()和free_pages()引导服务来分配和释放较大的缓冲区,这些缓冲区保证在4 KB边界上对齐。这些服务允许在任何可用地址,特定地址或特定地址下方分配缓冲区
内存分配的注意事项
UEFI驱动程序不应该理想化系统内存的布局。虽然allocate_pages允许在特定地址出分配缓冲区,但是强烈建议不要从特定地址分配或在特定地址以下分配。 allocate_pool()服务不允许调用者指定首选地址,因此该服务可以安全使用,并且不会影响UEFI驱动程序在不同平台上的兼容性
allocate_pages()服务有用在特定地址或指定地址范围分配内存的功能。主要通过AllocateType指定分配类型,AllocateType::AnyPages可安全使用,并提高了不同平台上UEFI驱动程序的兼容性。 AllocateType::MaxAddress(usize)和AllocateType::Address(usize)等类型可能会降低平台兼容性,因此不建议使用它们
尽管Alloc service可以进行特定的内存分配,但不要在UEFI驱动程序再特定的地址分配内存。在特定地址上分配内存可能会导致错误,包括某些平台上的灾难性故障。 UEFI驱动程序中的内存分配应动态进行
在访问分配的内存之前,请始终检查函数返回码以验证内存分配请求是否成功
UEFI内存管理只在Boot阶段有效当调用exit_boot_services后不在可用,部分分配的内存也会被释放
内存分配的关键点
- 为防止内存泄漏,每个分配操作必须具有相应的释放操作。必须注意某些UEFI服务会为调用方分配缓冲区,并让调用方释放这些内存
- 如果系统内存大于4 GB,则可以分配4 GB以上的缓冲区。因此,所有UEFI驱动程序必须注意指针可能包含4 GB以上的地址值,并且必须小心不要剥离高位地址
- 存储的结构和值必须在已分配内存中进行自然对齐,以最大程度地与所有CPU架构兼容
基本数据结构
MemoryType
MemoryType主要表示内存的范围, UEFI允许固件和操作系统在0x70000000…0xFFFFFFFF范围内的新内存类型。 单词我们在编译时不知道完整的内存类型集合,并且将此C枚举建模为Rust枚举是不安全的。
因此uefi-rs使用了newtype_enum宏
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 1newtype_enum! {
2 pub enum MemoryType: u32 => {
3 /// 不使用此枚举变量.
4 RESERVED = 0,
5 /// 已加载的UEFI应用程序的代码部分.
6 LOADER_CODE = 1,
7 /// 加载的UEFI应用程序的数据部分以及它分配的任何内存
8 LOADER_DATA = 2,
9 /// 引导驱动程序的代码.
10 /// 加载操作系统后可以重复使用
11 BOOT_SERVICES_CODE = 3,
12 /// 用于存储启动驱动程序数据的内存.
13 /// 加载操作系统后可以重复使用
14 BOOT_SERVICES_DATA = 4,
15 /// 运行时驱动程序的代码.
16 RUNTIME_SERVICES_CODE = 5,
17 /// 运行时服务的代码.
18 RUNTIME_SERVICES_DATA = 6,
19 /// 可自由使用的内存.
20 CONVENTIONAL = 7,
21 /// 无法使用的内存(内存错误).
22 UNUSABLE = 8,
23 /// 存放ACPI表的内存(ACPI高级配置和电源管理接口).
24 /// 解析后可以回收
25 ACPI_RECLAIM = 9,
26 /// 固件保留的地址.
27 ACPI_NON_VOLATILE = 10,
28 /// 用于内存映射I / O的区域.
29 MMIO = 11,
30 /// 用于内存映射的端口I / O的地址空间.
31 MMIO_PORT_SPACE = 12,
32 /// 属于处理器的地址空间
33 PAL_CODE = 13,
34 /// 可用且非易失的存储区.
35 PERSISTENT_MEMORY = 14,
36 }
37}
38
39
AllocateType
AllocateType 主要用于alloc_pages来分配内存
1
2
3
4
5
6
7
8
9
10
11 1#[derive(Debug, Copy, Clone)]
2pub enum AllocateType {
3 /// 分配任意的页面.
4 AnyPages,
5 /// 在给定地址以下的任何地址处分配页面.
6 MaxAddress(usize),
7 /// 在指定地址分配页面.
8 Address(usize),
9}
10
11
MemoryAttribute
MemoryAttribute用于描述内存范围功能的标志
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 1bitflags! {
2 pub struct MemoryAttribute: u64 {
3 /// 支持标记为不可缓存.
4 const UNCACHEABLE = 0x1;
5 /// 支持写合并.
6 const WRITE_COMBINE = 0x2;
7 /// 支持直写.
8 const WRITE_THROUGH = 0x4;
9 /// 支持回写.
10 const WRITE_BACK = 0x8;
11 /// 支持标记为不可缓存,已导出,并支持“获取并添加”信号量机制。
12 const UNCACHABLE_EXPORTED = 0x10;
13 /// 支持写保护.
14 const WRITE_PROTECT = 0x1000;
15 /// 支持读保护.
16 const READ_PROTECT = 0x2000;
17 /// 支持禁用代码执行.
18 const EXECUTE_PROTECT = 0x4000;
19 /// 永久内存.
20 const NON_VOLATILE = 0x8000;
21 /// 该存储器区域比其他存储器更可靠.
22 const MORE_RELIABLE = 0x10000;
23 /// 该内存范围可以设置为只读
24 const READ_ONLY = 0x20000;
25 /// 调用运行时服务时,操作系统必须映射此内存.
26 const RUNTIME = 0x8000_0000_0000_0000;
27 }
28}
29
30
31
AllocatePool基本使用
分配1个页大小的切片
1
2
3
4
5
6 1fn alloc_page(bt: &BootServices,) -> UefiResult<Result<&mut [u8]>>{
2 let ptr = bt.allocate_pool(MemoryType::LOADER_CODE,4096).log_warning()?;
3 ok( unsafe{ core::slice::from_raw_parts_mut(ptr,4096)})
4}
5
6
释放内存的切片
1
2
3
4
5
6
7
8 1fn alloc_and_free_page(bt: &BootServices,) -> UefiResult<Result<()>>{
2 let ptr = bt.allocate_pool(MemoryType::LOADER_CODE,4096).log_warning()?;
3 let slice = unsafe{ core::slice::from_raw_parts_mut(ptr,4096)};
4 bt.free_pool(slice.as_mut_ptr());
5 ok(())
6}
7
8
allocate_pool还可以作为内存分配器使用例如在uefi-rs/src/alloc.rs中
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 1use core::alloc::{GlobalAlloc, Layout};
2
3#[global_allocator]
4static ALLOCATOR: Allocator = Allocator;
5
6pub struct Allocator;
7
8unsafe impl GlobalAlloc for Allocator {
9 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
10 let mem_ty = MemoryType::LOADER_DATA;
11 let size = layout.size();
12 let align = layout.align();
13
14 // TODO: add support for other alignments.
15 if align > 8 {
16 // Unsupported alignment for allocation, UEFI can only allocate 8-byte aligned addresses
17 ptr::null_mut()
18 } else {
19 boot_services()
20 .as_ref()
21 .allocate_pool(mem_ty, size)
22 .warning_as_error()
23 .unwrap_or(ptr::null_mut())
24 }
25 }
26
27 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
28 boot_services()
29 .as_ref()
30 .free_pool(ptr)
31 .warning_as_error()
32 .unwrap();
33 }
34}
35
36
使用global_allocator宏将Allocator声明为全局内存分配器,Allocator 必须要实现GlobalAlloc trait
这样我们可以可以使用alloc中的Vec了
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 1#![no_std]
2#![no_main]
3#![feature(asm)]
4#![feature(slice_patterns)]
5#![feature(abi_efiapi)]
6#![feature(never_type)]
7#![feature(fn_traits)]
8
9#[macro_use]
10extern crate alloc;
11#[macro_use]
12extern crate log;
13extern crate uefi;
14
15#[entry]
16fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status {
17 if let Err(e) = uefi_services::init(&st).log_warning() {
18 info!("{:?}", e);
19 return e.status();
20 }
21 // 创建Vec
22 let mut values = vec![-5, 16, 23, 4, 0];
23 values.sort();
24 assert_eq!(values[..], [-5, 0, 4, 16, 23], "Failed to sort vector");
25
26 // 分配按照0x100对齐的结构体
27 #[repr(align(0x100))]
28 struct Block([u8; 0x100]);
29
30 let value = vec![Block([1; 0x100])];
31 assert_eq!(value.as_ptr() as usize % 0x100, 0, "Wrong alignment");
32}
33
34
AllocatePages基本使用
OSLoader程序应分配类型为LOADER_DATA的内存。该函数会返回u64,即使在32位平台上也返回“ u64”,因为某些硬件配置(例如Intel PAE)在32位处理器上启用了64位物理寻址。
分配1个页大小的切片
1
2
3
4
5
6 1fn alloc_one_page(bt: &BootServices,) -> UefiResult<Result<&mut [u8]>>{
2 let page = bt.allocate_pages(AllocateType::AnyPages,MemoryType::LOADER_DATA,1).log_warning()?;
3 ok(unsafe{ core::slice::from_raw_parts_mut(page as *mut u8,4096)})
4}
5
6
该函数在任意位置申请1个页大小的LOADER_DATA类型的内存,然后使用slice提供的from_raw_parts_mut将该内存转为切片
释放内存的切片
1
2
3
4
5
6
7
8 1fn alloc_and_free_page(bt: &BootServices,) -> UefiResult<Result<()>>{
2 let page = bt.allocate_pages(AllocateType::AnyPages,MemoryType::CONVENTIONAL,1).log_warning()?;
3 let slice = unsafe{ core::slice::from_raw_parts_mut(page as *mut u8,4096)}
4 bt.free_pages(slice.as_ptr() as u64,slice.len()).log_warning()?;
5 ok(())
6}
7
8
Protocol
Protocol的介绍如下
protocol是server和client之间的一种约定,双方根据这种约定互通信息。这里的server和client是一种广义的称呼,提供服务的称为server,使用服务的称为client。 TCP是一种protocol, client(应用程序)通过一组函数来压包和解包,压包和解包是server提供的服务,COM也是一种protocol,client通过CoCreateInstance(…)和GUID获得指向COM对象的指针,然后使用该指针获得COM对象提供的服务, GUID标示了这个COM对象
在uefi-rs中获取一个Protocol方法如下以SimpleFileSystem为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1#[entry]
2fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{
3 if let Err(e) = uefi_services::init(&st).log_warning(){
4 info!("{:?}",e);
5 // 如果服务初始化失败后则不能继续执行
6 return e.status();
7 }
8 // 获取BootServices
9 let bt = st.boot_services();
10 // 通过GUID来获取SimpleFileSystem (这里把结果写出来了方便了解该函数的返回值)
11 let fs: &UnsafeCell<SimpleFileSystem> = bt.locate_protocol::<SimpleFileSystem>().log_warning().unwrap();
12 //
13 let f: &mut SimpleFileSystem = unsafe{ &mut *fs.get() };
14 Status::SUCCESS
15}
16
17
这样便可以使用SimpleFileSystem Protocol
我们编写最简单的内核加载器只需要SimpleFileSystem因此我们需要只讲述SimpleFileSystemProtocol的使用(用于读取内核文件)
SimpleFileSystem Protocol
SimpleFileSystem 提供了一个最基础的文件系统,该文件系统的路径分隔符是反斜杠(与windows一致),文件系统的基本使用步骤如下
- 通过bt.locate_protocol::<SimpleFileSystem>()来获取SimpleFileSystem 实例
- 使用open_volume访问根目录(我们在QEMU中指定esp为根目录)
- 创建一个缓冲区用于存放读取的数据,然后使用read_entry开始读取
- 使用open打开文件(在我们的例子中以esp根目录)
- 处理文件
基本数据结构
在读取文件的过程中需要接触到以下数据结构
-
Directory: 通过字面意识得知Directory用来表示文件夹定义在uefi-rs/src/proto/media/file/dir.rs中结,定为pub struct Directory(RegularFile),可以知道Directory是对RegularFile的一次封装,主要用于读取文件夹中的内容
-
RegularFile: RegularFile是对文件的抽象,文件夹也是一种文件,普通文件也属于一种文件,uefi-rs种使用FileType来区分是普通文件还是文件夹,RegularFile定义在uefi-rs/src/proto/media/file/regular.rs,签名为pub struct RegularFile(FileHandle),RegularFile也是对FileHandle的一次封装,主要用于读取数据文件
-
FileHandle: FileHandle Volume上某些连续数据块的不透明句柄(对用户不透明),定义在uefi-rs/src/proto/media/file/mod.rs中,签名为pub struct FileHandle(*mut FileImpl) ,FileHandle也是对FileHandle的一次封装FileImpl(禁止套娃2333),主要用途是使用into_type获取FileType
-
FileInfo: FileInfo为文件的详细信息包括文件名(使用的是UCS-2编码),文件大小,创建时间,访问时间,文件属性等等信息,通过Directory的read_entry来获取
-
FileType: FileType用来区分普通文件和文件夹,定义如下
1
2
3
4
5
6
7
8 1pub enum FileType {
2 /// The file was a regular (data) file.
3 Regular(RegularFile),
4 /// The file was a directory.
5 Dir(Directory),
6}
7
8
-
FileMode: 用于表示文件的操作形式,定义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1#[derive(Debug, Copy, Clone, Eq, PartialEq)]
2#[repr(u64)]
3pub enum FileMode {
4 /// The file can be read from
5 Read = 1,
6
7 /// The file can be read from and written to
8 ReadWrite = 2 | 1,
9
10 /// The file can be read, written, and will be created if it does not exist
11 CreateReadWrite = (1 << 63) | 2 | 1,
12}
13
14
以下我们通过几个例子来说明SimpleFileSystem 的使用
遍历文件夹内容
这个例子用于遍历EFI/Boot文件夹的所有内容
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 1// 该函数用于分配一个页大小的内存
2pub fn alloc_one_page(bt: &BootServices) -> Result<&mut [u8; 4096]> {
3 let page = bt.allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, 1).log_warning()?;
4 let data = unsafe { &mut *(page as *mut [u8; 4096]) };
5 Ok(Completion::new(Status::SUCCESS, data))
6}
7// 用于分配指定大小的内存 按页对齐
8pub fn alloc_size(bt: &BootServices, mem_ty: MemoryType, want: usize) -> Result<&mut [u8]> {
9 let ptr = bt.allocate_pool(mem_ty,want).log_warning()?;
10 let data = unsafe { core::slice::from_raw_parts_mut(ptr as *mut u8, page_num) };
11 Ok(Completion::new(Status::SUCCESS, data))
12}
13
14fn walk(bt:&BootServices) -> UefiResult<Result<()>>{
15 //1. 通过locate_protocol获取SimpleFileSystem实例
16 let f = unsafe{&mut *bt.locate_protocol::<SimpleFileSystem>().log_warning()?.get()};
17 // 2. open_volume打开根目录
18 let mut volume = f.open_volume().log_warning()?;
19 // 3. 申请缓冲区 读取的文件夹数据会存于此
20 let en_buff = alloc_one_page(bt).log_warning()?;
21 // 4. 读取 根目录文件夹中的内容并放在缓冲区中
22 let dir = match volume.read_entry(en_buff).log_warning(){
23 Ok(info)=> info.unwrap(),
24 Err(e) => {
25 // 如果状态码为BUFFER_TOO_SMALL则表示声明的缓冲区过小
26 if e.status() == Status::BUFFER_TOO_SMALL{
27 // data将会返回所需要的缓冲区大小
28 let size = e.data().unwrap();
29 // 释放之前申请的缓冲区
30 let ptr = en_buff.as_ptr() as u64;;
31 bt.free_pages(ptr,1);
32 // 重新申请新的缓冲区
33 let buffer = alloc_size(bt,MemoryType::LOADER_DATA,size).log_warning()?;
34 // 再次读取 如果这次读取失败只会是 坏的数据卷,硬件设备错误 没有该media 中的一个
35 volume.read_entry(buffer).log_warning().unwrap().unwrap()
36 }else{
37 // 坏的数据卷,硬件设备错误 ,没有该media等错误无法处理只能panic
38 panic!("{:?}",e);
39 }
40 }
41 };
42 // 5. 以只读模式打开 EFI\Boot 目录
43 let efi = volume.open(r"EFI\Boot", FileMode::Read, dir.attribute()).log_warning()?.into_type().log_warning()?;
44 // 6. 获取具体的文件类型
45 let ty = efi.into_type().log_warning()?;
46 if let FileType::Dir(mut sub) = ty {
47 // 7. 读取文件夹中的内容
48 while let Ok(f_info) = sub.read_entry(en_buff) {
49 // 8. 获取读取的结果
50 let (status, f) = f_info.split();
51 if status == Status::SUCCESS {
52 let f = f.unwrap();
53 // 9. 如果读取成功 将当前文件的文件名转为u16切片 uefi使用的是UCS-2编码
54 // UCS-2是一种定长的编码方式,UCS-2仅仅简单的使用一个16位码元来表示码位,也就是说在0到0xFFFF的码位范围内,它和UTF-16基本一致,因此我们可以使用String::from_utf16_lossy来转为UTF-16编码
55 let u16_slice = files.file_name().to_u16_slice();
56 // 10. 将UCS-2转为UTF-16编码
57 let name = String::from_utf16_lossy(files.file_name().to_u16_slice());
58 info!("{}",name);
59 }
60 }
61 }else{
62 info!("is not folder!")
63 }
64}
65
66
读取文件内容
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 1fn read_file(bt: &BootServices, filename: &str) -> UefiResult<Result<Vec<u8>>> {
2 //1. 通过locate_protocol获取SimpleFileSystem实例
3 let f = unsafe{&mut *bt.locate_protocol::<SimpleFileSystem>().log_warning()?.get()};
4 // 2. open_volume打开根目录
5 let mut volume = f.open_volume().log_warning()?;
6 // 3. 申请缓冲区 读取的文件夹数据会存于此
7 let en_buff = alloc_one_page(bt).log_warning()?;
8 // 4. 读取 根目录文件夹中的内容并放在缓冲区中
9 let dir = match volume.read_entry(en_buff).log_warning(){
10 Ok(info)=> info.unwrap(),
11 Err(e) => {
12 // 如果状态码为BUFFER_TOO_SMALL则表示声明的缓冲区过小
13 if e.status() == Status::BUFFER_TOO_SMALL{
14 // data将会返回所需要的缓冲区大小
15 let size = e.data().unwrap();
16 // 释放之前申请的缓冲区
17 let ptr = en_buff.as_ptr() as u64;;
18 bt.free_pages(ptr,1);
19 // 重新申请新的缓冲区
20 let buffer = alloc_size(bt,MemoryType::LOADER_DATA,size).log_warning()?;
21 // 再次读取 如果这次读取失败只会是 坏的数据卷,硬件设备错误 没有该media 中的一个
22 volume.read_entry(buffer).log_warning().unwrap().unwrap()
23 }else{
24 // 坏的数据卷,硬件设备错误 ,没有该media等错误无法处理只能panic
25 panic!("{:?}",e);
26 }
27 }
28 };
29 // 5. 打开指定文件
30 let file = volume.open(filename, FileMode::Read, dir.attribute()).log_warning()?;
31 match file.into_type().log_warning()? {
32 FileType::Regular(mut k_file) => {
33 // 获取文件大小
34 let file_size = dir.file_size() as usize;
35 // 分配内存
36 if let Ok(buffer) = alloc_size(bt,MemoryType::LOADER_DATA, file_size as usize).log_warning() {
37 // 读取文件数据到缓冲区,该read方法会尽可能多的读取
38 match k_file.read(buffer) {
39 // 如果读取成功直接返回读取的数据
40 Ok(s) => { return ok(Vec::from(buffer)); }
41 Err(e) => {
42 // 如果分配的缓冲区过小则重新分配
43 if e.status() == Status::BUFFER_TOO_SMALL {
44 let size = e.data().unwrap();
45 let ptr = buffer.as_ptr() as u64;
46 let pages = file_size / 4096;
47 bt.free_pages(ptr, pages);
48 let buffer = mem::alloc_size(bt, MemoryType::LOADER_DATA, size).log_warning()?;
49 k_file.read(buffer).log_warning().unwrap();
50 return ok(Vec::from(buffer));
51 } else {
52 panic!("{:?}", e)
53 }
54 }
55 }
56 }
57 }
58 FileType::Dir(_) => {
59 // 如果检测到的是文件夹说明不是普通文件,这里也可以不用panic可以自定义异常
60 panic!("{} is not regular file!", filename)
61 }
62 }
63 // 如果到了这一步则说明没找到该文件
64 err::<Vec<u8>>(Error::NoSuchFile)
65}
66
67
这样我们完成了关于UEFI的内存管理和文件系统的基本使用
下一步要做什么
在下一篇文章中我们将要开发OSLoader所需要的基本功能,主要的工作是封装对FS和内存的操作使用起来更加简单