启动代码和编译系统移植 #
1. 简介 #
和所有操作系统 Kernel 一样,在运行用户态程序之前,Kernel 有一系列初始化工作。
这部分代码由小部分汇编代码 + Rust 代码实现,同时需要设计 linker scripts,以确保代码段按照设计的内存布局放置。
同时 SeL4 编译系统基于 CMake,因此我们需要让 ReL4 适配原有编译系统,达到无缝替换 SeL4 Kernel 的目标。
2. Linker scripts #
我们从 linker scripts 开始移植,seL4 中的 linker 脚本根据配置生成的,尤其是各个段的物理地址,虚拟地址等。
ReL4 中目前配置功能还不具备,简单根据各个 平台 定义的内存空间。下面是 spike 平台的 linker 脚本的主要内容
KERNEL_OFFSET = (0xFFFFFFFF80000000 + ((0x80000000 + 0x4000000) & ((1 << (30)) - 1))) - (0x80000000 + 0x4000000);
SECTIONS
{
. = (0xFFFFFFFF80000000 + ((0x80000000 + 0x4000000) & ((1 << (30)) - 1)));
.boot.text . : AT(ADDR(.boot.text) - KERNEL_OFFSET)
{
*(.boot.text)
}
.boot.rodata . : AT(ADDR(.boot.rodata) - KERNEL_OFFSET)
{
*(.boot.rodata)
}
.boot.data . : AT(ADDR(.boot.data) - KERNEL_OFFSET)
{
*(.boot.data)
}
.boot.bss . : AT(ADDR(.boot.bss) - KERNEL_OFFSET)
{
*(.boot.bss)
}
. = ALIGN(4K);
ki_boot_end = .;
.text . : AT(ADDR(.text) - KERNEL_OFFSET)
{
. = ALIGN(4K);
/* Standard kernel */
*(.text)
*(.text.*)
}
/* Start of data section */
_sdata = .;
.small : {
/* Small data that should be accessed relative to gp. ld has trouble
with the relaxation if they are not in a single section. */
__global_pointer$ = . + 0x800;
*(.srodata*)
*(.sdata*)
*(.sbss)
}
.rodata . : AT(ADDR(.rodata) - KERNEL_OFFSET)
{
*(.rodata)
*(.rodata.*)
}
.data . : AT(ADDR(.data) - KERNEL_OFFSET)
{
*(.data)
*(.data.*)
}
/* The kernel's idle thread section contains no code or data. */
._idle_thread . (NOLOAD): AT(ADDR(._idle_thread) - KERNEL_OFFSET)
{
*(._idle_thread)
}
. = ALIGN(4K);
.bss . (NOLOAD): AT(ADDR(.bss) - KERNEL_OFFSET)
{
*(.bss)
*(COMMON) /* fallback in case '-fno-common' is not used */
/* 4k breakpoint stack */
_breakpoint_stack_bottom = .;
. = . + 4K;
_breakpoint_stack_top = .;
/* large data such as the globals frame and global PD */
*(.bss.*)
}
...
ki_end = .;
}
其中包含的地址,如 0xFFFFFFFF80000000 0x80000000 0x4000000 等都是需要通过配置文件生成的,这部分在 reL4 中通过什么方式实现还需要讨论。
总的来说,没有什么特别的。值得说的就是 seL4 中将一部分代码和变量单独定义在了 boot.* section,reL4 会尽量和 seL4 保持相同,将这些代码也放在同样的段中,使其尽量靠近。
3. Startup 代码移植 #
该部分需要移植的代码主要包括一些汇编代码,和很少的 C 代码(大量的移植工作已经由其他同学完成了)。
汇编代码还是通过汇编实现,C 代码通过 Rust 重新实现。和 Linker 脚本系统相同,这些汇编代码同样是可配置的,暂时是通过 rust cfg + asm 的方式对这些汇编代码进行配置。
需要移植的代码如下(以 riscv spike 平台为例,aarch64 架构需要移植的汇编代码和 riscv 类似)
3.1 汇编代码 #
- head.S
head.S 是 kernel 的 entry 代码,主要是做一些寄存器初始化赋值,尤其是栈指针之类。完成后跳转到 init_kernel,最后通过 restore_user_context 回到用户态。
_start:
fence.i
.option push
.option norelax
1:auipc gp, %pcrel_hi(__global_pointer$)
addi gp, gp, %pcrel_lo(1b)
.option pop
la sp, (kernel_stack_alloc + (1ul << (12)))
csrw sscratch, x0
jal init_kernel
la ra, restore_user_context
jr ra
- trap.S
在 riscv 架构上,trap.S 是 kernel 所有 trap 的入口,包括中断、异常等等。
trap_entry 的前一段保存用户空间寄存器上下文,之后根据 scause 寄存器内容选择 handle_syscall, c_handle_exception, c_handle_interrupt
由于 handle_syscall 中代码根据宏进行配置,所以使用 rust asm!() 实现,以方便使用 rust cfg() 特性。其他部分和 seL4 保持一致。
pub fn handle_syscall() {
unsafe {
asm!(
"addi x1, x1, 4",
"sd x1, (34*(64 / 8))(t0)",
"li t3, -1",
"bne a7, t3, 1f",
"j c_handle_fastpath_call",
"1:",
"li t3, -2",
"bne a7, t3, 2f"
);
// c_handle_fastpath_reply_recv need third parameter if open mcs option
#[cfg(feature = "KERNEL_MCS")] {
asm!("mv a2, a6");
}
asm!(
"j c_handle_fastpath_reply_recv",
"2:",
"mv a2, a7",
"j c_handle_syscall"
);
}
}
3.2 C 代码 #
尽管 ReL4 中已经将 SeL4 中绝大部分 C 代码使用 Rust 实现,但目前仍然有极少数的 初始化阶段 和 CPU 架构相关的代码 使用 C 实现。这些代码通常是架构之间不通用的,我们这里还是以 riscv 架构为例。
3.2.1 Boot 阶段代码 #
Boot 阶段代码我们均放在 main.rs,主要是一个初始化函数和一些静态全局变量 (bss 段)
- init_kernel
init_kernel 是 head.S 中在完成基础的寄存器初始化后,调用的第一个函数。主要功能是
- Kernel 和 UserSpace 内存空间初始化
- 一些寄存器设置,比如中断入口寄存器,时钟中断使能等等
- 外设初始化,主要是 IRQ Controller
- 设备树加载
- rootserver 加载
总之就是使 kernel 做好准备,可以将控制权交给 rootserver
(本人对于初始化过程还不是很了解,可能有错漏)
#[no_mangle]
#[link_section = ".boot.text"]
pub fn init_kernel(
ui_p_reg_start: usize,
ui_p_reg_end: usize,
pv_offset: isize,
v_entry: usize,
dtb_addr_p: usize,
dtb_size: usize
) {
sel4_common::println!("Now we use rel4 kernel binary");
log::set_max_level(log::LevelFilter::Trace);
boot::interface::pRegsToR(
&avail_p_regs as *const p_region_t as *const usize,
core::mem::size_of_val(&avail_p_regs)/core::mem::size_of::<p_region_t>()
);
intStateIRQNodeToR(irqnode.0.as_ptr() as *mut usize);
let result = rust_try_init_kernel(ui_p_reg_start, ui_p_reg_end, pv_offset, v_entry, dtb_addr_p, dtb_size);
if !result {
log::error!("ERROR: kernel init failed");
panic!()
}
schedule();
activateThread();
}
- Boot 阶段变量
- avail_p_regs
- irqnode
Boot 阶段使用的变量主要是一些内存范围的定义,这些内存范围在初始化时被赋值到一些静态变量,用于后续内存分配等等。这部分设计可以优化的地方很多,比如不应该从一个全局变量赋值到另一个全局变量,变量中的值是写死的等等。后面会进一步优化。
3.2.2 Arch 相关代码 #
- handle_syscall
handle_syscall 在 seL4 中是汇编代码,但是由于其中有一些受配置选项影响的,所以使用 rust asm! + cfg 的方式实现配置功能,如下
#[cfg(feature = "KERNEL_MCS")]
{
asm!("mv a2, a6");
}
- c_handle_fastpath_call
c_handle_fastpath_call 是一个 fastpath_call 的 wrapper,按照 seL4 C 代码实现即可。
- c_handle_fastpath_reply_recv
c_handle_fastpath_reply_recv 和 c_handle_fastpath_call 一样,是 fastpath_reply_recv 的 wrapper,稍有不同的是,分为 mcs 和 no mcs 两个版本。Rust 无法像 C 一样对任意一行使用 #ifdef,因此只能写两遍函数实现。
#[no_mangle]
#[cfg(feature = "BUILD_BINARY")]
#[cfg(not(feature = "KERNEL_MCS"))]
pub fn c_handle_fastpath_reply_recv(cptr: usize, msgInfo: usize) {
use crate::kernel::fastpath::fastpath_reply_recv;
fastpath_reply_recv(cptr, msgInfo);
}
#[no_mangle]
#[cfg(feature = "BUILD_BINARY")]
#[cfg(feature = "KERNEL_MCS")]
pub fn c_handle_fastpath_reply_recv(cptr: usize, msgInfo: usize, reply: usize) {
use crate::kernel::fastpath::fastpath_reply_recv;
fastpath_reply_recv(cptr, msgInfo, reply);
}
4. TODOs #
需要设计一套配置系统,实现根据配置生成所有架构和平台相关代码的功能,避免每个平台导入都通过手写代码的方式。
Pure Rust 模式运行 sel4test 比旧模式要慢,还需要进一步调查
aarch64 架构移植
代码优化,避免冗余的全局变量
5. 移植遇到的坑 #
移植当中,遇到的问题记录如下
5.1 seL4 中预编译代码 #
seL4 中使用大量的预编译代码,根据配置使用 cpp 命令生成实际的代码。即使是 linker 脚本和汇编代码也通过该方式生成。这部分无法直接移植,需要仔细分析其生成前后的文件,理解其含义。
5.2 rust 编译产生的符号所在的段和 C 有些区别 #
比如 init_kernel 函数,默认会在 text.init_kernelxxxx 段,而不是 text 段。使用 seL4 原有的链接脚本时,会产生非常多的段(可以理解成每个函数会产生一个段),同时由于链接脚本中只定义了 text,data 等段放置的位置,这些 rust 编译产生的带后缀的段会被放在莫名其妙的位置,造成内存布局上 bss 段和 text 段的交叉,进一步造成修改 bss 段变量时会误修改 text 段数据。这是很严重的错误,会报非法指令(因为原有正常的指令都被改成 0 了)
# text.xxxxx 段的例子
[214] .text._ZN9se[...] PROGBITS ffffffff8401bd96 0001cd96
0000000000000290 0000000000000000 AX 0 0 2
[215] .text._ZN42_[...] PROGBITS ffffffff8401c026 0001d026
00000000000000b8 0000000000000000 AX 0 0 2
[216] .text.map_it[...] PROGBITS ffffffff8401c0de 0001d0de
00000000000000c4 0000000000000000 AX 0 0 2
[217] .text._ZN11s[...] PROGBITS ffffffff8401c1a2 0001d1a2
00000000000000e0 0000000000000000 AX 0 0 2
在链接脚本中,将 text.xxxxxx, bss.xxxxx 这些段全部归到 text, bss 段中即可解决该问题,例如
.text . : AT(ADDR(.text) - KERNEL_OFFSET)
{
. = ALIGN(4K);
/* Standard kernel */
*(.text)
# put text.xxxxx into .text section
*(.text.*)
}
5.3 pure rust 版本运行 rel4test 时间更长的问题 #
定位到直接原因是执行 seL4_Untyped_Retype syscall 时间过长,整个调用链为
seL4_Untyped_Retype -> slowpath -> handleSyscall -> handleInvocation -> decode_invocation -> decode_untyed_invocation -> invoke_untyped_retype -> reset_untyped_cap
在 reset_untyped_cap 执行时间变长,根因是执行下面循环代码,每次都会执行时间更长一点
while offset != -(BIT!(chunk) as isize) {
clear_memory(
GET_OFFSET_FREE_PTR(region_base, offset as usize) as *mut u8,
chunk,
);
prev_cap.set_capFreeIndex(OFFSET_TO_FREE_IDNEX(offset as usize) as u64);
let status = unsafe { preemptionPoint() };
if status != exception_t::EXCEPTION_NONE {
return status;
}
offset -= BIT!(chunk) as isize;
}
其中主要影响是 preemptionPoint() ,这个函数执行不知道为什么时间变长。经过比较,发现其中的一个全局变量 ksWorkUnitsCompleted 放置的位置和之前不同。
- 旧版本变量位置
# 位于 boot.bss 段
ffffffff84001d10 D ksWorkUnitsCompleted
因此我将 ksWorkUnitsCompleted 在 pure rust 版本中指定放在 boot.bss 段,解决了运行很慢的问题。
#[no_mangle]
#[link_section = ".boot.bss"]
pub static mut ksWorkUnitsCompleted: usize = 0;
虽然解决了该问题,但是不知道为什么这会造成这么大的影响。也许是缓存未命中?