实验环境

我使用的系统是 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
2
3
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.74.tar.xz
tar -xJf linux-6.12.74.tar.xz
cd linux-6.12.74

运行 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 -lhfile 命令查看一下 Image 的大小和文件信息:

1
2
ls -lh arch/riscv/boot/Image
file arch/riscv/boot/Image

检查 image

检查 image

至此,我们已经成功编译了 RISC-V Linux 内核,接下来我们需要构建根文件系统来运行这个内核.

3. 编译 BusyBox

我们使用 BusyBox 来构建根文件系统,首先下载 BusyBox 的源代码:

1
2
3
wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
tar -xjf busybox-1.37.0.tar.bz2
cd busybox-1.37.0

先生成默认配置:

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
2
curl -L 'https://hina.lysator.liu.se/gentoo-portage/sys-apps/busybox/files/busybox-1.36.1-kernel-6.8.patch' \
| patch -p1

对于第二个问题,我们需要手动编辑 libbb/hash_md5_sha.c 文件,把 shaNI 的相关代码限制在 x86 架构下:

1
vim libbb/hash_md5_sha.c

然后执行修改:

修改成这样

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

检查 busybox

4. 创建根文件系统

我们先手动创建根文件系统的目录结构:

1
2
3
4
5
6
7
cd ..
export ROOTFS=$PWD/rootfs
mkdir -p $ROOTFS

mkdir -p "${ROOTFS}"/{bin,sbin,etc,proc,sys,dev,tmp,usr/{bin,sbin},var,root}
mkdir -p "${ROOTFS}/etc/init.d"
chmod 1777 "${ROOTFS}/tmp"

接着把 busybox 安装到刚刚创建的根文件系统中:

1
2
cd busybox-1.37.0
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- CONFIG_PREFIX="${ROOTFS}" install

接着创建 /dev 目录下的设备节点,至少要有 console,否则很容易没有输出或无法交互:

1
2
3
4
sudo mknod -m 600 "${ROOTFS}/dev/console" c 5 1
sudo mknod -m 666 "${ROOTFS}/dev/null" c 1 3
sudo mknod -m 666 "${ROOTFS}/dev/tty" c 5 0
sudo mknod -m 666 "${ROOTFS}/dev/zero" c 1 5

5. 编写初始化脚本

我们需要编写一个简单的初始化脚本来启动系统并自动挂载 /proc/sys,创建一个 /etc/init.d/rcS 文件,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
cat > "${ROOTFS}/etc/init.d/rcS" <<'EOF'
#!/bin/sh

mount -t proc proc /proc
mount -t sysfs sys /sys

echo "Welcome, Arch Lunar."

exec /bin/sh
EOF

chmod +x "${ROOTFS}/etc/init.d/rcS"

initramfs, 内核默认会尝试执行根目录下的 /init. 所以我们用 /init 去调用 /etc/init.d/rcS

1
2
3
4
5
6
cat > "${ROOTFS}/init" <<'EOF'
#!/bin/sh
exec /etc/init.d/rcS
EOF

chmod +x "${ROOTFS}/init"

6. 打包成 cpio.gz 格式

我们用 fakerootcpio 命令把根文件系统打包成 cpio.gz 格式,注意可能需要先安装 fakerootcpio 包:

1
fakeroot -- sh -c 'find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz'

fakeroot 打包

检查 cpio

7. 编写 run.sh 脚本一键运行

我们编写一个 run.sh 脚本来一键运行 qemu,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
set -euo pipefail

KERNEL_IMAGE="${KERNEL_IMAGE:-./linux-6.12.74/arch/riscv/boot/Image}"
INITRAMFS="${INITRAMFS:-./initramfs.cpio.gz}"

# Checks
if [[ ! -f "$KERNEL_IMAGE" ]]; then
echo "ERROR: kernel Image not found: $KERNEL_IMAGE" >&2
exit 1
fi
if [[ ! -f "$INITRAMFS" ]]; then
echo "ERROR: initramfs not found: $INITRAMFS" >&2
exit 1
fi

# run qemu
exec qemu-system-riscv64 \
-machine virt \
-m 256M \
-nographic \
-kernel "$KERNEL_IMAGE" \
-initrd "$INITRAMFS" \
-append "console=ttyS0 earlycon=sbi rdinit=/init"
  • -nographic 参数表示不使用图形界面,直接在终端中显示输出;
  • -append 参数用来传递内核启动参数,这里我们指定了 console=ttyS0 来将控制台输出重定向到串口,earlycon=sbi 来启用早期控制台支持,以及 rdinit=/init 来指定内核启动后执行的初始化程序;
  • -kernel-initrd 参数分别指定了内核镜像和初始 RAM 文件系统。
-nographic 的作用

virt machine 下, qemu 默认会提供一个图形接口,同时仿真串口设备.如果不加 -nographic 参数, qemu 会在一个新的窗口中显示图形界面,而串口输出会被重定向到这个窗口中.如果加上 -nographic 参数, qemu 就不会启动图形界面,而是直接在当前终端中显示串口输出,这样我们就可以直接在终端中看到内核的启动日志和交互式 shell 的输出了.

8. 最终效果

1
1
1

 输出
 输出

总结/思考

为什么 x86 不能直接 make 编译 RISC-V 内核?

因为 x86RISC-V 是两种不同的处理器架构,它们的指令集、寄存器结构、内存管理等方面都有很大的差异.直接 make 默认使用宿主机的工具链来生成宿主机平台的二进制文件,所以我们不能直接在 x86 上编译 RISC-V 内核,而需要使用交叉编译工具链来生成适用于 RISC-V 架构的二进制文件.

宿主机与目标机的关系

在交叉编译中,宿主机 (host) 是指我们进行编译的计算机系统,而目标机 (target) 是指我们希望运行编译结果的计算机系统.在这个实验中,宿主机是运行 Arch Linux 的 x86_64 计算机,而目标机是我们通过 qemu 模拟的 RISC-V 虚拟机.交叉编译工具链就是为了在宿主机上生成适用于目标机的二进制文件而设计的.

BusyBox 里输入 ls 回车,最终触发内核哪个机制读取目录?

从用户态开始

  1. shell 首先解析命令,fork() 出一个子进程来执行 BusyBox ls applet
  2. execve()openat() 打开当前目录,获取目录的 fd,然后通过 getdents64 系统调用和 VFS 目录迭代机制把目录项从内核的文件系统实现 (ext4、tmpfs、ramfs 等) 拷贝到用户态。

完整报告 (PDF)