iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Software Development

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

Day 05|Hello Kernel:沒有 OS 時我們怎麼把文字印出來?

  • 分享至 

  • xImage
  •  

今天的終端機終於會出現核心自己印出的文字:

Hello Kernel!
sum=42 hex=2a

OpenSBI 的開機資訊和這兩行文字會出現在同一個視窗,但來源不同。
前者由韌體輸出,後者則從我們的 kernel_main() 開始,經由一個自己實作的字元輸出函式送出去。

把 I/O 的細節藏在介面後面

《Operating System Concepts》第 10 版第 2.1 節,把 I/O 操作列為作業系統提供的服務。
程式需要輸出資料,但不應每次都重新理解裝置控制器如何運作,介面的價值就是把這些細節集中處理。

今天先在核心內建立這種分工:格式化函式決定要印哪些字元,字元輸出函式再呼叫韌體。
目前還沒有使用者程式,所以這不是 U-mode 的系統呼叫 API,而是核心自己的 Console 功能。

《OS in 1,000 Lines》的〈Hello World!〉利用 SBI(Supervisor Binary Interface)輸出字元。
我們沿用 Day 01 固定下載的 OpenSBI 韌體,使用它支援的 Legacy Console Putchar,擴充編號為 0x01
Legacy 表示這是舊介面,換成其他韌體時要重新確認支援情況,不能假定所有 SBI 實作都提供它。

從一個字元開始

以下操作都在 30-days-os-kernel/tiny-kernel/,接續 Day 04。
新增 console.c

#include "kernel.h"
#include <stdarg.h>

int putchar(int ch) {
    register unsigned int a0 __asm__("a0") = (unsigned char) ch;
    register unsigned int a7 __asm__("a7") = 1;
    __asm__ __volatile__("ecall"
                         : "+r"(a0)
                         : "r"(a7)
                         : "memory");
    return ch;
}

static int print_string(const char *s) {
    int count = 0;
    if (!s)
        s = "(null)";
    while (*s) {
        putchar((unsigned char) *s++);
        count++;
    }
    return count;
}

static int print_unsigned(unsigned int value, unsigned int base) {
    const char digits[] = "0123456789abcdef";
    char buffer[32];
    unsigned int n = 0;
    do {
        buffer[n++] = digits[value % base];
        value /= base;
    } while (value);
    int count = (int) n;
    while (n)
        putchar(buffer[--n]);
    return count;
}

int printf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    int count = 0;
    while (*format) {
        if (*format != '%') {
            putchar((unsigned char) *format++);
            count++;
            continue;
        }
        format++;
        if (!*format) {
            putchar('%');
            count++;
            break;
        }
        switch (*format) {
        case 's':
            count += print_string(va_arg(args, const char *));
            break;
        case 'u':
            count += print_unsigned(va_arg(args, unsigned int), 10);
            break;
        case 'x':
            count += print_unsigned(va_arg(args, unsigned int), 16);
            break;
        case '%':
            putchar('%');
            count++;
            break;
        default:
            putchar('%');
            putchar((unsigned char) *format);
            count += 2;
            break;
        }
        format++;
    }
    va_end(args);
    return count;
}

putchar() 把字元放到 a0,把服務編號放到 a7,再執行 ecall
這個 wrapper 只用於本次的 Legacy Putchar,不是通用的 SBI v0.2 呼叫封裝。
此處也尚未把韌體回傳的錯誤轉成完整的 I/O 錯誤處理。

ecall 會觸發環境呼叫例外,在目前的設定下由 M-mode 的 OpenSBI 處理,再返回 S-mode 核心。
字元最後經過韌體的裝置驅動與 QEMU 模擬的序列埠,呈現在主機終端機。

格式化不需要作業系統幫忙

上面的 printf() 是本系列自己的最小版本,只支援 %s%u%x%%
%u%x 接收 unsigned int,字串則用 %s,不支援浮點數、欄寬或 %p

stdarg.h 提供可變參數的工具,並不依賴主機的 printf()
數字轉字串則是普通的整數運算,先反向保存餘數,再倒著輸出。
使用 do ... while 是為了確保輸入為零時,也會印出一個 0

這份程式以 %u 為主,讓後續計數器與位址的型別保持清楚。
列印 RV32 位址時會明確轉成 unsigned int,到 RV64 階段不能直接沿用這種轉型。

kernel.h 末尾加入:

int putchar(int ch);
int printf(const char *format, ...);

Makefile 的 SOURCES 更新為:

SOURCES := kernel.c asm_probe.S boot.S console.c

保留 Day 04 的 kernel.c 其他內容,將 kernel_main() 取代為:

void kernel_main(void) {
    probe_result = asm_probe(17, 25);
    printf("Hello %s!\n", "Kernel");
    printf("sum=%u hex=%x\n", probe_result, probe_result);
    printf("zero=%u percent=%%\n", 0u);
    printf("stack top=0x%x\n", (unsigned int) __stack_top);
    for (;;)
        __asm__ __volatile__("wfi");
}

wfi 是 Wait For Interrupt,允許處理器等待事件。
它放在迴圈中,是因為返回執行或被實作成近似 no-op 時,核心仍應停留在這個等待位置。
這不是排程器,也沒有建立可被喚醒的行程。

讓 Day 02 的結果真正出現在畫面上

tiny-kernel/ 執行:

make
make run

預期在 OpenSBI 訊息後看到:

Hello Kernel!
sum=42 hex=2a
zero=0 percent=%
stack top=0x...

最後一行的位址依本次建置而定,應與 make symbols 裡的 __stack_top 相符。
這份輸出同時檢查字串、十進位、十六進位、零值與百分比符號,避免只用一句 Hello World 就以為所有格式都正常。

接著可以把 asm_probe(17, 25) 改成 asm_probe(100, 23),重新編譯後檢查 sum=123 hex=7b
這能確認畫面上的數字真的來自本次運算,完成後可恢復原本的 42 範例。

有輸出之前,也要知道在哪裡找問題

如果只看到 OpenSBI,先用 Day 03 的 GDB 方法停在 putchar(),確認核心是否走到輸出函式。
若停在 ecall 周圍,檢查 a7 是否為 1,以及載入的韌體是否和 Day 01 相同。

如果字串正常但數字不對,先檢查格式和參數型別。
例如 %u 必須配合無號整數,不能拿它直接讀取指標或字串。
目前不支援的格式也不會自動得到標準函式庫的行為。

今天新增 console.c,修改 kernel.hkernel.c 與 Makefile,建議 commit 訊息為:

day05: add kernel console

Day 06 會把這條輸出路徑用在失敗分析,讓核心遇到不能繼續的狀況時,能留下檔名、行號與原因。

參考資料


上一篇
Day 04|Kernel Boot 深入:從 OpenSBI 逐步追到 kernel_main()
下一篇
Day 06|Kernel Panic:核心出錯時怎麼停下來並找出問題?
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言