从零开始的Linux发行版生活

总字数:6139字,预计阅读时间 10分 13秒。

总有些时候我们需要自己组装Linux操作系统,比如交叉编译、嵌入式开发和可信执行环境开发等等场景。本文便介绍如何使用Arch Linux作为基础在riscv架构上组装操作系统并使用QEMU运行。

初始化根文件系统

rootfs是Linux系统中除了内核之外的其他文件的总和,例如/usr/etc中重要的系统文件均属于rootfs的范围。在进行Linux系统的开发时,同一架构的rootfs之间基本上可以互换,例如可以把Arch Linux的rootfs替换到ubuntu系统中,而内核由于硬件的敏感性,通常需要使用特定厂商提供的内核(在更改合入upstream之前)。

实际上,除了各个发行版对于内核的修改,各个发行版之间主要的不同就是rootfs的不同。

首先创建一个rootfs文件夹并修改权限为root

mkdir rootfs
sudo chown root:root ./rootfs

然后使用pacstrap这个pacman的初始化工具在rootfs安装base软件包,最好也顺便装一个vim

sudo pacstrap \
	-C /usr/share/devtools/pacman.conf.d/extra-riscv64.conf
	-M ./rootfs \
	base vim

extra-riscv64.conf是在archlinuxcn/devtools-riscv64软件包中提供的便利工具,其中包括了archriscv该移植的pacman.conf文件,当然一般推荐修改一下该文件的镜像站点,以提高安装的速度。

然后清理一下pacman的缓存文件,缩小rootfs的大小,尤其是考虑到后面因为各种操作失误可能会反复解压rootfs文件。

sudo pacman  \
	--sysroot ./rootfs \
	--sync --clean --clean

然后设置一下该rootfsroot账号密码:

sudo usermod --root $(realpath ./rootfs) --password $(openssl passwd -6 "$password") root

就可以将rootfs打包为压缩包文件备用了。

sudo bsdtar --create \
    --auto-compress --options "compression-level=9"\
    --xattrs --acls\
    -f archriscv-rootfs.tar.zst -C rootfs/ .

初始化虚拟机镜像

首先,创建一个qcow2格式的QEMU虚拟机磁盘镜像:

qemu-img create -f qcow2 archriscv.img 10G

其中磁盘的大小可以自行定义。

为了能够像正常的磁盘一样进行读写,需要将该文件映射到一个块设备,而这通过qemu-nbd程序实现。首先需要加载该程序需要使用的内核驱动程序:

sudo modprobe nbd max_part=8

命令中的max_part指定了最多能够挂载的块设备(文件)个数。然后将该文件虚拟化为一个块设备:

sudo qemu-nbd -c /dev/nbd0 archriscv.img

挂载完毕之后就可以进行初始化虚拟机磁盘镜像的工作了。初始化虚拟机镜像主要涉及到如下几步:

  • 格式化磁盘
  • 安装内核
  • 设置引导程序

其中格式化磁盘和后续需要使用的启动引导方式有关系,当使用U-boot这一常用的嵌入式引导系统进行引导时,只需要将磁盘格式化为单个分区即可,只需要在该分区中设置extlinux/extlinux.conf文件,至于磁盘的分区表格式是GPT还是MBR无关紧要。而如果是使用UEFI引导,则需要使用GPT分区表,并创建一个ESP(EFI System Partition)分区。这里就以使用UEFI引导的格式化磁盘作为示例,硬盘分区如下表所示:

分区 格式 挂载点 大小
/dev/nbd0p1 FAT32 /boot 512M
/dev/nbd0p2 EXT4 / 余下的空间

在使用fdisk完成磁盘的分区之后,进行格式化并挂载到当前的mnt目录中:

sudo mkfs.fat -F 32 /dev/nbd0p1
sudo mkfs.ext4 /dev/nbd0p2
sudo mkdir mnt
sudo mount /dev/nbd0p2 mnt
sudo mkdir mnt/boot
sudo mount /dev/nbd0p1 mnt/boot

挂载完成之后解压上一步中备好的rootfs

cd mnt
sudo bsdtar -kpxf ../archriscv.tar.zst

然后使用systemd-nspawn工具进入rootfs中调用pacman安装内核:

