1. 从零开始:为什么要在QEMU上启动ARM64内核?

如果你是一名嵌入式开发者、内核爱好者,或者正在学习ARM64架构,那么“在QEMU上启动一个ARM64 Linux内核”几乎是你的必经之路。这听起来像是一个简单的“Hello World”级别的任务,但实际操作起来,你会发现它远不止敲几行命令那么简单。它是一把钥匙,能帮你打开一扇通往系统底层、硬件模拟和内核调试的大门,而无需购买任何真实的ARM64开发板。

我最初接触这个任务,是为了调试一个在真实硬件上难以复现的内核启动问题。物理板卡调试周期长、成本高,而QEMU提供的纯软件模拟环境,让我可以随意暂停、单步执行、修改内存,甚至“倒带”重放执行流,这种灵活性是硬件调试无法比拟的。对于学习而言,它意味着你可以亲手“组装”一个完整的虚拟计算机:从CPU、内存、到磁盘和网络设备,然后看着自己编译的内核从零开始,一步步点亮屏幕,最终进入命令行。这个过程能让你深刻理解“操作系统启动”这个黑盒子里究竟发生了什么。

简单来说,在QEMU上启动ARM64内核,核心价值在于 提供一个完全可控、可重复、低成本的学习与开发沙箱 。无论你是想研究内核初始化流程、驱动开发,还是为交叉编译的应用构建测试环境,这都是最理想的起点。接下来,我将带你走完从环境准备、内核编译、根文件系统制作,到最终成功启动并进入Shell的完整流程,并分享其中每一步我踩过的坑和总结的技巧。

2. 环境搭建与工具链选型:奠定坚实的基础

工欲善其事,必先利其器。在开始编译和运行之前,我们需要一个稳定且功能齐全的宿主环境,并选择正确的工具链。这里的每一个选择都直接影响后续步骤的顺利程度。

2.1 宿主系统与QEMU安装

我强烈推荐使用 Ubuntu 22.04 LTS 或 Debian 12 作为宿主系统。它们的软件源丰富,能方便地安装我们所需的所有工具,社区支持也最完善。如果你使用Windows或macOS,建议通过WSL2(Windows)或虚拟机(macOS)安装一个Ubuntu环境,以获得原生Linux的开发体验。

安装QEMU是整个流程中最简单的一步。在Ubuntu/Debian上,只需一条命令:

sudo apt update
sudo apt install qemu-system-arm qemu-utils

这条命令会安装用于模拟ARM架构的 qemu-system-arm (它同时支持32位和64位ARM),以及用于处理磁盘镜像的 qemu-img 工具。安装完成后,可以通过 qemu-system-aarch64 --version 来验证。

注意:有些教程会建议从源码编译QEMU以获得最新特性。但对于初学者和大多数开发场景,使用发行版提供的稳定版本完全足够,能避免许多依赖和编译问题。

2.2 交叉编译工具链的选择与安装

由于我们是在x86_64的电脑上编译ARM64架构的内核和程序,必须使用交叉编译工具链。这里有几个主流选择:

  1. Linaro GCC :曾经是ARM架构的事实标准,非常稳定。但近年来更新放缓。
  2. ARM官方 GNU Toolchain :由ARM公司直接维护,更新及时,对ARM新特性支持最好。
  3. 发行版提供的交叉编译器 :如 gcc-aarch64-linux-gnu 。这是最方便的选择。

对于新手和追求稳定性的用户,我推荐直接使用发行版提供的工具链,因为它与系统集成度最高,几乎不会出现库依赖问题。安装命令如下:

sudo apt install gcc-aarch64-linux-gnu

安装后,可以通过 aarch64-linux-gnu-gcc --version 检查是否安装成功。这个工具链包含了编译器、链接器、二进制工具(如objdump)以及针对ARM64的C库。

如果你需要更前沿的编译器特性(例如为ARMv8.5-A架构优化),可以考虑从ARM官网下载其官方工具链并手动配置路径。但请记住,这可能会引入动态库链接的复杂性。在本文的后续部分,我们将统一使用 aarch64-linux-gnu- 这个前缀。

2.3 内核源码获取

