目标

过程

我决定现在mac上进行这个实验,因为我目前不会进行图形化的自定义,只对内核和基本的用户空间自定义。所以理论上可以使用orbstack这一个轻量级的虚拟机软件
查询orbstack是否支持自定义的iso的时候,发现令人失望
ref: https://github.com/orbstack/orbstack/issues/11
因此还是只能使用一些比较重量级的方案
直接使用UTM吧,反正只是一个验证的方式
但是这有点风险,因为lfs的方式是直接在一个实机上的硬盘安装系统
我测,deepseek直接给我了一个更好的选择,qemu,这样直接不需要utm这种重量级的东西了

接下来的技术方案确定了,在mac上使用orbstack的container编译内核和用户空间,然后使用qemu运行,竟然有成功的先例
这下省心了

orbstack的使用->干净的构建环境

使用docker的时候没有找到合适的镜像,然后看了一些orbstack的vm,发现他可以模拟amd64,那么后续可以直接在这里直接进行amd64的编译,目前为了更快的编译,使用arm64的vm。
vm不需要特殊的配置,直接

orbctl create -a arm64 -u spriple arch

即可创建一个arch为arm64,默认用户为spriple的archlinux虚拟机
创建之后使用list来查看vm状态

> orbctl list                           
NAME  STATE    DISTRO     VERSION  ARCH   SIZE    IP
----  -----    ------     -------  ----   ----    --
arch  running  archlinux  current  arm64  1.1 GB  192.168.139.80

然后进入vm之中
kitty +kitten ssh arch@orb进入名为arch的虚拟机中
然后clone linux内核,选择一个比较稳定一点的版本

git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

这是用时最久的一个部分,我网络不好,因此只想clone某一个版本的linux内核

git clone --depth 1 --branch v6.18 --single-branch https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

这下快多了,询问ai之后,竟然还可以使用国内的镜像源

尝试过清华源,出了问题,最终还是使用ustc源

git clone https://mirrors.ustc.edu.cn/linux.git

快的起飞

部署必要的构建工具

sudo pacman -Syu fish

先弄一个fish,来帮我们更快的补全命令,之后构建的时候大概率需要换回bash
然后安装构建linux内核所需要的工具
原本的apt系列的命令为

sudo apt install -y build-essential flex bison libssl-dev libelf-dev libncurses-dev qemu-system-aarch64 gdb dwarves busybox-static e2fsprogs bc

pacman系列为

sudo pacman -Syu base-devel openssl ncurses libelf bc python gdb

其中base-devel包括了flex、bison
openssl包括了libssl-dev
libelf
ncurses

构建内核

spriple@arch ~/b/linux ((v6.18))> make ARCH=arm64 defconfig
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  HOSTCC  scripts/kconfig/confdata.o
  HOSTCC  scripts/kconfig/expr.o
  LEX     scripts/kconfig/lexer.lex.c
  YACC    scripts/kconfig/parser.tab.[ch]
  HOSTCC  scripts/kconfig/lexer.lex.o
  HOSTCC  scripts/kconfig/menu.o
  HOSTCC  scripts/kconfig/parser.tab.o
  HOSTCC  scripts/kconfig/preprocess.o
  HOSTCC  scripts/kconfig/symbol.o
  HOSTCC  scripts/kconfig/util.o
  HOSTLD  scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#

然后

make ARCH=arm64 menuconfig

关于menuconfig的众多选项,我们可以不需要管太多,一般只需要关注比较重要的部分即可

然后直接开始构建

make -j 8

然后使用一下命令检查是否构建出来了真东西

spriple@arch ~/b/linux ((v6.18))> file vmlinux
vmlinux: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), statically linked, BuildID[sha1]=8f13b2d3a7b73350ad0a739a9ebd4755b9d30199, with debug_info, not stripped

spriple@arch ~/b/linux ((v6.18))> file ./arch/arm64/boot/Image
./arch/arm64/boot/Image: Linux kernel ARM64 boot executable Image, little-endian, 4K pages

构建用户空间

然后开始构建用户空间,使用常用的busybox

sudo pacman -Syu busybox

然后确定下载的busybox是静态连接的

spriple@arch ~/b/rootfs> file /usr/bin/busybox
/usr/bin/busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped

