这个系列主要分析generic平台下fw_jump.elf这个文件对应的源码(主要我觉得相比于fw_payload和fw_dynamic,这个最简单),基于版本0.8(因为qemu5.2.0默认使用的这个版本作为bios,并且能够boot最新版的riscv-linux)
为了揭示opensbi在多核模式下的启动行为,我们使用qemu模拟两个cpu,因此涉及到多线程的调试。具体的介绍可以参考all stop mode。
太长不看版本:
all stop mode
模式下,一旦有一个线程中止,所有的线程都会被暂停。scheduler-locking = off
,单步调试或者continue
指令都会让所有线程同时运行。因此可能存在单步调试某一个线程,但另一个线程却执行了若干条指令,甚至断点到了另一个线程中。set scheduler-locking on
,使得调试指令会只运行当前线程。这样就可以用thread
命令来回切换线程,愉快地调试了。选择的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.elf
从fw.base.S
的_start
函数开始执行。第一件事是通过原子指令来设置地址_relocate_lottery
处的值来决定哪一个cpu来做重定位和初始化。没能抢到锁的cpu到地址_wait_relocate_copy_done
处反复循环,等待抢到锁的cpu(下称主cpu)完成重定位。
这里我们需要区分load address
和link 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_done
的link 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 address
和link address
存在重叠。如果存在的话,需要进行一系列的安全检查。
BRANGE
是定义在文件开头的宏,实际的效果是,如果第三个参数的值夹在前两个参数之间,则跳转到第四个参数的位置。这里的_start_hang
对应一个死循环。表明如果这种情况发生的话,fw_jump.elf
无法正常启动。
以第一个BRANGE
检查为例,如果_relocate_lattery
的load 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_status
的load 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 address
和laod address
不相等,就拿到_wait_for_boot_hart
的link address
,然后循环等待_boot_status
的load address
的值变为BOOT_STATUS_RELOCATE_DONE
(因此这时候其它cpu是运行在load address
中的),重定位完成后,跳转到_wait_for_boot_hart
的link address中,继续等待主cpu完成其它的初始化工作。
接下来,在其它cpu在_wait_for_boot_hart
中循环等待时,主cpu设置了临时的栈指针(为进入fw_platform_init对应的C代码做准备),并清零了bss段,调用了fw_save_info, fw_prev_arg1
等函数,这些函数在fw_jump.elf
的情况中什么也不干。
虽然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
大致做了以下的事情
generic_plat
, generic_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
)随后,主cpu在link address
的结尾处设置各个cpu的scratch space
和stack space
。具体来说,内存布局如下,这里的stack
和scratch
都通过上面分配的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 space
和scratch space
都是4096字节,前者用于对应cpu的栈,后者存放了sbi_scratch
结构体,剩余空间用来做临时的内存分配。
接下来,主cpu会copy设备树到FW_JUMP_FDT_ADDR
,这是下一级bootloader期望的设备树地址,默认情况下是0x82200000
。完成后,向_boot_status
写入BOOT_STATUS_BOOT_HART_DONE
,所有cpu进入到_start_warm
中
所有cpu首先用主cpu初始化好的platform结构体,拿到自己的hart index
,hart index
到hart id
的映射保存在platform.hart_index2id
中。根据hart index
设置自己的sp指针并拿到需要传入给sbi_init
的scratch
结构体指针(这些已在scratch init
阶段分配好)。随后进入到sbi_init
函数。
OK,第一篇就分析到这里。