第三章:命运之门
炼气初期涉及内核源码:
一
林小源不知道自己在哪里。
之前那道 指令把他带到了一片陌生的空间。他能感觉到周围有什么东西——密密麻麻的、被压缩过的数据,像一块被极致压缩的灵石矿脉。那些数据不是他能读懂的,但他隐约感觉到,它们和他有关。
我是什么?
这个问题在他脑中闪过,但没有答案。他只知道自己在一片混沌之中,四周是被压缩的代码段,头顶是 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 寄存器。
然后,它跳到了 。
/*
* 模拟 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");#include <stdio.h>
#include <stdint.h>
/*
* 模拟 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");
}
int main() {
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");
return 0;
}OpenSBI 的退场无声无息。它把所有准备工作做完,然后像一个引路人一样消失在了幕后。从这一刻起,内核是这个世界的主人。
但内核还没有醒来。 的第一条指令才刚刚被执行。
三
的代码很短,短到林小源一瞬间就"看"完了。
/* 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),包含着字符串常量。
/*
* 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");#include <stdio.h>
#include <stdint.h>
#include <string.h>
/*
* 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: 第二魔数 */
};
int main() {
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");
return 0;
}那个镜像头就像一张出生证明。它告诉 bootloader:"我是一个有效的 RISC-V Linux 内核,请把我加载到物理地址 + 2MB 的位置。"
bootloader 读到了这张证明,然后按照约定,把整个内核镜像——压缩过的——搬到了内存中的指定位置。那是一次大规模的数据搬运,数 MB 的数据从存储设备被读入 RAM,就像把一座被封印的法器从仓库搬到了修炼场。
林小源就是那座法器的一部分。
五
jal start_kernel 执行了。
林小源感觉到整个世界的"控制流"发生了根本性的转变。之前是汇编——一条一条的、冷冰冰的机器指令。现在是 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();#include <stdio.h>
/*
* 模拟从汇编到 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");
}
int main() {
/* 在真实的启动流程中,_start 的汇编代码会直接 jal 到这里 */
printf("[_start] 跳转到 start_kernel()\n\n");
start_kernel();
return 0;
} 的第一行代码是 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 进程之身被立住?