一次完整的计算机上电启动过程
Linux/Windows
计算机上电激活CPU,激活的CPU读取ROM中的boot程序,将指令寄存器设置为BIOS的第一条指令,开始执行BIOS的指令。在计算机刚上电时候,系统RAM处于未知状态,用ROM不需要进行初始化并且不受到计算机病毒的影响,因此这一阶段在ROM中执行是很方便的。(ROM段)
硬件自检。BIOS程序会从ROM中读取指令并将其加载到计算机的RAM(Random Access Memory)中执行诊断各设备的状态,另外还包括初始化CPU寄存器、设备控制器以及内存内容。(RAM段)
加载带有操作系统的硬盘,从引导设备列表中选择第一个设备(例如硬盘、USB驱动器或CD-ROM),并尝试从该设备的第一个扇区(称为“引导扇区”)加载引导程序到RAM中。(RAM段)
加载主引导记录MBR。对于Linux系统,引导扇区通常包含一个名为GRUB的引导加载程序,GRUB会读取其配置文件,以确定要加载的操作系统内核,而对于windows是Windows Boot Manager(Bootmgr)(Bootloader段)
windows默认写MBR,对于双系统应先安装windows再安装linux扫描磁盘分区表,识别含有操作系统的硬盘分区,也叫作活动分区,并加载活动分区,将控制权交给活动分区。(Bootloader段)
加载分区引导记录PBR,读取活动分区的第一个扇区(分区引导记录PBR),寻找并激活活动分区根目录下用于引导操作系统的程序(启动管理器),并加载启动管理器。(Bootloader段)
加载OS。(OS段)
Windows
计算机启动的四个阶段
- ROM stage:计算机上电后CPU的PC寄存器的值被设置为ROM的物理地址,并且运行ROM内的软件(一般叫做固件 firmware),对CPU进行一些初始化工作,将后续需要的bootloader的代码和数据加载到物理内存中,这一阶段直接在ROM中运行。
- RAM stage:检测并初始化CPU、以及主板等,这一阶段在RAM上运行。
- BootLoader stage:在内存中找到bootloader并执行bootloader的指令,完成对CPU的一些初始化工作,将操作系统镜像从硬盘加载到物理内存中,随后跳转将计算机的控制权转交给操作系统。这一阶段计算机的控制权属于bootloader(bootloader可以看做集成了BIOS和GRUB的功能)
- OS stage:这一阶段由操作系统控制计算机。
综上计算机的一次完整的启动过程伴随着计算机控制权的转移
ROM->BOOTLOADER->OS
QEMU
QEMU中ROM和SBI功能简单,没有能力负责加载bootloader和OS镜像,他们是在QEMU启动之前就被加载到QEMU的内存中的
QEMU的内存空间布局如下,QEMU接电后将必要文件加载到内存中,包括bootloader和内核镜像,将bootloader放到QEMU物理内存地址
0x8000 0000
处,内核镜像文件放在0x8020 0000
处内核镜像文件与可执行文件是不一样的,可执行文件包括了一些文件符号信息,而内核镜像是纯二进制指令,内核镜像由可执行文件进一步处理得到
static const struct MemmapEntry { hwaddr base; hwaddr size; } virt_memmap[] = { [VIRT_DEBUG] = { 0x0, 0x100 }, [VIRT_MROM] = { 0x1000, 0x11000 },//启动 ROM [VIRT_TEST] = { 0x100000, 0x1000 }, [VIRT_RTC] = { 0x101000, 0x1000 }, [VIRT_CLINT] = { 0x2000000, 0x10000 }, [VIRT_PLIC] = { 0xc000000, 0x4000000 }, [VIRT_UART0] = { 0x10000000, 0x100 },//UART 串口 [VIRT_VIRTIO] = { 0x10001000, 0x1000 }, [VIRT_FLASH] = { 0x20000000, 0x4000000 }, [VIRT_DRAM] = { 0x80000000, 0x0 },//设备内存 [VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 }, [VIRT_PCIE_PIO] = { 0x03000000, 0x00010000 }, [VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 }, };
CPU的程序计数器PC会被初始化为
0x1000
由固化在 QEMU模拟的计算机内存中的一小段汇编程序初始化并跳转执行bootloader,其起始地址为0x8000 0000
以下代码就是被写入ROM中的程序
uint32_t reset_vec[10] = { 0x00000297, /* 1: auipc t0, %pcrel_hi(fw_dyn) */ 0x02828613, /* addi a2, t0, %pcrel_lo(1b) */ 0xf1402573, /* csrr a0, mhartid */ 0, 0, 0x00028067, /* jr t0 */ start_addr, /* start: .dword */ start_addr_hi32, fdt_load_addr, /* fdt_laddr: .dword */ 0x00000000, /* fw_dyn: */ }; if (riscv_is_32bit(harts)) { reset_vec[3] = 0x0202a583; /* lw a1, 32(t0) */ reset_vec[4] = 0x0182a283; /* lw t0, 24(t0) */ } else { reset_vec[3] = 0x0202b583; /* ld a1, 32(t0) */ reset_vec[4] = 0x0182b283; /* ld t0, 24(t0) */ }
bootloader的bin文件放置在进入地址
0x8000 0000
后面对的是bootloader的第一条指令,对计算机进行一些初始化工作,然后跳转到内核镜像地址0x8020 0000
调试启动过程
启动QEMU并加载SBI以及内核镜像
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios ../bootloader/rustsbi-qemu.bin \
-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 \
-s -S
在另一个终端中输入
riscv64-unknown-elf-gdb \
-ex 'file target/riscv64gc-unknown-none-elf/release/os' \
-ex 'set arch riscv:rv64' \
-ex 'target remote localhost:1234'
对指令进行反汇编,可以看到QEMU的固件中共有5条指令,当数据为 0 的时候则会被反汇编为 unimp
指令。
(gdb) x/10i $pc
=> 0x1000: auipc t0,0x0
0x1004: addi a2,t0,40
0x1008: csrr a0,mhartid
0x100c: ld a1,32(t0)
0x1010: ld t0,24(t0)
0x1014: jr t0
0x1018: unimp
0x101a: 0x8000
0x101c: unimp
0x101e: unimp
AUIPC t0,0x0
Add Upper Immediate PC: rd = pc + imm[31:12]
对当前PC的值和立即数0x0值得高20位相加后保存到寄存器
t0
,可以看到立寄存器的值为4096(gdb) si 0x0000000000001004 in ?? () (gdb) p $t0 $1 = 4096
ADDI a2,t0,40
ADD Immeiate: rd = rs1 + imm[11:0]
将t0与40相加符号扩展的12位立即数加到寄存器a2
(gdb) si 0x0000000000001008 in ?? () (gdb) p $a2 $4 = 4136
csrr a0,mhartid
Control State Register Read: a0 = mhartid
从mhartid中读出经过0扩展后写入a0
0x000000000000100c in ?? () (gdb) p $a0
ld a1,32(t0)
从t0+32的内存中加载4B数据到a1,即从
0x1000 + 32 = 0x1020
中读取连续四字节内容存入a1寄存器,这里需要注意riscv是小端架构,因此实际存入a1的数据是0x1023 0x1022 0x1021 0x1020
中的数据使用gdb查看这些位置的数据,以及寄存器a1中的数据
(gdb) x/4bx 0x1020 0x1020: 0x00 0x00 0x00 0x87 (gdb) x/1bx 0x1023 0x1023: 0x87 (gdb) x/1wx 0x1020 0x1020: 0x87000000 (gdb) p/x $a1 $6 = 0x87000000
ld t0,24(t0)
从t0+32的内存中加载4B数据到t0,即从
0x1000 + 24 = 0x1018
中读取连续四字节内容存入a1寄存器,这里需要注意riscv是小端架构,因此实际存入a1的数据是0x101b 0x101a 0x1019 0x1018
中的数据使用gdb查看这些位置的数据,以及寄存器t0中的数据
(gdb) x/4bx 0x1018 0x1018: 0x00 0x00 0x00 0x80 (gdb) x/1bx 0x101b 0x101b: 0x80 (gdb) x/1wx 0x1018 0x1018: 0x80000000 (gdb) p/x $t0 $14 = 0x80000000
jr t0
跳转到t0中的地址即
0x8000 0000
进入bootloader
参考资料
os-lectures2023: OS Lectures 2022 Spring
rust-based-os-comp2023: 2023开源操作系统训练营
lab-uCore-Tutorial-Guide-2023S 文档 (learningos.github.io)
lab-rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.cn)
Introduction · GitBook (learningos.github.io)2020
Operating Systems: Three Easy Pieces (wisc.edu)
Index of /~remzi/OSTEP/Chinese (wisc.edu)
Introduction uCore Lab Documents (gitbooks.io)2015
操作系统-rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.cn)
文档信息
- 本文作者:wendaocsmaster
- 本文链接:https://wendaocsmaster.github.io/2023/03/01/Operating-system-a-complete-start-up-process/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)