Project RC

一种标准阿西的设计与实现。

不要在 C++ 基类构造函数中创建新线程

创建于
分类:Dev
标签:C++多线程基类构造函数

这个问题源于之前的另一篇文章 移植树莓派驱动框架 Circle 到自制操作系统 所做的事情,移植 Circle 驱动框架的过程中有一步需要用我们的 OS 的用户态设施(其实就是 pthread API)实现 Circle 的 CTask 类,这个类是 Circle 中的线程抽象,移植后一开始可以工作,之后频繁出现执行到基类的虚函数的情况,debug 之后在这里总结一下,顺便也从反汇编的角度整理一下 C++ 虚函数的实现方式。

简化问题

当时遇到问题时的代码可以简化为如下(pthread 使用 std::thread 代替了,效果一样):

#include <cassert>
#include <chrono>
#include <iostream>
#include <thread>

using namespace std;
using namespace std::literals::chrono_literals;

struct Task {
    thread t;

    Task() {
        t = thread(task_entry, this);
        this_thread::sleep_for(500ms); // 这里 sleep 是为了稳定复现 data race
    }

    virtual ~Task() = default;

    virtual void run() {
        assert(false); // 不应该执行到这里
    }

    void join() {
        if (t.joinable()) t.join();
    }

    static void task_entry(Task *task) {
        cout << "before run" << endl;
        task->run();
        cout << "after run" << endl;
    }
};

struct TaskImpl : Task {
    void run() override {
        cout << "TaskImpl run!" << endl;
    }
};

int main() {
    Task *t1 = new TaskImpl;
    t1->join();
    return 0;
}

这里的基本逻辑是,在 new TaskImpl 创建一个 Task 子类时,会开一个线程来执行这个 task,具体的就是运行 run 方法。理想情况下,由于 C++ 所支持的运行期多态,Task::task_entry 拿到的 task 参数实际上是一个 TaskImpl,对它调用 run 应该会动态地分发到 TaskImpl::run,但实际上上面的代码并不能稳定工作,报错如下:

before run
Assertion failed: (false), function run, file thread.cpp, line 20.
[1]    22139 abort      ./test

因为 thread(task_entry, this) 创建的新线程被调度的时候,TaskImpl 对象可能还没创建完,于是虚函数表指针可能还没有指向预期的虚表。

多态的实现

去掉那行 sleep_for,编译之后看反汇编的结果(以 x86_64 举例),截取 Task::task_entry(Task*) 中调用 run 的部分如下:

Task::task_entry(Task*):
                  # ......
0000000100001d5f  movq  %rdi, -0x8(%rbp)
                  # ......
0000000100001d81  movq  -0x8(%rbp), %rcx
0000000100001d85  movq  (%rcx), %rdx
0000000100001d88  movq  %rcx, %rdi
0000000100001d8b  movq  %rax, -0x10(%rbp)
0000000100001d8f  callq *0x10(%rdx)
                  # ......

1d5f 这一行中,Task *task 参数在 %rdi,之后在 1d81 行被挪到了 %rcx1d85 行取该指针指向的对象的最开头 8 个字节放到 %rdx,这就是虚函数表指针;然后又把 %rcx 放到 %rdi,即为 run 方法的隐藏参数 this 指针;接着 1d8f 行调用 %rdx 指向的虚函数表的第 0x10 字节处的虚函数指针。

搞清楚了虚函数如何被调用,再看下虚函数表指针是怎么设置的,截取 TaskTaskImpl 构造函数的一部分如下:

TaskImpl::TaskImpl():
                  # ......
0000000100001bda  callq Task::Task()
0000000100001bdf  movq  0x243a(%rip), %rax
0000000100001be6  addq  $0x10, %rax
0000000100001bec  movq  -0x10(%rbp), %rcx
0000000100001bf0  movq  %rax, (%rcx)
                  # ......

Task::Task():
                  # ......
0000000100001c08  movq  %rdi, -0x8(%rbp)
0000000100001c0c  movq  -0x8(%rbp), %rax
0000000100001c10  movq  0x2401(%rip), %rcx
0000000100001c17  addq  $0x10, %rcx
0000000100001c1b  movq  %rcx, (%rax)
                  # ......
0000000100001c4c  callq std::__1::thread::thread<void (&)(Task*), Task*, void>(void (&)(Task*), Task*&&)
                  # ......

