
1. 项目概述为什么要在QEMU上启动ARM64内核如果你是一名嵌入式开发者、内核爱好者或者正在学习ARM64架构那么“在QEMU上启动一个ARM64 Linux内核”几乎是你的必经之路。这听起来像是一个简单的“Hello World”级别的任务但实际操作起来你会发现它是一把打开系统底层世界的万能钥匙。它不仅仅是让一个内核跑起来而是让你在一个完全可控、可重复、无风险的虚拟环境中亲身体验从硬件初始化、设备树加载、内核解压到最终用户空间启动的完整链条。我最初接触这个项目是为了调试一个在真实ARM64开发板上难以复现的内核启动死锁问题。物理板卡调试周期长串口日志抓取麻烦而QEMU配合GDB可以随时暂停、单步执行内核代码查看任意内存和寄存器状态这种能力对于深入理解启动流程和排查疑难杂症是无价的。即使你没有任何硬件通过这个项目你也可以在普通的x86电脑上构建、运行和调试一个为ARM64架构编译的完整Linux系统。这对于学习交叉编译、设备树Device Tree、驱动模型、以及内核初始化顺序来说是一个绝佳的沙盒。简单来说这个项目能帮你解决几个核心问题学习与实验——无需硬件即可深入ARM64与Linux内核开发与调试——为真实驱动或内核模块开发提供快速验证环境教育与研究——理解计算机系统从“上电”到“登录”的全过程。接下来我将拆解整个过程从工具链准备到最终看到命令行提示符分享每一步的实操细节和那些容易踩坑的地方。2. 环境准备与工具链选型工欲善其事必先利其器。在x86主机上为ARM64目标构建和运行系统我们需要一套明确的工具组合。选择不当可能会导致编译失败、内核无法启动或者根文件系统挂载不上。2.1 核心工具安装与验证首先我们需要在宿主机通常是Ubuntu/Debian或Fedora等Linux发行版上安装QEMU和交叉编译工具链。1. QEMU系统模拟器安装QEMU是我们虚拟的“ARM64计算机”。我们需要的是支持ARM64架构全系统模拟的qemu-system-aarch64。# 在Ubuntu/Debian上 sudo apt update sudo apt install qemu-system-arm qemu-utils # 在Fedora/CentOS上 sudo dnf install qemu-system-aarch64安装后验证版本和架构支持qemu-system-aarch64 --version # 输出应包含system-aarch64字样表明支持ARM64全系统模拟。2. ARM64交叉编译工具链安装我们的主机是x86_64要编译ARM64的内核和BusyBox必须使用交叉编译器。推荐使用Linaro或ARM官方提供的GCC工具链它们对ARM架构的优化和支持最好。# 方法一使用包管理器安装简单但版本可能较旧 # Ubuntu/Debian sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu # 方法二下载预编译工具链推荐版本新且可控 # 例如从ARM官网下载https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads # 选择‘AArch64 GNU/Linux target (aarch64-none-linux-gnu)’对应的版本。 # 下载后解压并将其bin目录加入PATH环境变量。 wget https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu.tar.xz tar -xf arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu.tar.xz export PATHpwd/arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu/bin:$PATH验证交叉编译器aarch64-none-linux-gnu-gcc --version # 或如果使用方法一安装 aarch64-linux-gnu-gcc --version正确输出应显示编译器版本且目标target为aarch64-linux-gnu或类似。注意工具链的命名aarch64-none-linux-gnuvsaarch64-linux-gnu会影响后续编译命令。请务必记下你使用的编译器前缀并在后续所有make命令中正确设置CROSS_COMPILE变量。例如如果可执行文件叫aarch64-linux-gnu-gcc那么前缀就是aarch64-linux-gnu-。2.2 项目目录结构规划清晰的目录结构能让整个过程有条不紊。建议创建一个项目根目录例如qemu_arm64_boot并在其下建立如下子目录qemu_arm64_boot/ ├── build/ # 存放编译输出的内核镜像、设备树等 ├── linux/ # Linux内核源代码 ├── busybox/ # BusyBox源代码 ├── rootfs/ # 根文件系统目录 └── scripts/ # 存放一些辅助脚本如制作根文件系统镜像这个结构不是必须的但它能有效隔离源代码、构建产物和最终镜像避免混乱。3. 获取与配置Linux内核源码内核是整个系统的核心。我们需要获取源代码并为其配置适合在QEMU ARM64虚拟机上运行的选项。3.1 获取内核源码可以从内核官网https://www.kernel.org下载稳定版或者使用Git克隆主线开发版本。对于学习和实验稳定版如6.1.x, 6.6.x是更稳妥的选择。cd qemu_arm64_boot # 下载稳定版以6.6.30为例 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.30.tar.xz tar -xf linux-6.6.30.tar.xz mv linux-6.6.30 linux # 或者使用git克隆体积大但包含全历史 # git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git3.2 配置内核defconfig与手动微调进入内核源码目录首要任务是通过make命令配置内核。我们需要指定架构ARM64和交叉编译工具链。cd linux export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- # 请替换为你的交叉编译器前缀第一步使用默认配置。ARM64针对QEMU的“virt”机器有一个现成的默认配置defconfig它包含了启动所需的基本驱动如PL011串口、PCI、GIC中断控制器等。make defconfig这条命令会在当前目录生成.config文件。defconfig配置已经可以启动但为了获得一个更精简或功能更丰富的内核我们通常需要进行一些调整。第二步手动微调配置可选但推荐。运行make menuconfig可以进入一个基于ncurses的图形化配置界面。这里分享几个关键调整项make menuconfigGeneral setup - Initramfs source file(s):留空。我们将使用外部的根文件系统镜像不内置initramfs。Device Drivers - Character devices - Serial drivers:确保ARM AMBA PL011 serial port support被选中y。这是QEMU“virt”机器模拟的串口内核控制台输出全靠它。Kernel Features:可以关闭一些不必要的调试选项以减小内核体积例如Kernel debugging(CONFIG_DEBUG_KERNEL) 如果不是为了调试可以关掉。但为了学习保留KGDB等调试支持可能有用。File systems:确保你计划在根文件系统中使用的文件系统被支持。例如如果根文件系统镜像用ext4则需选中ext4支持 (y或m)。对于最简单的场景CPIO格式的initramfs可能更方便但这里我们以更接近真实场景的磁盘镜像为例。Boot options:可以设置默认命令行参数但我们更倾向于在QEMU启动时通过-append参数传递。配置完成后保存退出。.config文件就准备好了。3.3 编译内核与设备树配置完成后开始编译。-j参数指定并行编译的作业数通常设置为CPU核心数以加快编译速度。make -j$(nproc)编译成功后关键产物在arch/arm64/boot/目录下Image:压缩的内核镜像文件arch/arm64/boot/Image。这是QEMU可以直接加载的、未打包成任何其他格式的纯内核镜像。Image.gz:压缩的镜像如果配置了内核压缩。此外对于使用设备树Device Tree的ARM平台我们还需要设备树二进制文件.dtb。QEMU“virt”机器的设备树源码位于内核源码的arch/arm64/boot/dts/arm/目录。编译设备树make dtbs编译后virt机器的设备树文件是arch/arm64/boot/dts/arm/foundation-v8.dtb或qemu-virt.dtb取决于内核版本和配置。更通用的方法是查看arch/arm64/boot/dts/arm/目录下生成的.dtb文件通常名为virt.dtb。我们将把这个文件复制到我们的build目录备用。实操心得第一次编译可能会遇到一些依赖问题比如缺少libssl-dev、flex、bison等。根据编译错误提示安装相应的开发包即可。例如在Ubuntu上可能需要sudo apt install libssl-dev flex bison。4. 构建最小根文件系统BusyBox实战一个只有内核的系统是无法工作的它需要挂载一个根文件系统rootfs并在其中找到第一个用户空间进程通常是/sbin/init。我们将使用BusyBox来制作一个极简的根文件系统。BusyBox把许多常用的Unix工具如ls,cp,sh等打包进一个单一的可执行文件非常适合嵌入式环境。4.1 获取与编译BusyBox从BusyBox官网https://busybox.net/下载稳定版源码。cd qemu_arm64_boot wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xf busybox-1.36.1.tar.bz2 mv busybox-1.36.1 busybox cd busybox和内核一样需要配置并交叉编译。BusyBox的配置更简单一些。export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig # 使用默认配置defconfig已经包含了大部分常用工具。我们通常希望将BusyBox编译为静态链接这样它不依赖目标系统的动态库简化根文件系统制作。make menuconfig进入配置界面后找到Settings - Build static binary (no shared libs)将其选中按y键。 保存退出后开始编译make -j$(nproc)编译完成后会生成busybox可执行文件。使用file命令检查它是否是ARM64静态可执行文件file busybox # 期望输出busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]..., for GNU/Linux 3.7.0, stripped4.2 组装根文件系统目录现在我们创建一个根文件系统的目录结构并将BusyBox安装进去。cd .. # 回到项目根目录 qemu_arm64_boot sudo rm -rf rootfs # 清理旧的 mkdir -p rootfs cd rootfs使用BusyBox的install目标将其安装到rootfs目录。CONFIG_PREFIX指定安装路径。# 在busybox源码目录中执行 cd ../busybox make CONFIG_PREFIX../rootfs install执行后rootfs目录下会生成bin,sbin,usr/bin,usr/sbin,linuxrc等目录和链接。其中bin/busybox是主程序其他工具如ls,sh都是指向它的符号链接。4.3 完善根文件系统必备内容一个可运行的根文件系统还需要一些关键的目录和文件。1. 创建基本目录cd ../rootfs mkdir -p proc sys dev etc etc/init.d tmpproc,sys,dev: 内核通过虚拟文件系统暴露系统信息、设备和配置的挂载点。etc: 配置文件目录。tmp: 临时文件目录。2. 创建初始启动脚本内核挂载根文件系统后会尝试执行/sbin/init、/etc/init、/bin/init等。BusyBox安装后提供了/bin/sh和一个指向BusyBox的/linuxrc。我们可以创建一个简单的初始化脚本。 创建文件rootfs/etc/init.d/rcS#!/bin/sh # 这是一个最简单的初始化脚本 mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev echo Hello from the minimal ARM64 rootfs! exec /bin/sh然后赋予它可执行权限chmod x etc/init.d/rcS为了让系统启动时自动执行这个脚本我们需要创建/sbin/init作为指向BusyBox的链接并利用BusyBox的init功能。BusyBox的init会读取/etc/inittab配置文件。我们创建一个最简单的inittab 创建文件rootfs/etc/inittab::sysinit:/etc/init.d/rcS ::respawn:/bin/sh ::ctrlaltdel:/sbin/reboot这个配置表示系统初始化时执行/etc/init.d/rcS启动一个/bin/shshell并保持如果退出则重新启动在虚拟终端按下CtrlAltDel时执行重启。3. 创建设备节点在/dev目录下至少需要两个基本的设备节点console和null。在真实的系统中devtmpfs会在启动时自动创建它们但在我们静态的根文件系统镜像里可以先创建好。sudo mknod dev/console c 5 1 sudo mknod dev/null c 1 3注意创建字符设备节点需要root权限所以使用sudo。c表示字符设备5 1是console的主次设备号1 3是null的主次设备号。至此一个包含BusyBox和基本启动脚本的最小根文件系统目录就准备好了。5. 制作根文件系统镜像并整合启动有了内核和根文件系统目录我们需要将后者打包成一个QEMU能够识别的磁盘镜像格式。最常见的是ext4格式的磁盘镜像。5.1 创建ext4格式磁盘镜像我们将创建一个大小为256MB的空白镜像格式化为ext4并将rootfs目录的内容复制进去。cd .. # 回到项目根目录 dd if/dev/zero ofbuild/rootfs.ext4 bs1M count256 mkfs.ext4 build/rootfs.ext4现在挂载这个空白镜像将rootfs目录的内容复制进去mkdir -p /mnt/rootfs_temp sudo mount -o loop build/rootfs.ext4 /mnt/rootfs_temp sudo cp -a rootfs/* /mnt/rootfs_temp/ sudo umount /mnt/rootfs_temp rmdir /mnt/rootfs_temp-o loop选项允许我们将一个文件作为块设备挂载。-a参数保留文件属性和链接关系。5.2 准备最终启动文件将编译好的内核镜像和设备树文件也复制到build目录方便管理。cp linux/arch/arm64/boot/Image build/ cp linux/arch/arm64/boot/dts/arm/*.dtb build/ # 通常我们只需要virt.dtb全部复制也无妨现在build目录下应该至少有Image: ARM64内核镜像virt.dtb(或类似名称): 设备树二进制文件rootfs.ext4: 根文件系统ext4镜像6. QEMU启动命令详解与首次启动万事俱备只欠启动。QEMU的命令行参数是控制虚拟机的关键。一个最基础的启动命令如下qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -smp 2 \ -m 2G \ -kernel build/Image \ -dtb build/virt.dtb \ -drive filebuild/rootfs.ext4,formatraw,ifnone,idhd0 \ -device virtio-blk-device,drivehd0 \ -append root/dev/vda rw consolettyAMA0 \ -nographic让我们逐行拆解这个命令理解每个参数的意义-machine virt: 指定模拟的机器类型为virt。这是QEMU为虚拟化设计的一个通用平台支持ARM64并且设备模型相对简单标准。-cpu cortex-a57: 指定模拟的CPU型号。cortex-a57是一个经典的ARMv8-A 64位CPU。你也可以换成cortex-a53小核或max尝试模拟主机支持的所有特性。-smp 2: 模拟一个具有2个CPU核心的SMP系统。-m 2G: 为虚拟机分配2GB的RAM。-kernel build/Image: 指定要加载的内核镜像文件路径。-dtb build/virt.dtb: 指定要加载的设备树二进制文件DTB路径。设备树描述了虚拟硬件的拓扑结构内存布局、外设地址等内核依赖它来初始化驱动。-drive filebuild/rootfs.ext4,formatraw,ifnone,idhd0: 定义一个驱动器。file指向我们的根文件系统镜像formatraw表示是原始镜像格式ifnone表示先不指定接口类型idhd0给这个驱动器一个标识符。-device virtio-blk-device,drivehd0: 创建一个VirtIO块设备并将其连接到上面定义的hd0驱动器。VirtIO是一种高效的半虚拟化I/O协议。在客户机内核中这个设备会显示为/dev/vda第一个VirtIO块设备。-append root/dev/vda rw consolettyAMA0: 传递给内核的命令行参数。这是最关键的部分之一。root/dev/vda: 告诉内核根文件系统位于哪个设备上。对应我们创建的VirtIO块设备。rw: 以读写方式挂载根文件系统。consolettyAMA0: 指定内核控制台输出到ttyAMA0设备。ttyAMA0是QEMU“virt”机器模拟的PL011串口设备。内核必须配置了对应的驱动我们之前确认过并且QEMU会将此串口重定向到标准输入输出因为我们用了-nographic。-nographic: 禁用图形界面将QEMU作为命令行程序运行并将串口控制台连接到当前终端。这样所有内核启动日志和shell交互都将在你运行QEMU的终端中进行。要退出QEMU可以按CtrlA然后按X。在项目根目录执行上述命令确保路径正确。如果一切顺利你将看到内核启动日志如瀑布般在终端滚动最后出现类似以下的提示符... [ 1.234567] /dev/vda: recovering journal [ 1.345678] EXT4-fs (vda): mounted filesystem with ordered data mode. Quota mode: none. [ 1.456789] Run /sbin/init as init process Hello from the minimal ARM64 rootfs! / #恭喜你已经成功在QEMU上启动了一个ARM64 Linux内核并进入了由BusyBox提供的最小shell环境。你可以运行ls,cat /proc/cpuinfo等命令来验证系统。7. 进阶配置与调试技巧一次成功启动只是开始。在实际开发和调试中你可能会需要更复杂的配置。7.1 网络功能配置让虚拟机具备网络能力可以方便地通过网络下载文件或进行远程调试。QEMU支持多种网络后端最常用的是“user mode”网络NAT和“tap”桥接网络。User Mode网络简单主机单向访问虚拟机在启动命令中添加以下参数-netdev user,idmynet0,hostfwdtcp::2222-:22 \ -device virtio-net-device,netdevmynet0-netdev user,...: 创建一个用户模式网络后端ID为mynet0。hostfwdtcp::2222-:22: 将主机的2222端口转发到虚拟机的22端口SSH。这样你可以在主机上ssh -p 2222 rootlocalhost连接虚拟机。-device virtio-net-device,netdevmynet0: 创建一个VirtIO网络设备连接到mynet0后端。在虚拟机内你需要配置IP地址通常由QEMU内置的DHCP服务器分配默认是10.0.2.15/24并启动SSH服务需要在BusyBox中配置dropbear或类似轻量级SSH服务器。TAP桥接网络复杂虚拟机像真实主机一样接入局域网这需要主机系统配置桥接网络和TAP设备并需要root权限运行QEMU。配置更复杂但虚拟机可以获得一个与主机同网段的IP与局域网内其他机器直接通信。7.2 内核调试使用GDB这是QEMU最强大的功能之一。你可以像调试用户态程序一样单步调试Linux内核。首先在QEMU启动命令中添加-S -s参数qemu-system-aarch64 \ -machine virt ... \ -s -S \ -nographic-S: 在启动时暂停CPU等待调试器连接。-s: 是-gdb tcp::1234的简写在TCP的1234端口监听GDB连接。然后在另一个终端使用交叉编译工具链中的GDB通常是aarch64-linux-gnu-gdb来连接cd qemu_arm64_boot aarch64-linux-gnu-gdb linux/vmlinux # vmlinux是带有调试符号的内核ELF文件 (gdb) target remote localhost:1234 (gdb) break start_kernel # 在内核启动的早期函数设置断点 (gdb) continue连接成功后GDB会附着到被QEMU暂停的虚拟机。你可以设置断点、单步执行、查看变量和内存这对于分析内核启动流程或排查崩溃问题至关重要。注意事项调试时需要确保内核编译时开启了调试符号CONFIG_DEBUG_INFOy。在make menuconfig中Kernel hacking - Compile-time checks and compiler options - Compile the kernel with debug info需要选中。7.3 使用图形界面或VNC如果你不想只用命令行可以启用图形界面。去掉-nographic参数并添加VNC显示选项-vnc :0 -serial stdio-vnc :0: 开启VNC服务器监听在5900端口:0对应5900:1对应5901以此类推。你可以用VNC客户端连接localhost:5900查看图形界面。-serial stdio: 将第一个串口ttyAMA0重定向到标准输入输出这样你仍然可以在启动QEMU的终端看到内核日志。“virt”机器默认会加载一个简单的帧缓冲设备内核如果配置了CONFIG_FB和CONFIG_FB_VIRTUAL就可以在VNC窗口中显示一个图形控制台。8. 常见问题与排查实录即使按照步骤操作也可能会遇到各种问题。这里记录一些典型问题及其解决方法。问题1内核启动后卡住没有任何输出或最后出现“Kernel panic - not syncing: VFS: Unable to mount root fs”可能原因1内核命令行参数root指定错误。检查QEMU命令中-append的root参数。如果你使用的是VirtIO块设备应该是/dev/vda第一个或/dev/vdb第二个。如果你使用的是模拟的SATA或SCSI设备可能是/dev/sda。使用-device参数添加的设备名决定了它在内核中的节点。可能原因2内核缺少对应文件系统的驱动。确保内核配置中包含了你的根文件系统镜像所使用的文件系统如CONFIG_EXT4_FSy。在make menuconfig中检查File systems下的选项。可能原因3根文件系统镜像内容不正确。确保镜像已正确格式化并复制了文件。可以再次挂载镜像检查/sbin/init或/init文件是否存在且可执行。确保/dev/console和/dev/null设备节点已创建。排查技巧在内核命令行中添加init/bin/sh例如-append root/dev/vda rw consolettyAMA0 init/bin/sh。这会让内核在挂载根文件系统后直接执行/bin/sh跳过复杂的init流程。如果这样能进入shell说明根文件系统基本可访问问题可能出在init脚本/sbin/init或/etc/inittab上。问题2内核启动早期就崩溃或死锁串口无输出或输出乱码可能原因1设备树DTB不匹配。确保使用的.dtb文件是由你编译的内核源码树生成的并且对应-machine指定的平台这里是virt。不要使用其他内核版本或平台的设备树。可能原因2内核配置与QEMU模拟的硬件不匹配。确保内核配置包含了virt机器必要的驱动如CONFIG_SERIAL_AMBA_PL011y串口CONFIG_VIRTIO_BLKy块设备CONFIG_VIRTIO_NETy网络如果用了的话。使用defconfig通常能保证这些基本驱动被启用。排查技巧尝试最简启动。去掉所有不必要的-device参数只保留内核、内存和串口使用initramfs而不是磁盘镜像来排除根文件系统问题。也可以尝试更早的稳定内核版本如5.10.x。问题3在GDB中连接QEMU后断点无法命中或符号地址不对可能原因GDB加载的符号文件vmlinux与QEMU中运行的内核镜像不匹配。必须使用编译当前运行内核时生成的vmlinux文件位于内核源码根目录而不是arch/arm64/boot/Image。Image是压缩去除了调试符号的镜像。确保在GDB中加载的是正确的vmlinux路径。排查技巧在GDB中使用info files命令查看加载的符号段地址。在内核启动日志中会有一行类似[ 0.000000] .text : 0xffffff8008080000 - 0xffffff8008c00000的信息这是内核代码段的加载地址。GDB加载的符号地址应该落在这个范围内。如果不匹配说明符号文件不对。问题4BusyBox shell中某些命令如ps,top无法工作可能原因proc文件系统未挂载。我们的rcS脚本中包含了mount -t proc none /proc。如果这条命令执行失败例如路径错误proc就不会被挂载许多依赖它的工具就会出错。检查脚本是否正确执行或手动在shell中执行mount -t proc none /proc看看是否报错。可能原因BusyBox配置中未编译该命令。虽然defconfig包含了大部分命令但个别工具可能未被包含。可以重新进入make menuconfig在相应类别下查找并启用该命令然后重新编译安装BusyBox。这个过程就像搭积木每一步都环环相扣。从工具链的一个小版本不匹配到内核命令行参数的一个字母错误都可能导致启动失败。耐心查看串口输出的每一条内核信息尤其是错误信息它们是定位问题最直接的线索。当你第一次看到那个简单的/#提示符时你会感觉仿佛亲手点亮了一台微型的计算机这种成就感是阅读任何文档都无法替代的。