本文主要是介绍rCore-Tutorial-Book第二课(移除Rust std标准库依赖),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本节任务: 移除掉代码对
Rust std
标准库的依赖,并将自己的程序改造成为能被编译到RV64GC
裸机平台
文章目录
- 1. 移除 `println!` 宏
- 1.1 `rust`代码编译到指定目标平台
- 1.2 禁用 `rust-std` 标准库
- 1.3 提供`panic_handler` 功能
- 2. 移除`main` 函数
- 3. 分析被移除标准库的程序
- 3.1 安装`cargo-binutils` 工具集
- 3.2 分析二进制文件信息
- 3.2.1 分析文件格式
- 3.2.2 分析文件头信息
- 3.2.3 分析反汇编导出汇编程序
- 4. 额外知识点补充
- 4. 1本地编译与交叉编译
- 4. 2`#[panic_handler]`
- 4.3 `Rust`模块化编程
- 5. 参考文章
1. 移除 println!
宏
1.1 rust
代码编译到指定目标平台
指令格式:rustup target add <target-spec>
$ 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
,并输入以下内容[build] target = "riscv64gc-unknown-none-elf"
1.2 禁用 rust-std
标准库
代码:#![no_std]
放置位置:main.rs
文件开头
#![no_std]
fn main() {println!("Hello,world!");
}
补充理解:
- 执行
cargo build
后编译器会报错,因为禁用了 标准库后,println!
宏没有被实现,标准库实现了宏,并使用了名为write
的系统调用$ 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
编译错误cargo buildCompiling 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) -> !
函数签名
use core::panic::PanicInfo;#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {loop {}
}
补充理解:
- 需自己实现对致命错误的处理方法
- 通过
#[panic_handler]
属性通知编译器用panic
函数来对接panic!
宏- 将该子模块添加到项目中,我们还需要在
main.rs
的#![no_std]
的下方加上mod 模块名;
#![no_std] mod lang_items; fn main() {println!("Hello,world!"); }
- 目前
panic
函数没有实现任何功能,后序需要解析PanicInfo
打印出错位置 + 杀死应用程序。
2. 移除main
函数
代码:#[no_main]
放置位置:main.rs
文件开头
#[no_main]
#![no_std]
mod lang_items;
fn main() {//println!("Hello,world!");
}
补充理解:
- 没有
#[no_main]
运行cargo build
会有编译错误,错误提示告诉我们,fn main
需要标准库支持root@ww:/OSHomework/rustsrc/os/src# cargo buildCompiling 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
函数的情况下,编译器就不需要完成初始化工作。- 移除后,我们做到了第一步!通过编译器检查并生成执行码。
root@ww:/OSHomework/rustsrc/os/src# cargo buildCompiling 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-binutils
和 llvm-tools-preview
工具,用于后续分析二进制文件
$ 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
这个可执行文件的文件类型信息
$ 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
目的:查看目标文件的头部信息,包括文件类型、架构、入口地址等
$ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/osFile: 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: 1OS/ABI: SystemV (0x0)ABIVersion: 0Unused: (00 00 00 00 00 00 00)}Type: Executable (0x2)Machine: EM_RISCV (0xF3)Version: 1Entry: 0x0ProgramHeaderOffset: 0x40SectionHeaderOffset: 0x1908Flags [ (0x5)EF_RISCV_FLOAT_ABI_DOUBLE (0x4)EF_RISCV_RVC (0x1)]HeaderSize: 64ProgramHeaderEntrySize: 56ProgramHeaderCount: 4SectionHeaderEntrySize: 64SectionHeaderCount: 12StringTableSectionIndex: 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
在显示反汇编代码时同时显示源代码的对应部分.
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/ostarget/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
中,我们将项目中所有的语义项放在该模块中。- 创建的指令如下
$ 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)
这篇关于rCore-Tutorial-Book第二课(移除Rust std标准库依赖)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!