之后就可以复制这个命令

mkdir -p bin sbin etc proc sys dev lib usr/bin usr/sbin

cp /usr/bin/busybox ./bin/busybox

./bin/busybox --install -s ./bin

因为我们取巧在文件夹中安装的busybox,因此bin下面的全部命令都会被硬连接到绝对路径中(其实官方更推荐源码直接make)

spriple@arch ~/b/rootfs> file bin/init 
bin/init: symbolic link to /home/spriple/build/rootfs/bin/busybox

但是我们后面会将这个根文件系统放在一个img镜像中,因此必须使用相对路径
所以我们使用一个脚本修复一下

#!/bin/bash
cd ~/build/rootfs
prefix="/home/spriple/build/rootfs"
find . -type l | while read link; do
    target=$(readlink "$link")
    # 如果目标以prefix开头
    if [[ "$target" == "$prefix"* ]]; then
        # 去掉前缀得到目标在rootfs中的绝对路径(相对于rootfs根)
        newtarget="${target#$prefix}"
        # 确保newtarget以/开头
        # 计算从链接所在目录到newtarget的相对路径
        linkdir=$(dirname "$link")
        # 使用realpath工具计算相对路径,或者用python
        relpath=$(realpath --relative-to="$linkdir" ".$newtarget" 2>/dev/null)
        if [ $? -eq 0 ]; then
            echo "Fixing $link: $target -> $relpath"
            ln -snf "$relpath" "$link"
        else
            # 如果没有realpath,尝试用awk计算
            # 简单方法:如果newtarget就是/bin/busybox,且链接在bin目录下,relpath就是"busybox"
            # 更通用的:用perl
            relpath=$(perl -MFile::Spec -e 'print File::Spec->abs2rel(".'"$newtarget"'", "'"$linkdir"'")')
            ln -snf "$relpath" "$link"
        fi
    fi
done

然后chmod +x script.sh && bash script.sh
然后用命令检查一下是否生效

spriple@arch ~/b/rootfs> file bin/init
bin/init: symbolic link to busybox

然后需要定义一下启动的脚本

# 创建 init.d 目录
mkdir -p etc/init.d
# 创建 rcS 文件
cat <<EOF > etc/init.d/rcS
#!/bin/sh

# 挂载必要的伪文件系统
echo "Mounting /proc, /sys, /dev..."
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev

# 打印一些启动信息
echo "============================="
echo " Minimal BusyBox RootFS Booted"
echo " Running rcS..."
echo " Remounting rootfs rw..."
echo "============================="

# 重新挂载根文件系统为可读写模式
# (虽然 QEMU append 参数里也加了 rw,但这里再确认一下)
mount -o remount,rw /

# 启动一个交互式 Shell
echo "Starting shell..."
exec /bin/sh
EOF
# 给 rcS 脚本添加执行权限 
chmod +x etc/init.d/rcS

然后创建一个img文件,将根文件系统塞进去

dd if=/dev/zero of=rootfs.img bs=1M count=512

mkfs.ext4 rootfs.img

