本章节,我们通过跟踪,调试分析bbl如何启动linux
开始调试(本人对spike调试增加了一点可视化,读者也可自行修改 riscv-isa-sim/riscv/interactive.cc 实现个性化)
我们通过相关的link script,我们找到 ENTRY 函数
// riscv-pk/bbl/bbl.lds
......
ENTRY( reset_vector ) <<== 这就是我们想要的
跟踪这个入口,我们来到
reset_vector:
j do_reset
自然,这就是一个跳板
// riscv-pk/machine/mentry.S
do_reset:
li x1, 0
......
li x31, 0
csrw mscratch, x0 # Machine Scratch Register
# Typically, it is used to hold a pointer to a machine-mode hart-local context # space and swapped with a user register upon entry to an M-mode trap handler.
la t0, trap_vector
csrw mtvec, t0 # holds trap vector configuration,
# consisting of a vector base address (BASE) and a vector mode (MODE)
csrr t1, mtvec
阅读这段代码,我们了解到首先将mscratch寄存器清零,然后将trap_vector
赋值到了mtvec中,随后代码给栈赋值后,清空了bss,跳到C代码init_first_hart
中
注,hart即hardware thread
如果是 core 0 即第一个hart会运行如下代码
void init_first_hart(uintptr_t hartid, uintptr_t dtb)
{
// Confirm console as early as possible
query_uart(dtb);
query_uart16550(dtb);
query_htif(dtb); // <<== spike pick htif
printm("bbl loader\r\n");
hart_init();
hls_init(0); // this might get called again from parse_config_string
// Find the power button early as well so die() works
query_finisher(dtb);
query_mem(dtb);
query_harts(dtb);
query_clint(dtb);
query_plic(dtb);
query_chosen(dtb);
wake_harts();
plic_init();
hart_plic_init();
//prci_test();
memory_init();
boot_loader(dtb);
}
如上,这一段用于第一个(也就是 core 0 hart)的初始化,工作围绕 device tree blob 进行
- 查询并完成 console 相关设备注册
- hart初始化
- 其他如power,clint控制器等设备查询和注册
- memory初始化 .......
其中硬件注册,关于dtb的学习,可以阅读相蜗壳关博客 => http://www.wowotech.net/device_model/dt-code-file-struct-parse.html
这一部分细节我们先兜着,来到后面的的boot_loader
继续跟踪
void boot_loader(uintptr_t dtb)
{
filter_dtb(dtb);
#ifdef PK_ENABLE_LOGO
print_logo();
#endif
#ifdef PK_PRINT_DEVICE_TREE
fdt_print(dtb_output());
#endif
mb();
/* Use optional FDT preloaded external payload if present */
entry_point = kernel_start ? kernel_start : &_payload_start;
boot_other_hart(0);
}
在这里我们可以看到logo的打印,随后赋值了 entry_point 后跳入 boot_other_hart
函数中
void boot_other_hart(uintptr_t unused __attribute__((unused)))
{
const void* entry;
do {
entry = entry_point;
mb();
} while (!entry);
long hartid = read_csr(mhartid);
if ((1 << hartid) & disabled_hart_mask) {
while (1) {
__asm__ volatile("wfi");
#ifdef __riscv_div
__asm__ volatile("div x0, x0, x0");
#endif
}
}
#ifdef BBL_BOOT_MACHINE
enter_machine_mode(entry, hartid, dtb_output());
#else /* Run bbl in supervisor mode */
protect_memory();
enter_supervisor_mode(entry, hartid, dtb_output());
#endif
}
通过调试,最后是进入到 enter_supervisor_mode
,个人的猜测,宏BBL_BOOT_MACIHNE适用那种iot式的直接运行在machine mode下的系统,该宏还在前面用于了trap的delegate,这个后续讨论
似乎已经接近终点了
void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
{
uintptr_t mstatus = read_csr(mstatus);
mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S);
mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0);
write_csr(mstatus, mstatus);
write_csr(mscratch, MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE);
#ifndef __riscv_flen
uintptr_t *p_fcsr = MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE; // the x0's save slot
*p_fcsr = 0;
#endif
write_csr(mepc, fn);
register uintptr_t a0 asm ("a0") = arg0;
register uintptr_t a1 asm ("a1") = arg1;
asm volatile ("mret" : : "r" (a0), "r" (a1));
__builtin_unreachable();
}
这段代码通过将entry_point
写到mepc寄存器中,并最后适用汇编mret
,完成从machine mode进入supervisor mode 下 linux代码的任务
实际上,执行mret
来到的恰恰好也就是绑定在bbl上payload的开始地址处,即0x0000000080200000
,对应的,则是vmlinux的_start
,映射一下,则会发现vmlinux中的地址从0xffffffe000000000
达到了0x80200000
(这是因为spike只给了2G mem),偏移量为0xffffffdf7fe00000L
,若要进行调试,则从vmlinux符号表的函数中拿到地址,减去偏移然后在spike中断点调试即可
如果用调试模式,会发现奇怪的事实;代码一开始停止的地方,居然不是我们所期待的entry_point
(reset_vector的第一步是jump
,但实际停止的地方,执行的代码是:auipc t0, 0x0
)
其实,我们调试模式看到的这段代码,是spike所提供的bootload代码,我们看到 riscv/sim.cc 文件中的 reset()
,其检查条件变量dtb_enabled
(我们在spike_main中即完成了设置),然后调用make_dtb()
函数。
函数中,我们看到了启动时候使用的汇编代码
uint32_t reset_vec[reset_vec_size] = {
0x297, // auipc t0,0x0
0x28593 + (reset_vec_size * 4 << 20), // addi a1, t0, &dtb
0xf1402573, // csrr a0, mhartid
get_core(0)->get_xlen() == 32 ?
0x0182a283u : // lw t0,24(t0)
0x0182b283u, // ld t0,24(t0)
0x28067, // jr t0
0,
(uint32_t) (start_pc & 0xffffffff),
(uint32_t) (start_pc >> 32)
};
总结来说,make_dtb()
函数将为sim主类生成 rom 的实体。通过make_dts()
函数完成device tree的设置 (即仅读内存,rom存放bootloader,很真实),并将该 rom 添加到总线设备之中
如果真的想添加设备,那么 riscv/dts.cc 文件中的
make_dts()
一定是必经的一环,有时间可以阅读老的 spike 了解以下别的设备的添加
spike加载bbl并运行的过程相当的复杂,笔者在这里通过一个框图来总结,读者如果有兴趣,可以查看相关文件内的函数进行学习
根路径视为: riscv-isa-sim
0x00 main(spike_main/spike.cc)
- 整个spike程序的起点,其中有许多“宝贵”的初始化工作,值得耐心一读并了解spike的整体功能
main
函数中的auto return_code = s.run();
,开始启动整个征程
0x01 run(riscv/sim.cc)
- 顾名思义,开始运行模拟过程,代码其实仅有3行
int sim_t::run()
{
host = context_t::current(); // host设置为本线程 or 主线程的上下文
target.init(sim_thread_main, this); // 设置sim_thread_main线程,后续讲解
return htif_t::run(); // 调用上层抽象的 run()
}
context_t
定义在 fesvr/context.cc 中,host是指向context_t的指针- target是 context_t 实体,
init
函数内部将调用pthread_create
来创建子线程,子线程启动sim_thread_main
,这是hart模拟的起点 (线程启起来后,分析就比较繁琐,这里先回避掉) - 调用
htif_t::run()
,继续一步,完成启动前的配置
0x02 run(fesvr/htif.cc)
- 这里首先调用
start();
start()
后的代码主体循环就是处理从虚拟机器传来的调用了,此时已经完全启动
0x03 start(fesvr/htif.cc) 代码本身很清晰
void htif_t::start()
{
if (!targs.empty() && targs[0] != "none")
load_program();
reset();
}
- 首先通过
load_program
将目标程序,即bbl加载进入内存;具体实现就不阐述了 reset()
完成模拟器启动/重启
*0x04 reset(riscv/sim.cc) 注意继承的关系,这里调用的reset是sim.cc中的!代码本身只是一个跳板
void sim_t::reset()
{
if (dtb_enabled)
make_dtb();
}
到了这里,就和前面提到的dtb初始化挂钩了~,整个流程也就清晰如此
阅读过程中由于内容太多容易乱,重要是抓住重点;之后的内容,我们分析 spike 层对于硬件设备的模拟,以及 fesvr 层对模拟程序发出请求的响应。