sudo systemd-nspawn -D mnt pacman \ 
	--nonconfirm --needed \
	-Syu linux linux-firmware

接下来分别介绍使用U-boot启动和使用UEFI启动的操作方法。

使用U-boot启动

为了使用U-boot启动,需要手动编译U-boot并打包到OpenSBI中作为QEMU启动的固件。

首先编译U-boot:

git clone --filter=blob:none -b v2025.04 https://github.com/u-boot/u-boot.git
cd u-boot
make \
	CROSS_COMPILE=riscv64-linux-gnu- \
	qemu-riscv64_smode_defconfig
./scripts/config
make \
	CROSS_COMPILE=riscv64-linux-gnu- \
	olddefconfig
make CROSS_COMPILE=riscv64-linux-gnu- -j18

编译好之后检查当前目录下是否存在u-boot.bin的固件。

然后去编译OpenSBI并将u-boot.bin打包进来:

git clone --filter=blob:none -b v1.6 https://github.com/riscv-software-src/opensbi.git
cd opensbi
make \
	CROSS_COMPILE=riscv64-linux-gnu- \
	PLATFORM=generic \
	FW_PAYLOAD_PATH=../u-boot/u-boot.bin -j18

编译好的三个启动固件应当在./build/platform/generic/firmware目录中:

  • fw_dynamic.bin使用启动程序设置的地址进行跳转。
  • fw_jump.bin跳转到一个固定的地址执行。
  • fw_payload.bin执行编译打包的u-boot文件,这也是U-boot启动所需要的。

编译完成之后,在mnt文件中创建/boot/extlinux/extlinux.conf文件以告知U-boot启动Linux内核的参数:

menu title Arch RISC-V Boot Menu
timeout 100
default linux-fallback

label linux
    menu label Linux linux
    kernel /vmlinuz-linux
    initrd /initramfs-linux.img
    append earlyprintk rw root=UUID=903944ec-a4d3-4820-ac89-c0eac37721f9 rootwait console=ttyS0,115200 

label linux-fallback
    menu label Linux linux (fallback initramfs)
    kernel /vmlinuz-linux
    initrd /initramfs-linux-fallback.img
    append earlyprintk rw root=UUID=903944ec-a4d3-4820-ac89-c0eac37721f9 rootwait console=ttyS0,115200

文件中的UUID可以使用如下的指令获得:

findmnt mnt -o UUID -n

其中需要说明的是,文件中指定kernel和intird的时候使用的是/而不是/boot,这是因为虽然现在把该分区挂载到了/boot目录下,但是在U-boot进行启动时会将该分区挂载在/目录下,因此需要使用/。也是因为同样的原因,当只格式化为一个分区并只使用U-boot进行引导启动时,则需要将目录改为/boot

此时即可取消挂载镜像了:

sudo umount mnt/boot
sudo umount mnt
sudo qemu-nbd -d /dev/nbd0

使用如下的指令即可启动虚拟机:

#!/bin/bash

qemu-system-riscv64 \
    -nographic \
    -machine virt \
    -smp 8 \
    -m 4G \
    -bios opensbi/build/platform/generic/firmware/fw_payload.bin \
    -device virtio-blk-device,drive=hd0 \
    -drive file=archriscv-1.img,format=qcow2,id=hd0,if=none \
    -object rng-random,filename=/dev/urandom,id=rng0 \
    -device virtio-rng-device,rng=rng0 \
    -monitor unix:/tmp/qemu-monitor,server,nowait

使用UEFI启动

使用UEFI启动,就需要编译对应的UEFI固件,即开源固件EDK2。

git clone -b edk2-stable202505 --recursive-submodule https://github.com/tianocore/edk2.git
export WORKSPACE=`pwd`
export GCC5_RISCV64_PREFIX=riscv64-linux-gnu-
export PACKAGES_PATH=$WORKSPACE/edk2
export EDK_TOOLS_PATH=$WORKSPACE/edk2/BaseTools
source edk2/edksetup.sh --reconfig
make -C edk2/BaseTools -j18
source edk2/edksetup.sh BaseTools
build -a RISCV64 --buildtarget RELEASE -p OvmfPkg/RiscVVirt/RiscVVirtQemu.dsc -t GCC5

