在内核空间编写 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 – 请随意使用和修改此代码。