AbydOS开发日志 (0) - 工具链和构建系统的选择与调试

3/8/2024 OSCrossCompiling

# 背景

这是 AbydOS 的第一篇博文。近期在学习操作系统,想着自己搞一个玩玩,结合之前开发嵌入式的经验,以及烂尾了的 RISCV 模拟器工程,选定了带有 OpenSBI 支持的 RISCV64 平台。

本文主要记录工具链和构建系统的选择与调试过程。

# 开端

既然选定了平台,就开始构建 OpenSBI。根据 OpenSBI 的文档 (opens new window),尝试在 Ubuntu 20.04 (WSL1) 上安装了 gcc-riscv64-unknown-elfqemu-system 等软件包,使用

CROSS_COMPILE=riscv64-unknown-elf- make PLATFORM=generic
1

编译之,成功了。开跑,

CROSS_COMPILE=riscv64-unknown-elf- make PLATFORM=generic run
1

,测试 payload 转起来了。

# 新建文件夹

既然 OpenSBI 编译过了,那就可以新建文件夹了。一开始想,内核的构建比较复杂,OpenSBI 有现成的 Makefile,套娃一个目录结构然后稍微改一下 Makefile,应该可以单独编译 payload,然后再以 payload 为例搭建上层环境。经过一上午的努力,目录结构变成了这样:

AbydOS
  |
  +- kernel
  |     |
  |     +- main
  |          |
  |          +- objects.mk
  |          +- Kconfig
  |          +- test.c
  |          +- test.S
  |          +- test.ldS
  |
  +- platform
  |      |
  |      + qemu-virt
  |            |
  |            +- objects.mk
  |            +- Kconfig
  |            +- platform.c
  |
  +- Kconfig
  +- Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

爆改两个小时的 Makefile,终于是成功编译了。

# 尝试 CMake

改了两个小时的 Makefile,我已经在各种变量和奇怪的命令中晕头转向。这时候,我想到了现代 C++ 工程的神器:CMake。兴奋地写了两句:

cmake_minimum_required(VERSION 3.0.0)
project(AbydOS_Kernel VERSION 0.1.0 LANGUAGES C ASM)
1
2

VSCODE 选择工具链:GCC 9.3.0 riscv64-unknown-elf,Configure, 爆!

[cmake] CMake Error at /usr/share/cmake-3.16/Modules/CMakeTestCCompiler.cmake:60 (message):
[cmake]   The C compiler
[cmake] 
[cmake]     "/usr/bin/riscv64-unknown-elf-gcc"
[cmake] 
[cmake]   is not able to compile a simple test program.
[cmake] 
[cmake]   It fails with the following output:
[cmake] 
[cmake]     Change Dir: /home/wsl/AbydOS/build/CMakeFiles/CMakeTmp
[cmake]     
[cmake]     Run Build Command(s):/usr/bin/make cmTC_164d0/fast && /usr/bin/make -f CMakeFiles/cmTC_164d0.dir/build.make CMakeFiles/cmTC_164d0.dir/build
[cmake]     make[1]: Entering directory '/home/wsl/AbydOS/build/CMakeFiles/CMakeTmp'
[cmake]     Building C object CMakeFiles/cmTC_164d0.dir/testCCompiler.c.o
[cmake]     /usr/bin/riscv64-unknown-elf-gcc    -o CMakeFiles/cmTC_164d0.dir/testCCompiler.c.o   -c /home/wsl/AbydOS/build/CMakeFiles/CMakeTmp/testCCompiler.c
[cmake]     Linking C executable cmTC_164d0
[cmake]     /usr/bin/cmake -E cmake_link_script CMakeFiles/cmTC_164d0.dir/link.txt --verbose=1
[cmake]     /usr/bin/riscv64-unknown-elf-gcc      -rdynamic CMakeFiles/cmTC_164d0.dir/testCCompiler.c.o  -o cmTC_164d0 
[cmake]     riscv64-unknown-elf-gcc: error: unrecognized command line option '-rdynamic'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

于是上网开搜,一看 unknown-elf 工具链没有 crt,肯定不支持 '-rdynamic',总共有两种解决方案:

  1. 加入两个命令行参数:-DCMAKE_C_COMPILER_WORKS:STRING=1 -DCMAKE_CXX_COMPILER_WORKS:STRING=1,绕过检查
  2. 换工具链

考虑到绕过 CMake 的编译器检查可能不太好,选2,安装 gcc-riscv64-linux-gnu。再次尝试 Configure,过了!

于是从原始的 Makefile 抽一点编译参数出来,把 CMakeLists.txt 补全,尝试编译,过了!运行,哎,结果和原来不一样,一个全局const char* 变量 test_args 没有正常初始化,期望是指向 .rodata 段的,可是运行起来是0。

# 修修补补

出现全局变量不初始化的问题,根据嵌入式裸机开发的经验,我第一时间想到的原因是 .data 段没有正常初始化。但是链接脚本没有划分 Memory Region,也没有做 LMA 和 VMA 的指定,.data 段的数据就应该完好地写在 ELF 文件里面。于是上 IDA 分析,一看,这不是初始化得好好的嘛,指向了0x1000。(注意这个坑点!)

由于 QEMU 的手册没有明确内存空间分布,所以 kernel 的起始地址我设成了 0 ,然后使用 -fPIC 参数编译。这样一来,理论上,无论内核被加载到哪里,都应该能够正常跑。搞不定,觉得可能是编译参数不对,改了半天的编译参数,试图去掉自动添加的 '-rdynamic',无果。(后来明确,这个参数并不指示编译器动态链接)。一天就这样过去了。