#创建挂载点
sudo mkdir -p /mnt/rootfs_mount
#挂载rootfs.img,方便操作
sudo mount -o loop rootfs.img /mnt/rootfs_mount
## 将我们定义的根文件系统的内容cp过去
sudo cp -a rootfs/* /mnt/rootfs_mount/

sync

sudo umount /mnt/rootfs_mount

sudo rmdir /mnt/rootfs_mount

然后rootfs.img里面就有了我们自定义的根文件系统了

然后使用qemu启动rootfs

qemu-system-aarch64 \
	-machine virt \
	-cpu cortex-a57 \
	-m 1G \
	-nographic \
	-kernel build/linux/arch/arm64/boot/Image \
	-drive file=rootfs.img,format=raw,if=virtio,id=hd0 \
	-append "root=/dev/vda console=ttyAMA0 rw init=/bin/init"

然后即可看见启动日志和init脚本执行情况

[    0.435950] Run /bin/init as init process
starting pid 55, tty '': '/etc/init.d/rcS'
Mounting /proc, /sys, /dev...
mount: mounting none on /dev failed: Resource busy
=============================
 Minimal BusyBox RootFS Booted
 Running rcS...
 Remounting rootfs rw...
=============================
[    0.501618] EXT4-fs (vda): re-mounted 130b414a-7366-4d19-a319-6f0b6c03aca2.
Starting shell...
/bin/sh: can't access tty; job control turned off
~ # ls
bin         etc         lost+found  sbin        usr
dev         lib         proc        sys
~ # ps
PID   USER     TIME  COMMAND
    1 0         0:00 init
    2 0         0:00 [kthreadd]

打包为iso文件

使用grub-mkrescue

sudo pacman -Syu grub xorriso mtools

首先按照ai的说法直接使用grub

grub-mkrescue --output=~/custom-linux.iso .

出现错误

xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project. libburn : SORRY : Neither stdio-path nor its directory exist xorriso : FAILURE : Cannot acquire drive 'stdio:~/custom-linux.iso' xorriso : aborting : -abort_on 'FAILURE' encountered 'FAILURE' grub-mkrescue: error: `xorriso` invocation failed

询问ai得知,xorriso无法识别~,因此使用绝对路径

grub-mkrescue --output=/home/spriple/custom-linux.iso .

随后成功打包出iso文件
但是使用下面qemu命令测试的时候qemu没有反应

qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -cdrom ~/custom-linux.iso -nographic

第一个问题就是对于arm64的内核,qemu没有对应的uefi固件,因此我们需要下载对应的uefi固件
sudo pacman -Syu edk2-armvirt
随后uefi固件就是被下载到/usr/share/edk2/aarch64/QEMU_EFI.fd
然后我们用下面的命令启动

qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -bios /usr/share/edk2/aarch64/QEMU_EFI.fd  -cdrom ~/custom-linux.iso -nographic

此时虽然qemu有反应了,但是内核panic

[ 0.558533] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on "/dev/vda" or unknown-block(254,0) ]---

询问ai得知为没有virtio磁盘设备导致的
这种问题呢,我稍微仔细问了一下

rootfs和initramfs区别

rootfs的创建方式决定了它是一个文件,需要一个块设备,也就是盘作为载体,才能挂载,所以我们之前使用qemu使用它的时候,会自动创建一个块设备挂载它,因此使用起来ok
但是我们把他打包进入iso文件之后,它仅仅是一个iso文件系统中的一个文件,qemu不会给它做特殊处理,此时如果想要用的话,需要弄一个块设备(需要补充实验)
但是initramfs借助的是内存
gemini:内核根本不知道如何在没有 initramfs 的协助下,直接从 ISO9660 文件系统里找到 /bin/init 并正确执行它作为根分区。

因此我决定使用initramfs,更加简洁方便
然后使用下面命令重新打包根文件系统

sudo pacman -Syu cpio
cd ~/build/rootfs
find . -print0 | cpio --null -o --format=newc | gzip -9 > ./initramfs.cpio.gz

然后将initramfs拷贝到iso_build的boot目录下面,并且重新编辑grub配置

cp ~/build/rootfs/initramfs.cpio.gz ~/build/iso_build/boot/
set default=0
set timeout=5
menuentry "My Custom Linux System" {
	linux /boot/Image console=ttyAMA0 rdinit=/bin/init
	initrd /boot/initramfs.cpio.gz
}

然后还是使用grub-mksecure

grub-mkrescue -o ../custom-linux.iso .

然后直接使用qemu启动

qemu-system-aarch64 \
	 -M virt \
	 -cpu cortex-a57 \
	 -m 1G \
	 -nographic \
	 -bios /usr/share/edk2/aarch64/QEMU_EFI.fd \
	 -cdrom custom-linux.iso

启动成功

[    0.474067] Freeing unused kernel memory: 3264K
[    0.474590] Run /bin/init as init process
starting pid 50, tty '': '/etc/init.d/rcS'
Mounting /proc, /sys, /dev...
=============================
 Minimal BusyBox RootFS Booted
 Running rcS...
 Remounting rootfs rw...
=============================
Starting shell...
/bin/sh: can't access tty; job control turned off
~ # ls
bin   dev   etc   lib   proc  root  sbin  sys   usr
~ # uname -r
6.18.0
~ # dmesg | grep -i "initramfs"
[    0.228230] Unpacking initramfs...
~ #