OpenSBI背景介绍/编译/启动,及其和Linux交互
基于OpenSBI 1.2和Linux 5.10.110,分析RISC-V特有的OpenSBI及其和Linux Kernel的交互。
1 OpenSBI介绍
1.1 RISC-V基础
RISC-V Core支持三种Mode:User Mode、Supervisor Mode、Machine Mode:
ABI:Application Binary Interface;SBI:Supervisor Binary Interface;SEE:Supervisor Execution Environment。
如果支持虚拟化,还包括Hypervisor Mode:
HBI:Hypervisor Binary Interface;HEE:Hypervisor Execution Environment。
某一时刻HART仅运行在一种Mode。
1.2 什么是SBI?
SBI指的是RISC-V Supervisor Binary Interface。
SEE指的是Supervisor Execution Environment。
SBI是类系统调用的Supervisor和SEE之间的调用转换。
他们之间的关系如下图:
SBI的作用:
- 提高不同OS之间的代码复用。
- 提供不同平台共用的OS通用驱动。
- 提供直接访问M模式下专有资源接口。
1.3 典型的RISC-V启动流程
典型的RISC-V启动流程如下,以Loader为spl、Bootloader为U-Boot、OS为Linux为例。
- 系统POR之后,从ROM开始启动。将SPL加载到SRAM中。跳转到SPL运行。
- SPL进行DDR初始化,并将OpenSBI和U-Boot加载到DDR中。跳转到OpenSBI中运行。
- OpenSBI从DDR中开始执行,进行相关设置。跳转到U-Boot中执行。
- U-Boot加载Linux到DDR中,进行解析、解压等操作。最后跳转到Linux中运行。
- 最后处于运行态的仅有OpenSBI和Linux,Linux通过sbi指令和OpenSBI进行交互。
1.4 OpenSBI固件组成
SBI Library:通用功能抽象。
Platform Specific Library:某些平台特有的功能。
固件:提供三种不同类型的Runtime固件,包括Payload、Dynamic、Jump。
1.5 OpenSBI三种形态
固件的三种形态:
FW_PAYLOAD:OpenSBI和下一级启动固件绑定在一起。
缺点:
- 任何OpenSBI或者Bootloader改变,都需要重新编译。
- 无法从前一bootloader传递参数到FW_PAYLOAD。
FW_JUMP:跳转到一个固定的地址执行下一个固件。
优点:对于QEMU需要使用预编译的FW_JUMP很有用,不需要传递参数。
缺点:
- 前一bootload必须将下一boot镜像加载到固定地址。
- 没有传递参数的机会。
FW_DYNAMIC:跳转的时候传递参数个下一个固件。
优点:下一Bootloader不需要记载到指定地点。
缺点:前一bootloader必须传递号struct fw_dynamic_info参数。
将OpenSBI作为一个库:
2 OpenSBI代码
2.1 OpenSBI代码结构
. ├── docs--帮助文档,通过make docs生成LaTeX、html、pdf格式。 ├── firmware--OpenSBI启动汇编以及连接脚本等。 │ ├── external_deps.mk │ ├── fw_base.ldS │ ├── fw_base.S │ ├── fw_dynamic.elf.ldS │ ├── fw_dynamic.S │ ├── fw_jump.elf.ldS │ ├── fw_jump.S │ ├── fw_payload.elf.ldS │ ├── fw_payload.S │ ├── Kconfig │ ├── objects.mk │ └── payloads ├── include--头文件。 ├── lib │ ├── sbi--输出平台无关的libsbi.a库文件。 │ └── utils--参与输出平台相关的libplatsbi.a文件。 ├── Makefile ├── platform--不同平台差异部分。 │ ├── fpga │ ├── generic │ ├── kendryte │ ├── nuclei │ └── template ├── README.md ├── scripts │ ├── carray.sh │ ├── create-binary-archive.sh │ ├── d2c.sh │ └── Kconfiglib └── ThirdPartyNotices.md
2.2 启动代码和连接脚本
在firmware目录下存在三种Firmware的启动代码和连接脚本:
firmware/
├── external_deps.mk
├── fw_base.ldS
├── fw_base.S
├── fw_dynamic.elf.ldS
├── fw_dynamic.S
├── fw_jump.elf.ldS
├── fw_jump.S
├── fw_payload.elf.ldS
├── fw_payload.S
├── Kconfig
├── objects.mk
└── payloads
├── objects.mk
├── test.elf.ldS
├── test_head.S
└── test_main.c
2.2.1 fw_jump.elf.ld
FW_JUMP的链接脚本如下:
OUTPUT_ARCH(riscv) ENTRY(_start)--固件的入口函数。 SECTIONS { . = 0x80000000; PROVIDE(_fw_start = .);--镜像的起始地址,也是_start的地址。 . = ALIGN(0x1000); .text : { PROVIDE(_text_start = .); *(.entry) *(.text) . = ALIGN(8); PROVIDE(_text_end = .); } . = ALIGN(0x1000); .rodata : { PROVIDE(_rodata_start = .); *(.rodata .rodata.*) . = ALIGN(8); PROVIDE(_rodata_end = .); } . = ALIGN(0x1000); .data : { PROVIDE(_data_start = .); ... PROVIDE(_data_end = .); } .dynsym : { PROVIDE(__dyn_sym_start = .); *(.dynsym) PROVIDE(__dyn_sym_end = .); } .rela.dyn : { PROVIDE(__rel_dyn_start = .); *(.rela*) . = ALIGN(8); PROVIDE(__rel_dyn_end = .); } . = ALIGN(0x1000); .bss : { PROVIDE(_bss_start = .); ... PROVIDE(_bss_end = .); } . = ALIGN(0x1000); PROVIDE(_fw_end = .); PROVIDE(_fw_reloc_end = .); }
2.2.2 主要启动流程
整个镜像的入口是_fw_start(),进行了如下操作
- 根据需要对镜像进行重定位。
- 清空bss段。
- 填充struct sbi_scratch结构体。
- 设置Trap Handler。
- 调用sbi_init()。
_fw_start
_try_lottery--处理重定位。
_relocate_done
->_reset_regs--清指令cache、清寄存器。
->_bss_zero--bss段清零。
->_scratch_init--初始化struct sbi_scratch。
->_fdt_reloc_done
->_start_warm
->设置CSR_MTVEC异常处理函数为_trap_handler。
->sbi_platform_nascent_init--调用平台香瓜你的nascent_init()函数。
->sbi_init
->init_coldboot
->sbi_scratch_init
->sbi_domain_init
->sbi_hsm_init
->sbi_platform_early_init--调用平台相关的early_init()函数。
->sbi_hart_init
->sbi_console_init--初始化串口以及控制台。
->sbi_pmu_init
->sbi_platform_pmu_init
->sbi_boot_print_banner--打印OpenSBI字样横幅。
->sbi_irqchip_init
->sbi_platform_irqchip_init--调用平台相关的irqchip_init()函数。
->sbi_ipi_init
->sbi_platform_ipi_init--调用平台相关的ipi_init()函数。
->sbi_tlb_init
->sbi_ipi_event_create--增加TLB相关的操作函数集tlb_ops。
->sbi_platform_tlbr_flush_limit--调用平台相关的get_tlbr_flush_limit()函数。
->sbi_timer_init
->sbi_platform_timer_init--调用平台相关的timer_init()函数。
->sbi_ecall_init
->sbi_ecall_register_extension--注册sbi_ecall_exts指定范围ecall的处理函数。
->sbi_domain_finalize
->sbi_platform_domains_init--调用平台相关domains_init()函数。
->sbi_hsm_hart_start
->sbi_hart_pmp_configure--配置PMP内存保护生效。
->sbi_platform_final_init--调用平台相关的final_init()函数。
->sbi_boot_print_general
->sbi_boot_print_domains
->sbi_boot_print_hart
->wake_coldboot_harts
->sbi_scratch_offset_ptr
->sbi_hsm_prepare_next_jump
->sbi_hart_switch_mode--配置CSR_MSTATUS/CSR_MEPC/CSR_STVEC等寄存器,切换HART模式并通过mret跳转到下一个镜像启动。第一个参数为hardid,第二个参数指向FDT地址。
->_start_hang
_wait_relocate_copy_done
_wait_for_boot_hart
其中struct sbi_scratch是sbi_init()入参。
struct sbi_scratch { /** Start (or base) address of firmware linked to OpenSBI library */ unsigned long fw_start; /** Size (in bytes) of firmware linked to OpenSBI library */ unsigned long fw_size; /** Arg1 (or 'a1' register) of next booting stage for this HART */ unsigned long next_arg1;--下一镜像的参数。 /** Address of next booting stage for this HART */ unsigned long next_addr;--下一镜像的启动地址。 /** Privilege mode of next booting stage for this HART */ unsigned long next_mode;--下一镜像运行模式。 /** Warm boot entry point address for this HART */ unsigned long warmboot_addr; /** Address of sbi_platform */ unsigned long platform_addr;--指向具体平台的struct sbi_platform结构体。 /** Address of HART ID to sbi_scratch conversion function */ unsigned long hartid_to_scratch; /** Address of trap exit function */ unsigned long trap_exit; /** Temporary storage */ unsigned long tmp0; /** Options for OpenSBI library */ unsigned long options; };
2.3 平台无关
2.3.1 libsbi.a
libsbi.a的代码主要来源于lib/sbi。
2.3.1.1 sbi初始化
sbi_init()是C入口,入参为struct sbi_scratch。
2.3.1.2 Trap handler
在__start_warm()中将_trap_handler()设置给CSR_MTVEC。
_trap_handler()根据异常来分别进行处理:M-Mode调用_trap_handler_m_mode();S-Mode/U-Mode调用_trap_handler_s_mode()。无论是M-Mode还是S-Mode/U-Mode都会经过_trap_handler_all_mode()处理,主要是上下文保存,然后调用C函数sbi_trap_handler()进行处理,再恢复上下文,最后调用mret异常处理结束返回。
_trap_handler:
_trap_handler: TRAP_SAVE_AND_SETUP_SP_T0 TRAP_SAVE_MEPC_MSTATUS 0--保存CSR_MEPC和CSR_MSTATUS寄存器。 TRAP_SAVE_GENERAL_REGS_EXCEPT_SP_T0--保存SP和T0之外的通用寄存器。 TRAP_CALL_C_ROUTINE--调用sbi_trap_handler()函数。
_trap_exit: TRAP_RESTORE_GENERAL_REGS_EXCEPT_A0_T0 TRAP_RESTORE_MEPC_MSTATUS 0 TRAP_RESTORE_A0_T0 mret
MCAUSE是机器模式异常事件向量寄存器,用于保存触发异常的异常事件向量号,用于在异常服务程序中处理对应事件。最高位是中断标记位,为1是表示异常的来源是中断:
#define IRQ_S_SOFT 1 #define IRQ_VS_SOFT 2 #define IRQ_M_SOFT 3 #define IRQ_S_TIMER 5 #define IRQ_VS_TIMER 6 #define IRQ_M_TIMER 7 #define IRQ_S_EXT 9 #define IRQ_VS_EXT 10 #define IRQ_M_EXT 11 #define IRQ_S_GEXT 12 #define IRQ_PMU_OVF 13
为0时,表示异常来源不是中断,Exception Code按照异常解析。
#define CAUSE_MISALIGNED_FETCH 0x0 #define CAUSE_FETCH_ACCESS 0x1 #define CAUSE_ILLEGAL_INSTRUCTION 0x2 #define CAUSE_BREAKPOINT 0x3 #define CAUSE_MISALIGNED_LOAD 0x4 #define CAUSE_LOAD_ACCESS 0x5 #define CAUSE_MISALIGNED_STORE 0x6 #define CAUSE_STORE_ACCESS 0x7 #define CAUSE_USER_ECALL 0x8 #define CAUSE_SUPERVISOR_ECALL 0x9 #define CAUSE_VIRTUAL_SUPERVISOR_ECALL 0xa #define CAUSE_MACHINE_ECALL 0xb #define CAUSE_FETCH_PAGE_FAULT 0xc #define CAUSE_LOAD_PAGE_FAULT 0xd #define CAUSE_STORE_PAGE_FAULT 0xf #define CAUSE_FETCH_GUEST_PAGE_FAULT 0x14 #define CAUSE_LOAD_GUEST_PAGE_FAULT 0x15 #define CAUSE_VIRTUAL_INST_FAULT 0x16 #define CAUSE_STORE_GUEST_PAGE_FAULT 0x17
sbi_trap_handler()是Trap处理的C入口函数,进行送到M-Mode的中断和异常处理。
sbi_trap_handler
->sbi_trap_aia_irq/sbi_trap_nonaia_irq--进入中断处理。
->sbi_timer_process
->sbi_ipi_process
->sbi_irqchip_process
->进入异常处理。
->sbi_illegal_insn_handler--非法指令处理。
->sbi_misaligned_load_handler--非对齐加载处理。
->sbi_misaligned_store_handler--非对齐存储处理。
->sbi_ecall_handler--处理M或S模式下ecall调用。
->sbi_ecall_find_extension--根据extension_id找到ext并调用handle()进行处理。
->sbi_pmu_ctr_incr_fw
->sbi_trap_redirect
sbi_ecall_find_extension()在ecall_exts_list上查找,ecall_exts_list成员在sbi_ecall_init()中插入。
struct sbi_ecall_extension *sbi_ecall_find_extension(unsigned long extid);--根据extid查找对应struct sbi_ecall_extension。 int sbi_ecall_register_extension(struct sbi_ecall_extension *ext);--注册一个struct sbi_ecall_extension。 void sbi_ecall_unregister_extension(struct sbi_ecall_extension *ext);--移除一个struct sbi_ecall_extension。 int sbi_ecall_handler(struct sbi_trap_regs *regs);--根据struct sbi_trap_regs->a7内容调用对应的extension handler。 int sbi_ecall_init(void);--将sbi_ecall_exts内容逐个进行注册。
SBI支持的Extension ID:
#define SBI_EXT_0_1_SET_TIMER 0x0 #define SBI_EXT_0_1_CONSOLE_PUTCHAR 0x1 #define SBI_EXT_0_1_CONSOLE_GETCHAR 0x2 #define SBI_EXT_0_1_CLEAR_IPI 0x3 #define SBI_EXT_0_1_SEND_IPI 0x4 #define SBI_EXT_0_1_REMOTE_FENCE_I 0x5 #define SBI_EXT_0_1_REMOTE_SFENCE_VMA 0x6 #define SBI_EXT_0_1_REMOTE_SFENCE_VMA_ASID 0x7 #define SBI_EXT_0_1_SHUTDOWN 0x8 #define SBI_EXT_BASE 0x10 #define SBI_EXT_TIME 0x54494D45 #define SBI_EXT_IPI 0x735049 #define SBI_EXT_RFENCE 0x52464E43 #define SBI_EXT_HSM 0x48534D #define SBI_EXT_SRST 0x53525354 #define SBI_EXT_PMU 0x504D55
2.4 OpenSBI平台相关
libplatsbi.a = libsbi.a + struct sbi_platform
其中libsbi.a是平台独立部分代码,struct sbi_paltform是平台相关回调函数。
2.4.1 libplatsbi.a - struct sbi_platform
libplatsbi.a对应的文件为platform/generic/platform.c,以及一系列struct platform_override。
主要是实现了一个struct sbi_platform变量platform,如下:
const struct sbi_platform_operations platform_ops = { .nascent_init = generic_nascent_init, .early_init = generic_early_init, .final_init = generic_final_init, .early_exit = generic_early_exit, .final_exit = generic_final_exit, .extensions_init = generic_extensions_init, .domains_init = generic_domains_init, .console_init = generic_console_init, .irqchip_init = fdt_irqchip_init, .irqchip_exit = fdt_irqchip_exit, .ipi_init = fdt_ipi_init, .ipi_exit = fdt_ipi_exit, .pmu_init = generic_pmu_init, .pmu_xlate_to_mhpmevent = generic_pmu_xlate_to_mhpmevent, .get_tlbr_flush_limit = generic_tlbr_flush_limit, .timer_init = fdt_timer_init, .timer_exit = fdt_timer_exit, .vendor_ext_check = generic_vendor_ext_check, .vendor_ext_provider = generic_vendor_ext_provider, }; struct sbi_platform platform = { .opensbi_version = OPENSBI_VERSION,--OpenSBI软件版本号。 .platform_version = SBI_PLATFORM_VERSION(CONFIG_PLATFORM_GENERIC_MAJOR_VER, CONFIG_PLATFORM_GENERIC_MINOR_VER),--厂家自定义平台版本号。 .name = CONFIG_PLATFORM_GENERIC_NAME,--平台名称。 .features = SBI_PLATFORM_DEFAULT_FEATURES, .hart_count = SBI_HARTMASK_MAX_BITS,--HART总数。 .hart_index2id = generic_hart_index2id,--HART的index到id转换表格。 .hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,--每个HART预留给异常或中断栈空间大小。 .platform_ops_addr = (unsigned long)&platform_ops--指向sbi_platform_operations结构体,操作函数集。 };
2.4.2 增加新平台
如下步骤:
- 在platform目录下添加xyz目录。
- 在xyz目录中创建config.mk文件。
- 在xyz目录中创建objects.mk文件。
- 在platform.c实例化一个struct sbi_platform。
3 OpenSBI编译
OpenSBI在Buildroot中配置:
Bootloaders opensbi OpenSBI Version (Custom version) (1.2) OpenSBI version (generic) OpenSBI Platform Install fw_dynamic image Install fw_jump image Install fw_payload image Include Linux as OpenSBI Payload Additional build variables
单独编译命令:
make opensbi-clean-for-rebuild && make opensbi
编译配置在platform/generic/objects.mk中定义:
FW_TEXT_START=0x80000000
FW_JUMP_ADDR=$(shell printf "0x%X" $$(($(FW_TEXT_START) + 0x200000)))
FW_JUMP_FDT_ADDR=$(shell printf "0x%X" $$(($(FW_TEXT_START) + 0x2200000)))
FW_PAYLOAD_OFFSET=0x200000
FW_PAYLOAD_FDT_ADDR=$(FW_JUMP_FDT_ADDR)
在OpenSBI目录下生成帮助文档:
make docs
4 Linux中OpenSBI相关调用
Linux通过ecall进行SBI调用,让CPU核进入M-Mode进行处理。
Linux中SBI的应用包括3方面:
一是支持SBI v0.1规格。
二是支持Timer、IPI、RFence等。
三是支持扩展的HSM,对CPU进行Start/Stop。
4.1 sbi初始化
start_kernel
setup_arch
sbi_init
->sbi_set_power_off--设置pm_power_off为sbi_shutdown。
->sbi_get_spec_version--获取SBI版本号。
->如果版本高于0.1,则配置Timer/IPI/Rfence等函数指针。
4.2 sbi_ecall
sbi_ecall()通过ecall指令进入M-Mode触发ecall异常。
入参:
- a0-a5一共6个参数
- a6作为function id
- a7作为extension id
返回值:
- a0为sbiret.error
- a1为sbiret.value
ebi_call()发起ecall调用,并从a0/a1获取返回值:
struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5) { struct sbiret ret; register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0); register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1); register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2); register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3); register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4); register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5); register uintptr_t a6 asm ("a6") = (uintptr_t)(fid); register uintptr_t a7 asm ("a7") = (uintptr_t)(ext); asm volatile ("ecall" : "+r" (a0), "+r" (a1) : "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7) : "memory"); ret.error = a0; ret.value = a1; return ret; }
Linux下SBI Extension ID定义在arch/riscv/include/asm/sbi.h中,和OpenSBI中匹配:
enum sbi_ext_id { #ifdef CONFIG_RISCV_SBI_V01 SBI_EXT_0_1_SET_TIMER = 0x0, SBI_EXT_0_1_CONSOLE_PUTCHAR = 0x1, SBI_EXT_0_1_CONSOLE_GETCHAR = 0x2, SBI_EXT_0_1_CLEAR_IPI = 0x3, SBI_EXT_0_1_SEND_IPI = 0x4, SBI_EXT_0_1_REMOTE_FENCE_I = 0x5, SBI_EXT_0_1_REMOTE_SFENCE_VMA = 0x6, SBI_EXT_0_1_REMOTE_SFENCE_VMA_ASID = 0x7, SBI_EXT_0_1_SHUTDOWN = 0x8, #endif SBI_EXT_BASE = 0x10, SBI_EXT_TIME = 0x54494D45, SBI_EXT_IPI = 0x735049, SBI_EXT_RFENCE = 0x52464E43, SBI_EXT_HSM = 0x48534D, };
4.3 cpu_ops设置
如果CONFIG_RISCV_SBI定义了,那么cpu_ops会通过SBI进行处理。
start_kernel
setup_arch
setup_smp
cpu_set_ops--cpu_ops指向cpu_ops_sbi。
cpu_ops_sbi函数通过sbi实现其功能,主要包括:
- SBI_EXT_HSM_HART_START
- SBI_EXT_HSM_HART_STOP
- SBI_EXT_HSM_HART_STATUS
代码如下:
const struct cpu_operations cpu_ops_sbi = { .name = "sbi", .cpu_prepare = sbi_cpu_prepare, .cpu_start = sbi_cpu_start, #ifdef CONFIG_HOTPLUG_CPU .cpu_disable = sbi_cpu_disable, .cpu_stop = sbi_cpu_stop, .cpu_is_stopped = sbi_cpu_is_stopped, #endif };
5 参考文档
联系方式:arnoldlu@qq.com