OpenSBI 入門
聲明
本文爲本人原創,未經允許,嚴禁轉載。
FW_JUMP FW_PAYLOAD FW_DYNAMIC
FW_JUMP
OpenSBI 帶跳轉地址的固件(FW_JUMP)是一種僅處理下一個引導階段入口地址的固件。例如,它可以處理引導加載程序或操作系統內核的入口地址,但不直接包含下一階段的二進制代碼。
FW_JUMP 固件特別適用於在執行 OpenSBI 固件之前的引導階段能夠加載 OpenSBI 固件和後續引導階段二進制文件的情況。
啓用平臺 FW_JUMP 固件有以下兩種方法:
- 在頂層 make 命令行中指定 FW_JUMP=y。
- 在目標平臺的 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 固件有以下兩種方法:
- 在頂層 make 命令行中指定 FW_DYNAMIC=y。
- 在目標平臺的 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 固件可以通過以下任一方法實現:
- 在頂層 make 命令行中指定 FW_PAYLOAD=y。
- 在目標平臺的 objects.mk 配置文件中指定 FW_PAYLOAD=y。
編譯後的 FW_PAYLOAD 固件 ELF 文件名爲 fw_payload.elf。擴展的鏡像文件名爲 fw_payload.bin。這兩個文件都將在特定平臺的構建目錄下的 build/platform/<platform_subdir>/firmware 目錄中創建。
FW_JUMP 與 FW_DYNAMIC 的區別
fw_jump 與 fw_dynamic 都不含有 payload,區別就在於是否從前一個引導過程獲取信息。fw_jump 不會再前一個引導過程獲取信息,因此它所包含的下一階段的引導代碼的入口地址是靜態的,要求下一階段的代碼必須要加載到指定位置。fw_dynamic 可以從前一個引導過程獲取信息,因此下一階段的引導代碼的入口地址可以不固定。
FW_JUMP 與 FW_PAYLOAD 的區別
含有 payload 的 OpenSBI (FW_PAYLOAD)是一種固件,它直接包含了用於跟隨 OpenSBI 固件執行的啓動階段二進制文件。通常,這個負載將是一個引導加載程序或操作系統內核。
從 QEMU RISC-V Virt Machine Platform 案例中能看出 fw_jump.bin
和 fw_payload.elf
之間的區別。
- 當沒有 payload 的時候,就是說不指定 payload,此時要想 run 起來就必須使用
fw_payload.bin
- 當編譯 OpenSBI 時指定了
FW_PAYLOAD_PATH
的時候,可以只使用fw_payload.elf
作爲-bios
的參數,因爲 payload 已經打包進了fw_payload.elf
中了;也可以分開指定-bios
和kernel
的參數,例如-bios fw_jump.bin -kernel u-boot.bin
- 從 1 和 2 也能看出
fw_payload.bin
和fw_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 的大小,並將其從源地址拷貝到目標地址,完成重定位。具體操作如下:
- 首先,將目標地址按照指針大小對齊,並保存爲 t1。
- 然後,從源地址中讀取 FDT 大小,該大小爲大端格式,需要將其拆分爲四個字節:bit[31:24]、bit[23:16]、bit[15:8]和 bit[7:0],並組合成小端格式,保存在 t2 寄存器中。
- 接着,將 t1 加上 t2,得到目標 FDT 的結束地址,保存在 t2 寄存器中。這樣就確定了拷貝數據的範圍。
- 最後,循環拷貝數據,將源 FDT 中的數據拷貝到目標 FDT 中。循環次數爲源 FDT 大小除以指針大小,即源 FDT 中包含的指針數量。
完成拷貝後,將 BOOT_STATUS_BOOT_HART_DONE 保存到_boot_status 寄存器中,表示當前處理器已經完成啓動。最後,通過調用_start_warm 跳轉到下一步操作。
_start_warm
:在初始化過程中,需要禁用和清除所有中斷,並設置當前處理器的棧指針和 trap handler(異常處理函數)。
具體的操作如下:
- 首先,調用_reset_regs 函數,將寄存器狀態重置爲 0,以保證非引導處理器使用前的狀態乾淨、一致。
- 接着,禁用和清空所有中斷,即將 CSR_MIE 和 CSR_MIP 寄存器都設置爲 0。
- 獲取 platform 變量的地址,並讀取平臺配置信息,包括處理器數量(s7)和每個處理器的棧大小(s8)。
- 獲取當前處理器的 ID(s6),並判斷其是否超出了處理器數量的範圍。如果超出,則跳轉到_start_hang 標籤,表示出現了錯誤。
- 計算當前處理器對應的 scratch space 的地址,並將其保存到 CSR_MSCRATCH 寄存器中,作爲 SBI 運行時的全局變量。
- 將 scratch space 地址保存到 SP 寄存器中,作爲當前處理器的棧指針。
- 設置 trap handler 爲_trap_handler 函數,即當發生異常時會跳轉到該函數進行處理。同時,讀取 MTVEC 寄存器的值確保 trap handler 已經設置成功。
- 調用 sbi_init 函數進行 SBI 運行時的初始化。sbi_init 函數將會初始化各種全局變量、鎖、Hart Table 等。
- 最後,通過跳轉到_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"
時,腳本的執行過程如下:
- 腳本使用
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 *);
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 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
。