🚀 一个让内核"自带文件系统"的神技:把 init 程序直接塞进内核二进制!
在内核还不支持读取磁盘、不支持文件系统时,如果使用一个临时的方式支持一个简单的文件系统?
今天教你一招——把文件系统直接‘编译进内核'!
无需磁盘、无需驱动,内核启动即执行 init,极致简洁!"**
💡 问题:自制 OS 的第一道坎 —— "内核如何找到 init?"
当你写完第一个自制操作系统内核时,一定会遇到这个经典难题:
"我的内核已经运行了,但怎么加载并执行
init进程呢?"
传统方案:
- ✅ 从磁盘读取:需要实现文件系统、块设备驱动、分区表解析…
- ❌ 太复杂:对于初学者来说,这简直是"地狱级"任务。
有没有更简单的办法?
当然有!
🧠 核心思路:把"文件系统"直接塞进内核二进制!
什么是 initramfs?
在 Linux 中,initramfs(Initial RAM Filesystem)是一个内存中的根文件系统,它被压缩后嵌入到内核镜像中。内核启动时将其解压到内存,然后挂载为根文件系统,再执行 /sbin/init。
我们不搞复杂的 initramfs,而是用一个极简版的"内联文件系统":
把 init 程序、配置文件等,直接以二进制数据的形式,作为"静态数据"编译进内核!
🛠️ 实现方法:用汇编或 C 语言将文件内容"硬编码"进内核
把文件系统塞内核二进制
创建文件系统文件结构
build\rootfs_bin
|- init
|- busybox_x86
将内容打包为cpio格式
脚本文件:cpio_fs.sh
#!/bin/bash
# 接受一个参数,即要编译的目录
if [[ $# -eq 0 ]]; then
echo "请提供要编译的目录作为参数"
exit 1
fi
# 编译目录
BUILD_DIR="$1"
# 进入编译目录
cd "$BUILD_DIR"
cd rootfs_bin
ls -la ../rootfs.cpio
if [[ -f ../rootfs.cpio ]]; then
rm ../rootfs.cpio
fi
find . -print | cpio -ov --format=newc > ../rootfs.cpio
ls -la ../rootfs.cpio
cd -
用法:bash cpio_fs.sh build
将cpio文件编译进内核elf文件
; rootfs.asm - 内联文件系统
global __initramfs_start
global __initramfs_end
section .initramfs
__initramfs_start:
incbin "rootfs.cpio"
__initramfs_end:
// 在链接脚本中定义符号
// linker.ld
SECTIONS {
.initramfs : {
__initramfs_start = .;
*(.initramfs)
__initramfs_end = .;
}
}
🔍 内核如何"读取"这些数据?
步骤 1:获取数据地址和大小
// kernel.c
extern char __initramfs_start[];
extern char __initramfs_end[];
void load_init_from_ramfs(void) {
size_t fs_size = __initramfs_end - __initramfs_start;
char *fs_data = __initramfs_start;
// 现在 fs_data 指向整个"文件系统"的二进制数据
// 你可以按自己的格式解析它
parse_my_filesystem(fs_data, fs_size);
}
步骤 2:解析cpio文件格式
cpio(copy in / copy out)是一种用于归档和传输文件的 Unix 工具和文件格式。它最早出现在 Unix System III(1981 年)中,比 tar 更早被标准化(POSIX.1-1988 就包含了 cpio),尽管在日常使用中不如 tar 流行,但它在 Linux 内核 initramfs、RPM 包管理器等关键系统组件中被广泛使用。
一、cpio 的工作原理
cpio 本身不直接遍历目录,而是 从标准输入读取文件路径列表(通常由 find 或 echo 生成),然后:
-o(copy-out)模式:将这些文件打包成一个归档流,输出到 stdout。-i(copy-in)模式:从 stdin 读取归档流,解包到文件系统。-p(pass)模式:直接复制文件到指定目录(不生成归档)。
因此,cpio 的核心思想是:归档 = 文件列表 + 文件内容的线性拼接。
二、cpio 的归档格式
cpio 支持多种格式,主要分为三类:
1. binary(旧格式,已废弃)
- 非可移植,依赖字节序
- 已淘汰,不推荐使用
2. ASCII / "newc" 格式(最常用)
- 可读的文本头 + 二进制数据
- 被 Linux initramfs、RPM 等广泛采用
- 文件名和元数据以 ASCII 十六进制字符串存储
- 支持长文件名(最多 256 字节,默认)
- 所有字段固定长度(便于解析)
newc 格式的头部结构(每字段 8 字节,共 112 字节):
| 字段 | 长度 | 说明 |
|---|---|---|
c_magic | 6 字节 | 魔数 "070701"(newc)或 "070702"(crc) |
c_ino | 8 字节 | inode 号(十六进制 ASCII) |
c_mode | 8 字节 | 文件权限(如 0100644) |
c_uid | 8 字节 | 用户 ID |
c_gid | 8 字节 | 组 ID |
c_nlink | 8 字节 | 硬链接数 |
c_mtime | 8 字节 | 修改时间(Unix 时间戳) |
c_filesize | 8 字节 | 文件内容长度(字节) |
c_devmajor | 8 字节 | 设备主设备号(仅设备文件) |
c_devminor | 8 字节 | 设备次设备号 |
c_rdevmajor | 8 字节 | 特殊文件的主设备号 |
c_rdevminor | 8 字节 | 特殊文件的次设备号 |
c_namesize | 8 字节 | 文件名长度(包含结尾 \0) |
c_checksum | 8 字节 | 校验和(newc 中通常为 0) |
所有数值字段均为 8 字节 ASCII 十六进制字符串(小端?不,是字符串!比如 1024 存为
"00000400")
文件记录布局:
[112 字节头部] + [N 字节文件名 + \0] + [填充至 4 字节对齐] + [文件内容] + [填充至 4 字节对齐]
- 文件名后 必须 有 null 字符
\0 - 文件名和内容之间自动对齐到 4 字节边界(用
\0填充) - 文件内容后也对齐到 4 字节边界
归档结束标志:
- 一个特殊文件名为
"TRAILER!!!"的条目 - 其
c_filesize = 0 - 表示归档结束
3. crc 格式(newc + 校验和)
- 与 newc 类似,但
c_checksum字段包含文件内容的 CRC32 校验和 - 魔数为
"070702"
创建和解包 cpio 归档
创建归档(newc 格式):
# 方法1:使用 find
find /path/to/files -print0 | cpio -o -H newc --null > archive.cpio
# 方法2:手动指定文件
echo -e "file1.txt\nfile2.txt" | cpio -o -H newc > archive.cpio
解包归档:
cpio -i -d < archive.cpio
# -d:自动创建所需目录
列出内容:
cpio -it < archive.cpio
步骤 3:解析并执行 init
void parse_my_filesystem(char *data, size_t size) {
struct filesystem_header *header = (struct filesystem_header*)data;
if (header->magic != 0x12345678) {
panic("Invalid ramfs format!");
}
for (int i = 0; i < header->entry_count; i++) {
struct file_entry *entry = &header->entries[i];
if (strcmp(entry->nVjame, "init") == 0) {
// 找到 init 程序
char *init_data = data + entry->offset;
// 创建进程并执行
create_process(init_data, entry->size);
break;
}
}
}
完整代码:
// rootfs.c
extern char __initramfs_start[];
extern char __initramfs_end[];
int MemFS::load_initramfs(const void* data, size_t size)
{
const uint8_t* ptr = static_cast<const uint8_t*>(data);
const uint8_t* end = ptr + size;
log_info("Loading initramfs, data at %x, size: %d bytes\n", (unsigned int)data, size);
if(size < sizeof(CPIOHeader)) {
log_err("Invalid initramfs size: %d bytes (minimum required: %d bytes)\n", size,
sizeof(CPIOHeader));
return -1;
}
log_notice("Initramfs size validation passed: %d bytes\n", size);
while(ptr + sizeof(CPIOHeader) <= end) {
log_debug("Processing CPIO header at %x\n", (unsigned int)ptr);
const CPIOHeader* header = reinterpret_cast<const CPIOHeader*>(ptr);
// debug_info(header->magic);
if(!magic_match(header->magic)) {
log_err("Invalid CPIO header magic: %c %c %c\n", header->magic[0], header->magic[1],
header->magic[2]);
log_err(
"magic[3..6]: %c %c %c\n", header->magic[3], header->magic[4], header->magic[5]);
}
ptr += sizeof(CPIOHeader);
if(ptr >= end) {
log_err("Unexpected end of initramfs");
break;
}
// 获取文件名
uint16_t namesize = get_namesize(header);
log_info("namesize: %d\n", namesize);
if(ptr + namesize > end) {
log_err("Unexpected end of initramfs\n");
break;
}
const char* name = reinterpret_cast<const char*>(ptr);
log_debug("Found file: %s\n", name);
ptr += namesize;
ptr = (uint8_t*)(((uintptr_t)ptr + 3) / 4 * 4);
// 检查是否为结束标记
if(is_trailer(name)) {
log_debug("Trailer found, ending initramfs loading\n");
break;
}
// 获取文件数据
uint32_t filesize = get_filesize(header);
log_debug("filesize: %d\n", filesize);
if (strcmp(name, "init") == 0) {
// 创建进程并执行
create_process(ptr, filesize);
break;
}
if(ptr + filesize > end) {
log_debug("Unexpected end of initramfs\n");
break;
}
}
}
🎯 优势与适用场景
✅ 优势
- 极度简化:无需磁盘驱动、文件系统、分区表
- 快速启动:内核一启动就能执行 init,无需等待磁盘 I/O
- 调试友好:所有代码都在一个二进制中,调试方便
- 可移植性强:适用于任何能运行内核的环境(QEMU、物理机、嵌入式)
🎯 适用场景
- 自制操作系统学习:初学者的最佳起点
- 嵌入式系统:资源受限,需要最小化启动时间
- 引导加载程序:在进入完整操作系统前提供简单功能
- 安全/恢复环境:无需依赖外部存储
📦 实际应用:让内核"自带" init 进程
想象一下,你的内核二进制文件 kernel.bin 不仅包含内核代码,还包含了:
/sbin/init(你的初始化程序)/etc/fstab(文件系统挂载表)/bin/sh(Shell)/usr/bin/ls(常用命令)
这一切都编译进同一个文件!
启动流程变成:
1. CPU 上电 → BIOS 启动 → 加载 kernel.bin
2. 内核初始化 → 解析内联文件系统 → 找到 /sbin/init
3. 执行 /sbin/init → 用户空间启动
没有磁盘、没有驱动、没有文件系统——只有纯粹的内核和它的"内置文件"!
💡 高阶技巧:动态生成文件系统
如果你的 init 程序是用 C/C++ 编写的,可以写一个构建脚本,自动将源文件编译成二进制,再嵌入到内核中:
#!/bin/bash
# build.sh
# 编译 init 程序
gcc -o init init.c
# 将 init 程序转为 C 数组
xxd -i init > init_data.c
# 编译内核
make
# 最终的内核二进制就包含了 init 程序!
🚫 注意事项
- 内存占用:所有文件都加载到内存,不适合大文件
- 更新困难:修改文件需重新编译内核
- 安全性:所有代码都在内核中,一旦出错可能导致系统崩溃
✅ 总结:一个简单却强大的技巧
这个小技巧的核心思想是:
"不要去读取磁盘上的文件,而是把文件直接‘变成'内核的一部分。"
它完美解决了自制 OS 初期的"鸡生蛋、蛋生鸡"问题:
- 没有文件系统 → 无法读取 init
- 没有 init → 无法挂载文件系统
通过这个技巧,我们绕过了这个死循环,让内核自给自足。
关注我,带你从零开始,玩转操作系统开发!