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 idmv tp,t0 # save current hart id to tpbnez 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 spaceadd sp, sp, t0 # move the current hart stack pointer# to its place in the stack spacej start_kernel # hart 0 jump to cpark:wfij 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.generatedendif${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