opensbi入門

2023-07-16 21:00:40

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

參考資料