Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

FrostVista Tutorial -- (1) 基本启动框架

FrostVista的基本启动框架

或许大部分和人和我刚开始写OS一样会发懵,不知道该写啥,不知道应该去做什么。

但写 OS 最重要的迈出第一步,而不是在一开始就死磕复杂的启动原理。我们可以先搭建一个最简单的启动框架,让 OS 跑起来,再回过头去理解其中的细节。

所以说我们可以一开始写一个简单的RISCV-OS的启动框架,先用这个框架来实现启动测试再来回头理解这个框架。

使用的工具

要知道启动不同的架构的OS是需要点特殊的手段的,我们使用qemu来模拟RISCV架构,使用riscv-gcc进行编译,使用ld进行链接,使用qemu-system-riscv64进行模拟,使用make进行构建。

具体的工具如何下载和使用,我们就不详细介绍了,这些东西在浏览器或者AI里都可以很容易的找到。

启动框架

LD 链接脚本

/* linker.ld — RISC-V virt bare metal kernel, entry: _start at 0x80000000 */
OUTPUT_ARCH(riscv)
ENTRY(_start)

MEMORY
{
  RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}

SECTIONS
{
  /* QEMU virt DRAM start address */
  . = ORIGIN(RAM);

  /* .text.entry(_start) */
  .text :
  {
    . = ALIGN(4);
    *(.text.entry)           /*  _start  */
    *(.text .text.*)
    *(.rodata .rodata.*)
    . = ALIGN(0x1000);
    _divide = .;
  } > RAM

  . = ALIGN(16);
  .data :
  {
    _data_start = .;
    *(.sdata .sdata.* .data .data.*)
    _data_end = .;
  } > RAM

  . = ALIGN(16);
  .bss :
  {
    _bss_start = .;
    *(.sbss .sbss.* .bss .bss.* COMMON)
    _bss_end = .;
  } > RAM

  . = ALIGN(16);
  _stack_bottom = .;
  . = . + 0x4000;
  _stack_top = .;     /* alloc 16kb stack to kernel */

  . = ALIGN(0x1000);

  /* WARNING: High address mapping will cause address elevation*/
  _kernel_end = .;
}

关于LD链接脚本,需要知道一些简单的内容。

我们所编写的源代码,会编译成.text.data.bss.rodata等段,这些.text.data.bss.rodata段等,会分别被放到不同的内存区域中。

而我们需要自行组织这些区域,就需要使用LD链接脚本,将不同的段片放到不同的内存区域中。

在上面的组织中,我们的内存分配时这样的:

----------------------  <<--- 0x80000000
|      .text         |
|--------------------|
|      .data         |
|--------------------|
|      .bss          |
|--------------------|
|      .stack        |
|--------------------|
|      .kernel_end   |
----------------------  <<-- 0x80000000 + 128M

不过有些需要注意的时,在裸机程序运行的时候,需要自己组织stack,如果自己没有设置栈,那么程序会直接崩溃。

而在这里,OS刚刚开始启动的时候,可以简单的设置到我们的内核镜像的末尾,方便将他们作为一块连续的内存加载运行(也方便设置映射)。

而对于LD的链接脚本的使用,下面可以简单的介绍一下:

  1. ENTRY: 这个是入口,这里我们配置入口为_start(一般是汇编语言编写的程序),这个入口会作为程序入口,程序会从这里开始执行。
  2. MEMORY:这个是内存的配置,这里我们配置了一个内存区域,名字叫RAM,起始地址是0x80000000,长度是128M,但是这个并不是限制内存只有128MB,只是告诉内核这128MB内存是可用的,这也对应了QEMU virt机器设置分配的内存大小。
  3. SECTIONS:这个是段配置,这里我们配置了.text.data.bss.rodata等段,并设置它们在RAM内存中的起始地址。在这里面类似(.text .text.)的作用是将.text段中的所有段都放到.text段中。

汇编程序

.section .text.entry
.global _start
_start:
  # set stack pointer
  la sp, _stack_top

  la t0, _bss_start
  la t1, _bss_end
  # clear .bss section
1: 
  bgeu t0, t1, 2f
  sd zero, (t0)
  addi t0, t0, 8
  j 1b
2:
  call main
3:
  j 3b

汇编程序中,我们首先将sp指向_stack_top,然后初始化.bss段,然后调用main函数,然后进入一个死循环,这样我们的程序就启动了。

这里可能有点让人误解的地方,为什么begu的跳转地址使用2f,这个f是什么意思?为什么j 1b使用1b,这个b是什么意思?

GNU 汇编器(GAS)的语法中,b的意思是向上跳转,f的意思是向下跳转,所以j 1b的意思是向上跳转到标签为1的行,j 2f的意思是向下跳转到标签为2的行。

我们也使用了addi t0, t0, 8,因为我们在上面使用的是sd zero, (t0)指令,也就是存储双字,我使用的是RISCV64,所以在上面的清空.bss段中,就是清理了64位,所以我们就使用addi t0, t0, 8来指向下一个地址进行覆写。

C程序

#define UART_ADDR 0x10000000
#define UART_DATA_REG 0

void putchar(char ch) {
  *(volatile char *)(UART_ADDR + UART_DATA_REG) = ch;
}

__attribute__((noreturn)) void main() {
  char *str = "Hello World!\n";
  while (*str) {
    putchar(*str++);
  }
  while (1) {}
}

我们可以先实现一个简单的putchar函数,来实现文字的输出。

需要注意的时,因为qemu会自动配置简单的uart,所以在我们的写OS的初期可以直接使用,用来检测OS是否可以启动。

Makefile

MAKEFLAGS += -j$(shell nproc)

CROSS  = riscv64-elf
CC     = $(CROSS)-gcc
DUMP   = $(CROSS)-objdump


CFLAGS = -march=rv64imac_zicsr_zifencei -mabi=lp64 -mcmodel=medany \
         -nostdlib -nostartfiles -ffreestanding -O2 -Iinclude


LDFLAGS = -T linker.ld

SRCDIRS = . kernel/core kernel/driver kernel/arch/riscv kernel/mm kernel/tool

C_SRCS := $(foreach d,$(SRCDIRS),$(wildcard $(d)/*.c))
S_SRCS := $(foreach d,$(SRCDIRS),$(wildcard $(d)/*.S))

OBJS   := $(C_SRCS:.c=.o) $(S_SRCS:.S=.o)

QEMU      = qemu-system-riscv64
QEMUFLAGS = -machine virt -nographic -bios none -kernel kernel.elf

.PHONY: all clean run

all:
    $(MAKE) clean
    $(MAKE) -j$(nproc) kernel.elf
    $(MAKE) run

disasm: kernel.elf
    $(DUMP) -dS kernel.elf > disasm.txt

kernel.elf: $(OBJS) linker.ld
    $(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -o $@

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

%.o: %.S
    $(CC) $(CFLAGS) -c $< -o $@

run: kernel.elf
    $(QEMU) $(QEMUFLAGS)

clean:
    rm -f $(OBJS) kernel.elf disasm.txt

我们就不详细介绍了,这里面主要是编译,链接,运行,清理等操作。