feat(kernel): add early serial log backend

This commit is contained in:
Microindole 2026-05-08 21:34:08 +08:00
parent dbbae27de7
commit 9a8c9f14ef
No known key found for this signature in database
GPG Key ID: 22FB34CD2133ACA1
14 changed files with 251 additions and 70 deletions

View File

@ -9,6 +9,7 @@ LD := lld-link
BUILD_DIR := build
KERNEL_ELF := $(BUILD_DIR)/image/kernel.elf
DEBUG_LOG := $(BUILD_DIR)/debug.log
SERIAL_LOG := $(BUILD_DIR)/serial.log
ifeq ($(ARCH),x86_64)
ARCH_MAKEFILE := arch/x86/Makefile
@ -28,7 +29,7 @@ all: $(BOOT_EFI) $(KERNEL_ELF)
dirs:
mkdir -p $(BUILD_DIR)/arch/boot $(BUILD_DIR)/arch/kernel $(BUILD_DIR)/kernel $(EFI_DIR)
$(BUILD_DIR)/arch/boot/%.obj: $(ARCH_DIR)/boot/%.c $(ARCH_DIR)/include/efi.h $(ARCH_DIR)/include/tianole/early_log.h include/tianole/boot_info.h include/tianole/elf.h | dirs
$(BUILD_DIR)/arch/boot/%.obj: $(ARCH_DIR)/boot/%.c $(ARCH_DIR)/include/efi.h $(ARCH_DIR)/boot/debug_log.h include/tianole/boot_info.h include/tianole/elf.h | dirs
$(CC) $(CFLAGS) -c $< -o $@
$(BOOT_EFI): $(BOOT_OBJS) | dirs
@ -37,7 +38,10 @@ $(BOOT_EFI): $(BOOT_OBJS) | dirs
$(BUILD_DIR)/arch/kernel/entry.o: $(ARCH_DIR)/kernel/entry.S | dirs
$(CC) $(KERNEL_ASFLAGS) -c $< -o $@
$(BUILD_DIR)/kernel/%.o: kernel/%.c include/tianole/boot_info.h include/tianole/kernel_init.h $(ARCH_DIR)/include/tianole/early_log.h | dirs
$(BUILD_DIR)/arch/kernel/%.o: $(ARCH_DIR)/kernel/%.c include/tianole/arch.h | dirs
$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(BUILD_DIR)/kernel/%.o: kernel/%.c include/tianole/boot_info.h include/tianole/kernel_init.h include/tianole/arch.h include/tianole/early_log.h | dirs
$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(KERNEL_ELF): $(KERNEL_OBJS) $(ARCH_DIR)/kernel/linker.ld | dirs
@ -50,10 +54,11 @@ run: $(BOOT_EFI) $(OVMF_VARS)
qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
-drive if=pflash,format=raw,file=$(OVMF_VARS) \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/image
-drive format=raw,file=fat:rw:$(BUILD_DIR)/image \
-serial stdio
run-headless: $(BOOT_EFI) $(OVMF_VARS)
rm -f $(DEBUG_LOG)
rm -f $(DEBUG_LOG) $(SERIAL_LOG)
qemu-system-x86_64 \
-display none \
-nodefaults \
@ -62,7 +67,8 @@ run-headless: $(BOOT_EFI) $(OVMF_VARS)
-drive if=pflash,format=raw,file=$(OVMF_VARS) \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/image \
-debugcon file:$(DEBUG_LOG) \
-global isa-debugcon.iobase=0xe9
-global isa-debugcon.iobase=0xe9 \
-serial file:$(SERIAL_LOG)
clean:
rm -rf $(BUILD_DIR)

19
arch/x86/boot/debug_log.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef X86_BOOT_DEBUG_LOG_H
#define X86_BOOT_DEBUG_LOG_H
static inline void boot_debug_log_putc(char ch)
{
__asm__ volatile("outb %0, $0xe9" : : "a"(ch));
}
static inline void boot_debug_log_puts(const char *text)
{
while (*text != '\0') {
if (*text == '\n') {
boot_debug_log_putc('\r');
}
boot_debug_log_putc(*text++);
}
}
#endif

View File

