OpenSBI 入门

声明

本文为本人原创,未经允许,严禁转载。

FW_JUMP FW_PAYLOAD FW_DYNAMIC

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.binfw_payload.elf 之间的区别。

  1. 当没有 payload 的时候,就是说不指定 payload,此时要想 run 起来就必须使用 fw_payload.bin
  2. 当编译 OpenSBI 时指定了 FW_PAYLOAD_PATH 的时候,可以只使用 fw_payload.elf 作为 -bios 的参数,因为 payload 已经打包进了 fw_payload.elf 中了;也可以分开指定 -bioskernel 的参数,例如 -bios fw_jump.bin -kernel u-boot.bin
  3. 从 1 和 2 也能看出 fw_payload.binfw_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 的大小,并将其从源地址拷贝到目标地址,完成重定位。具体操作如下:

  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()函数

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

  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_extensionsbi_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[],并依次输出每个元素的地址,输出格式如下:
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" 后,脚本的输出结果为:

#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 IDfunction 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

参考资料

07-17 02:55