opensbi入门

FW_JUMP#

OpenSBI 带跳转地址的固件(FW_JUMP)是一种仅处理下一个引导阶段入口地址的固件。例如,它可以处理引导加载程序或操作系统内核的入口地址,但不直接包含下一阶段的二进制代码。

FW_JUMP 固件特别适用于在执行 OpenSBI 固件之前的引导阶段能够加载 OpenSBI 固件和后续引导阶段二进制文件的情况。

启用平台 FW_JUMP 固件有以下两种方法:

  1. 在顶层 make 命令行中指定 FW_JUMP=y。
  2. 在目标平台的 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 固件有以下两种方法:

  1. 在顶层 make 命令行中指定 FW_DYNAMIC=y。
  2. 在目标平台的 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 固件可以通过以下任一方法实现:

  1. 在顶层 make 命令行中指定 FW_PAYLOAD=y。
  2. 在目标平台的 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 之间的区别。

  1. 当没有 payload 的时候,就是说不指定 payload,此时要想 run 起来就必须使用 fw_payload.bin
  2. 当编译 OpenSBI 时指定了 FW_PAYLOAD_PATH 的时候,可以只使用 fw_payload.elf 作为 -bios 的参数,因为 payload 已经打包进了 fw_payload.elf 中了;也可以分开指定 -bios 和 kernel 的参数,例如 -bios fw_jump.bin -kernel u-boot.bin
  3. 从 1 和 2 也能看出 fw_payload.bin 和 fw_payload.elf 之间的区别,如果包含进了 payload 那就是使用 fw_payload.elf,否则使用 fw_payload.bin

回到顶部

FW_JUMP.elf 的内存布局#

 

Copy

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 的内存布局#

 

Copy

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 的内存布局#

 

Copy

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 的大小,并将其从源地址拷贝到目标地址,完成重定位。具体操作如下:

  1. 首先,将目标地址按照指针大小对齐,并保存为 t1。
  2. 然后,从源地址中读取 FDT 大小,该大小为大端格式,需要将其拆分为四个字节:bit[31:24]、bit[23:16]、bit[15:8]和 bit[7:0],并组合成小端格式,保存在 t2 寄存器中。
  3. 接着,将 t1 加上 t2,得到目标 FDT 的结束地址,保存在 t2 寄存器中。这样就确定了拷贝数据的范围。
  4. 最后,循环拷贝数据,将源 FDT 中的数据拷贝到目标 FDT 中。循环次数为源 FDT 大小除以指针大小,即源 FDT 中包含的指针数量。

完成拷贝后,将 BOOT_STATUS_BOOT_HART_DONE 保存到_boot_status 寄存器中,表示当前处理器已经完成启动。最后,通过调用_start_warm 跳转到下一步操作。

_start_warm:在初始化过程中,需要禁用和清除所有中断,并设置当前处理器的栈指针和 trap handler(异常处理函数)。

具体的操作如下:

  1. 首先,调用_reset_regs 函数,将寄存器状态重置为 0,以保证非引导处理器使用前的状态干净、一致。
  2. 接着,禁用和清空所有中断,即将 CSR_MIE 和 CSR_MIP 寄存器都设置为 0。
  3. 获取 platform 变量的地址,并读取平台配置信息,包括处理器数量(s7)和每个处理器的栈大小(s8)。
  4. 获取当前处理器的 ID(s6),并判断其是否超出了处理器数量的范围。如果超出,则跳转到_start_hang 标签,表示出现了错误。
  5. 计算当前处理器对应的 scratch space 的地址,并将其保存到 CSR_MSCRATCH 寄存器中,作为 SBI 运行时的全局变量。
  6. 将 scratch space 地址保存到 SP 寄存器中,作为当前处理器的栈指针。
  7. 设置 trap handler 为_trap_handler 函数,即当发生异常时会跳转到该函数进行处理。同时,读取 MTVEC 寄存器的值确保 trap handler 已经设置成功。
  8. 调用 sbi_init 函数进行 SBI 运行时的初始化。sbi_init 函数将会初始化各种全局变量、锁、Hart Table 等。
  9. 最后,通过跳转到_start_hang 标签等待处理器发生异常或被重置。

回到顶部

sbi_init()函数#

 

Copy

// 传入的参数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()#

 

Copy

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 的例子):

 

Copy

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 中的编译执行是:

 

Copy

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 的内容如下:

 

Copy

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" 时,脚本的执行过程如下:

  1. 脚本使用 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" 的变量名列表。
  2. 脚本读取 lib/sbi/sbi_ecall_exts.carray 文件,通过 cat 命令读取文件内容,并从中提取出 HEADER:TYPE: 和 NAME: 三个标签的值,分别为 sbi/sbi_ecall.hstruct sbi_ecall_extension 和 sbi_ecall_exts
  3. 脚本使用 printf 输出 #include <sbi/sbi_ecall.h> 头文件语句和每个变量的外部声明语句。由于选项 -l 指定了变量名列表,因此循环遍历该列表,输出一个形如 extern struct sbi_ecall_extension ecall_time; 的声明语句。这样做是因为我们将要把这些变量放到一个指针数组中,所以需要先声明它们。
  4. 接下来,脚本输出一个数组初始化代码,使用 printf 命令输出一个指针数组,其中元素是每个变量的地址。根据 sbi_ecall_exts.carray 文件中的内容,输出的数组类型为 struct sbi_ecall_extension *sbi_ecall_exts[],并依次输出每个元素的地址,输出格式如下:
 

Copy

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, };

  1. 最后,脚本使用 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" 后,脚本的输出结果为:

 

Copy

#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 相应时钟中断的函数定义如下:

 

Copy

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 中,

 

Copy

// 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 函数。

我们再看该函数的实现:

 

Copy

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 的最前面有这些指令:

 

Copy

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 的拓展,那么就需要加上这几行

 

Copy

carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_VENDOR) += ecall_helloworld libsbi-objs-$(CONFIG_SBI_ECALL_VENDOR) += sbi_ecall_helloworld.o

同时呢要注意,实现该拓展的文件名也应该是 ecall_helloworld.c

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野生的狒狒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值