Skip to content
This repository has been archived by the owner on Jul 23, 2024. It is now read-only.

Latest commit

 

History

History
234 lines (196 loc) · 8.43 KB

File metadata and controls

234 lines (196 loc) · 8.43 KB

spike+bbl运行linux框架, 启动过程

本章节,我们通过跟踪,调试分析bbl如何启动linux

找到入口

开始调试(本人对spike调试增加了一点可视化,读者也可自行修改 riscv-isa-sim/riscv/interactive.cc 实现个性化)

bbl 引导

我们通过相关的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中断点调试即可

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 层对模拟程序发出请求的响应。