本章最主要的目的是让应用和硬件分离,对访问硬件进行比较初级的抽象。
今天或许短暂,但过去即是永恒。
环境
Ubuntu18.04+vscode+docker+qemu5.0.0
toolchain: rustc 1.55.0-nightly
qemu能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。
这一节使用的都是qemu的User Mode模式,用户态模拟,不是完全的裸机。
老规矩,从”hello,world“开始。
当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译(编译器)、运行(操作系统),终于在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大門已经打开。
但现在这个“hello,world”需要在裸机的环境上实现。
操作系统虽然是软件,但它需要运行在裸机环境下。如果采取一般的开发形式是无法开发出OS的,其中一个重要的原因编译器编译出的应用软件在缺省情况下是要链接标准库(Rust编译器和C编译器都是这样的),而标准库是依赖于操作系统。所以需要跳出一些思维定式。
先以Rust工程为例来说明背后应用程序的执行环境和平台支持,然后一步一步移除。
$ cargo new os --bin
创建工程目录,传递了bin参数,意为直接创建一个可执行项目而不是库。
os
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
Cargo.toml 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。源码目录在src下,同时还会顺便初始化成Git本地仓库,所以可以使用git status查看仓库的情况
应用程序执行环境栈如下图所示:
graph LR
应用程序-- 函数调用 -->标准库-- 系统调用 --> 内核/操作系统-- 指令集 -->硬件平台
标准库等算是应用程序的执行环境。在我们通常不会注意到的地方,它们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。
从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的系统调用来实现。
从硬件的角度来看,它上面的一切都属于软件。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套指令集体系结构,使得软件可以通过ISA中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令。
上述就是层层抽象的过程,让我们以最小的代价使用硬件从而实现功能。
我们的应用位于最上层,通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成很多功能。
fn main() {
println!("Hello, world!");
}
在打印Hello, world!时使用的println!宏正是由Rust标准库std和GNU Libc库等提供的。
我们可以使用strace工具来观察运行程序并输出程序运行过程中向内核请求的所有的系统调用及其返回值。
$ strace target/debug/os
返回值如下
execve("target/debug/os", ["target/debug/os"], 0x7ffc064f9700 /* 75 vars */) = 0
brk(NULL) = 0x559269507000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
fstat(6, {st_mode=S_IFREG|0644, st_size=90147, ...}) = 0
mmap(NULL, 90147, PROT_READ, MAP_PRIVATE, 6, 0) = 0x7f9fe8233000
close(6) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 6
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300*\0\0\0\0\0\0"..., 832) = 832
fstat(6, {st_mode=S_IFREG|0644, st_size=96616, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9fe8231000
mmap(NULL, 2192432, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 6, 0) = 0x7f9fe7e09000
mprotect(0x7f9fe7e20000, 2093056, PROT_NONE) = 0
mmap(0x7f9fe801f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 6, 0x16000) = 0x7f9fe801f000
close(6) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/librt.so.1", O_RDONLY|O_CLOEXEC) = 6
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\"\0\0\0\0\0\0"..., 832) = 832
fstat(6, {st_mode=S_IFREG|0644, st_size=31680, ...}) = 0
mmap(NULL, 2128864, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 6, 0) = 0x7f9fe7c01000
mprotect(0x7f9fe7c08000, 2093056, PROT_NONE) = 0
mmap(0x7f9fe7e07000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 6, 0x6000) = 0x7f9fe7e07000
close(6) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 6
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000b\0\0\0\0\0\0"..., 832) = 832
fstat(6, {st_mode=S_IFREG|0755, st_size=144976, ...}) = 0
mmap(NULL, 2221184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 6, 0) = 0x7f9fe79e2000
mprotect(0x7f9fe79fc000, 2093056, PROT_NONE) = 0
mmap(0x7f9fe7bfb000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 6, 0x19000) = 0x7f9fe7bfb000
mmap(0x7f9fe7bfd000, 13440, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9fe7bfd000
close(6) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 6
read(6, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\16\0\0\0\0\0\0"..., 832) = 832
fstat(6, {st_mode=S_IFREG|0644, st_size=14560, ...}) = 0
mmap(NULL, 2109712, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 6, 0) = 0x7f9fe77de000
mprotect(0x7f9fe77e1000, 2093056, PROT_NONE) = 0
mmap(0x7f9fe79e0000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 6, 0x2000) = 0x7f9fe79e0000
close(6) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 6
read(6, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
fstat(6, {st_mode=S_IFREG|0755, st_size=2030928, ...}) = 0
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 6, 0) = 0x7f9fe73ed000
mprotect(0x7f9fe75d4000, 2097152, PROT_NONE) = 0
mmap(0x7f9fe77d4000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 6, 0x1e7000) = 0x7f9fe77d4000
mmap(0x7f9fe77da000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9fe77da000
close(6) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9fe822f000
arch_prctl(ARCH_SET_FS, 0x7f9fe8230280) = 0
mprotect(0x7f9fe77d4000, 16384, PROT_READ) = 0
mprotect(0x7f9fe79e0000, 4096, PROT_READ) = 0
mprotect(0x7f9fe7bfb000, 4096, PROT_READ) = 0
mprotect(0x7f9fe7e07000, 4096, PROT_READ) = 0
mprotect(0x7f9fe801f000, 4096, PROT_READ) = 0
mprotect(0x55926920c000, 12288, PROT_READ) = 0
mprotect(0x7f9fe824a000, 4096, PROT_READ) = 0
munmap(0x7f9fe8233000, 90147) = 0
set_tid_address(0x7f9fe8230550) = 6172
set_robust_list(0x7f9fe8230560, 24) = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7f9fe79e7cb0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f9fe79f4980}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f9fe79e7d50, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f9fe79f4980}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
poll([{fd=0, events=0}, {fd=1, events=0}, {fd=2, events=0}], 3, 0) = 0 (Timeout)
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[PIPE], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f9fe742c040}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
brk(NULL) = 0x559269507000
brk(0x559269528000) = 0x559269528000
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 6
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
fstat(6, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(6, "559268fca000-55926900c000 r-xp 0"..., 1024) = 1024
read(6, "b/x86_64-linux-gnu/libdl-2.27.so"..., 1024) = 1024
read(6, "48 /lib/x86_"..., 1024) = 1024
read(6, "97 /lib/x86_"..., 1024) = 430
close(6) = 0
sched_getaffinity(6172, 32, [0, 1]) = 16
rt_sigaction(SIGSEGV, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGSEGV, {sa_handler=0x559268fe5d10, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK|SA_SIGINFO, sa_restorer=0x7f9fe79f4980}, NULL, 8) = 0
rt_sigaction(SIGBUS, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGBUS, {sa_handler=0x559268fe5d10, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK|SA_SIGINFO, sa_restorer=0x7f9fe79f4980}, NULL, 8) = 0
sigaltstack(NULL, {ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9fe8247000
mprotect(0x7f9fe8247000, 4096, PROT_NONE) = 0
sigaltstack({ss_sp=0x7f9fe8248000, ss_flags=0, ss_size=8192}, NULL) = 0
write(1, "Hello, world!\n", 14Hello, world!
) = 14
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=8192}, NULL) = 0
munmap(0x7f9fe8247000, 12288) = 0
exit_group(0) = ?
+++ exited with 0 +++
与打印Hello,world!相关的仅有两个系统调用,其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。
write(1, "Hello, world!\n", 14Hello, world!
) = 14
exit_group(0)
对于用某种编程语言实现的应用程序源代码而言,编译器在将其通过编译、链接得到可执行文件的时候需要知道程序要在哪个平台上运行。这里平台主要是指CPU类型、操作系统类型和标准运行时库的组合。
$ rustc --version --verboserustc 1.49.0 (e1884a8e3 2020-12-29)
binary: rustc
commit-hash: e1884a8e3c3e813aada8254edfa120e85bf5ffca
commit-date: 2020-12-29
host: x86_64-unknown-linux-gnu
release: 1.49.0
从上可以得知,host 一项可以看出默认的目标平台是x86_64-unknown-linux-gnu,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是gnu libc(封装了Linux系统调用,并提供POSIX接口为主的函数库)。
现在,我们的目标是在另一个硬件平台上运行Hello, world!,CPU架构将从x86_64换成RISC-V。
Rust有一个对std裁剪过后的核心库core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了Rust语言相当一部分的核心机制,可以满足我们的大部分需求。Rust语言是一种面向系统(包括操作系统)开发的语言,所以在Rust语言生态中,有很多三方库也不依赖标准库std而仅仅依赖核心库core。对它们的使用可以很大程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗。
我们先构造一个小的执行环境,移除上文程序对Rust std标准库的依赖,使得它能够编译到裸机平台RV64GC或Linux-RV64上。
首先先将riscv64gc设为目标平台,准备交叉编译。
如果无此std,可以通过下列命令添加
rustup target add riscv64gc-unknown-none-elf
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
添加#![no_std]属性
#![no_std]
fn main() {}
不声明std,println!也无法使用,所以main.rs精简为上面的两行。
编译,报错。程序需要panic处理错误,所以无法编译。
$ cargo build
Compiling test123 v0.1.0 (/home/oslab/rCore2021/rCore-practice/test123)
error: `#[panic_handler]` function required, but not found
error: aborting due to previous error
error: could not compile `test123`
核心库中没有对panic!宏的实现,但是保留着一个PanicInfo的不可变引用。我们需要在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的位置。所以需要额外编写模块,自定义语义项#[panic_handler],将PanicInfo作为输入参数。在PanicInfo参数中包含了发生异常的文件和行号,该函数应该没有返回值,因此将它标记为发散函数,目前我们无法在此函数中执行太多操作,因此我们可以无限循环。
此外,前文也提到标准库作为执行环境,也负责初始化工作,再跳转到应用程序的入口点(main)。然而现在禁用了标准库,所以加入#![no_main]告诉编译器没有一般意义上的main函数,现在就无需初始化操作。
main.rs
#![no_std]
#![no_main]
mod lang_items;
lang_items.rs
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
目前panic_handler内部仅仅是一直循环。之后我们再进一步实现功能。
编译成功。
如果不加#![no_main]属性,则会编译出错,因为程序找不到入口函数。一般情况下当运行一个程序时main函数是第一个被调用的函数(程序的主入口)。大多数语言都会有自己的运行时系统,事先对垃圾回收,进程调度等进行初始化,之后执行main函数。
在链接标准库的Rust二进制文件中,会执行名为crt0(“ C runtime zero”)的C运行时库。该库为C应用程序设置环境,然后C运行时库会调用Rust的运行时库,使用start语言项声明。Rust的运行时库非常短,它只处理一些小事,例如设置堆栈溢出防护或在panic情况下打印堆栈信息等。运行时最终调用main函数。
目前生成的可执行文件无权访问Rust运行时库和crt0,因此我们需要定义自己的入口点,直接声明start语言项是无效的,因为它仍然需要crt0。我们需要直接覆盖crt0入口点。
分析一下可执行文件,依次查看一下文件格式、文件头信息,再查看汇编。
$ 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
$ 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: 0x1110
Flags [ (0x1)
EF_RISCV_RVC (0x1)
]
HeaderSize: 64
ProgramHeaderEntrySize: 56
ProgramHeaderCount: 3
SectionHeaderEntrySize: 64
SectionHeaderCount: 14
StringTableSectionIndex: 12
}
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
现在得到了一个合法的RV64执行程序,但是文件头信息里Entry为0,并无反汇编代码生成。说明这完全是个空壳。因为我们没有添加能让编译器找到的入口函数。
之前已经设计实现了一个最小执行环境来支持最简单的用户态程序,这里我们添加入口函数。使用#[no_mangle]禁用名称修饰,以确保Rust编译器确实输出名称为_start的函数。_
如果没有#[no_mangle]属性,则编译器将生成一些奇怪的符号,为每个函数赋予唯一的名称。所以该属性是必需的,因为我们需要在下一步中将入口点函数的名称告知链接器。此外,还必须将函数标记为extern “C”以告知编译器,该函数应使用C调用约定。
main.rs
#![no_std]
#![no_main]
mod lang_items;
#[no_mangle]
extern "C" fn _start() {
loop{};
}
$ 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
$ 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: 0x11120
ProgramHeaderOffset: 0x40
SectionHeaderOffset: 0x13B0
Flags [ (0x1)
EF_RISCV_RVC (0x1)
]
HeaderSize: 64
ProgramHeaderEntrySize: 56
ProgramHeaderCount: 4
SectionHeaderEntrySize: 64
SectionHeaderCount: 15
StringTableSectionIndex: 13
}
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
Disassembly of section .text:
0000000000011120 <_start>:
; loop{};
11120: 09 a0 j 2 <_start+0x2>
11122: 01 a0 j 0 <_start+0x2>
这次可以发现,入口地址0x11120,是合法的地址。但是因为程序的逻辑是执行死循环,所以是死循环的汇编代码,第一条地址和入口地址一致。如果将循环语句注释掉,则可以看到ret。
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
Disassembly of section .text:
0000000000011120 <_start>:
; }
11120: 82 80 ret
不过当执行时是跑不通的
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/riscv64gc-unknown-none-elf/debug/os`
target/riscv64gc-unknown-none-elf/debug/os: 1: target/riscv64gc-unknown-none-elf/debug/os: Syntax error: word unexpected (expecting ")")
在qemu中更是引发崩溃(段错误,核心已转储)
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
Segmentation fault (core dumped)
这是交叉编译常会出现的错误,错误类型预示着程序不完整。根本原因是因为这个执行环境缺少退出机制。操作系统会提供一个退出的系统调用服务接口,当程序调用就会正常退出。
首先封装对EXIT的系统调用,ID即为SYSCALL_WRITE。
#![feature(llvm_asm)]
#![no_std]
#![no_main]
mod lang_items;
const SYSCALL_EXIT: usize = 93;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
llvm_asm!("ecall"
: "={x10}" (ret)
: "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id)
: "memory"
: "volatile"
);
}
ret
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
#[no_mangle]
extern "C" fn _start() {
sys_exit(9);
}
feature只在nightly版本里有,全程使用的Rust都应该是nightly。而且需要放在程序的最前面。
$ rustup default nightly
这样会安装并将nightly设置为默认。
返回的结果确实是9。现在我们在没有任何显示功能的情况下,勉强完成了一个简陋的用户态最小化执行环境。
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
9
下面添加支持显示的模块。Rust的core库内建了以一系列帮助实现显示字符的基本Trait和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 println!功能。
过程和EXIT差不多。不过这里需要先导入fmt,迁移self和Write的作用域。
use core::fmt::{self, Write};
然后实现基于Write Trait的数据结构,完成Write Trait所需要的write_str函数,并用print函数进行包装。
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
sys_write(1, s.as_bytes());
Ok(())
}
}
pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
最后格式化宏
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
我们print一下Hello, world!
#[no_mangle]
extern "C" fn _start() {
println!("Hello, world!");
sys_exit(9);
}
main.rs
#![feature(llvm_asm)]
#![no_std]
#![no_main]
mod lang_items;
use core::fmt::{self, Write};
const SYSCALL_EXIT: usize = 93;
const SYSCALL_WRITE: usize = 64;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
llvm_asm!("ecall"
: "={x10}" (ret)
: "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id)
: "memory"
: "volatile"
);
}
ret
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
sys_write(1, s.as_bytes());
Ok(())
}
}
pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
#[no_mangle]
extern "C" fn _start() {
println!("Hello, world!");
sys_exit(9);
}
程序非常简陋,用户态执行环境和应用程序都放在一个文件里面。后面会重构。
重新编译
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
Hello, world!
9
到目前为止都不算是裸机程序,都还算用户态程序。之所以能调用Linux的系统调用,原因在与我们使用的qemu-riscv是user mode的模拟器,从某种程度上我们可以理解为我们的代码跑在Riscv版的Linux上。实际上如果我们使用qemu-system-riscv,是无法运行这个程序的,只有真正的“裸机”代码才能跑在上面。在操作系统支持下,实现一个基本的用户态执行环境比较容易。操作系统会帮助用户态执行环境完成了程序加载、程序退出、资源分配、资源回收等各种琐事。如果没有操作系统,那么实现一个支持在裸机上运行应用程序的执行环境,就要考虑更多的事情了。这些是后话。
2021/7/5