编译之后得到的两份固件应该在Build/RiscVVirtQemu/RELEASE_GCC5/FV目录下:

  • RISCV_VIRT_CODE.fd固件的代码部分。
  • RISCV_VIRT_VARS.fd固件的数据部分,可以被UEFI工具修改。

在启动之前首先将这两个文件填充到32M的大小以符合QEMU对于pflash文件的大小要求:

truncate -s 32M Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT_CODE.fd
truncate -s 32M Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT_VARS.fd

然后就可以使用如下的指令启动QEMU虚拟机了,这里复用U-boot中编译的OpenSBI固件,如果没有执行这一步可以选择删除下面指令中的-bios选项,使用QEMU自带的OpenSBI实现。

#!/bin/bash

qemu-system-riscv64  \
    -M virt,pflash0=pflash0,pflash1=pflash1,acpi=off \
    -m 4096 -smp 8  -nographic \
    -bios opensbi/build/platform/generic/firmware/fw_dynamic.bin \
    -blockdev node-name=pflash0,driver=file,read-only=on,filename=Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT_CODE.fd  \
    -blockdev node-name=pflash1,driver=file,filename=Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT_VARS.fd \
    -device virtio-blk-device,drive=hd0  \
    -drive file=archriscv-1.img,format=qcow2,id=hd0,if=none \
    -netdev user,id=n0 -device virtio-net,netdev=n0 \
    -monitor unix:/tmp/qemu-monitor,server,nowait

但是,这一步启动并不会进入Linux内核,这是因为还没有向UEFI注册需要启动的系统,使得UEFI可以识别到可以执行启动的磁盘。在普通的系统安装上,由于是使用安装镜像直接从UEFI启动的,在chroot环境中可以直接使用grub-install直接安装,但是在目前的systemd-nspawn环境中是缺少efivarfs等必要的文件系统的。

因此可以首先尝试在启动之后进入UEFI Shell之后,手动设置参数直接启动Linux内核。

image-20250527134233659

进入UEFI Shell之后,首先选择文件系统FS0:,然后使用如下的指令尝试手动启动Linux内核:

\vmlinuz-linux initrd=\initramfs-linux.img  earlyprintk rw root=UUID=903944ec-a4d3-4820-ac89-c0eac37721f9 rootwait console=ttyS0,115200

但是可能会遇到如下的问题:

image-20250527134421403

这里也尝试了使用mkinitcpio生成的Unified Kernel Image,放在EFI/Linux文件目录下,同样遇到了如下的问题:

image-20250527134540583

暂时不清楚这是EDK2的问题还是这里操作的问题,至少能确定这里编译内核时是启用了CONFIG_EFI_STUB选项的。

因此这里使用grub方式尝试绕过这个问题,首先在systemd-nswpan环境中使用如下的指令安装grub,虽然会因为环境问题报错,但是手动查看可以发现安装脚本已经将grubriscv64.efi文件复制到/boot/EFI/GRUB目录了。

此时再次进入UEFI Shell,手动指定启动grub,所幸这次启动成功,此时我们再从grub shell中尝试启动Linux,使用的指令如下:

linux (hd0,gpt1)/vmlinuz-linux earlyprintk rw root=UUID=903944ec-a4d3-4820-ac89-c0eac37721f9 rootwait console=ttyS0,115200
initrd (hd0,gpt1)/initramfs-linux.img
boot

image-20250527135748547

此时就可以正常的进入完成完整的安装过程了。

首次启动的时候推荐使用fallback initramfs,因为在chroot环境中生成的驱动可能不全。如果在使用主要的initramfs进行启动时遇到了无法挂载真/目录而进入emergency shell,同时在该Shell中也无法发现虚拟机的磁盘,就极有可能是系统缺少对应的驱动无法挂载。

例如在chroot环境中生成的initcpio包含如下的模块:

image-20250325160729310

而在进入系统之后,重新运行mkinitcpio之后包含的模块如下所示:

image-20250325161310820

文章作者:Ricardo Ren
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自 Ricardo's Blog ”。

2021 - 2025 © Ricardo Ren ,由 .NET 9.0.2 驱动。

Build Commit # eedfc1ffce

蜀ICP备2022004429号-1