欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Rust开发操作系统(第一章建立基本项目)

程序员文章站 2022-07-05 18:15:21
...

基本介绍

在本章中我们将要搭建一个开发操作系统的基本环境,因为使用的是Rust开发的效率将会提高不少,《使用Rust开发系统》是一个系列,首先会使用简单的方式编写一个内核,在编写过程中涉及到理论将会开其他的系列来补充

需要用的知识

很可惜…编写操作系统需要基础的编程知识和一些操作系统原理,不过我们会用开一个系列来讲述这些原理知识(加油^_^)

项目环境

我们使用rust写操作系统时使用的环境如下()

操作系统

Linux version 5.0.0-27-generic ([email protected]) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #28~18.04.1-Ubuntu

RUST 编译器

rustc 1.41.0-nightly (ded5ee001 2019-11-13)

安装

rust(Linux):

Rust安装请看这里

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

nightly安装(需要安装rustup 一般安装完rust后自带的)

rustup install nightly

在安装过程中可能会出现错误(众所周知的原因-_-|)多试几次就好了
查看安装的编译器

$>> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

项目初始化

建立一个项目需要用到Cargo,Cargo在安装Rust时就会安装

通过Cargo提供的命令很容易创建一个Rust项目

[email protected]:~: cargo new droll_os --bin --edition=2018
    Created binary (application) `droll_os` package

上面的命令意思是使用Cargo创建一个新的项目,名字为droll_os(洋娃娃系统) --bin参数表示要创建一个二进制文件,--edition 2018 指定我们要使用2018版的Rust

现在目录结构为

droll_os
├── Cargo.toml
└── src
    ├── .gitignore
    └── main.rs

Cargo.toml包含crate所需的配置(比如项目信息,依赖关系等等),src/main.rs文件中包含main函数(程序主入口),你可以使用cargo build来编译droll_os并且会在droll_os下放生成target文件夹,该文件夹包含着编译的输出结果,编译成的二进制文件包在target/debug目录下

顺便提一下,使用Cargo创建项目的时候会顺便初始化成Git本地仓库,所以可以使用git status查看仓库的情况

[email protected]:~/droll_os$ git status
位于分支 master

尚无提交

未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)

        .gitignore
        .idea/
        Cargo.toml
        src/

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

如果你使用的IDEA或Clion,可以使用.gitignore忽略跟踪.idea目录,因此我们在.gitignore文件中添加以下内容。