第二天,继续修改。由于 ELF 中有 .dynamic 和 。rela.* 段,怀疑是需要重定位,从 OpenSBI 里面抄了一段重定位的汇编代码,再次编译,还是不行。实在没辙了,上 gdb-multiarch 调试,才发现固件运行的地址不是 0x0,不能正常调。找了半天,终于在 OpenSBI 的 platform/generic/objects.mk 里面看到:

FW_TEXT_START=0x80000000
FW_DYNAMIC=y
FW_JUMP=y
ifeq ($(PLATFORM_RISCV_XLEN), 32)
  # This needs to be 4MB aligned for 32-bit system
  FW_JUMP_OFFSET=0x400000
else
  # This needs to be 2MB aligned for 64-bit system
  FW_JUMP_OFFSET=0x200000
endif
1
2
3
4
5
6
7
8
9
10

这样看,最终运行地址就是 0x80000000 + 0x200000,(

实际上这个值在 OpenSBI 的调试信息就有输出:

Domain0 Next Address      : 0x0000000080200000
Domain0 Next Arg1         : 0x0000000082200000
Domain0 Next Mode         : S-mode
1
2
3

),这样就直接链接脚本改起始地址到 0x80200000,再编译测试,原来跑不通的 fw_jump 固件也正常了。

自此,上 gdb ,就可以正常 debug 了。看了 test_args 的地址,是 0x80203028,和 ELF 中指示的 .data 段范围一致,再 print 一下,就是 0x0 ! 再上 IDA , 这次它的值不是 0x1000 了,而是 0x100401000,一个很怪异的值。看到.rodata段对应地址是 0x80201000,计算器启动,得到 0x100401000 - 0x80201000 = 0x80200000,正好是内核起始地址。于是怀疑全局指针没设对,但是转念想,我都 -fPIC了,寻址应该没有问题,调试出来的 test_args 地址和 IDA 看到的也一致。

这时候想起直接看 ELF 的值了,7z 解压 .data 段, Hex Editor 一看,这TM写的就是0!还是工具链的锅!于是改回 unknown-elf,跳过了 CMake 的编译器检查,终于是 Configure 过了。尝试编译,还是卡在 '-rdynamic' 上,这问题没有根本解决。看报错知道是链接参数的锅,这个链接参数又是 CMake 加上去的,一顿搜索之后找到了 CMake 的一个提交历史 (opens new window),清晰地显示了:

 # We pass this for historical reasons.  Projects may have
  # executables that use dlopen but do not set ENABLE_EXPORTS.
  set(CMAKE_SHARED_LIBRARY_LINK_${lang}_FLAGS "-rdynamic")
1
2
3

这样就知道参数从哪里来了。直接在工程的配置里面重写:

# get rid of '-rdynamic'
set(CMAKE_STATIC_LINKER_FLAGS " ")
set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")
set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
1
2
3
4

这样再配置和编译,就顺利通过了。

# 善后工作

配置好了构建系统和工具链,就该考虑 C++ 支持了。还是老办法,既然 C 的环境由 ASM 初始化,那么 C++ 环境就可以由 C 来初始化。根据经验,只需使用 extern "C" 修饰函数 k_main,然后就可以在 C 里面调用之,之后在 k_main 里面就可以完全使用 C++ 写了。

实践确实如此,不过写了类测试,发现加上析构函数之后编译不过,爆链接错误,链接到了一个叫做 _UnWind_Resume 的函数,在 libstdc++ 里面。想到C++ 的异常捕获机制,析构时会捕获异常,这里没有捕获异常的环境,于是加入 -fno-exceptions参数,再编译就过了。

经过这样的配置,不到 60 行的 CMakeLists,支持了 C++ 写内核,真是太舒服啦!语法特性完全都是可用的!

# 后记

这个配置花了两天多的时间才完成,太痛苦啦!

今天在 QEMU 的源码 (opens new window) 查到了设备内存布局:

static const MemMapEntry virt_memmap[] = {
    [VIRT_DEBUG] =        {        0x0,         0x100 },
    [VIRT_MROM] =         {     0x1000,        0xf000 },
    [VIRT_TEST] =         {   0x100000,        0x1000 },
    [VIRT_RTC] =          {   0x101000,        0x1000 },
    [VIRT_CLINT] =        {  0x2000000,       0x10000 },
    [VIRT_ACLINT_SSWI] =  {  0x2F00000,        0x4000 },
    [VIRT_PCIE_PIO] =     {  0x3000000,       0x10000 },
    [VIRT_PLATFORM_BUS] = {  0x4000000,     0x2000000 },
    [VIRT_PLIC] =         {  0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
    [VIRT_APLIC_M] =      {  0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) },
    [VIRT_APLIC_S] =      {  0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) },
    [VIRT_UART0] =        { 0x10000000,         0x100 },
    [VIRT_VIRTIO] =       { 0x10001000,        0x1000 },
    [VIRT_FW_CFG] =       { 0x10100000,          0x18 },
    [VIRT_FLASH] =        { 0x20000000,     0x4000000 },                       
    [VIRT_IMSIC_M] =      { 0x24000000, VIRT_IMSIC_MAX_SIZE },
    [VIRT_IMSIC_S] =      { 0x28000000, VIRT_IMSIC_MAX_SIZE },
    [VIRT_PCIE_ECAM] =    { 0x30000000,    0x10000000 },
    [VIRT_PCIE_MMIO] =    { 0x40000000,    0x40000000 },
    [VIRT_DRAM] =         { 0x80000000,           0x0 },                       
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

就这样吧,接着搞 MMU 去喽!