基于xv6 riscv实现学习os 其零:helloworld

2023-04-07 16:17:37 浏览数 (1)

学习os的时间开始了! pixiv:30933181

前言

这个系列的目的还是以讲解xv6-riscv的代码以及记录我在做的事情为主,也会掺杂许多mini-riscv-os的代码介绍(关于xv6-riscv和mini-riscv-os的链接请看参考),并非教程倾向(但也会尽可能讲解一些基础知识),很多细节不会讲到。如果想要更详细的教程我建议你查看参考资料中引用的内容,在这一期我会列出一部参考的项目。

compiler的坑还没走多远,我又要开新的坑了,这是我很久之前想做但不敢做的事情。以前也做过一些尝试,比如说《30天自制os》以及6.828,前者讲的相对比较容易理解一些,但是当时的我缺少实践,后者难度较高,看不懂课后习题只能去查看别人的实现,东抄西抄总算抄完了前四章的内容,最后只留下了一些概念的印象。对我来说学会什么东西只有通过具体去实现,自己很难从什么概念去理解某个东西,也因此之前学的很多知识其实都是非常肤浅无用的。

实现os这件事情看起来是挺吓人的,本身复杂的概念和各种实现,同时需要许多前置知识。同时大多数os的开始都是离谱的x86bootloader,我想这个应该劝退了非常多的人。但最近发现做一个最简易的os或许并没有那么可怕,搜了一些项目,最简单的功能很少的系统只有一两千行代码,相对比较容易学习,同时riscv的bootloader部分没有乱七八糟的历史遗留,十分简洁,不会再因为这个劝退别人。也在这里感谢他们的付出。

注意事项

  1. mini-riscv-os是针对riscv32,而xv6针对的是riscv64,导致一些汇编上、编译选项以及一些其他的内容会有所不同
  2. 代码引用会直接使用项目名/路径的格式

此后不再赘述

环境配置

交叉编译工具链

参考链接

https://pdos.csail.mit.edu/6.828/2019/tools.html

我是在mac(M1)下开发的,homebrew在安装riscv-tools的时候会提示需要安装一些依赖。在我配置的时候遇到了flock这个依赖搞不定的问题,发现直接brew install flock安装的flock是其他东西,因此需要卸载flock并且使用brew tap的命令,安装好依赖再去按riscv-tools

代码语言:javascript复制
brew uninstall flock
brew tap discoteq/discoteq
brew install flock
brew install riscv-tools

qemu

这个没什么好说的,直接用包管理安装就是

启动所需代码

bootloader

做了什么

  1. 设置栈的起始地址
  2. 跳转到c代码中

代码

mini-riscv-os/01-HelloOs/start.s

代码语言:javascript复制
.equ STACK_SIZE, 8192

.global _start

_start:
    # setup stacks per hart
    csrr t0, mhartid                # read current hart id
    slli t0, t0, 10                 # shift left the hart id by 1024
    la   sp, stacks   STACK_SIZE    # set the initial stack pointer 
                                    # to the end of the stack space
    add  sp, sp, t0                 # move the current hart stack pointer
                                    # to its place in the stack space

    # park harts with id != 0
    csrr a0, mhartid                # read current hart id
    bnez a0, park                   # if we're not on the hart 0
                                    # we park the hart

    j    os_main                    # hart 0 jump to c

park:
    wfi
    j park

stacks:
    .skip STACK_SIZE * 4            # allocate space for the harts stacks

csrr是从csr(Control and Status Register)寄存器中read值,而其中的csrr reg, mhartid则是将hart id读到对应的reg中。hart是riscv中硬件线程的最小单位,在riscv的spec中是这样描述的

A RISC-V compatible core might support multiple RISC-V-compatible hardware threads, or harts, through multithreading.

这里的代码判断如果hart id不是0就跳到park这个循环中。实质上是只开启了一个hart

xv6-riscv/kernel/entry.S

代码语言:javascript复制
# qemu -kernel loads the kernel at 0x80000000
        # and causes each hart (i.e. CPU) to jump there.
        # kernel.ld causes the following code to
        # be placed at 0x80000000.
.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

xv6的启动代码中考虑了多个hart启动的情况,给每一个hard都设置stack的起始地址。而stack的起始地址是写在其他的c代码中

代码语言:javascript复制
// entry.S needs one stack per CPU.
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

c代码

在c代码中打印出一个血统纯正的helloworld。这里其实隐含了很多的内容,但是暂且知道这样做就可以打印出helloworld即可。

对于xv6来说在进入os的main之前有许多设置状态的内容,这里暂且不讨论。

mini-riscv-os/01-HelloOs/os.c

代码语言:javascript复制
#include <stdint.h>

#define UART        0x10000000
#define UART_THR    (uint8_t*)(UART 0x00) // THR:transmitter holding register
#define UART_LSR    (uint8_t*)(UART 0x05) // LSR:line status register
#define UART_LSR_EMPTY_MASK 0x40          // LSR Bit 6: Transmitter empty; both the THR and LSR are empty

int lib_putc(char ch) {
	while ((*UART_LSR & UART_LSR_EMPTY_MASK) == 0);
	return *UART_THR = ch;
}

void lib_puts(char *s) {
	while (*s) lib_putc(*s  );
}

int os_main(void)
{
	lib_puts("hello, worldn");
	while (1) {}
	return 0;
}

ldscript

这里主要是需要指定这么几项内容

  1. 对于qemu来说,启动之后会读位于0x80000000这个地址的内容,因此我们需要将我们的内容放到这个地址开始。
  2. 指定OUTPUT_ARCH( “riscv” )
  3. 指定汇编入口地址,比如ENTRY( _entry )

xv6-riscv/kernel/entry.S

代码语言:javascript复制
OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  . = 0x80000000;

  .text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    _trampoline = .;
    *(trampsec)
    . = ALIGN(0x1000);
    ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
    PROVIDE(etext = .);
  }

  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }

  PROVIDE(end = .);
}

makefile

mini-riscv-os/01-HelloOs/Makefile

代码语言:javascript复制
CC = riscv64-unknown-elf-gcc
CFLAGS = -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32

QEMU = qemu-system-riscv32
QFLAGS = -nographic -smp 4 -machine virt -bios none

OBJDUMP = riscv64-unknown-elf-objdump

all: os.elf

os.elf: start.s os.c
	$(CC) $(CFLAGS) -T os.ld -o os.elf $^

qemu: $(TARGET)
	@qemu-system-riscv32 -M ? | grep virt >/dev/null || exit
	@echo "Press Ctrl-A and then X to exit QEMU"
	$(QEMU) $(QFLAGS) -kernel os.elf

clean:
	rm -f *.elf

这里没什么好讲的,绝大多数选项都用不到,唯一要注意的是-march的值

riscv是一种模块化的指令集,不同的名字代表支持的扩展指令集不同,关于详情参考

RISC-V#ISA_base_and_extensions

之后直接通过make命令编译出elf之后通过qemu启动就好

参考

https://github.com/cccriscv/mini-riscv-os

https://github.com/mit-pdos/xv6-riscv

Specifications - RISC-V International

0 人点赞