利用rust在ubuntu下自制操作系统实验一
一.搭建rust基本环境
(1)RUST 安装
步骤1:下载Rust,输入curl https://sh.rustup.rs -sSf | sh>
问题1.1:未安装curl
解决方法1-1:输入sudo apt install curl安装curl
(2)curl是什么
curl是一个利用URL语法在命令行下工作的文件传输工具,1997年首次发行。它支持文件上传和下载,所以是综合传输工具,但按传统,习惯称cURL为下载工具。cURL还包含了用于程序开发的libcurl。
(3)Cargo 是什么
Cargo 是 Rust 的构建系统和包管理器。
Rust 开发者常用 Cargo 来管理 Rust 工程和获取工程所依赖的库。在上个教程中我们曾使用 cargo new greeting 命令创建了一个名为 greeting 的工程,Cargo 新建了一个名为 greeting 的文件夹并在里面部署了一个 Rust 工程最典型的文件结构。这个 greeting 文件夹就是工程本身。
cargo是包管理器,我们可以通过这个工具轻松导入或者发布开源库。官方的管理仓库是:
cargo-fmt是源代码格式化工具
rustc是编译器
rustfmt是源代码格式化工具
rust-lldb是调试器
rls是为编辑器准备的代码提示工具,
rustdoc文档生成器
rust-gdb是调试器
rustup管理这套工具链下载更新的工具。
下载好的软件都在 home/.cargo/bin文件夹下,需要添加路径才可以在本地终端使用
(4)设置环境变量
1.打开终端并输入:
sudo gedit ~/.bashrc。
2.输入用户密码。这时输入的密码是不可见的。
前面的步骤会打开.bashrc文件,在其末尾添加:
export PATH=/opt/EmbedSky/4.3.3/bin:$PATH
其中/opt/EmbedSky/4.3.3/bin为你自己需要设置的环境变量路径。
4
使其立即生效,在终端执行:
source ~/.bashrc
(5)安装nutyly
安装成功
默认使用nightly版本
遇见错误
原因:命令行多了空格,命令识别不出来以及^符号书写不正确导致
成功
(6)安装xbuild
成功
(7)安装rust-src成功
现象4-4:安装llvm-tools-preview成功
(8)安装qemu
输入 sudo apt-get install qemu 安装
证明安装完成,在终端输入qemu,会弹出qemu窗口,如果没有弹出,可以先输入sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu将qemu与qemu-system-i386进行链接即可
(9)启动qumu
二.创建裸机程序
(1).禁用rust标准库!
利用cargo创建项目文档:
cargo new fjq_os
然后得到以下文件树
blog_os
├── Cargo.toml
└── src
└── main.rs
在这里,Cargo.toml
文件包含了包的配置(configuration),比如包的名称、作者、semver版本和项目依赖项;src/main.rs
文件包含包的根模块(root module)和main函数。我们可以使用cargo build
来编译这个包,然后在target/debug
文件夹内找到编译好的blog_os
二进制文件。
(2)利用cargo包编译
cargo build
①出现以下错误一:
②错误1原因
出现这个错误的原因是,println!宏是标准库的一部分,而我们的项目不再依赖于标准库。我们选择不再打印字符串。这也能解释得通,因为println!
将会向标准输出(standard output)打印字符,它依赖于特殊的文件描述符,而这是由操作系统提供的特性。
所以我们可以移除这行代码,使用一个空的main函数再次尝试编译:
#![no_std]
fn main() {
}
③出现错误2
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
现在我们发现,编译器缺少一个panic处理函数(panic handler function)和一个语言项(language item)。
(3)实现panic处理函数
// in main.rs
use core::panic::PanicInfo;
/// 这个函数将在panic时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
(4)eh_personality语言项
语言项是一些编译器需求的特殊函数或类型。举例来说,Rust的Copy
trait是一个这样的语言项,告诉编译器哪些类型需要遵循复制语义(copy semantics)——当我们查找Copy
trait的实现时,我们会发现,一个特殊的#[lang = "copy"]
属性将它定义为了一个语言项,达到与编译器联系的目的。
(5)禁用栈展开
在其它一些情况下,栈展开不是迫切需求的功能;因此,Rust提供了在panic时中止(abort on panic)的选项。这个选项能禁用栈展开相关的标志信息生成,也因此能缩小生成的二进制程序的长度。有许多方式能打开这个选项,最简单的方式是把下面的几行设置代码加入我们的Cargo.toml
:
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
这些选项能将dev
配置(dev profile)和release
配置(release profile)的panic策略设为abort
。dev
配置适用于cargo build
,而release
配置适用于cargo build --release
。现在编译器应该不再要求我们提供eh_personality
语言项实现。
①出现错误1
(6)start语言项
这里,我们的程序遗失了start
语言项,它将定义一个程序的入口点(entry point)。
我们通常会认为,当运行一个程序时,首先被调用的是main
函数。但是,大多数语言都拥有一个运行时系统(runtime system),它通常为垃圾回收(garbage collection)或绿色线程(software threads,或green threads)服务,如Java的GC或Go语言的协程(goroutine);这个运行时系统需要在main函数前启动,因为它需要让程序初始化。
在一个典型的使用标准库的Rust程序中,程序运行是从一个名为crt0
的运行时库开始的。crt0
意为C runtime zero,它能建立一个适合运行C语言程序的环境,这包含了栈的创建和可执行程序参数的传入。这之后,这个运行时库会调用Rust的运行时入口点,这个入口点被称作start语言项("start" language item)。Rust只拥有一个极小的运行时,它被设计为拥有较少的功能,如爆栈检测和打印堆栈轨迹(stack trace)。这之后,这个运行时将会调用main函数。
我们的独立式可执行程序并不能访问Rust运行时或crt0
库,所以我们需要定义自己的入口点。实现一个start
语言项并不能帮助我们,因为这之后程序依然要求crt0
库。所以,我们要做的是,直接重写整个crt0
库和它定义的入口点。
(7)重写入口点
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// 这个函数将在panic时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
,我们转而编写一个_start
函数:
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
我们使用no_mangle
标记这个函数,来对它禁用名称重整(name mangling)——这确保Rust编译器输出一个名为_start
的函数;否则,编译器可能最终生成名为_ZN3blog_os4_start7hb173fedf945531caE
的函数,无法让链接器正确辨别。
我们还将函数标记为extern "C"
,告诉编译器这个函数应当使用C语言的调用约定,而不是Rust语言的调用约定。函数名为_start
,是因为大多数系统默认使用这个名字作为入口点名称。
(8)链接错误
为了解决这个错误,我们需要告诉链接器,它不应该包含(include)C语言运行环境。我们可以选择提供特定的链接器参数(linker argument),也可以选择编译为裸机目标(bare metal target)。
(9)编译为裸机目标
在默认情况下,Rust尝试适配当前的系统环境,编译可执行程序。举个栗子,如果你使用x86_64
平台的Windows系统,Rust将尝试编译一个扩展名为.exe
的Windows可执行程序,并使用x86_64
指令集。这个环境又被称作你的宿主系统("host" system)。
为了描述不同的环境,Rust使用一个称为目标三元组(target triple)的字符串。要查看当前系统的目标三元组,我们可以运行rustc --version --verbose
:
①添加thumbv7em
rustup target add thumbv7em-none-eabihf
这行命令将为目标下载一个标准库和core库。这之后,我们就能为这个目标构建独立式可执行程序
(10)运行结果
了:
cargo build --target thumbv7em-none-eabihf
我们传递了--target
参数,来为裸机目标系统交叉编译(cross compile)我们的程序。我们的目标并不包括操作系统,所以链接器不会试着链接C语言运行环境,因此构建过程成功完成,不会产生链接器错误。
我们将使用这个方法编写自己的操作系统内核。我们不将编译到thumbv7em-none-eabihf
,而是使用描述x86_64
环境的自定义目标(custom target)。
三.构建最小内核
(1)目标配置清单
我们将把我们的内核编译到x86_64
架构,所以我们的配置清单将和上面的例子相似。现在,我们来创建一个名为x86_64-blog_os.json
的文件——当然也可以选用自己喜欢的文件名——里面包含这样的内容:
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
需要注意的是,因为我们要在裸机(bare metal)上运行内核,我们已经修改了llvm-target
的内容,并将os
配置项的值改为none
。
(2)编写内核程序
#![no_std] // 不链接Rust标准库
#![no_main] // 禁用所有Rust层级的入口点
use core::panic::PanicInfo;
/// 这个函数将在panic时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle] // 不重整函数名
pub extern "C" fn _start() -> ! {
// 因为编译器会寻找一个名为`_start`的函数,所以这个函数就是入口点
// 默认命名为`_start`
loop {}
}
尝试编译内核
cargo build --target x86_64-blog_os.json
报错:
error[E0463]: can't find crate for `core`
(或者是下面的错误)
通常状况下,core
库以预编译库(precompiled library)的形式与Rust编译器一同发布——这时,core
库只对支持的宿主系统有效,而我们自定义的目标系统无效。如果我们想为其它系统编译代码,我们需要为这些系统重新编译整个core
库。
解决方法:安装cargo xbuild库,即输入cargo install cargo-xbuild
再次编译
cargo xbuild --target x86_64-fjq_os.json
我们能看到,cargo xbuild
为我们自定义的目标交叉编译了core
、compiler_builtin
和alloc
三个部件。这些部件使用了大量的不稳定特性(unstable features),所以只能在nightly版本的Rust编译器中工作。这之后,cargo xbuild
成功地编译了我们的blog_os
包。
现在我们可以为裸机编译内核了;但是,我们提供给引导程序的入口点_start
函数还是空的。我们可以添加一些东西进去,不过我们可以先做一些优化工作。
(3)设置默认目标
为了避免每次使用cargo xbuild
时传递--target
参数,我们可以覆写默认的编译目标。我们创建一个名为.cargo/config
的cargo配置文件,添加下面的内容:
# in .cargo/config
[build]
target = "x86_64-fjq_os.json"
这里的配置告诉cargo
在没有显式声明目标的情况下,使用我们提供的x86_64-blog_os.json
作为目标配置。这意味着保存后,我们可以直接使用:
cargo xbuild
出现错误
上面说的是没有找到x86_64-fqj_os.json文件
于是把文件复制到home中:再次
cargo xbuild
出现错误,上面显示的是bootloader的版本不能匹配,因此选择0.9.8
成功:
(4)打印到屏幕:
最简单的方式是写入VGA字符缓冲区(VGA text buffer):这是一段映射到VGA硬件的特殊内存片段,包含着显示在屏幕上的内容。通常情况下,它能够存储25行、80列共2000个字符单元(character cell);每个字符单元能够显示一个ASCII字符,也能设置这个字符的前景色(foreground color)和背景色(background color)。
我们的实现就像这样:
static HELLO: &[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
(5)制作img文件:
> cargo bootimage
结果在运行这行命令之后,我们应该能在target/x86_64-blog_os/debug
目录内找到我们的映像文件bootimage-blog_os.bin
。我们可以在虚拟机内启动它,也可以刻录到U盘上以便在真机上启动。(需要注意的是,因为文件格式不同,这里的bin文件并不是一个光驱映像,所以将它刻录到光盘
(6)在QEMU中启动内核
现在我们可以在虚拟机中启动内核了。为了在QEMU中启动内核,我们使用下面的命令:
执行命令:
qemu-system-x86_64 -drive format=raw,file=bootimage-fjq_os.bin
结果显示
四、总结
这次实验是制作微型操作系统的一个预先准备实验。
(1)首先我们需要安装整个rust环境
(2)在安装rust环境的时候我利用了curl工具下载rust已经下载了rustc,然后设置环境变量
(3) 接着安装了各种依赖,链接,以及qemu虚拟机
以上教我学会了如下在linux下载东西,并且如何设置环境变量使用,验证各种东西是否安装成功
然后就是利用rust创建裸机程序以及构建最小内核,载创建裸机程序的时候,我们为了让rust禁用标准库,把rust语言改得面部全非,已经配上以及特有的指令让他运行了起来。
然后在制作一个最小内核程序的时候,我们链接了C语言,通过VGA显存地址打印出每一个字符的种类和颜色然后再在显示出来,最后在qemu的虚拟机上显示出了""hello world"!,这里让我发现和<<30天自制操作系统>>这本书很多地方有相似的地址,感觉两者之间最大的区别就是一个在window上利用各种bat脚本文件运行的,另一个是直接在ubuntu上用准确的利用每一行命令让我切身体会到,自制操作系统的艰难😊