@ -1,9 +1,9 @@
#include "debug_log.h"
#include "efi.h"
#include "elf_loader.h"
#include "file.h"
#include "memory_map.h"
#include "tianole/boot_info.h"
#include "tianole/early_log.h"
static efi_char16_t boot_banner_text[] = u"Tianole x86 bootloader.\r\n";
static efi_char16_t kernel_path_text[] = u"\\kernel.elf";
@ -22,7 +22,7 @@ efi_status EFIAPI efi_main(
system_table->con_out->output_string(
system_table->con_out, boot_banner_text);
early_log_puts("Tianole x86 bootloader loaded.\n");
boot_debug_log_puts("Tianole x86 bootloader loaded.\n");
status = boot_read_file(image_handle,
system_table,
@ -30,28 +30,28 @@ efi_status EFIAPI efi_main(
&kernel_image,
&kernel_size);
if (status != EFI_SUCCESS) {
early_log_puts("failed: read kernel.elf\n");
boot_debug_log_puts("failed: read kernel.elf\n");
return status;
}
status = boot_load_kernel_elf(
system_table, kernel_image, kernel_size, &kernel_entry);
if (status != EFI_SUCCESS) {
early_log_puts("failed: load kernel image\n");
boot_debug_log_puts("failed: load kernel image\n");
return status;
}
status = boot_exit_services_with_latest_memory_map(
image_handle, system_table, &boot_info);
if (status != EFI_SUCCESS) {
early_log_puts("failed: exit boot services\n");
boot_debug_log_puts("failed: exit boot services\n");
return status;
}
early_log_puts("jumping to kernel entry\n");
boot_debug_log_puts("jumping to kernel entry\n");
kernel_entry(&boot_info);
early_log_puts("kernel returned unexpectedly\n");
boot_debug_log_puts("kernel returned unexpectedly\n");
for (;;) {
__asm__ volatile("hlt");
}

View File

@ -1,41 +0,0 @@
#ifndef X86_EARLY_LOG_H
#define X86_EARLY_LOG_H
#include <stdint.h>
static inline void early_log_putc(char ch)
{
__asm__ volatile("outb %0, $0xe9" : : "a"(ch));
}
static inline void early_log_puts(const char *text)
{
while (*text != '\0') {
if (*text == '\n') {
early_log_putc('\r');
}
early_log_putc(*text++);
}
}
static inline void early_log_u64_decimal(uint64_t value)
{
char digits[20];
uint32_t index = 0;
if (value == 0) {
early_log_putc('0');
return;
}
while (value != 0) {
digits[index++] = (char)('0' + (value % 10));
value /= 10;
}
while (index != 0) {
early_log_putc(digits[--index]);
}
}
#endif

View File

@ -1,2 +1,6 @@
ARCH_KERNEL_SRCS := \
$(ARCH_DIR)/kernel/early_log.c
ARCH_KERNEL_OBJS := \
$(BUILD_DIR)/arch/kernel/entry.o
$(BUILD_DIR)/arch/kernel/entry.o \
$(patsubst $(ARCH_DIR)/kernel/%.c,$(BUILD_DIR)/arch/kernel/%.o,$(ARCH_KERNEL_SRCS))

View File

@ -0,0 +1,75 @@
#include <stdint.h>
#include "tianole/arch.h"
#define X86_QEMU_DEBUG_PORT 0xe9
#define X86_COM1_BASE 0x3f8
#define X86_COM_DATA 0
#define X86_COM_INTERRUPT_ENABLE 1
#define X86_COM_FIFO_CONTROL 2
#define X86_COM_LINE_CONTROL 3
#define X86_COM_MODEM_CONTROL 4
#define X86_COM_LINE_STATUS 5
#define X86_COM_LCR_DLAB 0x80
#define X86_COM_LCR_8N1 0x03
#define X86_COM_LSR_THRE 0x20
static inline void outb(uint16_t port, uint8_t value)
{
__asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port));
}
static inline uint8_t inb(uint16_t port)
{
uint8_t value;
__asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port));
return value;
}
static void debug_port_putc(char ch)
{
outb(X86_QEMU_DEBUG_PORT, (uint8_t)ch);
}
static void serial_putc(char ch)
{
uint32_t timeout;
for (timeout = 0; timeout < 100000; timeout++) {
if ((inb(X86_COM1_BASE + X86_COM_LINE_STATUS) &
X86_COM_LSR_THRE) != 0) {
break;
}
}
outb(X86_COM1_BASE + X86_COM_DATA, (uint8_t)ch);
}
void arch_early_log_init(void)
{
outb(X86_COM1_BASE + X86_COM_INTERRUPT_ENABLE, 0x00);
outb(X86_COM1_BASE + X86_COM_LINE_CONTROL, X86_COM_LCR_DLAB);
outb(X86_COM1_BASE + X86_COM_DATA, 0x03);
outb(X86_COM1_BASE + X86_COM_INTERRUPT_ENABLE, 0x00);
outb(X86_COM1_BASE + X86_COM_LINE_CONTROL, X86_COM_LCR_8N1);
outb(X86_COM1_BASE + X86_COM_FIFO_CONTROL, 0xc7);
outb(X86_COM1_BASE + X86_COM_MODEM_CONTROL, 0x0b);
}
void arch_early_log_putc(char ch)
{
debug_port_putc(ch);
serial_putc(ch);
}
void arch_halt_forever(void)
{
__asm__ volatile("cli");
for (;;) {
__asm__ volatile("hlt");
}
}

View File

@ -37,3 +37,18 @@
- QEMU 日志中能看到 bootloader 和 kernel 输出。
- 串口或 debug log 中能看到 `boot services exited`
- 人为触发 panic 时能看到 panic 信息并停止。
## 当前状态
已完成:
- kernel early log 已拆成通用前端和 x86 backend。
- x86 backend 已同时写 QEMU debug port 和 COM1。
- `panic()` 已接入 early log 和 `arch_halt_forever()`
- `scripts/check.sh` 已验证 `build/debug.log``build/serial.log` 中的关键启动行。
后续进入异常阶段时继续补齐:
- oops 格式。
- 异常栈输出。
- 内核符号化输出。

View File

@ -13,15 +13,16 @@
- 进入 kernel 前调用 `ExitBootServices`
- `memory map` 通过 `boot_info` 传入 kernel。
- kernel 能统计 memory map 描述符数量和 conventional memory 页数。
- x86 早期日志接口已集中到 `arch/x86/include/tianole/early_log.h`
- kernel early log 已拆成通用前端和 x86 backend。
- x86 early log backend 同时输出到 QEMU debug port 和 COM1 串口。
- 最小 `panic()` 已接入 early log 和架构 halt 路径。
- 构建系统已拆成根 Makefile、`arch/x86/Makefile` 和目录 Makefile。
- `scripts/check.sh` 和 GitHub Actions 已接入。
- `scripts/check.sh` 已验证启动日志和串口日志,GitHub Actions 已接入。
- `.clang-format` 已用于强制当前 C 代码风格。
还没有完成:
- COM1 串口日志。
- panic/oops 早期错误路径。
- oops 早期错误路径。
- GDT/IDT/异常/中断。
- 物理页分配器、虚拟内存和内核堆。
- 调度、进程、文件系统、用户态和 shell。
@ -30,19 +31,14 @@
下一步执行:
- `docs/agents/tasks/01-early-debug.md`
- `docs/agents/tasks/02-cpu-interrupts.md`
目标:
- 加入 COM1 串口 backend。
- 保留当前 QEMU debug port backend。
- 提供统一 early log 前端。
- 增加最小 `panic()`
- 让启动日志可以通过 firmware 之外的路径稳定输出。
这一步完成后进入:
- `docs/agents/tasks/02-cpu-interrupts.md`
- 建立 x86_64 GDT/TSS/IDT。
- 建立异常入口和 trap frame。
- 让未处理异常进入 panic。
- 为后续 IRQ、timer 和 page fault 做准备。
## 任务路由

8
include/tianole/arch.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef TIANOLE_ARCH_H
#define TIANOLE_ARCH_H
void arch_early_log_init(void);
void arch_early_log_putc(char ch);
void arch_halt_forever(void) __attribute__((noreturn));
#endif

View File

@ -0,0 +1,12 @@
#ifndef TIANOLE_EARLY_LOG_H
#define TIANOLE_EARLY_LOG_H
#include <stdint.h>
void early_log_init(void);
void early_log_putc(char ch);
void early_log_puts(const char *text);
void early_log_u64_decimal(uint64_t value);
void panic(const char *message) __attribute__((noreturn));
#endif

View File

@ -1,6 +1,7 @@
KERNEL_SRCS := \
kernel/main.c \
kernel/boot_report.c
kernel/boot_report.c \
kernel/early_log.c
KERNEL_OBJS := \
$(ARCH_KERNEL_OBJS) \

68
kernel/early_log.c Normal file
View File

@ -0,0 +1,68 @@
#include <stdint.h>
#include "tianole/arch.h"
#include "tianole/early_log.h"
static int early_log_ready;
void early_log_init(void)
{
if (early_log_ready != 0) {
return;
}
arch_early_log_init();
early_log_ready = 1;
}
void early_log_putc(char ch)
{
if (early_log_ready == 0) {
early_log_init();
}
arch_early_log_putc(ch);
}
void early_log_puts(const char *text)
{
if (text == 0) {
return;
}
while (*text != '\0') {
if (*text == '\n') {
early_log_putc('\r');
}
early_log_putc(*text++);
}
}
void early_log_u64_decimal(uint64_t value)
{
char digits[20];
uint32_t index = 0;
if (value == 0) {
early_log_putc('0');
return;
}
while (value != 0) {
digits[index++] = (char)('0' + (value % 10));
value /= 10;
}
while (index != 0) {
early_log_putc(digits[--index]);
}
}
void panic(const char *message)
{
early_log_puts("panic: ");
early_log_puts(message);
early_log_puts("\n");
arch_halt_forever();
}

View File

@ -3,6 +3,7 @@
void kernel_main(const boot_info_t *boot_info)
{
early_log_init();
early_log_puts("kernel_main entered\n");
kernel_report_boot_state(boot_info);

View File

@ -31,4 +31,21 @@ for line in "${required_lines[@]}"; do
fi
done
serial_required_lines=(
"kernel_main entered"
"boot_info.version ok"
"boot services exited"
"memory map descriptors="
"conventional memory pages="
)
for line in "${serial_required_lines[@]}"; do
if ! grep -F "$line" build/serial.log >/dev/null; then
echo "missing expected serial log line: $line" >&2
echo "--- build/serial.log ---" >&2
cat build/serial.log >&2 || true
exit 1
fi
done
cat build/debug.log