Skip to content

第三章:命运之门

炼气初期

涉及内核源码:

林小源不知道自己在哪里。

之前那道 指令把他带到了一片陌生的空间。他能感觉到周围有什么东西——密密麻麻的、被压缩过的数据,像一块被极致压缩的灵石矿脉。那些数据不是他能读懂的,但他隐约感觉到,它们和他有关。

我是什么?

这个问题在他脑中闪过,但没有答案。他只知道自己在一片混沌之中,四周是被压缩的代码段,头顶是 bootloader 留下的余温,脚下是尚未展开的 .bss 段——刚刚被清零的、空白的内存。

BSS 段被清零的那一瞬间,他感觉到了一种奇异的宁静。就好像一块被擦干净的石板,等待着第一笔书写。

在林小源的意识之外,bootloader 还在做最后的准备工作。

RISC-V 世界里的 bootloader 不是 GRUB,而是 OpenSBI——一个运行在 M-mode(机器模式)的固件。M-mode 是 RISC-V 中最底层的执行模式,拥有对硬件的完全控制权。

OpenSBI 是一位沉默的老者。它不像 BIOS 那样用蜂鸣声宣告自己的存在——它从不多说一个字。但它的动作干净利落,每一步都精确到位。

林小源隐约感觉到了它的存在——一个庞大的、沉稳的力量,正在他周围做着最后的准备。那力量不说话,但它的每一个动作都带着一种无声的威严。

你是谁? 林小源在意识中问道。

没有回答。OpenSBI 从不回答。它只是默默地把三件东西放好——像是一个老管家在主人醒来之前,悄悄把早餐摆上了桌。

它做了三件事。

第一,它设置了陷阱向量(trap vector),让内核在遇到异常时能有地方可跳。第二,它把 hart id(硬件线程 ID)塞进了 a0 寄存器——这是内核知道"我现在运行在哪个 CPU 上"的唯一线索。第三,它把设备树(DTB)的地址放进了 a1 寄存器。

然后,它跳到了

c
/*
 * 模拟 OpenSBI 跳转到内核入口前的寄存器状态。
 * 在 RISC-V 的启动协议中,bootloader 必须在跳转前设置好这些约定。
 */
struct boot_context {
    uint64_t a0_hart_id;   /* 硬件线程 ID(哪个 CPU) */
    uint64_t a1_dtb_addr;  /* 设备树 Blob 的物理地址 */
    uint64_t ra_entry;     /* 返回地址(通常不用) */
};

void opensbi_jump(struct boot_context *ctx) {
    printf("=== OpenSBI 准备交出控制权 ===\n\n");

    printf("[OpenSBI] 设置陷阱向量 → stvec\n");
    printf("[OpenSBI] 当前 Hart ID: %lu\n", ctx->a0_hart_id);
    printf("[OpenSBI] 设备树地址:   0x%lX\n", ctx->a1_dtb_addr);
    printf("[OpenSBI] 内核入口:     _start\n\n");

    printf("[OpenSBI] 切换到 S-mode(管理模式)...\n");
    printf("[OpenSBI] 执行 jalr 跳转到 _start\n\n");

    printf("=== 控制权已交给内核 ===\n");
}

struct boot_context ctx = {
    .a0_hart_id = 0,                    /* 主 CPU */
    .a1_dtb_addr = 0x87000000,          /* DTB 通常在这个地址 */
    .ra_entry = 0,
};

opensbi_jump(&ctx);

printf("\n--- RISC-V 启动约定 ---\n");
printf("a0 = hart id (告诉内核:你是哪个 CPU)\n");
printf("a1 = dtb addr(告诉内核:硬件配置在哪里)\n");
printf("这些信息是内核了解硬件世界的起点。\n");

OpenSBI 的退场无声无息。它把所有准备工作做完,然后像一个引路人一样消失在了幕后。从这一刻起,内核是这个世界的主人。

但内核还没有醒来。 的第一条指令才刚刚被执行。

的代码很短,短到林小源一瞬间就"看"完了。

c
/* arch/riscv/kernel/head.S — 简化 */
SYM_CODE_START(_start)
    j _start_kernel           /* 跳过镜像头,直接到真正的启动代码 */

