[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
OpenSBI 固件代码分析(四):coldboot
Corrector: TinyCorrect v0.2-rc1 - [spaces toc refs pangu autocorrect] Author: groot gr00t@foxmail.com Date: 2023/09/14 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V Linux 内核 SBI 调用技术分析 Sponsor: PLCT Lab, ISCAS
前言
之前的文章去给大家已经将 OpenSBI 的内容介绍的差不多了,只剩下一些 init_coldboot()
中的一些内容没有介绍,这篇文章就带领大家阅读该部分的内容。
如果将每一个函数的代码都在文章中展开,将会占用太多篇幅,这里就只展示函数调用,不展示函数内部代码。不过读者想要看里面的代码并不困难,在 VIM 中使用 ctags 或者使用各种 IDE 环境都可以很方便的查看源码之前的跳转关系,并且在 GitHub 中也可以进行代码的跳转。
init_coldboot
之前的文章中讲过,这里的 coldboot 并不是传统意义上的冷启动,而是完全初始化的意思。
在这个过程中,系统会初始化 OpenSBI 的一些基础要素,为 OpenSBI 的运行提供基础。
进入该函数之后,下面几个步骤会依次进行:
sbi scratch 的初始化
// lib/sbi/sbi_init.c: 271
/* Note: This has to be first thing in coldboot init sequence */
rc = sbi_scratch_init(scratch);
if (rc)
sbi_hart_hang();
该部分是初始化每一个核心的 scratch 空间,形成一个数组,该数组中的每一个元素都是一个 sbi_scratch
的数据结构,它的结构如下:
// include/sbi/sbi_scartch.h: 57
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;
/** Offset (in bytes) of the R/W section */
unsigned long fw_rw_offset;
/** Offset (in bytes) of the heap area */
unsigned long fw_heap_offset;
/** Size (in bytes) of the heap area */
unsigned long fw_heap_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;
/** 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;
};
这个数据结构是维护每个 hart 必要的,里面包含每个 hart 的基础信息。sbi_scratch_init
函数中做了如下操作:在总的 sbi_scratch 空间中找到每个 hart 的 scratch 的空间,并且把这些地址写在一个数组中,方便后面使用。
sbi heap 的初始化
// lib/sbi/sbi_init.c: 276
/* Note: This has to be second thing in coldboot init sequence */
rc = sbi_heap_init(scratch);
if (rc)
sbi_hart_hang();
在函数中首先会初始化用于堆管理的结构体,之后组织起三个链表,分别是:free_node_list
、free_space_list
和 used_space_list
,用于维护系统中的堆。
// opensbi/lib/sbi/sbi_heap.c: 185
SBI_INIT_LIST_HEAD(&hpctrl.free_node_list);
SBI_INIT_LIST_HEAD(&hpctrl.free_space_list);
SBI_INIT_LIST_HEAD(&hpctrl.used_space_list);
/* Prepare free node list */
for (i = 0; i < (hpctrl.hksize / sizeof(*n)); i++) {
n = (struct heap_node *)(hpctrl.hkbase + (sizeof(*n) * i));
SBI_INIT_LIST_HEAD(&n->head);
n->addr = n->size = 0;
sbi_list_add_tail(&n->head, &hpctrl.free_node_list);
}
/* Prepare free space list */
n = sbi_list_first_entry(&hpctrl.free_node_list,
struct heap_node, head);
sbi_list_del(&n->head);
n->addr = hpctrl.hkbase + hpctrl.hksize;
n->size = hpctrl.size - hpctrl.hksize;
sbi_list_add_tail(&n->head, &hpctrl.free_space_list);
sbi domain 的初始化
上一篇文章我们已经讲过 sbi domain 的含义以及其中的内容了,那么在 sbi domain
的初始化时做了什么操作呢?
// lib/sbi/sbi_init.c: 281
/* Note: This has to be the third thing in coldboot init sequence */
rc = sbi_domain_init(scratch, hartid);
if (rc)
sbi_hart_hang();
该函数实现了初始化 SBI 的根域(root domain)。它首先验证根域的内存区域设置,确保其对齐和大小的合法性。然后分配内存和初始化根域的内存区域,包括可执行、可读写以及超级用户权限的区域。接着,它设置根域的启动信息,将根域的可能处理器标记为有效,并最终注册根域。如果任何步骤失败,该函数都会进行相应的内存释放和错误处理。
sbi hsm 的初始化
// lib/sbi/sbi_init.c: 297
rc = sbi_hsm_init(scratch, hartid, true);
if (rc)
sbi_hart_hang();
之前讲过,OpenSBI 还支持从 OpenSBI v0.7 开始的 Hart State Management (Hart 状态管理 HSM) SBI 扩展。HSM 扩展允许 S 模式软件按照定义的顺序启动所有的 harts,而不是传统的随机启动 harts 的方法。因此,在 S-mode 下可以轻松支持许多所需的功能,例如 CPU 热插拔、kexec/kdump 等。OpenSBI 中的 HSM 扩展以一种非向后兼容的方式实现,以减轻维护负担并避免混淆。这就是为什么如果 S-mode 中不支持 HSM 扩展,使用 OpenSBI 的任何 S-mode 软件将无法启动多于 1 个 hart。
而 hsm 的初始化就是在这里此处完成的,该过程主要做了两件事情:
- 如果是启动核,将该核的状态标记为
SBI_HSM_STATE_START_PENDINM
,否则,标记为SBI_HSM_STATE_STOPPE
。 - 将所有核
hata->start_ticket
标记为 0
sbi platform 早期初始化
// lib/sbi/sbi_init.c: 301
rc = sbi_platform_early_init(plat, true);
if (rc)
sbi_hart_hang();
该过程是对当前 platform 的早期初始化,整个过程比较简单。首先是检查该 platform 是否定义了 early_init 这个函数,如果没有定义,直接返回 0,表示早期初始化成功。如果定义了这个函数,系统将进入这个函数,做一些早期的 platform 初始化工作,不过这部分工作是厂商的工作,具体的代码由厂商编写,这里不再对其进行解释。
sbi hart 初始化
// lib/sbi/sbi_init.c: 305
rc = sbi_hart_init(scratch, true);
if (rc)
sbi_hart_hang();
该函数首先清空特权寄存器的 mip
寄存器,防止在 S-mode 产生的某些中断/异常指令扰乱系统行为。如果系统的启动方式是 coldboot,就从堆中取一块内存空间分配给 hart_features_offset
,用来存放的 hart feature。
之后调用函数 hart_detect_features()
进行 hart feature 的探测。这个探测函数这里先不展开说明,不过我们可以知道的是,在这个探测函数执行完之后,hart 的特性都被加载到了 sbi_scratch
的对应空间中去了。
之后再执行 sbi_hart_reinit()
函数,期间进行了:
- 初始化 RISC-V 处理器的控制寄存器,配置其工作模式和性能监控,以确保正确的初始状态。
- 初始化 RISC-V 处理器的浮点单元,包括双精度(’D’)和单精度(’F’)浮点指令集。如果处理器不支持这些浮点指令集或未启用浮点状态(MSTATUS_FS),则直接返回成功。如果浮点指令集可用,它会初始化浮点寄存器和清除浮点控制和状态寄存器(CSR_FCSR)的值。
- 配置处理器的中断和异常的委托,以便将它们从 M-mode 委托到 S-mode,从而由 S 模式处理中断和异常,提高机器处理效率。
sbi console 初始化
// lib/sbi/sbi_init.c: 309
rc = sbi_console_init(scratch);
if (rc)
sbi_hart_hang();
这里就是简单的 console 的初始化,同 platform 早期初始化一样,该初始化的主要工作是由厂商完成,这里不做过多介绍。不过需要注意的一点是,console 的初始化并不是必须的。
sbi pmu 初始化
// lib/sbi/sbi_init.c: 313
rc = sbi_pmu_init(scratch, true);
if (rc) {
sbi_printf("%s: pmu init failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
因为 pmu 是现代处理器的一个重要部分,所以在一个成熟的 RISC-V 处理器中必须要有 pmu,OpenSBI 的这部分代码就是对 pmu 的初始化。
如果是 coldboot,该程序会做如下操作:
- 给 hardware event 分配一块记录空间
- 在 sbi scratch 空间中获取一块空间分配给 pmu hart state 用以存储一个 hart 的 pmu 状态
- 进入
sbi_platform_pmu_init(plat)
函数将平台的 pmu 进行初始化,该部分有厂商实现 - 确定可用的硬件计数器数量(num_hw_ctrs)。这些计数器用于测量不同类型的事件,比如 CPU 周期数和指令数。
之后系统获取到 sbi scratch 空间中的 pmu state,然后调用函数 pmu_reset_event_map(phs)
将一部分 pmu 的事件初始化为默认未开启,将所有的 firmware 的计数器值设置为 0。
系统默认启用前三个计数器,计数器 0 和计数器 2 并配置为测量 CPU 周期数(SBI_PMU_HW_CPU_CYCLES
)和指令数(SBI_PMU_HW_INSTRUCTIONS
)。计数器 1 暂且被置位无效,日后使用。
补充
在分析源码的时候可能会经常遇见一个函数 sbi_scratch_alloc_offset()
,如果不理解该函数的行为,很可能在分析过程中遇见不晓得困难,这里特此做一个补充。
sbi_scratch_alloc_offset
// lib/sbi/sbi_scratch.c: 43
// 根据 size 在堆中分配一块内存,返回分配内存的首地址
unsigned long sbi_scratch_alloc_offset(unsigned long size)
{
u32 i;
void *ptr;
unsigned long ret = 0;
struct sbi_scratch *rscratch;
/*
* We have a simple brain-dead allocator which never expects
* anything to be free-ed hence it keeps incrementing the
* next allocation offset until it runs-out of space.
*
* In future, we will have more sophisticated allocator which
* will allow us to re-claim free-ed space.
*/
if (!size)
return 0;
// groot: 两步操作,将 size 对齐到比 size 的地址大并且最接近 size 的内存中
size += __SIZEOF_POINTER__ - 1;
size &= ~((unsigned long)__SIZEOF_POINTER__ - 1);
// groot: 访问共享内存,加锁
spin_lock(&extra_lock);
// groot: 如果内存不能够给 size 分配,跳出
if (SBI_SCRATCH_SIZE < (extra_offset + size))
goto done;
// groot: 将内存分配给 ret,并且记录内存分配的变量 extra_offset 增加 size
ret = extra_offset;
extra_offset += size;
done:
spin_unlock(&extra_lock);
// groot: 如果内存分配成功
// 遍历所有的 hart,将每个有效的 hart 的对应内存都清零
if (ret) {
for (i = 0; i <= sbi_scratch_last_hartid(); i++) {
rscratch = sbi_hartid_to_scratch(i);
if (!rscratch)
continue;
ptr = sbi_scratch_offset_ptr(rscratch, ret);
sbi_memset(ptr, 0, size);
}
}
// groot: 返回新开辟的地址
return ret;
}
小结
这篇文章紧跟上一篇文章的脚本,分析了 OpenSBI coldboot 的行为,不过因为篇幅原因没有给出全部代码,读者可以根据文中代码的注释自行找出,然后对照该文章进行理解。
不过到这里 coldboot 的启动过程还没有结束,后面还有一系列行为,包括
- sbi_irqchip_init
- sbi_ipi_init
- sbi_tlb_init
- sbi_timer_init
- sbi_domain_finalize
- sbi_hart_pmp_configure
- sbi_platform_final_init
- sbi_ecall_init
如果在本篇文章中一次性全部分析完读者很可能就晕头转向了,所有笔者决定该篇文章暂时先分析到 sbi_boot_print_banner(scratch)
之前,也就是 sbi_pmu_init()
函数,等完全理解该篇文章的内容后,读者再进入下一环节吧!
参考资料
https://github.com/riscv-software-src/opensbi/tree/master
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期
- 实时分析工具 rtla timerlat 介绍(二):延迟测试原理
- 实时分析工具 rtla timerlat 介绍(一):交叉编译及使用