使用Rust开发操作系统(UEFI内存管理和文件系统使用)

释放双眼,带上耳机,听听看~!

在上一篇文章中我们简单介绍了UEFI的基本概念在本章中我们介绍uefi-rs库的内存管理和文件系统使用

文章目录

  • 基本结构

  • UEFI的HelloWorld!

  • 使用QEMU启动

  • 基本的数据结构

  • Result

  • 改造Result

    • 内存管理
  • 内存分配的注意事项
    * 内存分配的关键点
    * 基本数据结构

  • MemoryType
    * AllocateType
    * MemoryAttribute

    
    
    1
    2
    3
    1  * AllocatePool基本使用
    2  * AllocatePages基本使用
    3
    • Protocol
  • SimpleFileSystem Protocol

  • 基本数据结构
    * 遍历文件夹内容
    * 读取文件内容

  • 下一步要做什么

基本结构

uefi-rs中基本的结构已经画成脑图的形式
使用Rust开发操作系统(UEFI内存管理和文件系统使用)
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=&quot;0.4.0&quot;
3features = [&#x27;exts&#x27;]
4
5[dependencies.uefi-services]
6version = &quot;0.2.0&quot;
7features = [&quot;qemu&quot;]
8
9[dependencies.log]
10version = &quot;0.4.8&quot;
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&lt;Boot&gt;) -&gt; 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&lt;Boot&gt;) -&gt; Status{
3   if let Err(e) = uefi_services::init(&amp;st).log_warning(){
4        info!(&quot;{:?}&quot;,e);
5        // 如果服务初始化失败后则不能继续执行
6        return e.status();
7    }
8    info!(&quot;Hello World!&quot;);
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 = &quot;x86_64-unknown-uefi&quot;
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 = &quot;https://github.com/rust-lang/crates.io-index&quot;
4replace-with = &#x27;ustc&#x27;
5
6[source.ustc]
7registry = &quot;http://mirrors.ustc.edu.cn/crates.io-index&quot;
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=&lt;OVMF_CODE.fd的路径&gt;,readonly=on
3-drive if=pflash,format=raw,file=&lt;OVMF_VARS.fd的路径&gt;,readonly=on
4-drive format=raw,file=fat:rw:&lt;esp文件路径&gt;
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&lt;Output = (), ErrData = ()&gt; = core::result::Result&lt;Completion&lt;Output&gt;, Error&lt;ErrData&gt;&gt;;
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&lt;T&gt; {
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 =&gt; {
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&lt;T&gt; From&lt;T&gt; for Completion&lt;T&gt; {
2    fn from(result: T) -&gt; Self {
3        Completion::new(Status::SUCCESS, result)
4    }
5}
6
7impl From&lt;Status&gt; for Completion&lt;()&gt; {
8    fn from(status: Status) -&gt; Self {
9        Completion::new(status, ())
10    }
11}
12
13pub fn status(&amp;self) -&gt; Status {
14    self.status
15}
16
17pub fn split(self) -&gt; (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(&quot;执行成功&quot;)
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: &amp;BootServices)-&gt; uefi::Result&lt;()&gt;{
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!(&quot;{:?}&quot;,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: &amp;BootServices,size:usize)-&gt; uefi::Result&lt;Option&lt;&amp;mut [u8]&gt;&gt;{
3   if size &lt;= 0 {
4        Ok(Completion::from(None))
5    }
6    let ptr = bt.allocate_pool(MemoryType::LOADER_DATA,size).log_warning()?;
7    let slice:&amp;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&lt;T&gt; = uefi::Result&lt;T&gt;;
3// 表示应用程序的结果
4pub type Result&lt;T&gt; = core::result::Result&lt;T, Error&gt;;
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&lt;T&gt;(t: T) -&gt; UefiResult&lt;Result&lt;T&gt;&gt; {
2    Ok(Completion::from(core::result::Result::Ok(t)))
3}
4
5pub fn err&lt;T&gt;(e: Error) -&gt; UefiResult&lt;Result&lt;T&gt;&gt; {
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: &amp;BootServices, size: usize) -&gt; UefiResult&lt;Result&lt;&amp;mut [u8]&gt;&gt; {
2    if size &lt;= 0 {
3        return err::&lt;&amp;mut [u8]&gt;(Error::Io);
4    }
5    let ptr = bt.allocate_pool(MemoryType::LOADER_DATA, size).log_warning()?;
6    let slice: &amp;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 =&gt; {
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: &amp;BootServices,) -&gt; UefiResult&lt;Result&lt;&amp;mut [u8]&gt;&gt;{
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: &amp;BootServices,) -&gt; UefiResult&lt;Result&lt;()&gt;&gt;{
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(&amp;self, layout: Layout) -&gt; *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 &gt; 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(&amp;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&lt;Boot&gt;) -&gt; Status {
17    if let Err(e) = uefi_services::init(&amp;st).log_warning() {
18        info!(&quot;{:?}&quot;, 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], &quot;Failed to sort vector&quot;);
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, &quot;Wrong alignment&quot;);
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: &amp;BootServices,) -&gt; UefiResult&lt;Result&lt;&amp;mut [u8]&gt;&gt;{
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: &amp;BootServices,) -&gt; UefiResult&lt;Result&lt;()&gt;&gt;{
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&lt;Boot&gt;) -&gt; Status{
3   if let Err(e) = uefi_services::init(&amp;st).log_warning(){
4        info!(&quot;{:?}&quot;,e);
5        // 如果服务初始化失败后则不能继续执行
6        return e.status();
7    }
8    // 获取BootServices
9    let bt = st.boot_services();
10    // 通过GUID来获取SimpleFileSystem (这里把结果写出来了方便了解该函数的返回值)
11      let fs: &amp;UnsafeCell&lt;SimpleFileSystem&gt; =  bt.locate_protocol::&lt;SimpleFileSystem&gt;().log_warning().unwrap();
12      //
13      let f: &amp;mut SimpleFileSystem = unsafe{ &amp;mut *fs.get() };
14    Status::SUCCESS
15}
16
17

这样便可以使用SimpleFileSystem Protocol
我们编写最简单的内核加载器只需要SimpleFileSystem因此我们需要只讲述SimpleFileSystemProtocol的使用(用于读取内核文件)

SimpleFileSystem Protocol

SimpleFileSystem 提供了一个最基础的文件系统,该文件系统的路径分隔符是反斜杠(与windows一致),文件系统的基本使用步骤如下

  1. 通过bt.locate_protocol::<SimpleFileSystem>()来获取SimpleFileSystem 实例
  2. 使用open_volume访问根目录(我们在QEMU中指定esp为根目录)
  3. 创建一个缓冲区用于存放读取的数据,然后使用read_entry开始读取
  4. 使用open打开文件(在我们的例子中以esp根目录)
  5. 处理文件

基本数据结构

在读取文件的过程中需要接触到以下数据结构

  • 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 &lt;&lt; 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: &amp;BootServices) -&gt; Result&lt;&amp;mut [u8; 4096]&gt; {
3    let page = bt.allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, 1).log_warning()?;
4    let data = unsafe { &amp;mut *(page as *mut [u8; 4096]) };
5    Ok(Completion::new(Status::SUCCESS, data))
6}
7// 用于分配指定大小的内存 按页对齐
8pub fn alloc_size(bt: &amp;BootServices, mem_ty: MemoryType, want: usize) -&gt; Result&lt;&amp;mut [u8]&gt; {
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:&amp;BootServices) -&gt; UefiResult&lt;Result&lt;()&gt;&gt;{
15   //1. 通过locate_protocol获取SimpleFileSystem实例
16   let f = unsafe{&amp;mut *bt.locate_protocol::&lt;SimpleFileSystem&gt;().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)=&gt; info.unwrap(),
24        Err(e) =&gt; {
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!(&quot;{:?}&quot;,e);
39            }
40        }
41    };
42   // 5. 以只读模式打开 EFI\Boot 目录
43   let efi = volume.open(r&quot;EFI\Boot&quot;, 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!(&quot;{}&quot;,name);
59            }
60        }
61     }else{
62         info!(&quot;is not folder!&quot;)
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: &amp;BootServices, filename: &amp;str) -&gt; UefiResult&lt;Result&lt;Vec&lt;u8&gt;&gt;&gt; {
2    //1. 通过locate_protocol获取SimpleFileSystem实例
3    let f = unsafe{&amp;mut *bt.locate_protocol::&lt;SimpleFileSystem&gt;().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)=&gt; info.unwrap(),
11        Err(e) =&gt; {
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!(&quot;{:?}&quot;,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) =&gt; {
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) =&gt; { return ok(Vec::from(buffer)); }
41                    Err(e) =&gt; {
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!(&quot;{:?}&quot;, e)
53                        }
54                    }
55                }
56            }
57        }
58        FileType::Dir(_) =&gt; {
59          // 如果检测到的是文件夹说明不是普通文件,这里也可以不用panic可以自定义异常
60            panic!(&quot;{} is not regular file!&quot;, filename)
61        }
62    }
63    // 如果到了这一步则说明没找到该文件
64    err::&lt;Vec&lt;u8&gt;&gt;(Error::NoSuchFile)
65}
66
67

这样我们完成了关于UEFI的内存管理和文件系统的基本使用

下一步要做什么

在下一篇文章中我们将要开发OSLoader所需要的基本功能,主要的工作是封装对FS和内存的操作使用起来更加简单

给TA打赏
共{{data.count}}人
人已打赏
安全技术

C++回调函数

2022-1-11 12:36:11

安全技术

SpringBoot使用Sharding-JDBC分库分表

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索