本周开始连载 泰晓科技x开源之夏 2024 项目开发成果系列文章,本篇为 RISC-V OpenSBI 相关,记得收藏分享+关注。
零门槛转战 RISC-V + 嵌入式 Linux,跟上泰晓科技的 B 站公开课,备一支 RISC-V 实验箱走起:https://tinylab.org/tiny-riscv-box
为 OpenSBI 增加 Section GC 功能
概述
OpenSBI 目前并不支持 Section GC,Section GC 是指 Section Garbage Collection,即段(Section)的垃圾收集。在编译程序时,GCC 会将代码和数据组织成不同的段(Sections),例如 .text 段存储可执行代码,.data 段存储已初始化的全局变量等等。而 Section GC 则是指在链接阶段对这些节进行清理,去除未使用的代码和数据,以减小最终生成的可执行文件的大小。
实现这个功能时,我们需要分析其链接脚本,并且完备的测试这个改动是否会影响其正常功能。
理解编译器提供的 Section GC 功能
我们需要理解 Section GC 的功能和实现原理才能为 OpenSBI 增加这个功能,否则将无从下手。
参考以下三篇文章,我们能够了解 Section GC 的原理和实现细节。
Section GC 分析 - Part 1 原理简介 Section GC 分析 - Part 2 gold 源码解析 Section GC 分析 - Part 3 引用建立过程
链接器提供的的 --gc-sections
选项可以在链接时对未使用到的函数和变量进行裁剪。
对 section 执行 GC 操作的前提,链接前每个函数和数据都有自己的 section。但默认情况下,GCC 把函数统一放在了 .text
section 中。我们可以使用 -ffunction-sections
参数来让每个函数都有自己的 section。
默认情况下,编译器按照以下规则将数据放入各个段中:
段 | 数据类型 | 说明 |
---|---|---|
.text | 可执行代码 | 存放程序的机器指令 |
.rodata | 只读数据 | 存放不可修改的常量数据,例如字符串常量、全局常量等 |
.data | 初始化的可读写数据 | 存放已初始化的全局变量和静态变量,可以在程序运行时进行读写操作 |
.bss | 未初始化数据 | 存放未初始化的全局和静态变量 |
像这样所有代码都放在了代码段中,链接器不知道哪些函数和变量被使用了,无法进行裁剪。要想进行垃圾回收,需要让每个函数都有自己的节。
GCC 的 -ffunction-sections
和 -fdata-sections
选项会让每个函数或者变量拥有自己的节。我们在这里只详细介绍 -ffunction-sections
,-fdata-sections
同理。
以下是示例代码,包括了使用到的函数 fun()
和未使用到的函数 unused()
void fun(){
return;
}
void unused(){
return;
}
int main(){
fun();
}
启用 -ffunction-sections
选项,编译该文件,但不进行链接。
gcc -c test.c
查看目标文件符号表。
$ readelf -s test.o
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
5: 0000000000000000 11 FUNC GLOBAL DEFAULT 1 fun
6: 0000000000000000 11 FUNC GLOBAL DEFAULT 1 unused
7: 0000000000000000 25 FUNC GLOBAL DEFAULT 1 main
可以看到 fun()
和 unused()
函数没有单独的 section。
启用 -ffunction-sections
选项,编译该文件,但不进行链接。
gcc -c --function-sections test.c
查看目标文件符号表:
$ readelf -s test.o
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 4 .text.fun
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .text.unused
4: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .text.main
5: 0000000000000000 11 FUNC GLOBAL DEFAULT 4 fun
6: 0000000000000000 11 FUNC GLOBAL DEFAULT 5 unused
7: 0000000000000000 25 FUNC GLOBAL DEFAULT 6 main
可以看到 fun()
和 unused())
函数都有了各自的 section。
启用 --gc-sections
后,链接器将会删除没有别引用到的 section。
寻找被错误 GC 掉的节
在弄明白了 Section GC 的原理后,我们就可以尝试着修改 OpenSBI 代码了。
-CFLAGS = -g -Wall -Werror -ffreestanding -nostdlib -fno-stack-protector -fno-strict-aliasing
+CFLAGS = -g -Wall -Werror -ffreestanding -nostdlib -fno-stack-protector -fno-strict-aliasing -ffunction-sections -fdata-sections
ELFFLAGS += $(USE_LD_FLAG)
+ELFFLAGS += -Wl,--gc-sections
以上修改启动编译器和链接器的 Section GC 相关选项。
需要注意的是,由于代码需要合并入主线,我们修改的方式需要和原来的代码相适应,包括代码的顺序和位置。
然后我们编译 OpenSBI,并在 QEMU 中启动提前编译好的 Linux 内核和 OpenSBI。
CROSS_COMPILE=riscv64-linux-gnu- make PLATFORM=generic FW_PAYLOAD_PATH=./Image -j16
qemu-system-riscv64 -M virt -kernel ./Image -append 'rootwait root=/dev/vda ro' -nographic --bios ./build/platform/generic/firmware/fw_dynamic.bin
然而运行的时候没有任何输出。这个难题很棘手,没有输出不知道卡在了哪里。
开启 --print-gc-sections
后发现,有特别多的 .text.*
节被删除了。
根据上文对 Section GC 功能的解析,我们需要在链接脚本中,手动保留 .text.*
这样的节。
diff --git a/firmware/fw_base.ldS b/firmware/fw_base.ldS
index fb47984..a33746a 100644
--- a/firmware/fw_base.ldS
+++ b/firmware/fw_base.ldS
@@ -20,6 +20,7 @@
PROVIDE(_text_start = .);
*(.entry)
*(.text)
+ *(.text.*)
. = ALIGN(8);
PROVIDE(_text_end = .);
}
再次编译,成功运行。
一些汇编语法会导致无法建立 section 之间的引用,此时就需要使用在链接脚本中使用 KEEP()。在 OpenSBI 项目中,没有观察到这样的情况。
测试增加的 Section GC 功能是否会影响 OpenSBI 的正常功能
我们需要确保 Section GC 的引入不会破坏其功能。
测试三种引导方式
OpenSBI 有三种引导内核的方式:
FW_PAYLOAD:下一引导阶段被作为 payload 打包进来,通常是 U-Boot 或 Linux。这是兼容 Linux 的 RISC-V 硬件所使用的默认 firmware。
FW_JUMP:跳转到一个固定地址,该地址上需存有下一个加载器。QEMU 的早期版本曾经使用过它。
FW_DYNAMIC:根据前一个阶段传入的信息加载下一个阶段。通常是 U-Boot SPL 使用它。现在 QEMU 默认使用 FW_DYNAMIC。
我们需要确保这三种方式都能正常工作,编写以下 Bash 脚本:
CROSS_COMPILE=riscv64-linux-gnu- make PLATFORM=generic FW_PAYLOAD_PATH=$Image_PATH -j16
qemu-system-riscv64 -M virt -kernel ./Image -append 'rootwait root=/dev/vda ro' -nographic --bios ./build/platform/generic/firmware/fw_dynamic.bin
qemu-system-riscv64 -M virt -kernel ./Image -append 'rootwait root=/dev/vda ro' -nographic --bios ./build/platform/generic/firmware/fw_jump.bin
qemu-system-riscv64 -M virt -nographic --bios ./build/platform/generic/firmware/fw_payload.bin
都能够正常运行。
Payload 是否被正常引用
FW_PAYLOAD 打包了下一阶段的程序,例如内核。如果没有引用 payload,payload 将会被链接器删除。
分析这部分代码:
gcc -c test.c
0
fw_next_addr
会使用到 payload_bin
这个符号。
gcc -c test.c
1
payload_bin
· 位于 .payload
section 中。
可以得出结论:fw_next_addr
引用到了 payload_bin
,导致 .payload
section 被保留。所以 .payload
不需要额外的 KEEP()。
单元测试
此外 OpenSBI 还有 SBIUNIT tests 功能。开启后,每次运行 OpenSBI 时都会进行单元测试。
经过测试,也能正常运行。
完整 Linux 内核与用户态环境
gcc -c test.c
2
正常运行:
除此之外,我们还测试了 LLVM 对该功能的兼容性。
OpenSBI 的 PIE 支持
使用 riscv64-unknown-elf-gcc 工具链编译 OpenSBI v1.5.1 会报错 Your linker does not support creating PIEs, opensbi requires this.
。然而,README 中明确地写到使用不支持 PIE 的工具链会生成一个 static linked firmware images。这里存在一些问题。
经过调查,总结了 OpenSBI 中关于 PIE 的改动。
日期 | Commit SHA | Title | 备注 |
---|---|---|---|
2021 年 3 月 | 0f20e8 | firmware: Support position independent execution | 加入了 PIE 支持 |
2021 年 4 月 | bf3ef5 | firmware: Enable FW_PIC by default | 默认启用 PIE |
2024 年 3 月 | 76d7e9 | firmware: remove copy-base relocation | 强制要求 PIE,删除了宏 BOOT_STATUS_RELOCATE_DONE 等 |
目前看来是最新一次改动强制要求 PIE 后,没有修改文档。但是强制要求 PIE 的必要性仍然有待商榷。
暂时先修改文档,使其与代码行为一致。
增加 Kconfig 配置项
使用户可以在 menuconfig 中选择编译选项。
gcc -c test.c
3
同时需要修改 Makefile 文件:
gcc -c test.c
4
总结
经过以上完善的测试,我们可以把 Section GC 功能的 Patch 整理发送到上游了。
参考资料
Section GC 分析 - Part 1 原理简介 Section GC 分析 - Part 2 gold 源码解析 Section GC 分析 - Part 3 引用建立过程
首发地址:https://tinylab.org/section-gc-for-opensbi
技术服务:https://tinylab.org/ruma.tech
左下角 阅读原文 可访问外链。都看到这里了,就随手在看+分享一下吧 ;-)
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...