我们需要Linux内核的源代码。获取方式有两种:

  • 从内核官网下载稳定版本 :访问 kernel.org ,下载一个稳定版本(如 linux-6.6.x.tar.xz )。这种方式获取的代码最“纯净”。
  • 通过Git克隆 :如果你想跟踪最新开发动态或需要某个特定分支,可以使用Git:
    git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
    # 或者使用更快的国内镜像
    git clone https://mirrors.edge.kernel.org/pub/linux/kernel/git/torvalds/linux.git
    
    克隆后,可以通过 git tag 查看版本,并用 git checkout v6.6 切换到特定版本。

我建议初学者先从稳定版本(如6.6.x)开始,避免主开发分支可能存在的波动。将下载的压缩包解压到一个工作目录,我们的所有操作都将在这个目录内进行。

3. 配置与编译ARM64 Linux内核

拿到源码后,我们并不能直接编译。需要告诉编译系统,我们的目标是什么架构,需要哪些功能,不需要哪些功能。这个过程就是内核配置。

3.1 内核配置:defconfig与menuconfig

对于ARM64,内核已经为我们提供了一个基础的默认配置(defconfig)。这是最快的方式:

# 进入内核源码根目录
cd linux-6.6
# 导出交叉编译环境变量,这是关键一步!
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
# 加载ARM64的默认配置
make defconfig

执行 make defconfig 后,会在当前目录生成一个 .config 文件,它包含了启动一个最小系统所需的核心配置。

但是, defconfig 非常精简,可能缺少我们后续需要的功能,比如网络驱动、某些文件系统支持、或者内核模块。因此,我们通常需要在此基础上进行定制。使用 menuconfig 工具:

make menuconfig

这会打开一个基于ncurses的图形化配置界面。在这里,你可以像逛菜单一样浏览和修改成千上万个配置选项。

对于QEMU启动,有几个关键配置我建议你检查或开启(使用方向键导航,空格键选中 [*] 为编译进内核, [M] 为编译成模块, [ ] 为不编译):

  • Device Drivers -> Network device support -> Virtio network driver :确保启用(编译进内核或模块)。这是QEMU虚拟网卡的高性能驱动。
  • File systems -> The Extended 4 (ext4) filesystem :启用支持,因为我们的根文件系统很可能用ext4。
  • Kernel hacking -> Kernel low-level debugging functions :如果你需要早期串口调试信息,可以在这里配置串口地址。对于QEMU,默认的 PL011 串口在 0x9000000 。
  • General setup -> Initial RAM filesystem and RAM disk (initramfs) support :确保启用。我们下一步制作的initramfs需要这个支持。

配置完成后,保存并退出, .config 文件会被更新。

3.2 内核编译与产物解读

配置完成后,就可以开始编译了。使用 -j 参数可以指定并行编译的作业数,通常设置为CPU核心数,以加快编译速度:

make -j$(nproc)

编译过程可能需要几分钟到十几分钟,取决于你的电脑性能。编译成功后,在 arch/arm64/boot/ 目录下会生成几个重要文件:

  • Image :压缩后的内核镜像文件。这是QEMU可以直接引导的、最主要的文件。
  • Image.gz :如果配置了更高压缩率,可能会生成这个。
  • vmlinux :未压缩的、带完整调试信息的ELF可执行文件。主要用于调试(例如在GDB中加载符号),不能直接用于引导。

我们启动所需的核心文件就是 arch/arm64/boot/Image 。你可以把它复制到一个方便的地方,比如工作目录的根文件夹。

踩坑记录:编译时最常见的错误是缺少依赖。如果遇到关于 openssl 、 libelf 或 flex / bison 的错误,请使用 sudo apt install libssl-dev libelf-dev flex bison 来安装。另一个常见问题是交叉编译环境变量没有正确导出,导致编译成了x86内核。务必确认 ARCH 和 CROSS_COMPILE 环境变量已设置。

4. 构建根文件系统:内核的“生存环境”

内核启动后,需要挂载一个根文件系统(rootfs),才能加载并运行用户空间的程序(比如 /bin/bash )。没有根文件系统的内核,就像有了发动机但没有车轮和座椅的汽车,无法提供任何交互服务。我们将创建一个最简单的、基于 initramfs 的根文件系统。

4.1 使用BusyBox制作最小根文件系统

initramfs 是一个被编译进内核或作为独立镜像加载到内存中的临时根文件系统。我们使用BusyBox来制作它,因为BusyBox将许多常用的Unix工具(如 ls , cat , mkdir , sh )集成进了一个单一的二进制文件,非常节省空间。

