在内核空间编写 hello-world 程序

发布: (2026年4月28日 GMT+8 03:26)
4 分钟阅读
原文: Dev.to

Source: Dev.to

假设

  • 目标:单核 RV64I RISC‑V CPU,使用 OpenSBI 固件。
  • OpenSBI 通常加载在 0x80000000;其下的所有地址均保留给 MMIO 设备。
  • 为避免与 OpenSBI 代码重叠,建议将内核加载到如 0x80200000 的地址。
  • 内核对齐到 2 MiB,可使用大页(2 MiB)来减少 TLB 未命中。

MMIO(Memory‑mapped I/O,内存映射 I/O)是通过读写内存位置与外设通信的机制。RISC‑V 更倾向于使用 MMIO 而非端口 I/O。

常见的页大小有 4 KiB、2 MiB 和 1 GiB,由 MMU 强制实现。大页(2 MiB)有助于降低 TLB 压力。

为跳转到 C 代码做准备

我们需要一个小的汇编桩,在调用 C 代码之前设置好运行环境。

/* entry.S */
.section .stack
.global stack_top
.balign 16                # 128‑bit alignment (required)
.space 16384             # 16 KiB stack
stack_top:

.section .text
.global _start
.extern stack_top
.extern kernel_main

.section .text.entry     # Place this function at the start of kernel memory
.balign 8                 # 64‑bit alignment
_start:
    # Zero out the .bss section, 32 bits at a time
    la t0, __bss_start
    la t1, __bss_end
bss_loop:
    bge t0, t1, bss_done
    sw zero, 0(t0)
    addi t0, t0, 4
    j bss_loop
bss_done:

    # Load the stack pointer
    la sp, stack_top

    call kernel_main      # Jump to C code

# If kernel_main returns, spin forever
spin:
    j spin

链接脚本

/* linker.ld */
OUTPUT_ARCH(riscv)
ENTRY(_start)

MEMORY
{
    RAM (rwxa)      : ORIGIN = 0x80200000, LENGTH = 1G   /* Usable memory */
    STACKRAM (rw)  : ORIGIN = 0xc0200000, LENGTH = 16K  /* Separate stack region */
}

SECTIONS
{
    . = 0x80200000;                     /* Load address */

    .text : {
        *(.text.entry)                  /* Ensure _start is first */
        *(.text .text.*)
    } > RAM

    .rodata : {
        . = ALIGN(16);
        *(.rodata .rodata.*)
    } > RAM

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

    .bss : {
        . = ALIGN(16);
        __bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        . = ALIGN(4);
        __bss_end = .;
    } > RAM

    .stack (NOLOAD) : {                /* Not loaded from the binary */
        *(.stack)
    } > STACKRAM
}

UART 驱动

/* uart.h */
typedef unsigned char uchar;

void uart_putc(uchar c);
void uart_puts(const uchar *s);
/* uart.c */
#include 
#include "uart.h"

#define UART_BASE   0x10000000   /* May vary between boards */
#define UART_THR    0x00         /* Transmit Holding Register (write) */
#define UART_LSR    0x05         /* Line Status Register (read) */
#define LSR_TX_IDLE 0x20

#define UART_REG(reg) ((volatile uint8_t *)(UART_BASE + (reg)))

void uart_putc(uchar c) {
    /* Wait until UART is ready */
    while ((*UART_REG(UART_LSR) & LSR_TX_IDLE) == 0)
        ;

    __asm__ volatile ("fence o, o" ::: "memory");
    *UART_REG(UART_THR) = c;
}

void uart_puts(const uchar *s) {
    while (*s) {
        uart_putc(*s);
        s++;
    }
}

内核入口点

/* main.c */
#include "uart.h"

void kernel_main(void) {
    uart_puts("Hello from Kernel World!");
}

构建与运行

# Build
riscv64-unknown-elf-gcc main.c uart.c entry.S stack.S \
    -T linker.ld -o kernel.elf \
    -mcmodel=medany -ffreestanding -nostdlib -nostartfiles

# Run in QEMU
qemu-system-riscv64 -machine virt -cpu rv64 -m 2G \
    -nographic -bios default -kernel kernel.elf

给读者的挑战

  • 实现一个 printf()‑style 的函数,用于格式化输出。
  • 添加分页支持(设置页表并启用虚拟内存)。
  • 编写一个基本的异常处理程序,以捕获并报告异常。

MIT License – 请随意使用和修改此代码。

0 浏览
Back to Blog

相关文章

阅读更多 »