实验环境
我使用的系统是 Arch Linux,内核是 Linux-zen 6.18.9,更多信息见下图:

实验过程
1. 安装交叉编译工具链
我们需要安装 riscv64-linux-gnu-gcc 交叉编译工具链来编译 RISC-V Linux 内核和 BusyBox. 因为笔者使用的是 Arch Linux, 可以直接使用包管理器 pacman 来安装,其他 Linux 发行版可以使用相应的包管理器,如 apt 等等:
1 | sudo pacman -S riscv64-linux-gnu-gcc |
riscv64-linux-gnu-gcc 还是 riscv64-unknown-elf-gcc?
他们的本质区别在于 riscv64-linux-gnu-gcc 是针对 Linux 系统的交叉编译工具链,适用于编译运行在 Linux 上的 RISC-V 程序;而 riscv64-unknown-elf-gcc 是针对裸机 (bare-metal) 环境的交叉编译工具链,适用于编译运行在没有操作系统的 RISC-V 硬件上的程序。
但是,后面我们需要利用 BusyBox 构建根文件系统,而 BusyBox 需要 Linux 环境,所以我们选择安装 riscv64-linux-gnu-gcc.
安装完成后,运行下面的命令检查一下版本信息,确保安装成功:
1 | riscv64-linux-gnu-gcc --version |

2. 下载并编译 Linux 内核
这里,笔者下载的 Linux LTS 内核版本是 6.12.74, 可以从内核官方网站下载并解压:
1 | wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.74.tar.xz |
运行 make 命令用默认的配置对内核进行配置,注意要选择 RISC-V 架构 (ARCH=riscv) 和交叉编译工具链 (CROSS_COMPILE=riscv64-linux-gnu-):
1 | make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig |

接着,需要对内核进行编译,在此之前,需要先安装一些系统命令,如 bc (可以用 sudo pacman -S bc 安装),否则会在编译过程中报错.用下面的命令进行编译 Image:
1 | make -j8 ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- Image |
编译完成后,运行 ls -lh 和 file 命令查看一下 Image 的大小和文件信息:
1 | ls -lh arch/riscv/boot/Image |


至此,我们已经成功编译了 RISC-V Linux 内核,接下来我们需要构建根文件系统来运行这个内核.
3. 编译 BusyBox
我们使用 BusyBox 来构建根文件系统,首先下载 BusyBox 的源代码:
1 | wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2 |
先生成默认配置:
1 | make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig |
然后进入交互配置,将 Settings -> Build Options -> Build static binary (no shared libs) 选项勾选上:
1 | make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig |
ArchLinux 下进入 BusyBox 交互界面报错:ncurses not found
笔者第一次配置时,提示需要安装 ncurses-devel 包,但用 pacman 安装了 ncurses 包后,问题依旧没有解决.后来在 StackOverflow 上找到了答案.
解决这个问题,需要修改 scripts/kconfig/lxdialog/check-lxdialog.sh 文件,找到 check() 函数,在 main() 函数前面加上 int,改为 int main(),保存退出即可.如下图所示

进行编译:
1 | make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j8 |
ArchLinux 编译 BusyBox 报错
笔者一共遇到两个错误:一个是 tc.c 文件中 CBQ 相关的宏;另一个是 sha1 相关的编译报错.
CBQ 在 Linux 6.8 版本之后被移除了。好在其他的发行版已经有 patch 可以绕过 CBQ 了,执行以下命令修改 networking/tc.c 文件:
1 | curl -L 'https://hina.lysator.liu.se/gentoo-portage/sys-apps/busybox/files/busybox-1.36.1-kernel-6.8.patch' \ |
对于第二个问题,我们需要手动编辑 libbb/hash_md5_sha.c 文件,把 shaNI 的相关代码限制在 x86 架构下:
1 | vim libbb/hash_md5_sha.c |
然后执行修改:

编译完成后,运行 file 命令查看一下生成的 BusyBox 可执行文件:

4. 创建根文件系统
我们先手动创建根文件系统的目录结构:
1 | cd .. |
接着把 busybox 安装到刚刚创建的根文件系统中:
1 | cd busybox-1.37.0 |
接着创建 /dev 目录下的设备节点,至少要有 console,否则很容易没有输出或无法交互:
1 | sudo mknod -m 600 "${ROOTFS}/dev/console" c 5 1 |
5. 编写初始化脚本
我们需要编写一个简单的初始化脚本来启动系统并自动挂载 /proc 和 /sys,创建一个 /etc/init.d/rcS 文件,命令如下:
1 | cat > "${ROOTFS}/etc/init.d/rcS" <<'EOF' |
对 initramfs, 内核默认会尝试执行根目录下的 /init. 所以我们用 /init 去调用 /etc/init.d/rcS
1 | cat > "${ROOTFS}/init" <<'EOF' |
6. 打包成 cpio.gz 格式
我们用 fakeroot 和 cpio 命令把根文件系统打包成 cpio.gz 格式,注意可能需要先安装 fakeroot 和 cpio 包:
1 | fakeroot -- sh -c 'find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz' |


7. 编写 run.sh 脚本一键运行
我们编写一个 run.sh 脚本来一键运行 qemu,命令如下:
1 |
|
-nographic参数表示不使用图形界面,直接在终端中显示输出;-append参数用来传递内核启动参数,这里我们指定了console=ttyS0来将控制台输出重定向到串口,earlycon=sbi来启用早期控制台支持,以及rdinit=/init来指定内核启动后执行的初始化程序;-kernel和-initrd参数分别指定了内核镜像和初始 RAM 文件系统。
-nographic 的作用在 virt machine 下, qemu 默认会提供一个图形接口,同时仿真串口设备.如果不加 -nographic 参数, qemu 会在一个新的窗口中显示图形界面,而串口输出会被重定向到这个窗口中.如果加上 -nographic 参数, qemu 就不会启动图形界面,而是直接在当前终端中显示串口输出,这样我们就可以直接在终端中看到内核的启动日志和交互式 shell 的输出了.
8. 最终效果


总结/思考
为什么 x86 不能直接 make 编译 RISC-V 内核?
因为 x86 和 RISC-V 是两种不同的处理器架构,它们的指令集、寄存器结构、内存管理等方面都有很大的差异.直接 make 默认使用宿主机的工具链来生成宿主机平台的二进制文件,所以我们不能直接在 x86 上编译 RISC-V 内核,而需要使用交叉编译工具链来生成适用于 RISC-V 架构的二进制文件.
宿主机与目标机的关系
在交叉编译中,宿主机 (host) 是指我们进行编译的计算机系统,而目标机 (target) 是指我们希望运行编译结果的计算机系统.在这个实验中,宿主机是运行 Arch Linux 的 x86_64 计算机,而目标机是我们通过 qemu 模拟的 RISC-V 虚拟机.交叉编译工具链就是为了在宿主机上生成适用于目标机的二进制文件而设计的.
BusyBox 里输入 ls 回车,最终触发内核哪个机制读取目录?
从用户态开始
- shell 首先解析命令,
fork()出一个子进程来执行 BusyBoxlsapplet execve()用openat()打开当前目录,获取目录的fd,然后通过getdents64系统调用和 VFS 目录迭代机制把目录项从内核的文件系统实现 (ext4、tmpfs、ramfs 等) 拷贝到用户态。


