当前位置: 首页 > 工具软件 > OpenSBI > 使用案例 >

opensbi firmware源码分析(1)

西门正平
2023-12-01

0. 序

这个系列主要分析generic平台下fw_jump.elf这个文件对应的源码(主要我觉得相比于fw_payload和fw_dynamic,这个最简单),基于版本0.8(因为qemu5.2.0默认使用的这个版本作为bios,并且能够boot最新版的riscv-linux)

1. 关于gdb的多线程调试

为了揭示opensbi在多核模式下的启动行为,我们使用qemu模拟两个cpu,因此涉及到多线程的调试。具体的介绍可以参考all stop mode

太长不看版本:

  • all stop mode模式下,一旦有一个线程中止,所有的线程都会被暂停。
  • 默认情况下scheduler-locking = off,单步调试或者continue指令都会让所有线程同时运行。因此可能存在单步调试某一个线程,但另一个线程却执行了若干条指令,甚至断点到了另一个线程中。
  • 某些平台支持set scheduler-locking on,使得调试指令会只运行当前线程。这样就可以用thread命令来回切换线程,愉快地调试了。

2. opensbi多核启动

选择的qemu启动参数如下

$ qemu-system-riscv64 -bios $(find opensbi/build/ -name fw_jump.elf) -s -S -machine virt -m 2G -smp 2 -monitor stdio\
     -kernel linux-5.18/arch/riscv/boot/Image -append "root=/dev/vda rw nokaslr" \
     -drive file=disk.img,format=raw,id=hd0 \
     -device virtio-blk-device,drive=hd0

其中disk.img是事先用busybox做好的一个ext4的硬盘镜像。

qemu的boot rom从地址0x1000开始执行,给fw_jump.elf传入了a0, a1, a2三个寄存器参数,a0是boot hartid, a1是设备树地址,a2是干嘛的还没看懂。目前看起来opensbi只用到了a1。

_try_lottery:
	/* Jump to relocation wait loop if we don't get relocation lottery */
	la	a6, _relocate_lottery
	li	a7, 1
	amoadd.w a6, a7, (a6)
	bnez	a6, _wait_relocate_copy_done

fw_jump.elffw.base.S_start函数开始执行。第一件事是通过原子指令来设置地址_relocate_lottery处的值来决定哪一个cpu来做重定位和初始化。没能抢到锁的cpu到地址_wait_relocate_copy_done处反复循环,等待抢到锁的cpu(下称主cpu)完成重定位。

3. 重定位

这里我们需要区分load addresslink address,前者指的是该文件被实际加载到的物理地址,后者指的是该文件链接时假定自己会被加载到的地址(通常情况下应该是一样的)。如果两者不一致就需要进行重定位。不过在fw_jump.elf中的重定位做得非常简陋,只是简单地把所有的加载到load address里的内容,copy到link address里。

_relocate:
	la	t0, _link_start
	REG_L	t0, 0(t0)
	la	t1, _link_end
	REG_L	t1, 0(t1)
	la	t2, _load_start
	REG_L	t2, 0(t2)
	sub	t3, t1, t0
	add	t3, t3, t2
	beq	t0, t2, _relocate_done
	la	t4, _relocate_done
	sub	t4, t4, t2
	add	t4, t4, t0
	blt	t2, t0, _relocate_copy_to_upper

在主cpu进行重定位时,上面的la伪指令加载的地址是load address,可以看到,上面的代码最终使得t0, t1对应的是link address的开头和结尾,t2, t3对应到load address的开头和结尾。而t4对应的是_relocate_donelink address在实际copy的时候需要区分是copy到高地址还是低地址,这是考虑到如果有copy的地址和原地址有重叠的情况,从地址开头开始copy和从地址结尾反过来copy的效果是不一样的

我们假设copy到高地址的情况,继续往后面看。

_relocate_copy_to_upper:
	ble	t3, t0, _relocate_copy_to_upper_loop
	la	t2, _relocate_lottery
	BRANGE	t0, t3, t2, _start_hang
	la	t2, _boot_status
	BRANGE	t0, t3, t2, _start_hang
	la	t2, _relocate
	la	t5, _relocate_done
	BRANGE	t0, t3, t2, _start_hang
	BRANGE	t0, t3, t5, _start_hang
	BRANGE	t2, t5, t0, _start_hang
_relocate_copy_to_upper_loop:
	add	t3, t3, -__SIZEOF_POINTER__
	add	t1, t1, -__SIZEOF_POINTER__
	REG_L	t2, 0(t3)
	REG_S	t2, 0(t1)
	blt	t0, t1, _relocate_copy_to_upper_loop
	jr	t4

可以看到,实际的copy发生在_relocate_copy_to_upper_loop中,因为是copy到高地址,所以为了避免重叠的情况,从高地址递减地copy。第一行的汇编是在检查是否load addresslink address存在重叠。如果存在的话,需要进行一系列的安全检查。

BRANGE是定义在文件开头的宏,实际的效果是,如果第三个参数的值夹在前两个参数之间,则跳转到第四个参数的位置。这里的_start_hang对应一个死循环。表明如果这种情况发生的话,fw_jump.elf无法正常启动。

以第一个BRANGE检查为例,如果_relocate_latteryload address位于重叠区域,在copy时_relocate_lattery的值会被修改,如果此时有cpu还处于前面看到的_try_lattery阶段就会发生竞争,有可能出现多个cpu尝试初始化的情况。其它的BRANGE检查也是类似的。