可以看到 TaskImpl::TaskImpl 首先调用了 Task::Task,后者的最开头首先设置了 this 的虚函数表指针(1c101c1b 行),这时的虚函数表指针是指向 Task 类的虚函数表;接着 Task::Task 调用 thread::thread 创建新线程,返回;回到 TaskImpl::TaskImpl,再次设置了 this 的虚函数表指针(1bdf1bf0 行),这次指向的是 TaskImpl 类的虚表。

于是很容易发现,创建新线程发生在 this 的虚表指针指向 TaskImpl 虚表之前,如果新线程很快被调度,会导致在里面调用 run 方法实际运行的是 Task::run

教训

这次的 bug 再次强调了多线程编程的易错性,尤其在 C++ 的构造函数中,一定要尽量避免多线程,如果真的要多线程,也一定不能在新创建的线程中访问正在创建的对象 this 指针。

不要在 C++ 基类构造函数中创建新线程

在 QEMU 上使用 U-Boot 启动自制内核

创建于
分类:Dev
标签:QEMUU-BootOSARMAArch64

为了简单了解 U-Boot 的使用,花了些时间尝试在 QEMU 的 arm64 virt 虚拟平台上使用 U-Boot 启动自制 OS 内核,这里记录一下过程,以便以后查阅。

准备 OS 内核

要想运行,肯定得先有个内核,于是搞了个极简的 helloworld:qemu-virt-hello,能够启用 ARM timer,然后打印 tick。写这个 helloworld 的时候是直接用 QEMU 的 -kernel 参数传入 ELF 格式的内核来测试运行的,所以其实这次用 U-Boot 运行纯属学习目的,本身并没有简化什么。

上述内核编译完成后得到 ELF 格式的 build/kernel.img 和 objcopy 后的纯二进制的 build/kernel.bin

然后需要使用 mkimage 命令(Ubuntu 上需安装 u-boot-tools 包)生成 U-Boot 能够识别的 image 文件:

mkimage -A arm64 -C none -T kernel -a 0x40000000 -e 0x40000000 -n qemu-virt-hello -d build/kernel.bin uImage

生成的 uImage 文件即所需的 image。

编译 U-Boot

使用下面命令下载和编译 U-Boot:

git clone git@github.com:u-boot/u-boot.git --depth 1
cd u-boot

make qemu_arm64_defconfig # 生成针对 QEMU virt 的 config
make -j16 CROSS_COMPILE=aarch64-linux-gnu- # 使用 CROSS_COMPILE 指定的工具链构建

完成之后 u-boot 目录下会生成一个 u-boot.bin 文件,这是 U-Boot 的可直接执行的纯二进制格式,使用下面命令检查是否可以正常进入 U-Boot:

qemu-system-aarch64 -machine virt -cpu cortex-a57 -bios u-boot.bin -nographic

进去之后会有个 autoboot 倒计时,按任意键之后会结束倒计时,到 U-Boot 命令行。

准备 Device Tree Blob

虽然按理说 U-Boot 可以不指定设备树直接启动不需要设备树的 kernel,但我这边一直不能成功,还没搞懂为什么,所以还是先准备一个设备树。

前面编写测试内核时针对的 QEMU 虚拟平台参数是 -machine virt -cpu cortex-a57 -smp 1 -m 2G,所以这里可以使用下面命令来 dump 出设备树:

qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a57 -smp 1 -m 2G -nographic

这会在当前文件夹生成 virt.dtb 文件。

构造 Flash Image

QEMU virt 平台有两个 flash 区域,分别是 0x0000_0000~0x0400_0000 和 0x0400_0000~0x0800_0000,U-Boot 本身被放在前一个 flash,我们可以通过 QEMU 参数 -drive if=pflash,format=raw,index=1,file=/path/to/flash.img 参数传入一个原始二进制格式的 image 文件来作为后一个 flash。

这里为了简单起见,使用 fallocate 和 cat 简单地把前面得到的 uImagevirt.dtb 拼在一起:

# 把 uImage 和 virt.dtb 分别扩展到 32M
fallocate -l 32M uImage
fallocate -l 32M virt.dtb

# 拼接
cat uImage virt.dtb > flash.img

运行

使用下面命令运行 QEMU 并进入 U-Boot 命令行:

qemu-system-aarch64 -nographic \
    -machine virt -cpu cortex-a57 -smp 1 -m 2G \
    -bios u-boot.bin \
    -drive if=pflash,format=raw,index=1,file=flash.img

使用 flinfo 命令可以查看 flash 信息。由于前面在制作 flash.img 时简单的拼接了 uImagevirt.dtb,因此现在 uImage 在 0x0400_0000 位置,virt.dtb 在 0x0600_0000 位置。

使用 iminfo 0x04000000 可以显示位于 0x0400_0000 的 uImage 信息,大致如下:

=> iminfo 0x04000000

## Checking Image at 04000000 ...
   Legacy image found
   Image Name:   qemu-virt-hello
   Created:      2021-02-22  15:54:06 UTC
   Image Type:   AArch64 Linux Kernel Image (uncompressed)
   Data Size:    12416 Bytes = 12.1 KiB
   Load Address: 40000000
   Entry Point:  40000000
   Verifying Checksum ... OK

使用 fdt addr 0x06000000fdt print / 可以检查设备树是否正确,大致输出如下:

=> fdt addr 0x06000000
=> fdt print /
/ {
    interrupt-parent = <0x00008001>;
    #size-cells = <0x00000002>;
...

确认无误之后,使用 bootm 0x04000000 - 0x06000000 命令即可运行内核,如下:

=> bootm 0x04000000 - 0x06000000
## Booting kernel from Legacy Image at 04000000 ...
   Image Name:   qemu-virt-hello
   Created:      2021-02-22  15:54:06 UTC
   Image Type:   AArch64 Linux Kernel Image (uncompressed)
   Data Size:    12416 Bytes = 12.1 KiB
   Load Address: 40000000
   Entry Point:  40000000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 06000000
   Booting using the fdt blob at 0x6000000
   Loading Kernel Image
   Loading Device Tree to 00000000bede5000, end 00000000bede9cdb ... OK

Starting kernel ...

Booting...
...

参考资料

在 QEMU 上使用 U-Boot 启动自制内核

创建有多个分区的 img 文件并格式化

创建于
分类:Dev
标签:文件系统分区OS

在 QEMU 中运行 OS 并需要模拟从 SD 卡读取文件时,可以通过 -drive file=/path/to/sdcard.img,if=sd,format=raw 参数向虚拟机提供 SD 卡,这里记录一下制作 sdcard.img 的过程。

首先需要创建一个空的映像文件,可以使用 ddfallocate,这里以 fallocate 为例:

fallocate -l 200M sdcard.img

这里 200M 指的是这个映像文件占 200 MB,也就相当于是一个 200 MB 的 SD 卡。

然后使用 fdisk 对其进行分区,直接 fdisk sdcard.img 之后操作即可,比如这里分了两个分区:

Command (m for help): p
Disk sdcard.img: 200 MiB, 209715200 bytes, 409600 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb11e63d6

Device      Boot  Start    End Sectors  Size Id Type
sdcard.img1        2048 206847  204800  100M  c W95 FAT32 (LBA)
sdcard.img2      206848 409599  202752   99M 83 Linux

下一步就是要格式化成期望的文件系统,在这之前需要先把 sdcard.img 虚拟成一个块设备,通过如下命令做到:

losetup --partscan --show --find sdcard.img

成功后会输出一个设备路径,比如 /dev/loop0,与此同时还有两个分区 /dev/loop0p1/dev/loop0p2。之后使用形如下面的命令格式化分区:

mkfs.fat -F32 /dev/loop0p1

然后移除 loop 设备:

losetup -d /dev/loop0

到这里 sdcard.img 就制作完成了。

参考资料

创建有多个分区的 img 文件并格式化

2020 → 2021

创建于
分类:Misc
标签:年度总结

虽然现在已经是 2021 年的第二个月中旬了,却还是觉得需要补一个年度总结。而且越来越觉得,农历新年比元旦更适合用来总结过去的一年,因为往往这时候结束了过去一年的学习和工作,吃了年夜饭,看完了春晚,才觉得好像过去一年才算真的过去,身心也终于得到真正的放松。相比起来,元旦短短的假期,最多能匆匆忙忙出去吃一两顿好的,总还是缺了点“新年”的味道。

无聊的本科生活的落幕

回首 2020 年,最显然的一件事就是大学毕业,糊完了极其坑爹的杰普公司校企合作培训和毕业设计,终于可以离开那个浪费了我四年最好时光的学校。无聊的课堂教学、敷衍的课程实验、落后的课本,真的令人失望,当然了,这只能怪我自己初中和高中没有努力接受应试教育。

不过,大学期间也算是做了些积极的尝试,比如试图搞开源软件协会来促进计算机社团水平的发展,也尝试给大一新生做了 Python 相关的培训,都是一些有趣的经历。

另一方面,如果说我的大学还有什么值得怀念的话,那就只剩下自由的课余时间了吧。那个时候,可以随便翘课,整天待在宿舍学自己想学的东西,写自己想写的项目,几乎一切时间安排都是自由的,现在那样的状态已经一去不复返了。

研究生之梦的实现和趋于平淡

去年的另一件大事就是考研上岸,经历了出分前的紧张、出分后的激动、机试的完美感觉、面试前的浪、面试后的自我怀疑,最后听到海波老师在电话里的“欢迎加入 IPADS”,证明了为此付出的十个月没有白费。最终的初试+复试总成绩排名第一,似乎是很值得吹的了。

但对研究生生活的热情却逐渐变淡了。开学前的暑期培训时候还是非常有激情的,提前加入项目后更是晚上常常肝到 12 点之后,刚开学的一段时间也每天晚上基本都 11 点才从实验室回宿舍。可是渐渐地开始怀疑了,觉得好像实验室的项目也没有那么有意思,虽然对操作系统真的很感兴趣,但被分配任务去按部就班地完成好像终究缺了点乐趣。总结来说给的补贴太少,占用的时间太多,写的是屎山代码,又没有什么自由发挥的空间,有点憋屈。于是当初那个幻想着“为中国操作系统事业添砖加瓦、为中华民族伟大复兴尽一份力”的自己,逐渐开始变得现实了,现在只想在实验室规定的工作时间干活,其它时间都用来做自己的事,或者写写个人项目,或者出去耍一耍放松,总之是不想碰实验室的事情了。

曾经希望能搞点科研,现在也觉得好像很无趣,其实现在只能打廉价劳动力的工,并没有机会参与科研,但也并不期待。现在只想安安静静地混个毕业,期间能多点时间学自己想学的东西,最后找个还算不错的工作,也就足够了。我终究不是一个能够无私奉献自己的人。谁又是呢。

QQ 机器人

暑假的时候还有一件算蛮大的事,就是 CKYU 关闭,起因是晨风机器人作者据传被公安局喝茶,然后 CKYU 作者为了避免遭殃,主动撤了。其实这说起来非常好笑,本来就是灰色产业,为了避险,不再做了,明明很正常的事情,网上居然有人称之为“8.2 事件”,然后各种讲情怀,说得好像是什么不得了的情况一样。

CKYU 关了之后,CQHTTP 也就没用了,其实本来也没什么精力维护了,想着趁机也就不再管了。结果后来广大用户们在其它机器人平台发明了各种轮子,有的兼容 CQHTTP API,有的甚至可以直接加载 CQHTTP 插件。于是最后还是决定再做一件事情,就是把 CQHTTP 的文档重新整理了一下,做成了“OneBot 聊天机器人接口标准”,整理到最后甚至来了兴致,觉得把这玩意就作为接口标准继续维护也不错,可以推动各兼容项目长久保持兼容。可是这个兴致终究抵不过实验室各种事情的忙碌,最后也就没时间顾及了。

现在 OneBot 标准依然是许多项目开发时的参考文档,但未来何去何从,已经没有明确的方向了。

操作系统

虽然说前面吐槽了有些时候对实验室项目感到无趣,但写操作系统这件事本身一直是很有意思的,当不小心把精力投入到实验室的 OS 项目里之后,也常常会忘记自己已经说过要“堕落”。其实以前就一直梦想着以后写一个自己的 OS,去年经过 μCore 和 ChCore 实验的练习,以及翻阅了些 Linux 源码,似乎已经具备了写 OS 的能力,再加上对实验室的 OS 不太满意,没有发挥空间,于是年末开了个新坑,打算尝试写一个叫做 rcOS 的简易操作系统,算是满足自己的心愿和完美主义,希望 2021 年能写到自己满意的程度。

新的一年?

其实经过半年的研究生生活,对新的一年已经没有多少激动人心的期待,只希望能更好地调整好 work-life balance,多抽出些时间陪女朋友,以及写自己的 OS 和其它项目,多运动保持健康,就够了。

2020 → 2021

用 Rust 写操作系统的踩坑记录

创建于 更新于
分类:Dev
标签:RustOSSystem

持续更新中……

最近在尝试用 Rust 写一个简单的 OS,过程中遇到了不少问题,在这里记录下,以便自己以后查阅,也给其他写 OS 的朋友们提供参考。

编译 core crate 发生 segmentation fault

使用自定义 target 的时候需要指定 -Z build-std=core,alloc --target=targets/foo.json,这会为指定的 target 编译 corealloc crate,但是遇到如下报错:

$ make
    Updating crates.io index
   Compiling compiler_builtins v0.1.36 (/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/vendor/compiler_builtins)
   Compiling core v0.0.0 (/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/library/core)
   Compiling kernel v0.1.0 (/Users/richard/Projects/rcos/kernel)
   Compiling rustc-std-workspace-core v1.99.0 (/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/library/rustc-std-workspace-core)
error: could not compile `core`

Caused by:
  process didn't exit successfully: `rustc --crate-name core --edition=2018 /Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts --crate-type lib --emit=dep-info,metadata,link -C panic=abort -C embed-bitcode=no -C debuginfo=2 -C metadata=d1c28a1a3b0e7456 -C extra-filename=-d1c28a1a3b0e7456 --out-dir /Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps --target /Users/richard/Projects/rcos/kernel/targets/aarch64.json -Z force-unstable-if-unmarked -L dependency=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps -L dependency=/Users/richard/Projects/rcos/kernel/target/debug/deps --cap-lints allow` (signal: 11, SIGSEGV: invalid memory reference)
warning: build failed, waiting for other jobs to finish...
error: build failed
make: *** [target/aarch64/debug/kernel] Error 101

报错信息具体就是 rustc 在编译 core 的时候发生了 segmentation fault,直接运行报错的那句命令也是一样的效果,后来发现使用 release 编译就不会报错,于是发现问题跟 -C opt-level= 编译选项有关,opt-level 等于 0 就会报错,大于等于 1 就没问题,可能是 rustc 的 bug。具体解决方法是在 Cargo.toml 中针对 core 包修改 opt-level,如下:

[profile.dev.package.core]
opt-level = 1

内核链接错误,报 undefined symbol: memcpy

报错信息如下:

$ make
...
error: linking with `rust-lld` failed: exit code: 1
  |
  = note: "rust-lld" "-flavor" "gnu" "-Ttarget/aarch64/linker.ld" "--eh-frame-hdr" "-L" "/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/aarch64/lib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/kernel-ff049f1d4f391e89.1fogdgfwiaz79eo9.rcgu.o" ... "-o" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/kernel-ff049f1d4f391e89" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/kernel-ff049f1d4f391e89.27nsv6g895dy2t94.rcgu.o" "--gc-sections" "-O1" "-L" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps" "-L" "/Users/richard/Projects/rcos/kernel/target/debug/deps" "-L" "/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/aarch64/lib" "-Bstatic" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libbuddy_system_allocator-6c2e94bd40abd09c.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libspin-452aa8ee04b5ffb7.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libspin-453077918cb4bcee.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/liballoc-359e1687c65b650d.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/librustc_std_workspace_core-9cbf353238b20cd5.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libcore-81456d7491ea4ea4.rlib" "/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libcompiler_builtins-482ad68e57d9fb9c.rlib" "-Bdynamic"
  = note: rust-lld: error: undefined symbol: memcpy
          >>> referenced by mod.rs:182 (/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:182)
          >>>               /Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/kernel-ff049f1d4f391e89.2ec5pxgnruw0e9q7.rcgu.o:(_$LT$$RF$mut$u20$W$u20$as$u20$core..fmt..Write$GT$::write_fmt::h14b1afbd1a65c84b (.llvm.3212942262376902672))
          >>> referenced by mod.rs:447 (/Users/richard/.rustup/toolchains/nightly-2020-11-25-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:447)
          >>>               core-81456d7491ea4ea4.core.2iv2qs8o-cgu.9.rcgu.o:(_$LT$core..fmt..Arguments$u20$as$u20$core..fmt..Display$GT$::fmt::haadaeb7738e71625) in archive /Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libcore-81456d7491ea4ea4.rlib


error: aborting due to previous error; 35 warnings emitted

error: could not compile `kernel`

Caused by:
  process didn't exit successfully: `rustc --crate-name kernel --edition=2018 src/main.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type bin --emit=dep-info,link -C opt-level=2 -C embed-bitcode=no -C debuginfo=2 -C debug-assertions=on -C metadata=ff049f1d4f391e89 -C extra-filename=-ff049f1d4f391e89 --out-dir /Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps --target /Users/richard/Projects/rcos/kernel/targets/aarch64.json -C incremental=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/incremental -L dependency=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps -L dependency=/Users/richard/Projects/rcos/kernel/target/debug/deps --extern 'noprelude:alloc=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/liballoc-359e1687c65b650d.rlib' --extern buddy_system_allocator=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libbuddy_system_allocator-6c2e94bd40abd09c.rlib --extern 'noprelude:compiler_builtins=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libcompiler_builtins-482ad68e57d9fb9c.rlib' --extern 'noprelude:core=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libcore-81456d7491ea4ea4.rlib' --extern spin=/Users/richard/Projects/rcos/kernel/target/aarch64/debug/deps/libspin-453077918cb4bcee.rlib -Z unstable-options --cfg 'arch="aarch64"' --cfg 'machine="virt"'` (exit code: 1)
make: *** [target/aarch64/debug/kernel] Error 101

意思是 core::fmt 包里面引用了 memcpy,然而 no_std 的情况下没有 memcpy 这符号。

后来在 Writing an OS in Rust (First Edition) 找到解决方案,方法就是链接一个 rlibc,这提供了 memcpymemmove 等函数的实现。

但事情并没有这么简单,虽然上面的方案可以用,但 rlibc 已经是弃用状态了,作者推荐使用 compiler_builtin 替代,这个 crate 的 README 里让添加 dependency,但其实这玩意在使用 -Z build-std=core,alloc 编译的情况下会自动编译,不需要手动添加 dependency,但是自动编译的情况下不会加上 mem feature,而我们正是要它的 mem feature 才能解决链接问题,找了一圈发现这个问题已经在几个 issue 里讨论过了,最后有一个 PR 解决了这问题,现在只需要再加上 -Z build-std-features=compiler-builtins-mem 参数就可以把 mem feature 传给 compiler_builtins crate。

相关链接:

build.rs 中自动选择 linker script

由于不同 arch 和 machine 可能需要不同的 linker script,因此 linker.ld 可能放在 src/arch/<arch>/ 也可能在 src/arch/<arch>/machine/<machine>/,于是想在 build.rs 中根据传入的环境变量来自动选择对应的 linker.ld

但 cargo 只支持在 build.rs 中输出 rustc-cdylib-link-arg,是针对编译动态库的,于是一开始选择在自定义 target 的 JSON 或在 .cargo/config.toml 中写死 linker script 路径为 target 目录中的某个地方,然后在 build.rs 中把对应的 linker.ld 拷到那个地方。

后来翻了半天 issue 找到了同类问题,然后发现有一个 PR 已经提供了 unstable 支持,允许在 build.rs 中生成 rustc-link-arg(虽然 PR 中修改了 unstable.md 文档,但奇怪的是 master 分支文档却没有),可以用在任何目标类型,于是这件事情就简化成了在 build.rs 中找到需要的 linker.ld,然后输出 cargo:rustc-link-arg=-T<linker_ld_path>,完美解决问题。

相关链接:

生成位置无关代码

在还没有启动 MMU 的时候,PC 寄存器首先会是物理地址,但内核最终需要使用 0xffff 开头的高地址,通常在 linker script 中可进行配置,使代码中使用的绝对寻址拿到的都是高地址,然后启用编译器的相关选项使生成的代码在访问静态变量、调用函数时都采用位置无关的方式。这样就可以在内核的 boot 阶段首先使用低地址,同时该阶段的代码可以随意访问内核其它部分的函数或静态变量,然后在配置好页表、启用 MMU 之后,使用绝对寻址跳转到高地址。

要让 Rust 生成位置无关代码,在自定义 target 中添加 "position-independent-executables": true 即可。

相关链接:

用 Rust 写操作系统的踩坑记录