iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 4

Day 04|Kernel Boot 深入:從 OpenSBI 逐步追到 kernel_main()

  • 分享至 

  • xImage
  •  

Day 04|Kernel Boot 深入:從 OpenSBI 逐步追到 kernel_main()

如果開機後讀到一個全域變數是零,能證明核心的清零程式真的執行過嗎?
在模擬器裡,載入器可能早已把那段記憶體清成零,單看結果很容易錯過沒有執行的初始化步驟。

今天會在核心進入點暫停,先用 GDB 把測試變數填成 0xdeadbeef,再讓自己的啟動程式把它清掉。
當資料從我們指定的非零值變成零,才能清楚看到初始化造成的改變。

核心必須先替 C 準備環境

《Operating System Concepts》第 10 版第 2.5 節分別介紹連結器與載入器,第 2.9.2 節則說明開機時如何找到並啟動核心。
連結器安排程式的位址,載入器把映像放進記憶體,接著才輪到 CPU 從指定位置執行。

我們的實驗由 QEMU 載入 ELF,OpenSBI 完成前置工作後進入核心。
但 C 函式需要的堆疊,以及未初始化靜態資料的零值,仍必須由啟動流程確保。
今天把這些責任放到一個可以單步執行的 boot.S

《OS in 1,000 Lines》的〈核心啟動〉使用 C 裡的 naked 函式作為進入點。
這篇延伸同一條啟動流程,將組合語言獨立成檔案,並在進入 C 之前完成 .bss 清零。

把進入點寫成獨立組語

以下都在 30-days-os-kernel/tiny-kernel/ 進行,接續 Day 03。
新增 boot.S

.section .text.boot
.balign 4
.globl boot
.type boot, @function
boot:
    la   sp, __stack_top
    la   t0, __bss
    la   t1, __bss_end
1:
    bgeu t0, t1, 2f
    sb   zero, 0(t0)
    addi t0, t0, 1
    j    1b
2:
    call kernel_main
3:
    j    3b
.size boot, . - boot

1b 表示向後尋找標號 12f 表示向前尋找標號 2
清零採用逐 byte 寫入,起訖長度不必剛好是四的倍數。
kernel_main() 正常不會返回,最後的迴圈則保留一個明確的意外返回落點。

新增 kernel.h,讓接下來不同來源檔共用型別:

#pragma once
#include <stddef.h>
#include <stdint.h>

extern char __bss[], __bss_end[], __stack_top[];
void *memset(void *buf, int value, size_t n);

這兩個標頭提供型別與巨集,沒有把主機的 C 函式庫連進核心。
使用 size_t 等標準名稱,也能減少自己宣告和編譯器預期不一致的問題。

用以下內容完整取代 kernel.c,因此原本 C 版本的 boot() 也會一起移除:

#include "kernel.h"
#include "csr.h"

extern uint32_t asm_probe(uint32_t a, uint32_t b);
volatile uint32_t probe_result;
volatile uint32_t csr_snapshot[3];
volatile uint32_t bss_probe;

void *memset(void *buf, int value, size_t n) {
    uint8_t *p = buf;
    while (n--)
        *p++ = (uint8_t) value;
    return buf;
}

__attribute__((noinline))
void privilege_checkpoint(void) {
    __asm__ __volatile__("nop");
}

void kernel_main(void) {
    probe_result = asm_probe(17, 25);
    csr_snapshot[0] = READ_CSR(sstatus);
    csr_snapshot[1] = READ_CSR(sie);
    csr_snapshot[2] = READ_CSR(satp);
    privilege_checkpoint();
    for (;;)
        __asm__ __volatile__("nop");
}

bss_probe 沒有設定初始值,應該落在 .bss
memset() 暫時保留給後續功能使用,這一天的開機清零已由 boot.S 完成。

把記憶體區段收齊

完整的 kernel.ld 改為:

OUTPUT_ARCH(riscv)
ENTRY(boot)

SECTIONS {
    . = 0x80200000;
    .text : {
        KEEP(*(.text.boot));
        *(.text .text.*);
    }
    .rodata : ALIGN(4) { *(.rodata .rodata.* .srodata .srodata.*); }
    .data : ALIGN(4) { *(.data .data.* .sdata .sdata.*); }
    .bss (NOLOAD) : ALIGN(4) {
        __bss = .;
        *(.bss .bss.* .sbss .sbss.*);
        *(COMMON);
        __bss_end = .;
    }
    .stack (NOLOAD) : ALIGN(16) {
        __stack_bottom = .;
        . += 128 * 1024;
        __stack_top = .;
    }
}

.sdata 等名稱是工具鏈可能使用的小型資料區段,也要被明確安排。
NOLOAD 區段保留執行時的空間,不需要把整段零值塞進 ELF 的檔案內容。
堆疊獨立於 .bss,兩者的範圍現在可以分開檢查。

在 Makefile 更新來源清單,並於既有 CFLAGS 定義之後追加選項:

SOURCES := kernel.c asm_probe.S boot.S
CFLAGS += -msmall-data-limit=0 -Wl,--no-relax

這個實驗尚未設定全域指標 gp,因此先停用小型資料配置策略與連結器 relaxation,讓組語裡的位址載入維持容易追蹤的形式。
保留 Day 03 加入的標頭相依規則。

在清零前動手改記憶體

tiny-kernel/ 編譯:

make
llvm-readelf -S build/kernel.elf
llvm-nm -n build/kernel.elf

依 Day 03 的雙終端機方式啟動 QEMU 與 GDB,QEMU 仍須包含 -S
GDB 連線後輸入:

target remote 127.0.0.1:1234
hbreak *boot
continue
set var bss_probe = 0xdeadbeef
p/x bss_probe
x/6i $pc

此時 CPU 停在 boot 第一條指令,OpenSBI 已把控制權交到核心,但我們自己的 .bss 迴圈尚未執行。

繼續設定下一個檢查點:

hbreak *kernel_main
continue
p/x bss_probe
p/x $sp
p/x (unsigned int) &__stack_top
p/x ((unsigned int) $sp & 15)

預期 bss_probe 已變成 0x0sp 等於 __stack_top,最後的對齊檢查也是 0x0
這裡使用 *kernel_main,讓斷點停在第一條機器指令之前,避免 C 函式先執行 prologue 移動了堆疊。

若要逐條看 la 和迴圈如何展開,可以重新啟動實驗,停在 boot 後使用 si
la 是偽指令,可能展開成多條真正的 CPU 指令,不能把一次 si 當成執行完整一行原始組語。

一個有用的失敗對照

在個人的實驗副本中,暫時跳過清零迴圈,再重複剛才的 GDB 寫入測試。
如果 bss_probe 仍保留 0xdeadbeef,便能確認這份測試確實能抓到「初始化被漏掉」的情況。
完成觀察後恢復清零迴圈並重新編譯,保存的是正常啟動版本。

若出現重複 boot 符號,檢查 kernel.c 是否還留著 Day 01 的 naked 函式。
bss_probe 不在 __bss__bss_end 之間,先檢查連結器腳本與符號表,再懷疑清零迴圈。

保存這次可單步驗證的開機流程

今天新增 boot.Skernel.h,修改 kernel.ckernel.ld 與 Makefile。
建議 commit 訊息為:

day04: boot into kernel_main

Day 05 會替這個已能可靠進入 C 的核心接上文字輸出,讓除錯資訊直接出現在終端機。

參考資料


上一篇
Day 03|Privilege Mode:Kernel 為什麼比 Application 有權力?
下一篇
Day 05|Hello Kernel:沒有 OS 時我們怎麼把文字印出來?
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言