一个最小的内核
-
启动
-
编译
-
LLVM
-
Target Triple
- Data Layout
-
重新编译core库
-
运行
-
下一步要做什么
在上一节中我们搭建了编写内核程序的最基本的项目结构,并且使用了nightly版的Rust编译器,在本节中我们构建一个最小的适用于x86结构的64位系统,我们
启动
当你按下电源按钮的时候,它开始执行存储在主板ROM中的固件代码,此代码执行开机自检,检测可用的RAM,然后预处理CPU和硬件,接下来,引导系统将会接管,它将会完成模式转换,搜索内核入口地址并跳入内核
在x86有2种固件:第一种为基本输入输出系统(Basic Input Output System, BIOS),和新出现的统一可扩展固件接口(Unified Extensible Firmware Interface, UEFI), BIOS已经有些年头了,但是很简单,它可以支持1980年以后的所有x86机器,UEFI更现代化,具有更多的功能,但是设置起来也更复杂
大多数的x86系统都支持BIOS,包括最新的UEFI,UEFI可以模拟BIOS,这很棒,因为你可以在最近几个世纪的所有机器上使用相同的引导逻辑,但是,这种广泛的兼容性同时也是BIOS引导的最大缺点,因为在引导之前将CPU置于称为实模式的16位兼容模式,以便1980年代的古老引导程序仍然可以使用。
我们暂时采用phil-opp编写的bootimage(x86_64),后面我们会自己实现一个
在Cargo.toml中添加如下内容
1
2
3
4 1[dependencies]
2bootloader = "0.8.0"
3
4
只添加依赖不能创建一个Bootloader镜像,我们需要在bootloader编译完后链接到我们的内核,cargo不知支持后期制作脚本
为了解决这个问题,我们需要一个bootimage工具,该工具首先编译内核和引导程序,然后将它们链接在一起以创建可引导磁盘映像,执行以下命令来安装该工具
1
2
3 1 cargo install bootimage --version "^0.7.7"
2
3
^0.7.7叫做caret requirement, 它的含义是0.7.7版本或以后的版本,例如在现有的版本中发现一个bug并发布了0.7.8或0.7.9版本,则cargo将自动使用最新版本,只要它仍然是0.7.x版本即可
但是,它不会选择版本0.8.0,因为它不被认为兼容
请注意,默认情况下,Cargo.toml中的依赖项要求是脱字符,此规则应用于我们的引导加载程序依赖项
为了能够使用bootimage来编译bootloader,我们需要llvm-tools-preview组件可以通过执行rustup component add llvm-tools-preview来安装它(由于众所周知的原因,安装超级慢╮(╯▽╰)╭)
编译
cargo可以通过–target参数指定不同的系统,该参数使用target triple表示了CPU架构,发行商,系统和ABI
例如 我们的CPU结构是x86_64的发行商为苹果,系统是macos可以用以下形式表示
1
2
3 1x86_64-apple-darwin
2
3
然后我们就可以使用以下命令来编译了(我的系统是Ubuntu)
1
2
3 1cargo build --target x86_64-unknown-linux-gnu
2
3
现在我们只有一个参数命令就这么长了,后面参数还会添加许多参数,这样有些麻烦,我们可以写成shell脚本或使用make
LLVM
在编译之前我们需要了解以下LLVM的一些参数
Target Triple
使用target triple字符串描述要编译的目标机器语法格式如下
1
2
3
4 1ARCHITECTURE-VENDOR-OPERATING_SYSTEM
2ARCHITECTURE-VENDOR-OPERATING_SYSTEM-ENVIRONMENT
3
4
这样LLVM在编译的时候会将它传递到后端,以便LLVM为适当的体系结构生成代码
因为我们的程序要在裸机上直接运行,所以就不能使用本机系统的target triple我们可以使用llvm-target=x86_64-unknown-none即可,我们没有指定平台所以我们需要提供其他参数来告诉LLVM如何编译
Data Layout
LLVM需要大多数字段才能为该平台生成代码。例如data-layout字段定义各种整数,浮点数和指针类型的大小。
使用Data Layout指定特定于目标的数据布局字符串,该字符串指定如何在内存中布局数据,语法如下
布局规范由一系列规范列表组成,并用减号字符(’-’)分隔。每个规范都以字母开头,并且可以在字母之后包含其他信息,以定义数据布局的某些方面。规范的格式如下
E
指定目标以大端顺序格式存储数据
e
指定目标以小端顺序格式存储数据
S<size>
以位为单位指定堆栈的对齐方式,堆栈对齐必须是8位的倍数。
堆栈变量的对齐提升仅限于自然堆栈对齐,以避免动态堆栈重新对齐.
如果省略,则自然堆栈对齐方式默认为“未指定”,这不会阻止任何对齐方式提升
n<size1>:<size2>:<size3>…:
这为位目标CPU指定了一组本地整数宽度。例如,它可能包含32位PowerPC的n32,对于PowerPC 64是n32:64或对于X86-64是n8:16:32:64。这组元素被认为可以有效地支持大多数一般的算术运算。
i<size>:<abi>:<pref>
这指定了给定位的整数类型的对齐方式。值必须在[1,2^23)范围内。
f<size>:<abi>:<pref>:
这指定了给定<size>位的浮点类型的对齐方式。只有目标支持的<size>值才有效。所有目标都支持32(浮动)和64(双)。一些目标也支持80或128(不同长度的双重版本)
我们可以使用e表示我们的目标机器采用小端模式;
我们编译时的文件需要支持ELF因此我们使用了m:e;
紧接着我们的系统是64位因此我们需要指定int64类型的对齐方式i64:64;
指定完整数对齐方式后我们需要指定浮点数对齐方式f80:128
对于在n规则那里我们了解到X86-64 的本地整数宽度为n8:16:32:64我们直接使用即可
最后我们指定堆栈的自然对齐位数为128位S128
我们将上述定义的规则用-连在一起就成了这样
1
2
3 1e-m:e-i64:64-f80:128-n8:16:32:64-S128
2
3
我们暂时需要知道这两个就可以了,如果想看全部的内容可以参考LLVM参考手册
现在我们编译时指定的参数越来越多,幸运的是,rust可以使用json来指定连接参数,因此我们可以指定一个x86_64.json文件
1
2
3
4
5
6 1{
2 "llvm-target": "x86_64-unknown-none",
3 "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
4}
5
6
接下来我们还需要添加一些其他参数我们要编写的系统是x86_64的,我们需要添加如下内容
1
2
3 1"arch": "x86_64"
2
3
然后指定一下系统为小端序,指针占用大小,和int的占用大小(以C语言为准)
1
2
3
4
5 1"target-endian": "little",
2"target-pointer-width": "64",
3"target-c-int-width": "32",
4
5
紧接着我们在添加2个参数
1
2
3
4 1"os": "none",
2"executables": true
3
4
os表示要编译的系统,我们的程序会直接运行到裸机上,所以指定为none即可,当然我们的程序必须是可运行的
1
2
3
4 1"linker-flavor": "ld.lld",
2"linker": "rust-lld",
3
4
如果使用平台默认链接器可能不支持Linux targets,所以我们使用Rust附带的跨平台LLD链接器链接内核
1
2
3 1"panic-strategy": "abort",
2
3
此设置指定目标不支持在发生Panic时展开堆栈,因此,该程序应直接中止,这与我们之前在Cargo.toml文件中定义的panic = "abort"字段作用类似,我们可以从Cargo.toml从移除
注意,与Cargo.toml选项相反,此目标选项在我们稍后重新编译核心库时也适用。因此,即使保留Cargo.toml选项,也请确保添加此选项
1
2
3 1"disable-redzone": true,
2
3
因为我们在写操作系统,因此我们需要处理某些中断,为了安全地做到这一点,我们必须禁用某种称为“红色区域”的堆栈指针优化,否则会导致堆栈损坏,更多信息请查看disabling the red zone.
1
2
3 1"features": "-mmx,-sse,+soft-float",
2
3
features字段启用/禁用目标功能,我们通过在mmx和sse功能前面加上一个减号来禁用它们,并通过在其前面加上一个加号来启用soft-float功能
我们的整个项目文件的内容如下
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1#![no_main]
2#![no_std]
3
4use core::panic::PanicInfo;
5
6#[no_mangle]
7pub extern "C" fn _start() -> !{
8 loop {
9
10 }
11}
12
13#[panic_handler]
14fn panic(info: &PanicInfo) -> !{
15 loop{}
16}
17
18
x86_64.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1{
2 "llvm-target": "x86_64-unknown-none",
3 "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
4 "arch": "x86_64",
5 "target-endian": "little",
6 "target-pointer-width": "64",
7 "target-c-int-width": "32",
8 "os": "none",
9 "executables": true,
10 "linker-flavor": "ld.lld",
11 "linker": "rust-lld",
12 "panic-strategy": "abort",
13 "disable-redzone": true,
14 "features": "-mmx,-sse,+soft-float"
15}
16
17
Cargo.toml
1
2
3
4
5
6
7
8
9
10
11 1[package]
2name = "droll_os"
3version = "0.1.0"
4authors = ["admins <admins@qq.com>"]
5edition = "2018"
6
7# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
9[dependencies]
10
11
项目结构为
1
2
3
4
5
6
7
8
9
10 1droll_os
2├── Cargo.lock
3├── Cargo.toml
4├── src
5│ └── main.rs
6└── x86_64.json
7
81 directory, 4 files
9
10
重新编译core库
如果我们使用cargo build –target x86_64.json将会得到一个错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1admins@admins:~/droll_os$ cargo build --target x86_64.json
2 Compiling droll_os v0.1.0 (/home/admins/droll_os)
3error[E0463]: can't find crate for `core`
4 |
5 = note: the `x86_64-15483094206494168174` target may not be installed
6
7error: aborting due to previous error
8
9For more information about this error, try `rustc --explain E0463`.
10error: could not compile `droll_os`.
11
12To learn more, run the command again with --verbose.
13
14
15
这段错误信息告诉我们Rust找不到core库,该库包含基本的Rust类型,例如Result,Option和Iterators,并隐式链接到所有no_std crates
问题是核心库与Rust编译器一起作为预编译的库分发。因此,它仅对受支持的host triples(例如x86_64-unknown-linux-gnu)有效,而对我们的自定义目标无效。如果我们要为其他目标编译代码,则需要首先为这些目标重新编译核心。
我们将会使用xbuild,它是对cargo bbuild的一次封装,可自动交叉编译核心和其他内置库。我们可以通过执行以下命令进行安装(还是因为众所周知的原因。。这个也超慢。。)
1
2
3 1cargo install cargo-xbuild
2
3
现在我们可以使用xbuild来替换build(也需要下载。。。超慢。。)
1
2
3
4
5
6
7
8
9
10
11 1admins@admins:~/droll_os$ cargo xbuild --target x86_64.json
2
3Compiling core v0.0.0 (/…/rust/src/libcore)
4 Compiling compiler_builtins v0.1.5
5 Compiling rustc-std-workspace-core v1.0.0 (/…/rust/src/tools/rustc-std-workspace-core)
6 Compiling alloc v0.0.0 (/tmp/xargo.PB7fj9KZJhAI)
7 Finished release [optimized + debuginfo] target(s) in 45.18s
8 Compiling blog_os v0.1.0 (file:///…/blog_os)
9 Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
10
11
我们可以看到xbuild正在为我们的目标系统编译 core,compiler_builtin和alloc,由于这些库在内部使用了许多不稳定的功能,因此仅适用于nightly Rust编译器。最终xbuild构建好了我们所需要的carte
因为我们经常要使用该命令,我们可以创建一个.cargo/config文件,只适用于当前项目
.cargo/config
1
2
3
4
5 1# build settings
2[build]
3target = "x86-64.json"
4
5
目录结构为
1
2
3
4
5
6
7
8
9
10
11 1.
2├── .cargo
3│ └── config
4├── Cargo.lock
5├── Cargo.toml
6├── .gitignore
7├── src
8│ └── main.rs
9└── x86_64.json
10
11
这样我们可以省略–target参数最终可以使用cargo xbuild
忠告:千万不要没事干删除target目录下的内容。。。不然需要重新下载编译。。血的教训
运行
现在我们可以通过QEMU来运行
1
2
3 1qemu-system-x86_64 -drive format=raw,file=target/x86-64/debug/bootimage-kernel.bin
2
3
注意file文件的路径不要写错了,是执行cargo bootimage成功以后输出的路径
也可以将其写入USB并在真实计算机上引导它
1
2
3 1dd if=target/x86-64/debug/bootimage-kernel.bin of=/dev/sdX && sync
2
3
运行后将会弹出一个QEMU的黑窗口,里面什么都没有,当然了…我们没有编写任何可以显示字符的代码别担心我们将在下篇文章中编写显示字符的代码
下一步要做什么
在下一篇文章中我们通过往VGA写入数据可以在屏幕上显示字符,小伙伴们加油!