AbydOS开发日志 (0) - 工具链和构建系统的选择与调试
背景
这是 AbydOS 的第一篇博文。近期在学习操作系统,想着自己搞一个玩玩,结合之前开发嵌入式的经验,以及烂尾了的 RISCV 模拟器工程,选定了带有 OpenSBI 支持的 RISCV64 平台。
本文主要记录工具链和构建系统的选择与调试过程。
开端
既然选定了平台,就开始构建 OpenSBI。根据 OpenSBI 的文档,尝试在 Ubuntu 20.04 (WSL1) 上安装了 gcc-riscv64-unknown-elf
和 qemu-system
等软件包,使用
CROSS_COMPILE=riscv64-unknown-elf- make PLATFORM=generic
编译之,成功了。开跑,
CROSS_COMPILE=riscv64-unknown-elf- make PLATFORM=generic run
,测试 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
爆改两个小时的 Makefile,终于是成功编译了。
尝试 CMake
改了两个小时的 Makefile,我已经在各种变量和奇怪的命令中晕头转向。这时候,我想到了现代 C++ 工程的神器:CMake。兴奋地写了两句:
cmake_minimum_required(VERSION 3.0.0)
project(AbydOS_Kernel VERSION 0.1.0 LANGUAGES C ASM)
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'
于是上网开搜,一看 unknown-elf 工具链没有 crt,肯定不支持 '-rdynamic',总共有两种解决方案:
- 加入两个命令行参数:-DCMAKE_C_COMPILER_WORKS:STRING=1 -DCMAKE_CXX_COMPILER_WORKS:STRING=1,绕过检查
- 换工具链
考虑到绕过 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
这样看,最终运行地址就是 0x80000000 + 0x200000,(
实际上这个值在 OpenSBI 的调试信息就有输出:
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000082200000
Domain0 Next Mode : S-mode
),这样就直接链接脚本改起始地址到 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 的一个提交历史,清晰地显示了:
# 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")
这样就知道参数从哪里来了。直接在工程的配置里面重写:
# get rid of '-rdynamic'
set(CMAKE_STATIC_LINKER_FLAGS " ")
set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")
set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
这样再配置和编译,就顺利通过了。
善后工作
配置好了构建系统和工具链,就该考虑 C++ 支持了。还是老办法,既然 C 的环境由 ASM 初始化,那么 C++ 环境就可以由 C 来初始化。根据经验,只需使用 extern "C" 修饰函数 k_main,然后就可以在 C 里面调用之,之后在 k_main 里面就可以完全使用 C++ 写了。
实践确实如此,不过写了类测试,发现加上析构函数之后编译不过,爆链接错误,链接到了一个叫做 _UnWind_Resume 的函数,在 libstdc++ 里面。想到C++ 的异常捕获机制,析构时会捕获异常,这里没有捕获异常的环境,于是加入 -fno-exceptions
参数,再编译就过了。
经过这样的配置,不到 60 行的 CMakeLists,支持了 C++ 写内核,真是太舒服啦!语法特性完全都是可用的!
后记
这个配置花了两天多的时间才完成,太痛苦啦!
今天在 QEMU 的源码 查到了设备内存布局:
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 },
};
就这样吧,接着搞 MMU 去喽!