阅读材料
- Xv6代码:entry.S、start.c、main.c
- 教材2.6节
执行的第一行汇编指令
链接器脚本文件(kernel.ld)指定了整个内核程序的入口点在 _entry
这段汇编代码为每个HART分配了 4KB 的栈空间,并且sp寄存器指向栈顶(栈是从高地址向低地址增长),然后跳转到 start 函数
这里 stack0 是一个全局变量(定义在start.c),是一个字节数组,因此位于内核源码链接后生成的可执行文件.bss段
.section .text
.global _entry
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0
li a0, 1024*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
# jump to start() in start.c
call start
spin:
j spin
start函数
首先设置 mstatus 寄存器中 MPP 字段,这样内核执行 mret 指令后返回S模式
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
设置 mepc 寄存器为 main 函数的地址,因为内核执行 mret 指令后,硬件会将 mepc 寄存器中的值加载到 PC 当中去
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
暂时禁用虚拟地址,因此现在访问的都是物理地址
// disable paging for now.
w_satp(0);
将所有中断和异常都代理给S模式(操作系统),并使能S模式下的外部中断、时钟中断、软件中断
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
配置 PMP 寄存器,让所有物理内存都具有读权限、写权限、执行权限。
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
调用 timerinit 函数,对每个核心的时钟初始化,为时钟中断做准备
通过启用 sstc 扩展,支持 stimecmp 寄存器,让S模式也能处理时钟中断。
// ask each hart to generate timer interrupts.
void timerinit()
{
// enable supervisor-mode timer interrupts.
w_mie(r_mie() | MIE_STIE);
// enable the sstc extension (i.e. stimecmp).
w_menvcfg(r_menvcfg() | (1L << 63));
// allow supervisor to use stimecmp and time.
w_mcounteren(r_mcounteren() | 2);
// ask for the very first timer interrupt.
w_stimecmp(r_time() + 1000000);
}
旧版本的Xv6时钟初始化比较复杂,新版本利用了RISC-V中一些新的特性(stimecmp),简化了代码逻辑
旧版本代码剖析参考:
3. 处理时钟中断 | XV6 源代码阅读指南 (gitbook.io)
将每个核心的 hartid 号存入到 tp 寄存器中,后面的代码经常会用到 hartid 号
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
通过汇编指令 mret 返回到S模式中,入口点为 main 函数
// switch to supervisor mode and jump to main().
asm volatile("mret");
main函数
在main函数之前执行的代码可以认为是配置支持操作系统的基本硬件环境,main函数中的的代码才是对操作系统组件的初始化
注意:此时每个HART的sp寄存器保存了栈地址,tp寄存器保存了HART ID
void main()
{
if (cpuid() == 0)
{
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode table
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
userinit(); // first user process
__sync_synchronize();
started = 1;
}
else
{
while (started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler();
}
具体函数不展开讲了,会在相应章节中分析。这里只需知道 hart0 是主核心,负责大部分的初始化工作,其他核心负责次要初始化工作。并且在初始化结束后,每个核心都各自调用 scheduler 函数,该函数会切换到应用进程,标志着操作系统正式启动起来了。