rCore-Tutorial-Book第二课(移除Rust std标准库依赖)

本节任务: 移除掉代码对 Rust std标准库的依赖,并将自己的程序改造成为能被编译到 RV64GC 裸机平台

文章目录

    • [1. 移除 `println!` 宏](#1. 移除 println! 宏)
      • [1.1 `rust`代码编译到指定目标平台](#1.1 rust代码编译到指定目标平台)
      • [1.2 禁用 `rust-std` 标准库](#1.2 禁用 rust-std 标准库)
      • [1.3 提供`panic_handler` 功能](#1.3 提供panic_handler 功能)
    • [2. 移除`main` 函数](#2. 移除main 函数)
    • [3. 分析被移除标准库的程序](#3. 分析被移除标准库的程序)
      • [3.1 安装`cargo-binutils` 工具集](#3.1 安装cargo-binutils 工具集)
      • [3.2 分析二进制文件信息](#3.2 分析二进制文件信息)
        • [3.2.1 分析文件格式](#3.2.1 分析文件格式)
        • [3.2.2 分析文件头信息](#3.2.2 分析文件头信息)
        • [3.2.3 分析反汇编导出汇编程序](#3.2.3 分析反汇编导出汇编程序)
    • [4. 额外知识点补充](#4. 额外知识点补充)
      • [4. 1本地编译与交叉编译](#4. 1本地编译与交叉编译)
      • 4. 2`#[panic_handler]`
      • [4.3 `Rust`模块化编程](#4.3 Rust模块化编程)
    • [5. 参考文章](#5. 参考文章)

1. 移除 println!

1.1 rust代码编译到指定目标平台

指令格式:rustup target add <target-spec>

shell 复制代码
$ rustup target add riscv64gc-unknown-none-elf

补充理解:

  • Linux 系统上,默认编译的目标平台是 x86_64-unknown-linux-gnu
  • riscv64gc-unknown-none-elf 是特定目标三元组,指定了编译器应该生成的目标平台和运行环境
  • riscv64gc 表示 RISC-V 64 位指令集架构,unknown 表示目标操作系统为未知,none 表示不使用标准库,elf 表示生成的目标文件格式为 ELF
  • 编译器运行的开发平台 (x86_64)可执行文件运行的目标平台 (riscv-64)不同的情况。我们把这种情况称为 交叉编译 (Cross Compile)
  • 偷懒技巧,不想每次 cargo build 加上指令 --target 参数,可以在根目录下.cargo 创建 config.toml,并输入以下内容
toml 复制代码
[build]
target = "riscv64gc-unknown-none-elf"

1.2 禁用 rust-std 标准库

代码:#![no_std]

放置位置:main.rs 文件开头

rust 复制代码
#![no_std]
fn main() {
    println!("Hello,world!");
}

补充理解:

  • 执行 cargo build 后编译器会报错,因为禁用了 标准库后,println! 宏没有被实现,标准库实现了宏,并使用了名为write的系统调用
shell 复制代码
$ cargo build
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error: cannot find macro `println` in this scope
--> src/main.rs:4:5
|
4 |     println!("Hello, world!");
|     ^^^^^^^
  • 注释掉 println!("Hello,world!") 语句后再次执行 cargo build,会引发没有实现 panic_handler 编译错误
shell 复制代码
cargo build
 Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error: `#[panic_handler]` function required, but not found

1.3 提供panic_handler 功能

代码:#[panic_handler]

需要实现:fn(&PanicInfo) -> ! 函数签名

rust 复制代码
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

补充理解:

  • 自己实现对致命错误的处理方法
  • 通过 #[panic_handler] 属性通知编译器用panic函数来对接 panic!
  • 将该子模块添加到项目 中,我们还需要在 main.rs#![no_std] 的下方加上 mod 模块名;
rust 复制代码
#![no_std]
mod lang_items;
fn main() {
  println!("Hello,world!");
}
  • 目前 panic 函数没有实现任何功能,后序需要解析 PanicInfo 打印出错位置 + 杀死应用程序。

2. 移除main 函数

代码:#[no_main]

放置位置:main.rs 文件开头

rust 复制代码
#[no_main]
#![no_std]
mod lang_items;
fn main() {
    //println!("Hello,world!");
}

补充理解:

  • 没有 #[no_main] 运行 cargo build 会有编译错误,错误提示告诉我们,fn main 需要标准库支持
shell 复制代码
root@ww:/OSHomework/rustsrc/os/src# cargo build
   Compiling os v0.1.0 (/OSHomework/rustsrc/os)
error: using `fn main` requires the standard library
  |
  = help: use `#![no_main]` to bypass the Rust generated entrypoint and declare a platform specific entrypoint yourself, usually with `#[no_mangle]`

error: could not compile `os` (bin "os") due to 1 previous error
  • 语言标准库三方库 作为应用程序的执行环境 ,需要负责在执行应用程序之前 进行一些初始化工作 ,然后才跳转到应用程序的入口点,跳转到我们编写的 main 函数
  • 因为我们禁用了标准库,所以编译器找不到 fn main
  • 解决方案是在 main.rs 的开头加入设置 #![no_main] 告诉编译器我们没有一般意义上的 main 函数,并将原来的 main 函数删除。在失去了 main 函数的情况下,编译器就不需要完成初始化工作。
  • 移除后,我们做到了第一步!通过编译器检查并生成执行码。
shell 复制代码
root@ww:/OSHomework/rustsrc/os/src# cargo build
   Compiling os v0.1.0 (/OSHomework/rustsrc/os)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s

3. 分析被移除标准库的程序

上述操作已经通过了 rust 编译器的检查和编译,形成了二进制代码 ,如何去查看这个二进制代码呢?

为了分析二进制可执行程序,我们需要安装 cargo-binutils 工具集

3.1 安装cargo-binutils 工具集

指令:cargo install cargo-binutils \

rustup component add llvm-tools-preview

目的:安装 cargo-binutilsllvm-tools-preview 工具,用于后续分析二进制文件

shell 复制代码
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview

3.2 分析二进制文件信息

3.2.1 分析文件格式

指令:file target/riscv64gc-unknown-none-elf/debug/os

目的:查看编译后在 target/riscv64gc-unknown-none-elf/debug/os 这个可执行文件的文件类型信息

shell 复制代码
$ file target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped

补充理解:

  • ELF 64-bit LSB executable: 这表示该文件是一个 ELF 格式的可执行文件,采用的是 64 位格式,并且是小端序(Little-Endian)字节顺序。
  • UCB RISC-V, version 1 (SYSV): 这说明该可执行文件是为 RISC-V 架构生成的,采用的是UC Berkeley的指令集版本,并且符合 System V ABI 规范。
  • statically linked: 这表示该可执行文件是静态链接的,意味着它包含了所有需要的库文件和依赖,而不依赖于外部共享库。
  • with debug_info: 这表示该可执行文件包含调试信息,可以用于调试程序。
  • not stripped: 这表示该可执行文件未被剥离(stripped),即保留了符号表和其他调试信息。
3.2.2 分析文件头信息

指令:rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os

目的:查看目标文件的头部信息,包括文件类型、架构、入口地址等

shell 复制代码
$ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os

File: target/riscv64gc-unknown-none-elf/debug/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <Not found>
ElfHeader {
  Ident {
    Magic: (7F 45 4C 46)
    Class: 64-bit (0x2)
    DataEncoding: LittleEndian (0x1)
    FileVersion: 1
    OS/ABI: SystemV (0x0)
    ABIVersion: 0
    Unused: (00 00 00 00 00 00 00)
  }
  Type: Executable (0x2)
  Machine: EM_RISCV (0xF3)
  Version: 1
  Entry: 0x0
  ProgramHeaderOffset: 0x40
  SectionHeaderOffset: 0x1908
  Flags [ (0x5)
    EF_RISCV_FLOAT_ABI_DOUBLE (0x4)
    EF_RISCV_RVC (0x1)
  ]
  HeaderSize: 64
  ProgramHeaderEntrySize: 56
  ProgramHeaderCount: 4
  SectionHeaderEntrySize: 64
  SectionHeaderCount: 12
  StringTableSectionIndex: 10
}

补充理解:

  • 入口地址Entry: 0x0 ,从 C/C++ 等语言中得来的经验告诉我们, 0 一般表示 NULL 或空指针,因此等于 0 的入口地址看上去无法对应到任何指令。
  • File: target/riscv64gc-unknown-none-elf/debug/os: 指定的目标文件路径。
  • Format: elf64-littleriscv: 文件格式为 ELF 64 位小端 RISC-V 格式,表示这是一个针对 RISC-V 架构的 64 位 ELF 格式文件。
  • Arch: riscv64: 架构为 RISC-V 64 位,表示这个文件是为 RISC-V 64 位架构生成的。
  • AddressSize: 64bit: 地址大小为 64 位。
  • LoadName: <Not found>: 未找到加载名称。
  • Ident: ELF 头部标识信息,包括文件魔数、类别、数据编码、操作系统/ABI 等信息。
  • Type: Executable: 文件类型为可执行文件。
  • Machine: EM_RISCV: 机器码表示为 EM_RISCV,即 RISC-V 架构。
  • Entry: 0x0: 入口地址为 0x0,表示程序的执行从地址 0x0 开始。
  • ProgramHeaderOffset: 0x40: 程序头偏移地址为 0x40
  • SectionHeaderOffset: 0x1908: 节头偏移地址为 0x1908
  • Flags: 标志字段,包括 RISC-V 相关的标志信息。
  • HeaderSize: 64: 头部大小为 64 字节。
  • ProgramHeaderEntrySize: 56: 程序头条目大小为 56 字节。
  • ProgramHeaderCount: 4: 程序头数量为 4 个。
  • SectionHeaderEntrySize: 64: 节头条目大小为 64 字节。
  • SectionHeaderCount: 12: 节头数量为 12 个。
  • StringTableSectionIndex: 10: 字符串表节索引为 10。
3.2.3 分析反汇编导出汇编程序

指令:rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os

目的:对指定的目标文件进行反汇编,并且输出反汇编的结果以及源代码的对应部分,-S 选项指示 rust-objdump 在显示反汇编代码时同时显示源代码的对应部分.

shell 复制代码
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
	target/riscv64gc-unknown-none-elf/debug/os:       file format elf64-littleriscv

补充理解:

  • 可以看到没有生成汇编代码,所以,我们可以断定,这个二进制程序虽然合法,但它是一个空程序。
  • 产生该现象的原因是:目前我们的程序(参考上面的源代码)没有进行任何有意义的工作,由于我们移除了 main 函数并将项目设置为 #![no_main] ,它甚至没有一个传统意义上的入口点(即程序首条被执行的指令所在的位置),因此 Rust 编译器会生成一个空程序。

4. 额外知识点补充

4. 1本地编译与交叉编译

本地编译与交叉编译:

下面指的 平台 主要由CPU硬件和操作系统这两个要素组成。

  • 本地编译,即在当前开发平台下编译出来的程序,也只是放到这个平台下运行。如在 Linux x86-64 平台上编写代码并编译成可在 Linux x86-64 同样平台上执行的程序。

  • 交叉编译,是一个与本地编译相对应的概念,即在一种平台上编译出在另一种平台上运行的程序。程序编译的环境与程序运行的环境不一样。如我们后续会讲到,在Linux x86-64 开发平台上,编写代码并编译成可在 rCore Tutorial(这是我们要编写的操作系统内核)和 riscv64gc(这是CPU硬件)构成的目标平台上执行的程序。

4. 2#[panic_handler]

#[panic_handler]

#[panic_handler]是一种编译指导属性,用于标记核心库 core中的 panic!要对接的函数

  • 函数需实现对致命错误的具体处理
  • 函数需有 fn(&PanicInfo) -> ! 函数签名
  • 函数可通过 PanicInfo 数据结构获取致命错误的相关信息

4.3 Rust模块化编程

Rust模块化编程

  • 将一个软件工程项目划分为多个子模块分别进行实现是一种被广泛应用的编程技巧,它有助于促进复用代码,并显著提升代码的可读性和可维护性。因此,众多编程语言均对模块化编程提供了支持,Rust 语言也不例外。
  • 每个通过 Cargo 工具创建的 Rust 项目均是一个模块,取决于 Rust 项目类型的不同,模块的根所在的位置也不同。当使用 --bin 创建一个可执行的 Rust 项目时,模块的根是 src/main.rs 文件;而当使用 --lib 创建一个 Rust 库项目时,模块的根是 src/lib.rs 文件。在模块的根文件中,我们需要声明所有可能会用到的子模块。如果不声明的话,即使子模块对应的文件存在,Rust 编译器也不会用到它们。如上面的代码片段中,我们就在根文件 src/main.rs 中通过 mod lang_items; 声明了子模块 lang_items ,该子模块实现在文件 src/lang_item.rs 中,我们将项目中所有的语义项放在该模块中。
  • 创建的指令如下
shell 复制代码
$ cargo new fileName --bin
$ cargo new fileName --lib
  • 当一个子模块比较复杂的时候,它往往不会被放在一个独立的文件中,而是放在一个 src 目录下与子模块同名的子目录之下,在后面的章节中我们常会用到这种方法。例如第二章代码(参见代码仓库的 ch2 分支)中的 syscall 子模块就放在 src/syscall 目录下。对于这样的子模块,其所在目录下的 mod.rs 为该模块的根,其中可以进而声明它的子模块。同样,这些子模块既可以放在一个文件中,也可以放在一个目录下。
  • 我们可以使用绝对路径或相对路径来引用其他模块或当前模块的内容。参考上面的 use core::panic::PanicInfo; ,类似 C++ ,我们将模块的名字按照层级由浅到深排列,并在相邻层级之间使用分隔符 :: 进行分隔。路径的最后一级(如 PanicInfo)则表示我们具体要引用或访问的内容,可能是变量、类型或者方法名。当通过绝对路径进行引用时,路径最开头可能是项目依赖的一个外部库的名字,或者是 crate 表示项目自身的根模块。

5. 参考文章

移除标准库依赖 - rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.cn)

相关推荐
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼2 小时前
Java基础-单例模式的实现
java·开发语言·单例模式
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
通信仿真实验室3 小时前
(10)MATLAB莱斯(Rician)衰落信道仿真1
开发语言·matlab
勿语&3 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589367 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰7 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer7 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
IT良7 小时前
c#增删改查 (数据操作的基础)
开发语言·c#