本文為本人原創,未經允許,嚴禁轉載。
OpenSBI 帶跳轉地址的韌體(FW_JUMP)是一種僅處理下一個引導階段入口地址的韌體。例如,它可以處理引導載入程式或作業系統核心的入口地址,但不直接包含下一階段的二進位制程式碼。
FW_JUMP 韌體特別適用於在執行 OpenSBI 韌體之前的引導階段能夠載入 OpenSBI 韌體和後續引導階段二進位制檔案的情況。
啟用平臺 FW_JUMP 韌體有以下兩種方法:
編譯後的 FW_JUMP 韌體 ELF 檔名為 fw_jump.elf。擴充套件的映象檔名為 fw_jump.bin。這兩個檔案都會被建立在特定平臺的構建目錄下的 build/platform/<platform_subdir>/firmware 目錄中。
OpenSBI 帶動態資訊的韌體(FW_DYNAMIC)是一種可以從前一個引導階段獲取關於下一引導階段(例如引導載入程式或作業系統)和執行時 OpenSBI 庫選項的資訊的韌體。
前一個引導階段將通過在記憶體中建立 struct fw_dynamic_info
結構體並通過 RISC-V CPU 的 a2
暫存器傳遞其地址給 FW_DYNAMIC 來傳遞資訊。在 RV64 上,地址必須對齊到 8 位元組;在 RV32 上,地址必須對齊到 4 位元組。
FW_DYNAMIC 韌體特別適用於在執行 OpenSBI 韌體之前的引導階段能夠載入 OpenSBI 韌體和後續引導階段二進位制檔案的情況。
啟用平臺 FW_DYNAMIC 韌體有以下兩種方法:
編譯後的 FW_DYNAMIC 韌體 ELF 檔名為 fw_dynamic.elf。擴充套件的映象檔名為 fw_dynamic.bin。這兩個檔案都會被建立在特定平臺的構建目錄下的 build/platform/<platform_subdir>/firmware 目錄中。
FW_PAYLOAD 是一種直接包含引導階段二進位制檔案的韌體,用於引導 OpenSBI 韌體的執行。通常情況下,這個 payload 將是一個引導載入程式或作業系統核心。
FW_PAYLOAD 韌體特別適用於在 OpenSBI 韌體之前執行的引導階段無法同時載入 OpenSBI 韌體和後續引導階段的情況。
FW_PAYLOAD 韌體也適用於在 OpenSBI 韌體之前的引導階段未傳遞扁平裝置樹(FDT 檔案)的情況。在這種情況下,FW_PAYLOAD 韌體允許將扁平裝置樹嵌入到最終韌體的.rodata 部分中。
啟用 FW_PAYLOAD 韌體可以通過以下任一方法實現:
編譯後的 FW_PAYLOAD 韌體 ELF 檔名為 fw_payload.elf。擴充套件的映象檔名為 fw_payload.bin。這兩個檔案都將在特定平臺的構建目錄下的 build/platform/<platform_subdir>/firmware 目錄中建立。
fw_jump 與 fw_dynamic 都不含有 payload,區別就在於是否從前一個引導過程獲取資訊。fw_jump 不會再前一個引導過程獲取資訊,因此它所包含的下一階段的引導程式碼的入口地址是靜態的,要求下一階段的程式碼必須要載入到指定位置。fw_dynamic 可以從前一個引導過程獲取資訊,因此下一階段的引導程式碼的入口地址可以不固定。
含有 payload 的 OpenSBI (FW_PAYLOAD)是一種韌體,它直接包含了用於跟隨 OpenSBI 韌體執行的啟動階段二進位制檔案。通常,這個負載將是一個引導載入程式或作業系統核心。
從 QEMU RISC-V Virt Machine Platform 案例中能看出 fw_jump.bin
和 fw_payload.elf
之間的區別。
fw_payload.bin
FW_PAYLOAD_PATH
的時候,可以只使用 fw_payload.elf
作為 -bios
的引數,因為 payload 已經打包進了 fw_payload.elf
中了;也可以分開指定 -bios
和 kernel
的引數,例如 -bios fw_jump.bin -kernel u-boot.bin
fw_payload.bin
和 fw_payload.elf
之間的區別,如果包含進了 payload 那就是使用 fw_payload.elf
,否則使用 fw_payload.bin
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
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
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
下面的分析針對的是 v0.6
的程式碼。
實際上這裡的程式碼我也覺得很複雜,這裡只對過程進行分析,具體每一行的程式碼的作用是什麼,我也不太清楚。
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 的大小,並將其從源地址拷貝到目標地址,完成重定位。具體操作如下:
完成拷貝後,將 BOOT_STATUS_BOOT_HART_DONE 儲存到_boot_status 暫存器中,表示當前處理器已經完成啟動。最後,通過呼叫_start_warm 跳轉到下一步操作。
_start_warm
:在初始化過程中,需要禁用和清除所有中斷,並設定當前處理器的棧指標和 trap handler(例外處理函數)。
具體的操作如下:
// 傳入的引數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);
}
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 的編譯過程中,有一個過程比較特殊,必須要理解該過程才能夠為 opensbi 進行功能拓展。在執行 make PLATFORM=generic
時查閱輸出 log 可以看到如下的幾條特殊輸出(原本的輸出只有 CARRY 那一行,我在 makefile 中新增了兩行輸出,除了生成 sbi_ecall_exts.c
檔案外,還會自動在 build 目錄下生成其它的.c 檔案,這裡只舉 sbi_ecall_exts.c
的例子):
Generating C array for /root/opensbi/build/lib/sbi/sbi_ecall_exts.c from /root/opensbi/lib/sbi/sbi_ecall_exts.carray with variables: ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor
CARRAY lib/sbi/sbi_ecall_exts.c
Done generating C array for /root/opensbi/build/lib/sbi/sbi_ecall_exts.c.
從輸出中我們不難看出,編譯時會根據 lib/sbi/sbi_ecall_exts.carray
檔案和 ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor
變數自動生成 build/lib/sbi/sbi_ecall_exts.c
檔案。在 makefile 中的編譯執行是:
compile_carray = $(CMD_PREFIX)mkdir -p `dirname $(1)`; \
echo " CARRAY $(subst $(build_dir)/,,$(1))"; \
$(eval CARRAY_VAR_LIST := $(carray-$(subst .c,,$(shell basename $(1)))-y)) \
$(info Generating C array for $(1) from $(2) with variables: $(CARRAY_VAR_LIST)) \
$(src_dir)/scripts/carray.sh -i $(2) -l "$(CARRAY_VAR_LIST)" > $(1); \
echo " Done generating C array for $(1)."
從這幾條 shell 指令中不難看出,build/lib/sbi/sbi_ecall_exts.c
檔案是使用 scripts/carray.sh
指令碼生成的。在這個生成 sbi_ecall_exts.c
例子中,執行指令碼的完整指令是 scripts/carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
。
sbi_ecall_exts.carray
的內容如下:
HEADER: sbi/sbi_ecall.h
TYPE: struct sbi_ecall_extension
NAME: sbi_ecall_exts
當執行 carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
時,指令碼的執行過程如下:
getopts
命令解析命令列選項,並檢查是否正確指定了輸入檔案和相應的值。具體來說,選項 -i
指定了一個值為 lib/sbi/sbi_ecall_exts.carray
的輸入檔案,選項 -l
指定了一個值為 "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
的變數名列表。lib/sbi/sbi_ecall_exts.carray
檔案,通過 cat
命令讀取檔案內容,並從中提取出 HEADER:
、TYPE:
和 NAME:
三個標籤的值,分別為 sbi/sbi_ecall.h
、struct sbi_ecall_extension
和 sbi_ecall_exts
。printf
輸出 #include <sbi/sbi_ecall.h>
標頭檔案語句和每個變數的外部宣告語句。由於選項 -l
指定了變數名列表,因此迴圈遍歷該列表,輸出一個形如 extern struct sbi_ecall_extension ecall_time;
的宣告語句。這樣做是因為我們將要把這些變數放到一個指標陣列中,所以需要先宣告它們。printf
命令輸出一個指標陣列,其中元素是每個變數的地址。根據 sbi_ecall_exts.carray
檔案中的內容,輸出的陣列型別為 struct sbi_ecall_extension *sbi_ecall_exts[]
,並依次輸出每個元素的地址,輸出格式如下:struct sbi_ecall_extension *sbi_ecall_exts[] = {
&ecall_time,&ecall_rfence,&ecall_ipi,&ecall_base,&ecall_hsm,&ecall_srst,&ecall_pmu,&ecall_legacy,&ecall_vendor,
};
printf
輸出一個賦值語句,計算陣列中元素數量的大小,並將該值賦給 sbi_ecall_exts_size
變數。輸出的賦值語句格式為:unsigned long sbi_ecall_exts_size = sizeof(sbi_ecall_exts) / sizeof(struct sbi_ecall_extension *);
綜上所述,執行命令 carray.sh -i lib/sbi/sbi_ecall_exts.carray -l "ecall_time ecall_rfence ecall_ipi ecall_base ecall_hsm ecall_srst ecall_pmu ecall_legacy ecall_vendor"
後,指令碼的輸出結果為:
#include <sbi/sbi_ecall.h>
extern struct sbi_ecall_extension ecall_time;
extern struct sbi_ecall_extension ecall_rfence;
extern struct sbi_ecall_extension ecall_ipi;
extern struct sbi_ecall_extension ecall_base;
extern struct sbi_ecall_extension ecall_hsm;
extern struct sbi_ecall_extension ecall_srst;
extern struct sbi_ecall_extension ecall_pmu;
extern struct sbi_ecall_extension ecall_legacy;
extern struct sbi_ecall_extension ecall_vendor;
struct sbi_ecall_extension *sbi_ecall_exts[] = {
&ecall_time,
&ecall_rfence,
&ecall_ipi,
&ecall_base,
&ecall_hsm,
&ecall_srst,
&ecall_pmu,
&ecall_legacy,
&ecall_vendor,
};
unsigned long sbi_ecall_exts_size = sizeof(sbi_ecall_exts) / sizeof(struct sbi_ecall_extension *);
以 Linux 5.19 的核心為例,某個呼叫 opensbi 相應時鐘中斷的函數定義如下:
static void __sbi_set_timer_v02(uint64_t stime_value)
{
#if __riscv_xlen == 32
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER, stime_value,
stime_value >> 32, 0, 0, 0, 0);
#else
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER, stime_value, 0,
0, 0, 0, 0);
#endif
}
sbi_ecall
的前兩個引數分別是 extension ID
和 function ID
(分別簡稱 EID 和 FID,讀者參見 SBI 的標準檔案),後面的若干個引數都是實際傳遞給 opensbi 實現的引數。前兩個引數的作用是:opensbi 根據這兩個引數分發給相應的拓展和拓展中的函數的。
在前文中我們說 CARRAY 自動生成了 build/lib/sbi/sbi_ecall_exts.c
,裡面有一個 sbi_ecall_exts
陣列分別指向了不同的拓展實現,例如 ecall_time
變數。該變數定義在 lib/sbi/sbi_ecall_time.c
中,
// extid_start 和 extid_end定義了 EID的範圍,一般情況下兩個值相同即可,表示只佔用這一個拓展號
// 如果佔用多個拓展號
// sbi_ecall_time_handler 就是處理常式
// 當S-mode的程式碼傳入的引數的 EID 在 [extid_start, extid_end]時,
// opensbi就會將處理常式轉發給 sbi_ecall_time_handler 函數進行處理
struct sbi_ecall_extension ecall_time = {
.extid_start = SBI_EXT_TIME,
.extid_end = SBI_EXT_TIME,
.handle = sbi_ecall_time_handler,
};
我們可以看出,Linux 核心中的程式碼使用的 EID 是 SBI_EXT_TIME = 0x54494D45
,opensbi 中給 ecall_time
分配的 EID 也是 SBI_EXT_TIME = 0x54494D45
,因此 opensbi 在接收到 Linux 核心的呼叫請求之後,就會自動呼叫 sbi_ecall_time_handler
函數。
我們再看該函數的實現:
static int sbi_ecall_time_handler(unsigned long extid, unsigned long funcid,
const struct sbi_trap_regs *regs,
unsigned long *out_val,
struct sbi_trap_info *out_trap)
{
int ret = 0;
if (funcid == SBI_EXT_TIME_SET_TIMER) {
#if __riscv_xlen == 32
sbi_timer_event_start((((u64)regs->a1 << 32) | (u64)regs->a0));
#else
sbi_timer_event_start((u64)regs->a0);
#endif
} else
ret = SBI_ENOTSUPP;
return ret;
}
在這個函數中,也是首先傳入 EID 和 FID,因為我們知道 struct sbi_ecall_extension
是可以分配一個 EID 的區間的,因此在處理常式內部依然需要根據 EID 進行細緻的分發,由於 ecall_time
僅佔用了一個 EID,就不需要再在處理常式內部進行二次分發了,但是需要在處理常式內部根據 FID 進行分發。該處理常式僅實現了一個具體的處理常式,如果說要根據情況呼叫不同的函數,那麼就可以根據 FID 的值進行二次分發了。
在前面分析 opensbi 的拓展的編譯的時候,有一個問題我沒提,就是 Makefile 是怎麼知道要在生成 sbi_ecall_exts.c
的時候傳遞哪些引數呢?
實際上是在 lib/sbi/objects.mk
的最前面有這些指令:
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_TIME) += ecall_time
libsbi-objs-$(CONFIG_SBI_ECALL_TIME) += sbi_ecall_time.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_RFENCE) += ecall_rfence
libsbi-objs-$(CONFIG_SBI_ECALL_RFENCE) += sbi_ecall_rfence.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_IPI) += ecall_ipi
libsbi-objs-$(CONFIG_SBI_ECALL_IPI) += sbi_ecall_ipi.o
carray-sbi_ecall_exts-y += ecall_base
libsbi-objs-y += sbi_ecall_base.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_HSM) += ecall_hsm
libsbi-objs-$(CONFIG_SBI_ECALL_HSM) += sbi_ecall_hsm.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_SRST) += ecall_srst
libsbi-objs-$(CONFIG_SBI_ECALL_SRST) += sbi_ecall_srst.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_PMU) += ecall_pmu
libsbi-objs-$(CONFIG_SBI_ECALL_PMU) += sbi_ecall_pmu.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_LEGACY) += ecall_legacy
libsbi-objs-$(CONFIG_SBI_ECALL_LEGACY) += sbi_ecall_legacy.o
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_VENDOR) += ecall_vendor
libsbi-objs-$(CONFIG_SBI_ECALL_VENDOR) += sbi_ecall_vendor.o
我們觀察發現,這裡其實就是新增拓展的地方。如果你自己自定義了一個 ecall_helloworld
的拓展,那麼就需要加上這幾行
carray-sbi_ecall_exts-$(CONFIG_SBI_ECALL_VENDOR) += ecall_helloworld
libsbi-objs-$(CONFIG_SBI_ECALL_VENDOR) += sbi_ecall_helloworld.o
同時呢要注意,實現該拓展的檔名也應該是 ecall_helloworld.c
。