# 忽略idea
.idea/*

如果有依赖关系的话,cargo会创建一个Cargo.lock文件来自动跟踪每个依赖关系,该文件不属于我们的代码所以加入到.gitignore文件中,至于target文件,我们后面需要编译rust的core库,时间可能会很长,编译生成的文件在target目录中,可以按照需求来添加。

*.lock

最终我们的.gitignore文件内容就是这样的

/target
**/*.rs.bk
# 忽略idea
.idea/*
*.lock

然后我们需要将其他文件添加到git进行跟踪

 [email protected]:~/droll_os: git add .gitignore Cargo.toml src/

再次查看

位于分支 master

尚无提交

要提交的变更:
  (使用 "git rm --cached <文件>..." 以取消暂存)

        新文件:   .gitignore
        新文件:   Cargo.toml
        新文件:   src/main.rs

这样我们的项目就初始化完成了。

我们需要rust的新特性,因此我们要使用nightly 版本的编译器,通过一下命令切换

[email protected]:~/droll_os: rustup override set nightly

准备工作

在进入内核之前我们需要做一些准备工作

现在我们的src/main.rs文件内容是这样的

fn main() {
    println!("Hello, world!");
}

这是一个正常的Rust入口,但是我们的程序是不依赖与系统的所以我们需要改变一下规则

添加no_std属性

因为我们的代码将会运行到无操作系统的机器上,我们的文件将会成这样

#![no_std]

fn main() {
    println!("Hello, world!");
}

因为我们声明了不使用std库,println!宏也无法使用

所以我们的文件将会成这样子

#![no_std]

fn main() {}

接着我们取运行代码

[email protected]:~/droll_os$ cargo run 
   Compiling droll_os v0.1.0 (/home/admins/droll_os)
error: `#[panic_handler]` function required, but not found

error: language item required, but not found: `eh_personality`

error: aborting due to 2 previous errors

error: could not compile `droll_os`.

To learn more, run the command again with --verbose.

现在编译器提示没有panic_handler函数和language item

panic_handler

panic_handler属性定义了当发生了panic时编译器需要调用的函数,但是在no_std环境下我们需要自己定义一个

虽然std库不能使用了但是我们可以使用编译器提供的core包

因此我们的文件增加以下代码

// 在main.rs文件中
use core::panic::PanicInfo;

// 该函数在panic时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

在PanicInfo参数中包含了发生异常的文件和行号,该函数应该没有返回值,因此将它标记为发散函数,目前我们无法在此函数中执行太多操作,因此我们可以无限循环

language item

在报错信息中我们看到这样一行error: language item required, but not found: 'eh_personality'

语言项(language item)是编译器内部所需的特殊功能和类型,例如:Copy trait是一种语言项,它告诉编译器哪些类型具有复制语义

在查看具体实现时,我们看到它具有特殊的#[lang =“ copy”]属性,该属性将其定义为语言项

语言项可以自己实现,但是不建议这样做,因为语言项高度不稳定甚至没有基础的类型检查,编译器甚至不会检查函数参数类型是否正确,幸运的是,有更稳定的方法来修复上述语言项错误。

eh_personality语言项目标记用于实现堆栈展开(unwinding)的功能,默认情况下,发生panic时,Rust使用展开来运行所有活动堆栈变量的析构函数,这样可以确保释放所有使用的内存,并允许父线程捕获panic并继续执行,展开是一个复杂的过程,需要一些特定于操作系统的库

在一些情景中不希望出现展开,Rust提供了一个选择,可以在panic时中止,这样会减小生成二进制的大小,我们可以在多个地方禁用展开功能。最简单的方法是将以下行添加到我们的Cargo.toml

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

这样为dev信息(使用cargo build命令)和release信息(使用cargo build --release)的panic策略设置为abort(终止)因此,现在不再需要eh_personality语言项

现在我们修复了之前的错误,我们再次执行命令会发生一下错误

[email protected]:~/droll_os$ cargo run 
   Compiling droll_os v0.1.0 (/home/admins/droll_os)
error: requires `start` lang_item

error: aborting due to previous error

error: could not compile `droll_os`.

To learn more, run the command again with --verbose.

现在我们的程序缺少定义主入口的 start语言项

一般情况下当你运行一个程序时main函数是第一个被调用的函数(程序的主入口),大多数语言都会有自己的运行时系统(比如Java,golang,Python)这样可以运行垃圾回收器或者协程调度,这样运行时将会在main函数执行前调用并初始化自己

在链接标准库的Rust二进制文件中,执行从名为crt0(“ C runtime zero”)的C运行时库开始,该库为C应用程序设置了环境包括设置堆栈,设置参数等等,然后C运行时会调用Rust的运行时,使用start语言项声明,Rust的运行时非常短,它只处理一些小事情,例如设置堆栈溢出防护或在panic情况下打印堆栈信息,然后,运行时最终调用main函数

我们的生成的可执行文件无权访问Rust运行时和crt0,因此我们需要定义自己的入口点,直接声明start语言项是无效的,因为它仍然需要crt0。我们需要直接覆盖crt0入口点。

为了告诉Rust编译器我们不想使用普通的入口点在文件的开头添加#![no_main]属性表示该程序不能使用普通的main函数作为主入口

现在文件的内容为

#![no_std]
#![no_main]

use core::panic::PanicInfo;


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

你会发现main函数不见了,因为main函数不在需要底层的运行时去调用它,我们可以为我们的操作系统重写入口点

#![no_std]
#![no_main]

use core::panic::PanicInfo;


#[no_mangle]
pub extern "C" fn _start() -> !{
    loop{}
}

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

使用#[no_mangle]禁用名称修饰,以确保Rust编译器确实输出名称为_start的函数,

如果没有该属性,则编译器将生成一些奇怪的_ZN3blog_os4_start7hb173fedf945531caE符号,为每个函数赋予唯一的名称,该属性是必需的,因为我们需要在下一步中将入口点函数的名称告知链接器。

我们还必须将函数标记为extern "C"告诉编译器该函数应使用C调用约定

那么为什么要使用_start作为函数名呢?

我们在linux写汇编程序的时候也是需要一个入口点,这个入口点为_start,如下代码

value:
	.int 1,2,3,4,5,6,7,8,9
.section .bss
	.lcomm string,9
.section .text
.global _start
_start:
	movl $9,%ecx
l:
	movl value(,%edi,4),%eax
	movl %eax,string(,%edi,4)

	movl $4,%eax
	movl $1,%ebx
	movl $string,%ecx
	movl $22,%edx
	int $0x80

	movl $1,%eax
	movl $0,%ebx
	int $0x80

这是一个汇编程序将会打印1-9的数字,我们可以看到.global _start,程序将会从_start标签开始执行,这是大多数系统的默认入口点名称。

_start()函数直接由操作系统或引导程序(bootloader)调用,该函数没有返回值,在操作系统中将会使用exit系统调用来退出,因为我们的程序直接运行在无操作系统的电脑上,因此最好的退出该函数的方法就是关机(笑)

现在我们cargo build一下,又出现了令人发指的错误。这是一个链接错误。

链接器是一个将生成的代码组合成可执行文件的程序,在不同的平台上提示的错误信息是不同的,但是基本上都是一个错误:链接器的默认配置假定我们的程序依赖于C运行时,尽管它没有依赖C运行时。

为了解决这个问题,我们需要告诉编译器我们的程序不需要依赖C运行时,我们可以指定特定的参数来告诉链接器或通过编译成裸机目标来实现。

通常情况下,rust编译器会根据你当前的系统来编译成可执行文件例如你在使用x86_64的Windows系统,rust会编译成适用于Windows64位的.exe可执行程序,为了描述不同的环境,rust会将环境描述成target triple字符串。你可以使用rustc --version --verbose来查看这些信息。

[email protected]:~/droll_os$ rustc --version --verbose
rustc 1.41.0-nightly (ded5ee001 2019-11-13)
binary: rustc
commit-hash: ded5ee0013f6126f885baf5e072c20ba8b86ee6a
commit-date: 2019-11-13
host: x86_64-unknown-linux-gnu
release: 1.41.0-nightly
LLVM version: 9.0

这是从Linux机器上输出的信息,可以看到host字段为x86_64-unknown-linux-gnu意味着当前系统的CPU架构为x86_64,提供商为unknown,操作系统为linux,ABI为gnu

Rust编译器和链接器假设存在底层操作系统(例如Linux或Windows)默认使用C运行时,所以连接器会出错

为了避免这个问题,我们可以编译成不依赖于底层操作系统的程序

例如裸机为thumbv7em-none-eabihf用与表示嵌入式ARM系统,其他细节暂时忽略,最重要的是它不需要系统的依赖,为了能够为此目标进行编译,我们需要将其添加到rustup中

rustup target add thumbv7em-none-eabihf

这会下载系统的标准(和核心)库的副本。现在,我们可以为此目标构建独立的可执行文件

cargo build --target thumbv7em-none-eabihf

通过--target参数我们交叉编译了适用于裸机可执行文件,由于目标系统没有操作系统,因此链接程序不会尝试链接C运行时,并且我们的构建成功而没有任何链接程序错误。

我们将使用x86_64裸机环境来代替thumbv7em-none-eabihf

参数

现在,我们讨论在Linux,Windows和macOS上发生的链接器错误,并说明如何通过将其他参数传递给链接器来解决它们。请注意,操作系统之间的可执行文件格式和链接器有所不同,因此每个系统都需要不同的参数集。

现在我们的文件内容如下

src/main.rs

#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn _start() -> !{
    loop {

    }
}

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

Cargo.toml

[package]
name = "droll_os"
version = "0.1.0"
authors = ["amdins <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[dependencies]

现在我们可以直接可以使用cargo build --target thumbv7em-none-eabihf来编译或者我们可以根据平台的不同使用不同的参数

# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"

请注意,这只是Rust二进制文件的基础示例。如果文件要做更多操作,例如,在调用_start函数时初始化堆栈。则可能需要更多步骤

下一步要做什么

在下一篇文章中我们要做一些系统启动前的一些准备工作,并使用phil-opp编写的bootimage启动并加载我们的第一个内核,以后我们可能需要自己实现一个bootimage,为了方便期间先用大佬们写好的(笑)