🚀 一个让内核"自带文件系统"的神技:把 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 本身不直接遍历目录,而是 从标准输入读取文件路径列表(通常由 findecho 生成),然后:

  • -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 → 无法挂载文件系统

通过这个技巧,我们绕过了这个死循环,让内核自给自足


关注我,带你从零开始,玩转操作系统开发!