理论讲解:加载C内核
1. 从汇编到C
到目前为止我们一直在写汇编。汇编适合底层操作(模式切换、硬件初始化),但用它写整个OS既痛苦又低效。
解决方案: 用汇编完成最小的初始化工作,然后跳转到C代码。
汇编代码(Bootloader) C代码(内核)
┌────────────────┐ ┌────────────────┐
│ 加载内核到内存 │ │ void kernel_main() │
│ 设置GDT │ call │ { │
│ 切换保护模式 │ ────────→ │ // 内核逻辑 │
│ 设置栈 │ │ } │
│ 调用C函数 │ │ │
└────────────────┘ └────────────────┘
2. C运行时环境
C代码需要几个基本条件:
- 栈 — 函数调用和局部变量(已在保护模式切换时设置)
- BSS段清零 — 未初始化全局变量必须为0
- 入口点 — 一个汇编桩来
call kernel_main
内核入口(kernel_entry.asm)
[BITS 32]
[EXTERN kernel_main] ; 声明C函数
global kernel_entry ; 导出给链接器
kernel_entry:
call kernel_main ; 调用C入口
jmp $ ; 如果返回则死循环3. 最简单的C内核
void kernel_main(void) {
char *video_memory = (char*)0xB8000;
const char *msg = "Hello from C kernel!";
for (int i = 0; msg[i] != '\0'; i++) {
video_memory[i * 2] = msg[i];
video_memory[i * 2 + 1] = 0x0F;
}
while (1) {
__asm__ volatile("hlt");
}
}要点:
- 没有
#include <stdio.h>—— freestanding环境,没有标准库 - 直接写
0xB8000输出到屏幕 while(1) hlt防止CPU继续执行随机内存
4. 链接脚本
链接器需要知道如何安排各个段,以及内核在内存中的加载地址:
ENTRY(kernel_entry) /* 入口点 */
SECTIONS {
. = 0x1000; /* 内核加载地址 */
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
.bss : { *(COMMON) *(.bss) }
}. = 0x1000 必须与Bootloader中的 KERNEL_OFFSET 一致。
5. 编译和链接过程
kernel_entry.asm ──→ nasm ──→ kernel_entry.o ─┐
├─→ ld ──→ kernel.bin
kernel.c ──→ gcc -m32 -ffreestanding ──→ kernel.o ─┘
│
使用 linker.ld
boot.asm ──→ nasm ──→ boot.bin
boot.bin + kernel.bin ──→ cat ──→ os-image.bin
关键编译选项:
-m32— 生成32位代码-ffreestanding— 独立环境,不依赖标准库-fno-pie— 禁用位置无关代码-c— 只编译不链接--oformat binary— 输出原始二进制(不是ELF)