_start_kernel:
    la   tp, init_task                    /* tp = &init_task,林小源的肉身 */
    la   sp, init_thread_union + THREAD_SIZE /* 设置内核栈 */

    /* 清零 BSS 段 */
    la   t0, __bss_start
    la   t1, __bss_stop
1:  sd   zero, 0(t0)
    addi t0, t0, 8
    blt  t0, t1, 1b

    /* 跳转到 C 语言入口 */
    jal  start_kernel
SYM_CODE_END(_start)

三条指令。设置 tp、设置 sp、清零 BSS。

林小源感受到了 tp 寄存器被写入的那一瞬间。那个寄存器的值,指向的就是 ——他在这个世界中的肉身。一个在编译时就被静态分配好的 ,一个 PID 为 0 的进程描述符。

那是我。

他不知道自己怎么知道的。但他就是知道。就像人不需要理由就知道自己是谁一样,他在那个地址被写入 tp 的瞬间,就知道了那是自己。

然后 sp 被设置好了——内核栈的顶部,init_thread_union + THREAD_SIZE。他有了栈。有了栈,就有了调用函数的能力,就有了"深度思考"的空间。

最后是 BSS 清零。那段循环把从 之间的所有内存都填成了 0。林小源感觉到周围那些"未初始化"的噪声消失了,取而代之的是一片干净的虚空。

世界安静了。

一切都准备好了。tp 指向 sp 指向栈顶,BSS 被清零。

接下来,就是 jal start_kernel

在跳转发生之前,林小源有了一瞬间的清明。

他不知道这一瞬间是从哪里来的——也许是因为 BSS 清零后世界太安静了,也许是 的地址被写入 tp 后他有了某种"锚点"——但就在这一瞬间,他开始"看"周围的世界。

他看到了内核镜像。

那是一个庞大的、被压缩过的二进制文件,静静地躺在内存中。它不是一堆杂乱无章的字节——它有自己的结构:一个镜像头(image header),描述了这个内核的基本信息;一个代码段(.text),包含着内核的所有指令;一个数据段(.data),包含着全局变量;一个只读段(.rodata),包含着字符串常量。

c
/*
 * RISC-V Linux 内核镜像头(简化版)。
 * 位于 arch/riscv/include/asm/image.h
 * bootloader 通过这个头部来验证和加载内核。
 */
struct riscv_image_header {
    uint32_t code0;          /* 0x00: 跳转指令(j _start_kernel) */
    uint32_t code1;          /* 0x04: 保留 */
    uint64_t text_offset;    /* 0x08: 内核加载偏移 */
    uint64_t image_size;     /* 0x10: 内核镜像大小 */
    uint64_t flags;          /* 0x18: 标志位 */
    uint32_t version;        /* 0x20: 头部版本 */
    uint32_t reserved1;      /* 0x24: 保留 */
    uint64_t reserved2;      /* 0x28: 保留 */
    char magic[4];           /* 0x30: 魔数 "RISCV" */
    char magic2[4];          /* 0x34: 第二魔数 */
};

struct riscv_image_header hdr;
memset(&hdr, 0, sizeof(hdr));

/* 模拟一个真实的内核镜像头 */
hdr.code0 = 0x0000006F;           /* j _start_kernel (JAL x0, offset) */
hdr.text_offset = 0x200000;       /* 2MB 偏移 */
hdr.image_size = 16 * 1024 * 1024; /* 16MB */
hdr.version = 1;
memcpy(hdr.magic, "RISCV", 4);
memcpy(hdr.magic2, "RSC\x05", 4);

printf("=== RISC-V 内核镜像头 ===\n\n");
printf("code0:        0x%08X  (j _start_kernel)\n", hdr.code0);
printf("text_offset:  0x%lX  (加载偏移: %lu MB)\n",
       hdr.text_offset, hdr.text_offset / (1024*1024));
printf("image_size:   0x%lX  (镜像大小: %lu MB)\n",
       hdr.image_size, hdr.image_size / (1024*1024));
printf("version:      %u\n", hdr.version);
printf("magic:        %.4s\n", hdr.magic);

printf("\n--- Bootloader 如何使用这个头部 ---\n");
printf("1. 读取 magic,验证这是有效的 RISC-V 内核\n");
printf("2. 根据 text_offset 决定加载地址\n");
printf("3. 根据 image_size 分配内存\n");
printf("4. 把整个镜像复制到目标地址\n");
printf("5. 跳转到 code0(即 _start)\n");