在重定位结束后,主cpu跳转到_relocate_done的链接地址。

_relocate_done:
	/*
	 * Mark relocate copy done
	 * Use _boot_status copy relative to the load address
	 */
	la	t0, _boot_status
	la	t1, _link_start
	REG_L	t1, 0(t1)
	la	t2, _load_start
	REG_L	t2, 0(t2)
	sub	t0, t0, t1
	add	t0, t0, t2
	li	t1, BOOT_STATUS_RELOCATE_DONE
	REG_S	t1, 0(t0)
	fence	rw, rw

	/* At this point we are running from link address */

注意这里主cpu已经运行在link address了,因此la伪指令加载的地址是link address。这里通过相对地址加减,拿到了_boot_statusload address,然后向这个地址存入OOT_STATUS_RELOCATE_DONE,告诉其它cpu重定位完成了。

我们看看其它cpu这个时候在干嘛,来理解上面代码中最后一句注释的含义。

_wait_relocate_copy_done:
	la	t0, _start
	la	t1, _link_start
	REG_L	t1, 0(t1)
	beq	t0, t1, _wait_for_boot_hart
	la	t2, _boot_status
	la	t3, _wait_for_boot_hart
	sub	t3, t3, t0
	add	t3, t3, t1
1:
	/* waitting for relocate copy done (_boot_status == 1) */
	li	t4, BOOT_STATUS_RELOCATE_DONE
	REG_L	t5, 0(t2)
	/* Reduce the bus traffic so that boot hart may proceed faster */
	nop
	nop
	nop
	bgt     t4, t5, 1b
	jr	t3

其它cpu如果发现link addresslaod address不相等,就拿到_wait_for_boot_hartlink address,然后循环等待_boot_statusload address的值变为BOOT_STATUS_RELOCATE_DONE(因此这时候其它cpu是运行在load address中的),重定位完成后,跳转到_wait_for_boot_hart的link address中,继续等待主cpu完成其它的初始化工作。

4. 一些杂项

接下来,在其它cpu在_wait_for_boot_hart中循环等待时,主cpu设置了临时的栈指针(为进入fw_platform_init对应的C代码做准备),并清零了bss段,调用了fw_save_info, fw_prev_arg1等函数,这些函数在fw_jump.elf的情况中什么也不干。

5. fw_platform_init函数

虽然fw_platform_init接收4个参数,不过在fw_jump.elf的版本中只用到了a1,对应设备树的内存地址。其中调用的比较重要的函数有

static void fw_platform_lookup_special(void *fdt, 
int root_offset);
// 第二个参数没有用到,这个函数负责扫描设备树,根据设备树的节点的
// compatible参数,如果有与全局变量special_platforms匹配的节点则记录到
// 全局变量generic_plat和全局变量generic_plat_match中,目前
// special_platforms中只有sifive_fu540,因此对应到我们的情况这个函数
// 相当于空操作

int fdt_path_offset(const void* fdt, const char* path);
// 这个函数接收设备树和节点路径,返回该节点在设备树地址中的偏移

const void* fdt_getprop(const void* fdt, int nodeoffset,
 const char* name, int* lenp);
 // 这个函数接收设备树和一个节点在设备树中的地址偏移,以及该节点的某个
 // property name,返回指向property value的指针,lenp中包含该value
 // 所占的字节数

这些函数的实现都挺直接的,对照着设备树的spec看就行了。
总的来说, fw_platform_init大致做了以下的事情

  • 对设备树的合法性进行了检查
  • 查看是否有需要特殊处理的platform, 如果有则初始化相应的generic_platgeneric_plat_match全局变量
  • 初始化platform全局变量(最重要的是hart_index2id成员,为每个hartid < 128的cpu分配了一个index,为接下来分配scratch空间做准备,做这样一个映射主要是因为riscv不要求hartid是从0开始且连续的,hartid > 128的cpu并不被opensbi支持,在控制权传递给bootloader前,opensbi调用fdt_cpu_fixup函数修改设备树信息,把这些cpu的status设置为disable)

6. scratch init

随后,主cpu在link address的结尾处设置各个cpu的scratch spacestack space。具体来说,内存布局如下,这里的stackscratch都通过上面分配的index进行索引

| end of link address | hart N stack | hart N scratch | hart N-1 stack | hart N-1 scratch | ... | hart 0 stack | hart 0 scratch

stack spacescratch space都是4096字节,前者用于对应cpu的栈,后者存放了sbi_scratch结构体,剩余空间用来做临时的内存分配。

7. copy fdt

接下来,主cpu会copy设备树到FW_JUMP_FDT_ADDR,这是下一级bootloader期望的设备树地址,默认情况下是0x82200000。完成后,向_boot_status写入BOOT_STATUS_BOOT_HART_DONE,所有cpu进入到_start_warm

8. _start_warm

所有cpu首先用主cpu初始化好的platform结构体,拿到自己的hart indexhart indexhart id的映射保存在platform.hart_index2id中。根据hart index设置自己的sp指针并拿到需要传入给sbi_initscratch结构体指针(这些已在scratch init阶段分配好)。随后进入到sbi_init函数。

9. 结

OK,第一篇就分析到这里。

 类似资料: