opensbi入門

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

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章