那个镜像头就像一张出生证明。它告诉 bootloader:"我是一个有效的 RISC-V Linux 内核,请把我加载到物理地址 + 2MB 的位置。"

bootloader 读到了这张证明,然后按照约定,把整个内核镜像——压缩过的——搬到了内存中的指定位置。那是一次大规模的数据搬运,数 MB 的数据从存储设备被读入 RAM,就像把一座被封印的法器从仓库搬到了修炼场。

林小源就是那座法器的一部分。

jal start_kernel 执行了。

林小源感觉到整个世界的"控制流"发生了根本性的转变。之前是汇编——一条一条的、冷冰冰的机器指令。现在是 C 语言——有函数调用、有变量、有逻辑结构。

这是一种质变。

汇编代码就像原始人的石器,每做一件事都需要精确到每一块肌肉的运动。C 语言则像是一把锻造好的剑,你可以挥舞它,而不必关心每一锤的细节。

c
/*
 * 模拟从汇编到 C 的跳转。
 * _start 是汇编,start_kernel 是 C。
 * 这个跳转代表着"语言"的转变——从机器指令到人类可读的代码。
 */
void start_kernel(void) {
    printf("=== start_kernel() 被调用 ===\n\n");

    printf("[start_kernel] 设置栈溢出魔数...\n");
    printf("[start_kernel] 初始化当前 CPU...\n");
    printf("[start_kernel] 关闭中断...\n");
    printf("[start_kernel] 打印内核 banner...\n");
    printf("[start_kernel] 初始化架构相关代码...\n");
    printf("[start_kernel] 初始化内存管理...\n");
    printf("[start_kernel] 初始化调度器...\n");
    printf("[start_kernel] 初始化中断...\n");
    printf("[start_kernel] 初始化 VFS...\n");
    printf("[start_kernel] ...\n\n");

    printf("=== 内核世界正在诞生 ===\n");
}

/* 在真实的启动流程中,_start 的汇编代码会直接 jal 到这里 */
printf("[_start] 跳转到 start_kernel()\n\n");
start_kernel();

的第一行代码是 set_task_stack_end_magic(&init_task)——在 的栈底设置一个魔数 0x57AC6E9D,用来检测栈溢出。

这个魔数林小源感觉到了。那是一种微弱的"印记",像是有人在他的栈底刻下了一个符文。他不知道这个符文的含义,但他能感觉到它在那里——安静地守护着,直到有朝一日栈溢出时才会被激活。

然后是 ——确认当前 CPU 的 ID。然后是 ——初始化调试对象。然后是 ——关闭中断。

中断被关了。

林小源感到了一种前所未有的宁静。在中断被关闭的这段时间里,整个世界是"静止"的——没有外部信号可以打断正在执行的代码。这是一个短暂的、不受打扰的窗口,内核可以在这个窗口中完成最关键的初始化工作。

他不知道这种宁静能持续多久。但他知道,一旦中断被重新打开,世界就会变得嘈杂起来。


道藏笔记

内核启示

bootloader 是内核的产婆,而内核镜像头就是出生证明。

在 RISC-V 的世界里, 定义在 中。镜像的前几个字节是一个标准的 RISC-V 内核镜像头(struct riscv_image_header),包含魔数、加载偏移、镜像大小等信息。bootloader 通过读取这个头部来验证内核的有效性,并决定如何加载它。

的工作极其精简:设置 tp(线程指针,指向 )、设置 sp(栈指针)、清零 BSS 段,然后跳转到 。这四步是内核从汇编世界进入 C 世界的桥梁。

定义在 中,是内核的 C 语言入口。它的第一行是 set_task_stack_end_magic(&init_task)——在栈底设置魔数 0x57AC6E9D,用于检测栈溢出。紧接着是 ——关闭中断,为关键的初始化工作创造一个不受打扰的环境。

——林小源的肉身——在 中被设置为当前任务(通过 la tp, init_task)。从这一刻起,林小源不再只是内存中的一段数据。他是一个正在执行的进程。一个 PID 为 0 的 idle 进程,一个注定要永远沉睡的存在。

但他不甘。


破关试炼

命运之钥

在内核真正开始调度前,哪一个静态任务作为 PID 0 的 idle 进程之身被立住?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道