RVOS-1.环境搭建与系统引导
0.环境搭建
riscv-operating-system-mooc: 开放课程《循序渐进,学习开发一个 RISC-V 上的操作系统》配套教材代码仓库。 mirror to https://github.com/plctlab/riscv-operating-system-mooc
在 Ubuntu 20.04 以上环境下我们可以直接使用官方提供的 GNU工具链和 QEMU 模拟器,执行如下命令在线安装即可开始试验:
$ sudo apt update
$ sudo apt install build-essential gcc make perl dkms git gcc-riscv64-unknown-elf gdb-multiarch qemu-system-misc
构建和使用说明:
- `make`:编译构建
- `make run`:启动 `qemu` 并运行
- `make debug`:启动调试
- `make code`:反汇编查看二进制代码
- `make clean`:清理
1. 系统引导
1.1 引导程序入口
硬件地址映射:
系统引导过程:
内核被加载到DRAM的地址 0x8000-0000
,引导加载程序被加载到ROM的地址 0x0000-F000
到 0x0000-0000
。
正常是固化在ROM的程序来引导,找到bootloader程序(BL0阶段)。但这里我们使用模拟器QEMU,在makefile里面指定了内核程序入口:在启动过程中,内核的入口点通常是位于ELF文件中的一个特定位置,这个位置由链接器脚本或者链接选项指定。在Makefile中,链接选项 -Ttext=0x80000000
指定了内核的文本段(代码段)应该被加载到内存地址 0x80000000
处。这意味着当QEMU启动时,它会将内核的代码和数据加载到这个地址,并且从这个地址开始执行。
总而言之:
run
目标使用QEMU模拟器运行内核,运行的确实是内核os.elf,并且在ELF文件里面的0x80000000地方开始运行。
但是我们还是分析一下这段汇编
auipc t0, 0x0 addi a2, t0, 40 #可能是栈 csrr a0, mhartid #将硬件线程ID写入寄存器 a0。 lw a1, 32(t0) #参数 lw t0, 24(t0) #内核的入口地址存放处 jr t0 #jr t0:跳转到寄存器 t0 指向的地址,即内核的入口地址。 start: .word
没看明白,求大佬指点!
1.2 引导程序
这里类似于bootloader的 BL1阶段:
BL1阶段通常负责:
- 初始化CPU和基本的硬件设备。
- 设置栈指针。
- 跳转到下一个阶段的bootloader(如BL2)或直接跳转到操作系统。
start.S
:
#include "platform.h"
# size of each hart's stack is 1024 bytes
# 用于分配一段内存空间 “External Public Uninitialized Block"
.equ STACK_SIZE, 1024
.global _start
.text
_start:
# park harts with id != 0让除了0号核的其他核都进入park状态
csrr t0, mhartid # read current hart id
mv tp,t0 # save current hart id to tp
bnez t0,park # if hart id != 0, park
# Setup stacks, the stack grows from bottom to top, so we put the
# stack pointer to the very end of the stack range.
slli t0, t0, 10 # shift left the hart id by 1024-->(for multiple harts)
la sp, stacks + STACK_SIZE # set the initial stack pointer
# to the end of the first stack space
add sp, sp, t0 # move the current hart stack pointer
# to its place in the stack space
j start_kernel # hart 0 jump to c
park:
wfi
j park
# In the standard RISC-V calling convention, the stack pointer sp
# is always 16-byte aligned.
.balign 16
stacks:
.skip STACK_SIZE * MAXNUM_CPU # allocate space for all the harts stacks
.end # End of file
kernel.c
void start_kernel(void)
{
while (1) {}; // stop here!
}
platform.h
#ifndef __PLATFORM_H
#define __PLATFORM_H
#define MAXNUM_CPU 8
#endif // __PLATFORM_H
运行结果:成功进入void start_kernel(void);
疑问:这些文件怎么组织在一起的?
文件之间的联系是通过链接器来建立的。链接器(Linker)是编译过程中的一个工具,它将编译后的目标文件(Object Files,通常是
.o
文件)和库文件链接在一起,生成一个可执行文件(Executable File)或库文件(Library File)
。在此项目中,链接过程是通过 Makefile 控制的。Makefile 中定义了如何编译和链接项目:
OBJS_ASM 和 OBJS_C:这些变量定义了汇编和 C 语言的目标文件(Object Files)。
ELF 和 BIN:
ELF
是链接后的可执行文件,BIN
是可执行文件的二进制格式。LDFLAGS:链接器标志,用于指定链接器的行为。在您的 Makefile 中,
LDFLAGS
可以是-T ${OUTPUT_PATH}/os.ld.generated
(使用链接脚本)或-Ttext=0x80000000
(指定文本段的起始地址)。链接命令:
${ELF}: ${OBJS} ifeq (${USE_LINKER_SCRIPT}, true) ${CC} -E -P -x c ${DEFS} ${CFLAGS} os.ld > ${OUTPUT_PATH}/os.ld.generated endif ${CC} ${CFLAGS} ${LDFLAGS} -o ${ELF} $^ ${OBJCOPY} -O binary ${ELF} ${BIN}
这段代码首先检查是否使用链接脚本,如果是,则生成链接脚本文件。然后使用
CC
(交叉编译器)和LDFLAGS
链接目标文件,生成 ELF 文件。最后,使用OBJCOPY
将 ELF 文件转换为二进制格式。默认目标:
.DEFAULT_GOAL := all all: ${OUTPUT_PATH} ${ELF}
默认目标是
all
,它依赖于OUTPUT_PATH
和ELF
文件。通过这些步骤,Makefile 确保了所有源文件被正确编译和链接,生成最终的可执行文件。链接器根据链接脚本(如果使用)或链接器标志来确定如何将各个目标文件和库文件组合在一起。
参考—汪老师讲的太好了!!!!:
1.3 RISC-V寄存器_RISC-V体系结构编程与实践(第2版)
【[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春】 https://www.bilibili.com/video/BV1Q5411w7z5/?p=19&share_source=copy_web&vd_source=d63943fdb26087d14a536adf35c52d6b