第一步:下载并编译BusyBox

# 下载稳定版BusyBox
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

和编译内核一样,需要配置交叉编译:

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

然后进行配置。BusyBox有一个非常详细的 menuconfig ,但对于最小系统,我们只需要确保使用静态链接,避免运行时依赖外部库:

make menuconfig

在配置界面中,进入: Settings -> Build static binary (no shared libs) 选择为 [*] (按Y键)。 其他设置可以保持默认。保存退出后,开始编译安装:

make -j$(nproc)
make install

编译安装后,会在 _install 目录下生成基本的Linux文件系统结构( bin , sbin , usr , linuxrc )。

第二步:构建initramfs目录结构 我们在一个独立目录(例如 rootfs )中构建完整的文件系统:

mkdir -p rootfs
cd rootfs
cp -r /path/to/busybox-1.36.1/_install/* .
mkdir -p proc sys dev etc/init.d tmp

关键的一步是创建初始化脚本。当内核挂载initramfs后,它会尝试执行 /init 文件。我们来创建它:

cat > init << EOF
#!/bin/sh
# 挂载虚拟文件系统
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# 打印欢迎信息
echo "Hello from ARM64 QEMU!"
# 启动一个shell
exec /bin/sh
EOF
chmod +x init

这个脚本完成了最基本的初始化:挂载 proc 、 sys 、 dev 等虚拟文件系统,然后启动一个 sh shell。 exec 命令使得shell成为进程1(init进程)。

第三步:打包成initramfs镜像 现在,我们需要把这个 rootfs 目录打包成内核能识别的格式。有两种常用方式:

  1. cpio归档(最常用) :
    cd rootfs
    find . | cpio -H newc -ov --owner root:root > ../initramfs.cpio
    cd ..
    gzip -f initramfs.cpio  # 生成 initramfs.cpio.gz
    
  2. 在内核编译时直接集成(推荐用于简单调试) : 在内核的 make menuconfig 中, General setup -> Initramfs source file(s) 里,直接填写 rootfs 目录的 绝对路径 。这样编译出的内核就自带了根文件系统,无需额外指定启动参数。这对于最简单的启动测试非常方便。

我们采用第一种方式,生成独立的 initramfs.cpio.gz 文件。

5. 启动与调试:让内核在QEMU中跑起来

万事俱备,现在可以启动我们的虚拟ARM64机器了。QEMU的命令行参数非常丰富,我们从一个最简单的命令开始,逐步增加功能。

5.1 基础启动命令解析

最基本的启动命令如下:

qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -smp 2 \
    -m 2G \
    -kernel ./arch/arm64/boot/Image \
    -initrd ./initramfs.cpio.gz \
    -append "console=ttyAMA0 earlycon=pl011,0x9000000 root=/dev/ram0 rw" \
    -nographic

让我们逐行解析这个命令:

  • -machine virt :指定机器类型为 virt 。这是QEMU为虚拟化设计的一个通用平台,支持PCI、网络、磁盘等大量虚拟设备,非常适合开发和测试。
  • -cpu cortex-a57 :指定CPU模型为Cortex-A57。你可以换成 cortex-a53 , cortex-a72 或 max (模拟主机支持的最强特性)。
  • -smp 2 :模拟一个具有2个CPU核心的SMP系统。
  • -m 2G :为虚拟机分配2GB内存。
  • -kernel ./arch/arm64/boot/Image :指定要引导的内核镜像路径。
  • -initrd ./initramfs.cpio.gz :指定初始内存磁盘(initramfs)镜像。
  • -append “…” :传递给内核的命令行参数。这是 极其关键 的一部分:
    • console=ttyAMA0 :告诉内核将控制台输出到 ttyAMA0 设备(即ARM的PL011串口)。
    • earlycon=pl011,0x9000000 :启用早期控制台,这样在内核解压后、初始化完成前就能看到打印信息,对于调试启动故障至关重要。
    • root=/dev/ram0 rw :指定根设备为 /dev/ram0 (即我们的initramfs所在的内存盘),并以读写方式挂载。
  • -nographic :禁用图形化输出,将所有输出重定向到当前终端(标准输入/输出)。同时,通过快捷键 Ctrl-A 然后按 X 可以强制退出QEMU。

执行这条命令,如果一切顺利,你将看到内核解压、启动,最后打印出 Hello from ARM64 QEMU! 并进入BusyBox的shell提示符( / # )。恭喜你,一个最小的ARM64 Linux系统已经成功运行!

5.2 进阶:添加磁盘与网络支持

只有内存文件系统是不够的。要运行更复杂的应用,我们需要持久化存储和网络。

添加虚拟磁盘 首先,创建一个空的磁盘镜像文件(例如一个1GB的qcow2格式镜像):

qemu-img create -f qcow2 arm64disk.qcow2 1G

-f qcow2 格式支持动态分配空间,更节省宿主机磁盘。 然后,在启动命令中增加磁盘设备:

    -drive file=arm64disk.qcow2,format=qcow2,if=none,id=hd0 \
    -device virtio-blk-device,drive=hd0 \

这为虚拟机添加了一个Virtio-BLK块设备。启动后,在系统内你可以用 fdisk -l 看到它(可能是 /dev/vda ),然后进行分区、格式化(如 mkfs.ext4 /dev/vda1 )、挂载使用。

添加虚拟网络 为了让虚拟机能够访问外部网络,我们添加一个用户模式网络栈(User Networking):

    -netdev user,id=mynet0,hostfwd=tcp::2222-:22 \
    -device virtio-net-device,netdev=mynet0 \
  • -netdev user :创建一个用户模式的网络后端,QEMU会内置一个简单的TCP/IP栈,并执行NAT让虚拟机访问外网。
  • hostfwd=tcp::2222-:22 :将宿主机的2222端口转发到虚拟机的22端口(SSH)。这样你可以在宿主机上通过 ssh -p 2222 root@localhost 登录虚拟机。
  • -device virtio-net-device :为虚拟机添加一个Virtio网络设备,并连接到前面定义的 mynet0 网络后端。

现在,一个功能更完整的启动命令看起来像这样:

qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -smp 2 -m 2G \
    -kernel ./arch/arm64/boot/Image \
    -initrd ./initramfs.cpio.gz \
    -append "console=ttyAMA0 earlycon root=/dev/ram0 rw" \
    -drive file=arm64disk.qcow2,format=qcow2,if=none,id=hd0 \
    -device virtio-blk-device,drive=hd0 \
    -netdev user,id=mynet0,hostfwd=tcp::2222-:22 \
    -device virtio-net-device,netdev=mynet0 \
    -nographic

5.3 内核调试实战:使用GDB连接QEMU

QEMU最强大的功能之一是支持GDB调试。这对于分析内核崩溃、理解启动流程、学习内核源码至关重要。

首先,需要在启动QEMU时开启GDB服务器监听:

    -S \
    -gdb tcp::1234
  • -S :在启动时冻结CPU,等待GDB连接后再开始执行。
  • -gdb tcp::1234 :在TCP的1234端口启动GDB服务器。

然后,在另一个终端,使用交叉编译工具链中的GDB( aarch64-linux-gnu-gdb )来连接。 关键一步是加载带调试符号的 vmlinux 文件 :

aarch64-linux-gnu-gdb ./vmlinux
(gdb) target remote localhost:1234
(gdb) break start_kernel  # 在内核主入口函数处设置断点
(gdb) continue  # 继续执行,会停在断点处

连接成功后,你就可以像调试普通程序一样单步执行( step / next )、查看变量( print )、查看回溯( backtrace )了。这对于追踪那些“开机就挂”的诡异问题,或者单纯想看看内核初始化是如何一步步进行的,是无可替代的工具。

调试技巧:在内核配置中,确保 Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info 是开启的,这样 vmlinux 才会包含完整的调试符号。另外,使用 layout src 命令可以在GDB中打开一个源码窗口,使调试过程更加直观。

6. 常见问题排查与解决方案

即使按照步骤操作,你也可能会遇到一些问题。下面是我在多次实践中总结的几个典型问题和解决方法。

6.1 内核启动后卡住或无输出

这是最常见的问题,通常与控制台配置有关。

  • 症状 :QEMU启动后,只打印一两行(如“Booting Linux on physical CPU 0x0000000000 [0x411fd070]”),然后光标闪烁,再无输出。
  • 排查 :
    1. 首先检查QEMU命令中的 -append 参数。 确保 console 和 earlycon 参数正确 。对于 virt 机器,串口是 ttyAMA0 ,早期控制台是 pl011,0x9000000 。一个字母错误都会导致输出到“虚空”。
    2. 检查内核配置。执行 make menuconfig ,进入 Device Drivers -> Character devices -> Serial drivers ,确保 ARM AMBA PL011 serial port support 被编译进内核( [*] ),而不是模块( [M] )。
    3. 尝试在 -append 参数中增加 ignore_loglevel 或 earlyprintk ,强制打印所有级别的内核信息。
    4. 使用 -d 参数让QEMU输出更多内部日志: -d cpu_reset,guest_errors 。有时能发现CPU异常或非法指令。

6.2 内核Panic:无法挂载根文件系统

  • 症状 :内核启动一段时间后,打印出“Kernel panic - not syncing: VFS: Unable to mount root fs”或类似错误。
  • 排查 :
    1. 检查initramfs :确认 -initrd 参数指向的文件路径正确,并且文件没有损坏。可以用 file initramfs.cpio.gz 检查,或尝试解压 gunzip -c initramfs.cpio.gz | cpio -t 列出内容。
    2. 检查内核命令行 :确认 root= 参数指定了正确的设备。对于initramfs,通常是 root=/dev/ram0 。如果你使用了磁盘,可能是 root=/dev/vda1 。
    3. 检查内核文件系统支持 :确保内核编译了对应文件系统的支持。对于initramfs(cpio格式),需要 CONFIG_BLK_DEV_INITRD=y 和 CONFIG_RD_GZIP=y 等。对于ext4磁盘,需要 CONFIG_EXT4_FS=y 。可以在 .config 文件中搜索这些配置项。
    4. 检查init文件 :确保initramfs根目录下的 /init 文件存在、可执行,并且脚本语法正确。一个简单的测试方法是,在 -append 参数中加入 init=/bin/sh ,尝试直接启动shell,绕过你的init脚本。

6.3 网络功能无法使用

  • 症状 :在虚拟机内无法ping通宿主机或外网(如 8.8.8.8 )。
  • 排查 :
    1. 检查设备识别 :在虚拟机内执行 ip addr 或 ifconfig -a ,看是否有 eth0 这样的网络接口出现。如果没有,说明Virtio网络驱动未加载。
    2. 检查内核驱动 :确认内核配置中 Device Drivers -> Network device support -> Virtio network driver 已启用。如果是编译成模块( [M] ),则需要确保initramfs中包含了 virtio_net.ko 模块,并在 /init 脚本中 insmod 它。
    3. 用户模式网络限制 :QEMU的用户模式网络( -netdev user )默认可以访问外网,但外部无法直接访问虚拟机(除了端口转发)。 ping 宿主机IP可能不通,但 ping 一个外网地址(如 8.8.8.8 )应该是可以的。如果不行,检查宿主机防火墙是否阻止了QEMU的NAT流量。
    4. 尝试TAP网络 :对于更复杂的网络需求(如虚拟机需要独立的IP、需要被局域网内其他机器访问),可以配置TAP网络,但这需要宿主机端的桥接配置,步骤更复杂。

6.4 性能优化与高级用法建议

当基本功能跑通后,你可能会关心如何让它更好用。

  • 启用KVM加速 :如果你的宿主机是Linux,并且CPU支持虚拟化(Intel VT-x或AMD-V),可以启用KVM来获得接近原生的性能。将 -machine virt 改为 -machine virt,accel=kvm 即可。使用 -enable-kvm 参数(旧版本)也可以。启用后,你会感觉系统响应速度有质的提升。
  • 使用图形化界面 :去掉 -nographic 参数,QEMU会弹出一个图形窗口。你可以配合 -cdrom 参数加载一个完整的ARM64发行版ISO(如Ubuntu Server for ARM)进行安装,得到一个带有图形界面的完整Linux系统。
  • 制作更复杂的根文件系统 :除了BusyBox,你可以使用 debootstrap 为ARM64构建一个Debian/Ubuntu的根文件系统,然后打包进磁盘镜像。这样你就拥有了一个带 apt 包管理器的完整系统,可以安装任何你需要的软件。
  • 自动化脚本 :将编译内核、制作根文件系统、启动QEMU的命令写成一个Shell脚本,可以极大提高效率。更进一步,可以研究使用 buildroot 或 yocto 这类构建系统,它们能自动化地生成交叉工具链、内核、根文件系统,并打包成完整的SD卡镜像。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