OpenSBI 入门
声明
本文为本人原创,未经允许,严禁转载。
FW_JUMP FW_PAYLOAD FW_DYNAMIC
FW_JUMP
OpenSBI 带跳转地址的固件(FW_JUMP)是一种仅处理下一个引导阶段入口地址的固件。例如,它可以处理引导加载程序或操作系统内核的入口地址,但不直接包含下一阶段的二进制代码。
FW_JUMP 固件特别适用于在执行 OpenSBI 固件之前的引导阶段能够加载 OpenSBI 固件和后续引导阶段二进制文件的情况。
启用平台 FW_JUMP 固件有以下两种方法:
- 在顶层 make 命令行中指定 FW_JUMP=y。
- 在目标平台的 objects.mk 配置文件中指定 FW_JUMP=y。
编译后的 FW_JUMP 固件 ELF 文件名为 fw_jump.elf。扩展的镜像文件名为 fw_jump.bin。这两个文件都会被创建在特定平台的构建目录下的 build/platform/<platform_subdir>/firmware 目录中。
FW_DYNAMIC
OpenSBI 带动态信息的固件(FW_DYNAMIC)是一种可以从前一个引导阶段获取关于下一引导阶段(例如引导加载程序或操作系统)和运行时 OpenSBI 库选项的信息的固件。
前一个引导阶段将通过在内存中创建 struct fw_dynamic_info
结构体并通过 RISC-V CPU 的 a2
寄存器传递其地址给 FW_DYNAMIC 来传递信息。在 RV64 上,地址必须对齐到 8 字节;在 RV32 上,地址必须对齐到 4 字节。
FW_DYNAMIC 固件特别适用于在执行 OpenSBI 固件之前的引导阶段能够加载 OpenSBI 固件和后续引导阶段二进制文件的情况。
启用平台 FW_DYNAMIC 固件有以下两种方法:
- 在顶层 make 命令行中指定 FW_DYNAMIC=y。
- 在目标平台的 objects.mk 配置文件中指定 FW_DYNAMIC=y。
编译后的 FW_DYNAMIC 固件 ELF 文件名为 fw_dynamic.elf。扩展的镜像文件名为 fw_dynamic.bin。这两个文件都会被创建在特定平台的构建目录下的 build/platform/<platform_subdir>/firmware 目录中。
FW_PAYLOAD
FW_PAYLOAD 是一种直接包含引导阶段二进制文件的固件,用于引导 OpenSBI 固件的执行。通常情况下,这个 payload 将是一个引导加载程序或操作系统内核。
FW_PAYLOAD 固件特别适用于在 OpenSBI 固件之前执行的引导阶段无法同时加载 OpenSBI 固件和后续引导阶段的情况。
FW_PAYLOAD 固件也适用于在 OpenSBI 固件之前的引导阶段未传递扁平设备树(FDT 文件)的情况。在这种情况下,FW_PAYLOAD 固件允许将扁平设备树嵌入到最终固件的.rodata 部分中。
启用 FW_PAYLOAD 固件可以通过以下任一方法实现:
- 在顶层 make 命令行中指定 FW_PAYLOAD=y。
- 在目标平台的 objects.mk 配置文件中指定 FW_PAYLOAD=y。
编译后的 FW_PAYLOAD 固件 ELF 文件名为 fw_payload.elf。扩展的镜像文件名为 fw_payload.bin。这两个文件都将在特定平台的构建目录下的 build/platform/<platform_subdir>/firmware 目录中创建。
FW_JUMP 与 FW_DYNAMIC 的区别
fw_jump 与 fw_dynamic 都不含有 payload,区别就在于是否从前一个引导过程获取信息。fw_jump 不会再前一个引导过程获取信息,因此它所包含的下一阶段的引导代码的入口地址是静态的,要求下一阶段的代码必须要加载到指定位置。fw_dynamic 可以从前一个引导过程获取信息,因此下一阶段的引导代码的入口地址可以不固定。
FW_JUMP 与 FW_PAYLOAD 的区别
含有 payload 的 OpenSBI (FW_PAYLOAD)是一种固件,它直接包含了用于跟随 OpenSBI 固件执行的启动阶段二进制文件。通常,这个负载将是一个引导加载程序或操作系统内核。
从 QEMU RISC-V Virt Machine Platform 案例中能看出 fw_jump.bin
和 fw_payload.elf
之间的区别。
- 当没有 payload 的时候,就是说不指定 payload,此时要想 run 起来就必须使用
fw_payload.bin
- 当编译 OpenSBI 时指定了
FW_PAYLOAD_PATH
的时候,可以只使用fw_payload.elf
作为-bios
的参数,因为 payload 已经打包进了fw_payload.elf
中了;也可以分开指定-bios
和kernel
的参数,例如-bios fw_jump.bin -kernel u-boot.bin
- 从 1 和 2 也能看出
fw_payload.bin
和fw_payload.elf
之间的区别,如果包含进了 payload 那就是使用fw_payload.elf
,否则使用fw_payload.bin
FW_JUMP.elf 的内存布局
readelf -SW fw_jump.elf
There are 27 section headers, starting at offset 0xade10:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000080000000 000120 026240 00 WAX 0 0 16
[ 2] .rodata PROGBITS 0000000080027000 027120 0025c8 00 A 0 0 8
[ 3] .dynstr STRTAB 00000000800295c8 0296e8 000317 00 A 0 0 1
[ 4] .hash HASH 00000000800298e0 029a00 0000e4 04 A 11 0 8
[ 5] .gnu.hash GNU_HASH 00000000800299c8 029ae8 000104 00 A 11 0 8
[ 6] .data PROGBITS 000000008002a000 02a120 001610 00 WA 0 0 8
[ 7] .dynamic DYNAMIC 000000008002b610 02b730 000110 10 WA 3 0 8
[ 8] .got PROGBITS 000000008002b720 02b840 000130 08 WA 0 0 8
[ 9] .got.plt PROGBITS 000000008002b850 02b970 000010 08 WA 0 0 8
[10] .htif PROGBITS 000000008002b860 02b980 000010 00 WA 0 0 8
[11] .dynsym DYNSYM 000000008002b870 02b990 000390 18 A 3 2 8
[12] .rela.dyn RELA 000000008002bc00 02bd20 001fc8 18 A 11 0 8
[13] .bss NOBITS 000000008002e000 02dce8 014f58 00 WA 0 0 8
[14] .riscv.attributes RISCV_ATTRIBUTES 0000000000000000 02dce8 000042 00 0 0 1
[15] .comment PROGBITS 0000000000000000 02dd2a 00001b 01 MS 0 0 1
[16] .debug_line PROGBITS 0000000000000000 02dd45 01ecf9 00 0 0 1
[17] .debug_line_str PROGBITS 0000000000000000 04ca3e 001c6e 01 MS 0 0 1
[18] .debug_info PROGBITS 0000000000000000 04e6ac 032f27 00 0 0 1
[19] .debug_abbrev PROGBITS 0000000000000000 0815d3 00ba1f 00 0 0 1
[20] .debug_aranges PROGBITS 0000000000000000 08d000 001520 00 0 0 16
[21] .debug_str PROGBITS 0000000000000000 08e520 006b11 01 MS 0 0 1
[22] .debug_rnglists PROGBITS 0000000000000000 095031 0000d2 00 0 0 1
[23] .debug_frame PROGBITS 0000000000000000 095108 00ada0 00 0 0 8
[24] .symtab SYMTAB 0000000000000000 09fea8 008dc0 18 25 1024 8
[25] .strtab STRTAB 0000000000000000 0a8c68 0050a4 00 0 0 1
[26] .shstrtab STRTAB 0000000000000000 0add0c 0000fd 00 0 0 1
FW_PAYLOAD.elf 的内存布局
readelf -SW fw_payload.elf
There are 28 section headers, starting at offset 0xaffa8:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000080000000 000160 026240 00 WAX 0 0 16
[ 2] .rodata PROGBITS 0000000080027000 027160 0025c8 00 A 0 0 8
[ 3] .dynstr STRTAB 00000000800295c8 029728 000317 00 A 0 0 1
[ 4] .hash HASH 00000000800298e0 029a40 0000e4 04 A 11 0 8
[ 5] .gnu.hash GNU_HASH 00000000800299c8 029b28 000104 00 A 11 0 8
[ 6] .data PROGBITS 000000008002a000 02a160 001610 00 WA 0 0 8
[ 7] .dynamic DYNAMIC 000000008002b610 02b770 000110 10 WA 3 0 8
[ 8] .got PROGBITS 000000008002b720 02b880 000130 08 WA 0 0 8
[ 9] .got.plt PROGBITS 000000008002b850 02b9b0 000010 08 WA 0 0 8
[10] .htif PROGBITS 000000008002b860 02b9c0 000010 00 WA 0 0 8
[11] .dynsym DYNSYM 000000008002b870 02b9d0 000390 18 A 3 2 8
[12] .rela.dyn RELA 000000008002bc00 02bd60 001fc8 18 A 11 0 8
[13] .bss NOBITS 000000008002e000 02dd28 014f58 00 WA 0 0 8
[14] .payload PROGBITS 0000000080200000 02dd30 002128 00 AX 0 0 16
[15] .riscv.attributes RISCV_ATTRIBUTES 0000000000000000 02fe58 000042 00 0 0 1
[16] .comment PROGBITS 0000000000000000 02fe9a 00001b 01 MS 0 0 1
[17] .debug_line PROGBITS 0000000000000000 02feb5 01ecf3 00 0 0 1
[18] .debug_line_str PROGBITS 0000000000000000 04eba8 001c71 01 MS 0 0 1
[19] .debug_info PROGBITS 0000000000000000 050819 032f27 00 0 0 1
[20] .debug_abbrev PROGBITS 0000000000000000 083740 00ba1f 00 0 0 1
[21] .debug_aranges PROGBITS 0000000000000000 08f160 001520 00 0 0 16
[22] .debug_str PROGBITS 0000000000000000 090680 006b11 01 MS 0 0 1
[23] .debug_rnglists PROGBITS 0000000000000000 097191 0000d2 00 0 0 1
[24] .debug_frame PROGBITS 0000000000000000 097268 00ada0 00 0 0 8
[25] .symtab SYMTAB 0000000000000000 0a2008 008df0 18 26 1025 8
[26] .strtab STRTAB 0000000000000000 0aadf8 0050a8 00 0 0 1
[27] .shstrtab STRTAB 0000000000000000 0afea0 000106 00 0 0 1
FW_DYNAMIC.elf 的内存布局
readelf -SW fw_jump.elf
There are 27 section headers, starting at offset 0xade10:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000080000000 000120 026240 00 WAX 0 0 16
[ 2] .rodata PROGBITS 0000000080027000 027120 0025c8 00 A 0 0 8
[ 3] .dynstr STRTAB 00000000800295c8 0296e8 000317 00 A 0 0 1
[ 4] .hash HASH 00000000800298e0 029a00 0000e4 04 A 11 0 8
[ 5] .gnu.hash GNU_HASH 00000000800299c8 029ae8 000104 00 A 11 0 8
[ 6] .data PROGBITS 000000008002a000 02a120 001610 00 WA 0 0 8
[ 7] .dynamic DYNAMIC 000000008002b610 02b730 000110 10 WA 3 0 8
[ 8] .got PROGBITS 000000008002b720 02b840 000130 08 WA 0 0 8
[ 9] .got.plt PROGBITS 000000008002b850 02b970 000010 08 WA 0 0 8
[10] .htif PROGBITS 000000008002b860 02b980 000010 00 WA 0 0 8
[11] .dynsym DYNSYM 000000008002b870 02b990 000390 18 A 3 2 8
[12] .rela.dyn RELA 000000008002bc00 02bd20 001fc8 18 A 11 0 8
[13] .bss NOBITS 000000008002e000 02dce8 014f58 00 WA 0 0 8
[14] .riscv.attributes RISCV_ATTRIBUTES 0000000000000000 02dce8 000042 00 0 0 1
[15] .comment PROGBITS 0000000000000000 02dd2a 00001b 01 MS 0 0 1
[16] .debug_line PROGBITS 0000000000000000 02dd45 01ecf9 00 0 0 1
[17] .debug_line_str PROGBITS 0000000000000000 04ca3e 001c6e 01 MS 0 0 1
[18] .debug_info PROGBITS 0000000000000000 04e6ac 032f27 00 0 0 1
[19] .debug_abbrev PROGBITS 0000000000000000 0815d3 00ba1f 00 0 0 1
[20] .debug_aranges PROGBITS 0000000000000000 08d000 001520 00 0 0 16
[21] .debug_str PROGBITS 0000000000000000 08e520 006b11 01 MS 0 0 1
[22] .debug_rnglists PROGBITS 0000000000000000 095031 0000d2 00 0 0 1
[23] .debug_frame PROGBITS 0000000000000000 095108 00ada0 00 0 0 8
[24] .symtab SYMTAB 0000000000000000 09fea8 008dc0 18 25 1024 8
[25] .strtab STRTAB 0000000000000000 0a8c68 0050a4 00 0 0 1
[26] .shstrtab STRTAB 0000000000000000 0add0c 0000fd 00 0 0 1
OpenSBI 启动过程
下面的分析针对的是 v0.6
的代码。
fw_base.S
实际上这里的代码我也觉得很复杂,这里只对过程进行分析,具体每一行的代码的作用是什么,我也不太清楚。
OpenSBI 的启动代码在 fw_base.S
中,起始地址是 _start
。
在 _start 标签下,首先通过 fw_boot_hart
函数获取启动处理器的 ID,并将其保存到寄存器 a6 中。
然后检查该处理器是否为启动处理器,如果不是则跳转到等待重定位完成的循环中,否则执行接下来的步骤。
如果未成功获取启动处理器 ID 或者获取到的处理器 ID 不是当前处理器,则执行随机选择重定位目标地址的过程,即尝试从 _relocate_lottery
标签处开始循环等待。
接着根据 _link_start
和 _link_end
标签获取链接地址, _load_start
标签获取加载地址,并判断二者是否相同。如果不同,则需要进行重定位操作。
在重定位时,如果加载地址低于链接地址,则从上向下复制,反之则从下向上复制。重定位过程中会使用 _relocate_lottery
标签进行循环等待,直至重定位完成。
最后,等待重定位完成的循环中,程序会一直等待直到 _boot_status
值变为 1,表示重定位完成,才跳转到 _wait_for_boot_hart
标签处等待启动处理器的启动。
当重定位完成之后(_relocate_done
之后的代码),就会准备启动 HART。请注意 la a4, platform
这一行的指令,所谓 platform 就是一个变量,如果在编译时指定的平台是 qemu/virt
的话,那么 platform 变量就定义在 platform/qemu/virt/platform.c
文件中,其它平台类似。之后的代码如果想要获得 platform
变量的指针,只需要调用 sbi_platform_ptr
函数。
之后是准备 scratch 空间。这里所谓的 scratch 空间是指 struct sbi_scratch
结构体,起始就是初始化这个结构体。这个结构体将会传递给 sbi_init()
函数。
_bss_zero
标签下的代码就是将 bss 段清零。
_prev_arg1_override_done
标签下的代码用于重定位一个已经扁平化的设备树(Flattend Device Tree, FDT)的代码。
首先,通过前一个启动阶段传递过来的参数 a0、a1 和 a2,保存了当前 FDT 的源地址指针。接着,通过调用函数 fw_next_arg1()获取下一个启动阶段传递过来的参数 a1,即将被重定位到的 FDT 的目标地址指针。如果 a1 为 0 或者 a1 等于当前 FDT 的源地址指针,则说明不需要进行重定位,直接跳转到_fdt_reloc_done 标签处。
如果需要进行重定位,则需要计算出源 FDT 的大小,并将其从源地址拷贝到目标地址,完成重定位。具体操作如下:
- 首先,将目标地址按照指针大小对齐,并保存为 t1。
- 然后,从源地址中读取 FDT 大小,该大小为大端格式,需要将其拆分为四个字节:bit[31:24]、bit[23:16]、bit[15:8]和 bit[7:0],并组合成小端格式,保存在 t2 寄存器中。
- 接着,将 t1 加上 t2,得到目标 FDT 的结束地址,保存在 t2 寄存器中。这样就确定了拷贝数据的范围。
- 最后,循环拷贝数据,将源 FDT 中的数据拷贝到目标 FDT 中。循环次数为源 FDT 大小除以指针大小,即源 FDT 中包含的指针数量。
完成拷贝后,将 BOOT_STATUS_BOOT_HART_DONE 保存到_boot_status 寄存器中,表示当前处理器已经完成启动。最后,通过调用_start_warm 跳转到下一步操作。
_start_warm
:在初始化过程中,需要禁用和清除所有中断,并设置当前处理器的栈指针和 trap handler(异常处理函数)。
具体的操作如下:
- 首先,调用_reset_regs 函数,将寄存器状态重置为 0,以保证非引导处理器使用前的状态干净、一致。
- 接着,禁用和清空所有中断,即将 CSR_MIE 和 CSR_MIP 寄存器都设置为 0。
- 获取 platform 变量的地址,并读取平台配置信息,包括处理器数量(s7)和每个处理器的栈大小(s8)。
- 获取当前处理器的 ID(s6),并判断其是否超出了处理器数量的范围。如果超出,则跳转到_start_hang 标签,表示出现了错误。
- 计算当前处理器对应的 scratch space 的地址,并将其保存到 CSR_MSCRATCH 寄存器中,作为 SBI 运行时的全局变量。
- 将 scratch space 地址保存到 SP 寄存器中,作为当前处理器的栈指针。
- 设置 trap handler 为_trap_handler 函数,即当发生异常时会跳转到该函数进行处理。同时,读取 MTVEC 寄存器的值确保 trap handler 已经设置成功。
- 调用 sbi_init 函数进行 SBI 运行时的初始化。sbi_init 函数将会初始化各种全局变量、锁、Hart Table 等。
- 最后,通过跳转到_start_hang 标签等待处理器发生异常或被重置。
sbi_init()函数
// 传入的参数scratch 已经在fw_base.S中初始化好了
void __noreturn sbi_init(struct sbi_scratch *scratch)
{
bool next_mode_supported = FALSE;
bool coldboot = FALSE;
u32 hartid = current_hartid();
// plat 就定义在 platform 文件夹下面,你编译的时候指定的是哪个平台,就看相应平台的代码
const struct sbi_platform *plat = sbi_platform_ptr(scratch);
if ((SBI_HARTMASK_MAX_BITS <= hartid) ||
sbi_platform_hart_invalid(plat, hartid))
sbi_hart_hang();
switch (scratch->next_mode) {
case PRV_M:
next_mode_supported = TRUE;
break;
case PRV_S:
if (misa_extension('S'))
next_mode_supported = TRUE;
break;
case PRV_U:
if (misa_extension('U'))
next_mode_supported = TRUE;
break;
default:
sbi_hart_hang();
}
/*
* Only the HART supporting privilege mode specified in the
* scratch->next_mode should be allowed to become the coldboot
* HART because the coldboot HART will be directly jumping to
* the next booting stage.
*
* We use a lottery mechanism to select coldboot HART among
* HARTs which satisfy above condition.
*/
// 使用原子指令避免多个hart的多次冷启动
// 使得只有一个hart进行冷启动
if (next_mode_supported && atomic_xchg(&coldboot_lottery, 1) == 0)
coldboot = TRUE;
/*
* Do platform specific nascent (very early) initialization so
* that platform can initialize platform specific per-HART CSRs
* or per-HART devices.
*/
if (sbi_platform_nascent_init(plat))
sbi_hart_hang();
// 只有一个hart会执行冷启动,其它hart都会执行热启动
// 热启动中有个函数叫 sbi_hsm_init 会等待冷启动完成才会继续向下执行
if (coldboot)
init_coldboot(scratch, hartid);
else
init_warmboot(scratch, hartid);
}
init_coldboot()
static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
{
int rc;
unsigned long *init_count;
const struct sbi_platform *plat = sbi_platform_ptr(scratch);
/* Note: This has to be first thing in coldboot init sequence */
// 其实就是初始化了 hartid_to_scratch_table ,可以方便地根据 hartid 获取相应的
// struct sbi_scratch *
rc = sbi_scratch_init(scratch);
if (rc)
sbi_hart_hang();
/* Note: This has to be second thing in coldboot init sequence */
// 这个函数初始化了 struct sbi_domain_memregion root_fw_region; 变量
// root_fw_region = {.base = scratch->fw_start,
// .order = log2roundup(scratch->size), .flags = 0};
// root_memregs[0] = root_fw_region;
// root_memregs[1] = {0, log2roundup(~0UL)=64,
// (SBI_DOMAIN_MEMREGION_READABLE |
// SBI_DOMAIN_MEMREGION_WRITEABLE |
// SBI_DOMAIN_MEMREGION_EXECUTABLE)};
// root_memregs[2] = {0, 0, 0};
/*
struct sbi_domain root = {
.name = "root",
.possible_harts = &root_hmask, //记录的就是哪些hart是可用的
.regions = root_memregs,
.system_reset_allowed = TRUE,
.boot_harid = cold_hartid,
.next_arg1 = scratch->next_arg1,
.next_addr = scratch->next_addr,
.next_mode = scratch->next_mode
};
最后调用了 sbi_domain_register 函数
*/
rc = sbi_domain_init(scratch, hartid);
if (rc)
sbi_hart_hang();
// 这里获得的是 scratch 空间中的空闲空间地址相对scratch空间首地址的偏移
// scratch 空间的大小是 SBI_SCRATCH_SIZE = 4KB
// struct sbi_scratch 占用的空间是 10 * __SIZEOF_POINTER__
init_count_offset = sbi_scratch_alloc_offset(__SIZEOF_POINTER__);
if (!init_count_offset)
sbi_hart_hang();
// 这里将当前 hart 的状态设置成了 SBI_HSM_STATE_START_PENDING
// 将其它 hart 的状态设置成了 SBI_HSM_STATE_STOPPED
// 如果当前的 hart 不是执行冷启动的hart,就会调用 sbi_hsm_hart_wait
// 只有当 hart 状态被设置为 SBI_HSM_STATE_START_PENDING 才会跳出 sbi_hsm_hart_wait 函数
// 因此该函数会阻止热启动的 hart 继续执行,直到冷启动完成并将执行热启动的hart的状态修改为
// SBI_HSM_STATE_START_PENDING
rc = sbi_hsm_init(scratch, hartid, TRUE);
if (rc)
sbi_hart_hang();
// 实际上调用的就是 (struct sbi_platform)->platform_ops_addr->early_init()
rc = sbi_system_early_init(scratch, TRUE);
if (rc)
sbi_hart_hang();
// 这里的函数比较复杂,后文讲解
rc = sbi_hart_init(scratch, hartid, TRUE);
if (rc)
sbi_hart_hang();
// 实际上调用的就是 (struct sbi_platform)->platform_ops_addr->console_init()
rc = sbi_console_init(scratch);
if (rc)
sbi_hart_hang();
// 关于 pmu 参见
// https://github.com/riscv-software-src/opensbi/blob/master/docs/pmu_support.md
rc = sbi_pmu_init(scratch, TRUE);
if (rc)
sbi_hart_hang();
sbi_boot_print_banner(scratch);
// 实际上调用的就是 (struct sbi_platform)->platform_ops_addr->irqchip_init()
rc = sbi_irqchip_init(scratch, TRUE);
if (rc) {
sbi_printf("%s: irqchip init failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
// 实际上调用的就是 (struct sbi_platform)->platform_ops_addr->ipi_init()
rc = sbi_ipi_init(scratch, TRUE);
if (rc) {
sbi_printf("%s: ipi init failed (error %d)\n", __func__, rc);
sbi_hart_hang();
}
/*
static struct sbi_ipi_event_ops tlb_ops = {
.name = "IPI_TLB",
.update = tlb_update,
.sync = tlb_sync,
.process = tlb_process,
};
将 该变量注册到了
static const struct sbi_ipi_event_ops *ipi_ops_array[SBI_IPI_EVENT_MAX];
中
*/
rc = sbi_tlb_init(scratch, TRUE);
if (rc) {
sbi_printf("%s: tlb init failed (error %d)\n", __func__, rc);
sbi_hart_hang();
}
// 实际上调用的就是 (struct sbi_platform)->platform_ops_addr->timer_init()
rc = sbi_timer_init(scratch, TRUE);
if (rc) {
sbi_printf("%s: timer init failed (error %d)\n", __func__, rc);
sbi_hart_hang();
}
// 函数中使用到了 sbi_ecall_exts 该变量定义在
// build/lib/sbi/sbi_ecall_exts.c
// 后面会介绍该文件的生成
rc = sbi_ecall_init();
if (rc) {
sbi_printf("%s: ecall init failed (error %d)\n", __func__, rc);
sbi_hart_hang();
}
/*
* Note: Finalize domains after HSM initialization so that we
* can startup non-root domains.
* Note: Finalize domains before HART PMP configuration so
* that we use correct domain for configuring PMP.
*/
rc = sbi_domain_finalize(scratch, hartid);
if (rc) {
sbi_printf("%s: domain finalize failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
rc = sbi_hart_pmp_configure(scratch);
if (rc) {
sbi_printf("%s: PMP configure failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
/*
* Note: Platform final initialization should be last so that
* it sees correct domain assignment and PMP configuration.
*/
rc = sbi_platform_final_init(plat, TRUE);
if (rc) {
sbi_printf("%s: platform final init failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
sbi_boot_print_general(scratch);
sbi_boot_print_domains(scratch);
sbi_boot_print_hart(scratch, hartid);
wake_coldboot_harts(scratch, hartid);
init_count = sbi_scratch_offset_ptr(scratch, init_count_offset);
(*init_count)++;
sbi_hsm_prepare_next_jump(scratch, hartid);
// 从这里切换到启动过程的下一个阶段
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE);
}
OpenSBI 的编译与功能拓展
CARRAY 编译
在 opensbi 的编译过程中,有一个过程比较特殊,必须要理解该过程才能够为 opensbi 进行功能拓展。在执行 make PLATFORM=generic
时查阅输出 log 可以看到如下的几条特殊输出(原本的输出只有 CARRY 那一行,我在 makefile 中添加了两行输出,除了生成 sbi_ecall_exts.c
文件外,还会自动在 build 目录下生成其它的.c 文件,这里只举 sbi_ecall_exts.c
的例子):
Generating C array for /root/opensbi/build/lib/sbi/sbi_ecall_exts.c from /root/opensbi/lib/sbi/sbi_ecall_exts.carray with variables: ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor
CARRAY lib/sbi/sbi_ecall_exts.c
Done generating C array for /root/opensbi/build/lib/sbi/sbi_ecall_exts.c.
从输出中我们不难看出,编译时会根据 lib/sbi/sbi_ecall_exts.carray
文件和 ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor
变量自动生成 build/lib/sbi/sbi_ecall_exts.c
文件。在 makefile 中的编译执行是:
compile_carray = $(CMD_PREFIX)mkdir -p `dirname $(1)`; \
echo " CARRAY $(subst $(build_dir)/,,$(1))"; \
$(eval CARRAY_VAR_LIST := $(carray-$(subst .c,,$(shell basename $(1)))-y)) \
$(info Generating C array for $(1) from $(2) with variables: $(CARRAY_VAR_LIST)) \
$(src_dir)/scripts/carray.sh -i $(2) -l "$(CARRAY_VAR_LIST)" > $(1); \
echo " Done generating C array for $(1)."
从这几条 shell 指令中不难看出,build/lib/sbi/sbi_ecall_exts.c
文件是使用 scripts/carray.sh
脚本生成的。在这个生成 sbi_ecall_exts.c
例子中,运行脚本的完整指令是 scripts/carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
。
sbi_ecall_exts.carray
的内容如下:
HEADER: sbi/sbi_ecall.h
TYPE: struct sbi_ecall_extension
NAME: sbi_ecall_exts
当执行 carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
时,脚本的执行过程如下:
- 脚本使用
getopts
命令解析命令行选项,并检查是否正确指定了输入文件和相应的值。具体来说,选项-i
指定了一个值为lib/sbi/sbi_ecall_exts.carray
的输入文件,选项-l
指定了一个值为"ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
的变量名列表。 - 脚本读取
lib/sbi/sbi_ecall_exts.carray
文件,通过cat
命令读取文件内容,并从中提取出HEADER:
、TYPE:
和NAME:
三个标签的值,分别为sbi/sbi_ecall.h
、struct sbi_ecall_extension
和sbi_ecall_exts
。 - 脚本使用
printf
输出#include <sbi/sbi_ecall.h>
头文件语句和每个变量的外部声明语句。由于选项-l
指定了变量名列表,因此循环遍历该列表,输出一个形如extern struct sbi_ecall_extension ecall_time;
的声明语句。这样做是因为我们将要把这些变量放到一个指针数组中,所以需要先声明它们。 - 接下来,脚本输出一个数组初始化代码,使用
printf
命令输出一个指针数组,其中元素是每个变量的地址。根据sbi_ecall_exts.carray
文件中的内容,输出的数组类型为struct sbi_ecall_extension *sbi_ecall_exts[]
,并依次输出每个元素的地址,输出格式如下:
struct sbi_ecall_extension *sbi_ecall_exts[] = {
&ecall_time,&ecall_rfence,&ecall_ipi,&ecall_base,&ecall_hsm,&ecall_srst,&ecall_pmu,&ecall_legacy,&ecall_vendor,
};
- 最后,脚本使用
printf
输出一个赋值语句,计算数组中元素数量的大小,并将该值赋给sbi_ecall_exts_size
变量。输出的赋值语句格式为:unsigned long sbi_ecall_exts_size = sizeof(sbi_ecall_exts) / sizeof(struct sbi_ecall_extension *);
综上所述,执行命令 carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
后,脚本的输出结果为:
#include <sbi/sbi_ecall.h>
extern struct sbi_ecall_extension ecall_time;
extern struct sbi_ecall_extension ecall_rfence;
extern struct sbi_ecall_extension ecall_ipi;
extern struct sbi_ecall_extension ecall_base;
extern struct sbi_ecall_extension ecall_hsm;
extern struct sbi_ecall_extension ecall_srst;
extern struct sbi_ecall_extension ecall_pmu;
extern struct sbi_ecall_extension ecall_legacy;
extern struct sbi_ecall_extension ecall_vendor;
struct sbi_ecall_extension *sbi_ecall_exts[] = {
&ecall_time,
&ecall_rfence,
&ecall_ipi,
&ecall_base,
&ecall_hsm,
&ecall_srst,
&ecall_pmu,
&ecall_legacy,
&ecall_vendor,
};
unsigned long sbi_ecall_exts_size = sizeof(sbi_ecall_exts) / sizeof(struct sbi_ecall_extension *);
S-mode 如何调用 opensbi 提供的功能
以 Linux 5.19 的内核为例,某个调用 opensbi 相应时钟中断的函数定义如下:
static void __sbi_set_timer_v02(uint64_t stime_value)
{
#if __riscv_xlen == 32
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER, stime_value,
stime_value >> 32, 0, 0, 0, 0);
#else
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER, stime_value, 0,
0, 0, 0, 0);
#endif
}
sbi_ecall
的前两个参数分别是 extension ID
和 function ID
(分别简称 EID 和 FID,读者参见 SBI 的标准文件),后面的若干个参数都是实际传递给 opensbi 实现的参数。前两个参数的作用是:opensbi 根据这两个参数分发给相应的拓展和拓展中的函数的。
在前文中我们说 CARRAY 自动生成了 build/lib/sbi/sbi_ecall_exts.c
,里面有一个 sbi_ecall_exts
数组分别指向了不同的拓展实现,例如 ecall_time
变量。该变量定义在 lib/sbi/sbi_ecall_time.c
中,
// extid_start 和 extid_end定义了 EID的范围,一般情况下两个值相同即可,表示只占用这一个拓展号
// 如果占用多个拓展号
// sbi_ecall_time_handler 就是处理函数
// 当S-mode的代码传入的参数的 EID 在 [extid_start, extid_end]时,
// opensbi就会将处理函数转发给 sbi_ecall_time_handler 函数进行处理
struct sbi_ecall_extension ecall_time = {
.extid_start = SBI_EXT_TIME,
.extid_end = SBI_EXT_TIME,
.handle = sbi_ecall_time_handler,
};
我们可以看出,Linux 内核中的代码使用的 EID 是 SBI_EXT_TIME = 0x54494D45
,opensbi 中给 ecall_time
分配的 EID 也是 SBI_EXT_TIME = 0x54494D45
,因此 opensbi 在接收到 Linux 内核的调用请求之后,就会自动调用 sbi_ecall_time_handler
函数。
我们再看该函数的实现:
static int sbi_ecall_time_handler(unsigned long extid, unsigned long funcid,
const struct sbi_trap_regs *regs,
unsigned long *out_val,
struct sbi_trap_info *out_trap)
{
int ret = 0;
if (funcid == SBI_EXT_TIME_SET_TIMER) {
#if __riscv_xlen == 32
sbi_timer_event_start((((u64)regs->a1 << 32) | (u64)regs->a0));
#else
sbi_timer_event_start((u64)regs->a0);
#endif
} else
ret = SBI_ENOTSUPP;
return ret;
}
在这个函数中,也是首先传入 EID 和 FID,因为我们知道 struct sbi_ecall_extension
是可以分配一个 EID 的区间的,因此在处理函数内部依然需要根据 EID 进行细致的分发,由于 ecall_time
仅占用了一个 EID,就不需要再在处理函数内部进行二次分发了,但是需要在处理函数内部根据 FID 进行分发。该处理函数仅实现了一个具体的处理函数,如果说要根据情况调用不同的函数,那么就可以根据 FID 的值进行二次分发了。
添加一个新的拓展
在前面分析 opensbi 的拓展的编译的时候,有一个问题我没提,就是 Makefile 是怎么知道要在生成 sbi_ecall_exts.c
的时候传递哪些参数呢?
实际上是在 lib/sbi/objects.mk
的最前面有这些指令:
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_TIME) += ecall_time
libsbi-objs-$(CONFIG_SBI_ECALL_TIME) += sbi_ecall_time.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_RFENCE) += ecall_rfence
libsbi-objs-$(CONFIG_SBI_ECALL_RFENCE) += sbi_ecall_rfence.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_IPI) += ecall_ipi
libsbi-objs-$(CONFIG_SBI_ECALL_IPI) += sbi_ecall_ipi.o
carray-sbi_ecall_exts-y += ecall_base
libsbi-objs-y += sbi_ecall_base.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_HSM) += ecall_hsm
libsbi-objs-$(CONFIG_SBI_ECALL_HSM) += sbi_ecall_hsm.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_SRST) += ecall_srst
libsbi-objs-$(CONFIG_SBI_ECALL_SRST) += sbi_ecall_srst.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_PMU) += ecall_pmu
libsbi-objs-$(CONFIG_SBI_ECALL_PMU) += sbi_ecall_pmu.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_LEGACY) += ecall_legacy
libsbi-objs-$(CONFIG_SBI_ECALL_LEGACY) += sbi_ecall_legacy.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_VENDOR) += ecall_vendor
libsbi-objs-$(CONFIG_SBI_ECALL_VENDOR) += sbi_ecall_vendor.o
我们观察发现,这里其实就是添加拓展的地方。如果你自己自定义了一个 ecall_helloworld
的拓展,那么就需要加上这几行
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_VENDOR) += ecall_helloworld
libsbi-objs-$(CONFIG_SBI_ECALL_VENDOR) += sbi_ecall_helloworld.o
同时呢要注意,实现该拓展的文件名也应该是 ecall_helloworld.c
。