
1. 先把预期摆正x86 主机上跑 arm64 虚拟机到底能得到什么上个月为了验证一个交叉编译出来的 arm64 二进制包我在 x86 工作站上开了一台 arm64 虚拟机。同一份最小化安装镜像同样给 4 核 4GB 内存x86 那台 6 分钟进系统arm64 这台跑了 47 分钟才看到登录提示符。这中间没有配置写错也没有镜像损坏就是这类环境最真实的一面在 x86 主机上用 QEMU 创建 arm64 架构下的 Linux 虚拟机CPU 指令是一条一条被翻译执行的速度天然要打一个大折扣。所以动手之前先把两件事想清楚。第一你要的是跑起来能用还是跑起来够快。第二你手上的主机是什么架构。如果主机的 CPU 本身就是 arm64比如苹果 M 系列芯片的笔记本、arm64 服务器那 QEMU 能借助硬件虚拟化能力arm64 虚拟机跑起来几乎和原生一样快如果主机是 x86_64那就只能用纯软件仿真性能掉一个数量级是常态但换来的是不需要任何 arm64 物理设备就能完成软件包验证、内核模块编译冒烟、发行版安装流程演练、驱动加载测试这类工作。我见过很多人卡在这里装完之后发现怎么这么卡然后开始怀疑参数、怀疑镜像、怀疑磁盘格式折腾一整天。其实根源就是没接受仿真运行这个前提。后面我会给出两种具体的创建方式——一种是直接手搓qemu-system-aarch64命令行另一种是交给 libvirt 用virt-install做托管式部署。两种方法我都在实际项目里用过各有取舍下面把能踩的坑、能省的步骤一次讲透。1.1 arm64 与 amd64 在虚拟化层面的三处硬差异很多人第一次创建 arm64 虚拟机会失败不是因为命令写错而是因为思维还停留在 x86 上。arm64 平台的虚拟机有三处地方和 amd64 完全不同必须提前建立认知。第一没有传统 BIOS只有 UEFI 固件。x86 上你可以不加任何固件参数QEMU 会给你一份默认的 SeaBIOS开机就能看到引导画面。arm64 的virt机器没有这种东西它需要一份 arm64 版的 UEFI 固件文件名通常叫QEMU_EFI.fd或者AAVMF_CODE.fd你不提供虚拟机上电之后就是一片空白或者直接退出。这不是可选优化是必需项。第二没有默认显示设备。x86 机器默认挂一块 VGA 兼容显卡装系统时图形界面自动就出来了。arm64 的virt机器默认什么都没接你要看图形界面得自己加virtio-gpu-pci并且配上 USB 键盘鼠标不看图形界面就得把串口console用起来。这也是为什么 arm64 服务器发行版的安装镜像默认输出都往串口走。第三CPU 型号必须显式挑。x86 上写-cpu host表示直通宿主机 CPU 特性arm64 在 x86 主机上跑-cpu host直接报错因为宿主和客户机根本不是同一套指令集。你得从cortex-a53、cortex-a57、cortex-a72、max、neoverse-n1里选一个这个选择会直接影响客户机能否用上某些指令集扩展。把这三条记住后面所有参数就都能理解了而不是照着抄。1.2 仿真与硬件加速什么情况下才谈得上能用QEMU 的加速后端有三种常见形态搞清楚它们的适用边界比记住参数重要得多加速方式可用条件性能量级典型场景KVM宿主与客户机同架构且宿主为 arm64接近原生arm64 服务器上跑多台 arm64 虚拟机HVFmacOS 宿主且为 Apple Silicon接近原生Mac 本地做 arm64 开发环境TCG任意架构组合原生性能的 1/8 至 1/20x86 主机上验证 arm64 软件包、跑安装流程我实测过的几个数字供参考在 8 核 x86 工作站上cortex-a72四核 TCG 客户机编译一个中等规模的 C 项目耗时大约是同一台机器上原生编译的 12 倍跑apt装几百个包大约 15 到 20 分钟纯 IO 类操作拷贝文件、打包反而没慢那么多因为这类任务 CPU 占比低。判断标准很简单你的任务如果对 CPU 算力敏感就别在 x86 上用 TCG 跑 arm64 虚拟机如果任务只是验证 arm64 上能不能装、能不能起、配置对不对那 TCG 完全够用还省下一台物理机的钱。另外有一点要提前说清楚跨架构的硬件虚拟化直通是不存在的x86 主机上无论你怎么写-accel kvm都不可能让 arm64 客户机用上 KVM这是硬件层面的限制不是软件配置问题。2. 落地前的清单主机依赖、固件与镜像怎么选我习惯在动手前把三样东西备齐宿主机的 QEMU 及配套工具、arm64 版的 UEFI 固件、以及一份靠谱的系统镜像。这三样缺任何一样后面都会以各种奇怪的报错形式回报你。2.1 主机侧软件包与版本核对不同发行版包名不一样我列一下实际用的# Debian / Ubuntu sudo apt update sudo apt install -y qemu-system-arm qemu-utils qemu-efi-aarch64 \ libvirt-daemon-system libvirt-clients virtinst # Fedora / RHEL 系 sudo dnf install -y qemu-kvm qemu-img edk2-aarch64 \ libvirt libvirt-daemon-kvm virt-install注意 Debian 系里 arm64 的 QEMU 二进制打在qemu-system-arm这个包里名字看着像只支持 32 位其实qemu-system-aarch64也在里面别被包名误导。装完先做版本核对qemu-system-aarch64 --version qemu-system-aarch64 -machine virt -cpu help | head -20 virt-host-validate 2/dev/null | head -20第二条命令能列出所有可选的 arm64 CPU 型号如果你看到的列表里没有cortex-a72或max说明包版本太老建议升级到发行版自带的较新版本。另外virt-host-validate在跨架构场景下会报几条 KVM 相关的警告直接忽略即可因为我们本来就要走 TCG。提示QEMU 6.0 以下版本对 arm64 的virt机器支持稍弱尤其是 GICv3 和virtio-blk-pci的 bootindex 处理上有已知问题。如果条件允许尽量用 7.x 及以上版本省掉很多无谓的排查。2.2 UEFI 固件是 arm64 虚拟机的BIOS来源与配法固件文件的位置随发行版不同发行版固件路径说明Debian / Ubuntu/usr/share/AAVMF/AAVMF_CODE.fd只读代码区Debian / Ubuntu/usr/share/AAVMF/AAVMF_VARS.fd变量存储模板需要拷贝一份用Fedora / RHEL/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw代码区Fedora / RHEL/usr/share/edk2/aarch64/QEMU_VARS-pflash.raw变量存储模板这里有个关键细节固件分成代码区和变量区两部分。代码区只读可以多个虚拟机共用同一份变量区保存的是 UEFI 启动项、启动顺序这些会变的内容每台虚拟机必须有自己独立的一份否则会互相干扰。所以正确做法是把VARS文件拷贝一份出来用虚拟磁盘pflash的方式挂上去而不是图省事用-bios一次性加载。用-bios能不能跑能安装过程一般没问题但装完之后 UEFI 记不住启动项每次开机都要手动进引导菜单选硬盘反而更麻烦。我一开始就是图快用了-bios结果每次重启虚拟机都得敲一次命令两天之后老老实实换成了双 pflash 方案。2.3 选 ISO 安装盘还是 cloud image这一步很多人随便选其实两种镜像的用法差别不小对比项ISO 安装镜像cloud image首次启动耗时TCG30 到 90 分钟3 到 8 分钟分区自由度完全可控固定布局改起来麻烦初始账号安装时自己设需要通过 cloud-init 注入适合场景演练真实安装流程、验证分区方案快速起一台可用的 arm64 环境推荐度跨架构仿真按需优先我现在的习惯是如果只是要一台能登录、能编译、能测试的 arm64 环境直接用 cloud image 加 cloud-init 注入 SSH 公钥几分钟就能 ssh 进去只有需要验证分区、引导方式、加密盘这类安装流程本身才去走 ISO。这一点在 TCG 环境下尤其明显——省下的不只是第一次安装的几十分钟还有后续每次重建环境的时间。3. 方法一qemu-system-aarch64 手工命令行直启命令行方式最大的好处是所见即所得虚拟机由哪几块设备组成、每个设备什么参数全部写在一行命令里没有隐藏的中间层。排查问题时这一点非常值钱。3.1 十条参数搭出最小可引导环境先建目录、准备固件变量区和系统盘mkdir -p ~/vms/arm64 cd ~/vms/arm64 # 拷贝一份可写的 UEFI 变量存储 cp /usr/share/AAVMF/AAVMF_VARS.fd ./AAVMF_VARS.fd # 系统盘qcow2 格式上限 40G元数据预分配减少碎片 qemu-img create -f qcow2 -o preallocationmetadata arm64-root.qcow2 40G # 校验镜像完整性避免用坏文件折腾半天 sha256sum ubuntu-24.04.2-live-server-arm64.iso然后启动安装qemu-system-aarch64 \ -name arm64-install \ -machine virt,gic-version3 \ -accel tcg,threadmulti \ -cpu cortex-a72 \ -smp 4,sockets1,cores4,threads1 \ -m 4096 \ -drive ifpflash,formatraw,readonlyon,file/usr/share/AAVMF/AAVMF_CODE.fd \ -drive ifpflash,formatraw,file./AAVMF_VARS.fd \ -drive ifnone,idhd0,formatqcow2,file./arm64-root.qcow2 \ -device virtio-blk-pci,drivehd0,bootindex1 \ -drive ifnone,idcd0,mediacdrom,formatraw,file./ubuntu-24.04.2-live-server-arm64.iso \ -device virtio-scsi-pci,idscsi0 \ -device scsi-cd,drivecd0,bootindex2 \ -netdev user,idnet0,hostfwdtcp:127.0.0.1:2222-:22 \ -device virtio-net-pci,netdevnet0 \ -device virtio-rng-pci \ -nographic敲下去之后正常情况下十几秒内串口就会滚出 UEFI 固件的启动日志然后是 GRUB 菜单接着是安装器的输出。虚拟机上电到看到画面如果超过一分钟还是空白先别急着改参数看看终端有没有报错——多半是固件路径不对。3.2 每个参数为什么这么写连点成线把参数当成咒语背下来毫无意义知道每一段在干什么出问题时才知道该删哪一段参数作用不写会怎样-machine virt,gic-version3选通用虚拟平台并指定中断控制器版本老内核可能卡在 earlycon 无输出-accel tcg,threadmulti开启多线程 TCG每个 vCPU 一个宿主线程单线程执行多核客户机反而更慢-cpu cortex-a72指定客户机 CPU 型号必须显式指定host在此场景不可用-smp 4,sockets1,cores4,threads1拓扑写清楚有些内核按 topology 做调度模糊描述会有意外-m 4096内存 4G低于 1G 时 UEFI 可能直接引导失败两条ifpflash固件代码区 可写变量区只用-bios会丢失 UEFI 启动项virtio-blk-pci,bootindex1虚拟硬盘引导优先级 1没有 bootindex 时引导顺序不可控virtio-scsi-pciscsi-cd光驱挂载arm64 的 virt 机器没有 IDE 总线ide-cd加不上-netdev user,hostfwd...用户态网络加端口转发客户机能出网但外部访问不进来-device virtio-rng-pci给客户机提供熵源启动时 systemd 等随机数会卡几十秒-nographic串口输出到当前终端不开图形窗口需要自己接显示设备那张表里我个人认为最容易被忽略的是virtio-rng-pci。arm64 客户机在 TCG 下熵池积累特别慢没有这个设备systemd 起服务时会一直等随机数表现出来就是卡在某一行不动。加一行参数能省下几十秒的等待值得。3.3 图形窗口与文本串口两种看安装界面的方式-nographic适合服务器镜像因为它把串口直接接到当前终端。退出方式是Ctrl-A然后按X注意不是Ctrl-C。如果想同时保留监视器等能力可以改用-serial mon:stdio。需要图形界面的时候把-nographic换掉-device virtio-gpu-pci \ -device qemu-xhci -device usb-kbd -device usb-tablet \ -display gtk \ -serial mon:stdiousb-tablet比usb-mouse好用鼠标指针能和宿主对齐不用来回点一下才动。但实话讲在 TCG 环境下装带桌面环境的发行版是一件非常考验耐心的事我试过一次从开始装到进桌面花了将近四个小时全程鼠标都拖影。所以除非你在验证桌面相关的功能否则强烈建议走最小化安装。服务端场景我还常用一种后台跑的写法适合长时间安装任务关掉终端也不影响-display none \ -serial file:serial.log \ -daemonize -pidfile qemu.pid \ -monitor unix:mon.sock,server,nowait装的时候tail -f serial.log看进度中途想查状态就连监视器socat - UNIX-CONNECT:mon.sock进去敲info status、info block都能看。这套组合我在远程服务器上操作时用得最多。3.4 装完之后改引导顺序、端口转发、日常启停系统装好后把光驱那两行删掉硬盘保留bootindex1网络换成开机即用qemu-system-aarch64 \ -name arm64-run \ -machine virt,gic-version3 \ -accel tcg,threadmulti \ -cpu cortex-a72 -smp 4 -m 4096 \ -drive ifpflash,formatraw,readonlyon,file/usr/share/AAVMF/AAVMF_CODE.fd \ -drive ifpflash,formatraw,file./AAVMF_VARS.fd \ -drive ifnone,idhd0,formatqcow2,file./arm64-root.qcow2 \ -device virtio-blk-pci,drivehd0,bootindex1 \ -netdev user,idnet0,hostfwdtcp:127.0.0.1:2222-:22,hostfwdtcp:127.0.0.1:8080-:80 \ -device virtio-net-pci,netdevnet0 \ -device virtio-rng-pci \ -nographichostfwd可以叠加多条主机上访问127.0.0.1:8080就等于访问客户机的 80 端口调试 Web 服务很方便。SSH 是ssh -p 2222 用户名127.0.0.1。日常启停我直接写成两个 shell 脚本一个start.sh一个stop.shstop.sh里用监视器发system_powerdown走优雅关机实在不行再kill。因为 arm64 客户机在 TCG 下关机也要等十几秒直接Ctrl-C容易把文件系统搞成需要 fsck 的状态我吃过这个亏。4. 方法二交给 libvirtvirt-install 一条命令托管命令行方式什么都好就是每次要改点东西都得手工编辑一大串参数时间长了容易出错。做了几个 arm64 环境之后我逐渐把长期使用的虚拟机迁到了 libvirt 下面。4.1 从手搓命令切换到声明式定义的理由libvirt 带来的东西不是更简单而是可管理。它把虚拟机的定义存成一份 XML你可以随时virsh dumpxml查看完整配置可以virsh edit改一个字段可以用virsh snapshot-create-as打快照可以用virsh autostart设开机自启。这些能力在命令行模式下要么得自己写脚本实现要么干脆没有。另一个实际收益是日志和状态统一。命令行模式下虚拟机崩了你得自己去翻终端输出用 libvirtjournalctl -u libvirtd加上/var/log/libvirt/qemu/虚拟机名.log出错信息一目了然。曾经有一次我的 arm64 虚拟机跑到一半挂掉命令行方式下只能看到终端最后几行换成 libvirt 之后立刻在日志里看到是内存不足被 OOM 杀掉的问题定位时间从半小时缩短到两分钟。代价是多了一层抽象某些 QEMU 新特性和极冷门的参数不一定能从 libvirt 的 XML 里直接表达。我的做法是长期存在的环境用 libvirt临时验证、需要精细控制内核参数的环境还是手搓命令行。4.2 virt-install 的关键参数与 virsh 常用动作先确认 libvirt 已经识别到 aarch64 的模拟器virsh capabilities | grep -A3 aarch64 systemctl enable --now libvirtd能看到/usr/bin/qemu-system-aarch64就说明没问题。然后一条命令创建virt-install \ --connect qemu:///system \ --name arm64-vm \ --arch aarch64 \ --machine virt \ --cpu cortex-a72 \ --vcpus 4 \ --memory 4096 \ --disk path/var/lib/libvirt/images/arm64-vm.qcow2,size40,formatqcow2,busvirtio,cachenone,ionative,discardunmap \ --cdrom /var/lib/libvirt/images/ubuntu-24.04.2-live-server-arm64.iso \ --network networkdefault,modelvirtio \ --graphics none \ --console pty,target_typeserial \ --boot uefi \ --osinfo nameubuntu24.04几个关键点值得展开说--arch aarch64配合--machine virt告诉 libvirt 走的是跨架构仿真路径它会自动选择qemu-system-aarch64并在需要时加上-accel tcg。--boot uefi依赖 libvirt 的固件自动选择能力。如果你的发行版版本偏老这条可能报firmware 找不到那就换成长写法手动指定--boot loader/usr/share/AAVMF/AAVMF_CODE.fd,loader_royes,loader_typepflash,nvram_template/usr/share/AAVMF/AAVMF_VARS.fd--graphics none加上--console pty,target_typeserial是我最常用的组合因为服务端镜像走串口最省事。想用图形安装就换成--graphics vnc,listen0.0.0.0然后virsh vncdisplay arm64-vm拿到地址用任意 VNC 客户端连。--osinfo是较新版本 virt-install 的写法老版本叫--os-variant。这个参数看着无关紧要其实很有用它决定了 libvirt 给虚拟机生成的默认设备模型选错了可能导致网卡型号不被客户机识别。装好之后常用的几条virsh list --all # 看状态 virsh console arm64-vm # 串口登录退出按 Ctrl-] virsh domifaddr arm64-vm # 查客户机 IP virsh net-dhcp-leases default # 从 DHCP 租约反查 virsh snapshot-create-as arm64-vm snap1 装完基础环境 virsh edit arm64-vm # 改配置改完重启生效 virsh autostart arm64-vm # 开机自启virsh domifaddr这条命令我几乎每次都用。libvirt 默认网络是 NAT 模式客户机拿到的是 192.168.122.x 网段的地址用这条命令比进客户机敲ip a快得多。注意virsh console只有在客户机内部为串口设备启用了 getty 才能看到登录提示符否则你只能看到内核日志敲键盘没反应。这个问题后面第 6 章会专门讲怎么配。4.3 网络与远程访问default NAT、桥接、VNC 与串口登录libvirt 默认网络default本质是一台 NAT 路由器宿主机上多出virbr0这个虚拟网桥客户机通过它出网宿主机可以直接访问客户机的所有端口但局域网里其它机器访问不到。对绝大多数开发验证场景这已经够了。需要局域网其它机器访问时有两条路。一条是改 libvirt 网络定义加静态端口转发virsh net-edit default在forward modenat里加一段port start8080 end8080/之类的映射改完virsh net-destroy default virsh net-start default生效。另一条是直接用桥接网络把客户机接到物理网卡所在的二层网络里相当于客户机在局域网里隐身成一台独立设备。桥接配置需要宿主机网络支持有线网卡比较稳无线网卡通常桥不了或者桥完不通这一点先试再说别配完发现宿主机掉线。串口登录的配置在客户机内部装好系统后执行sudo systemctl enable --now serial-gettyttyAMA0.servicettyAMA0是 arm64virt机器上 PL011 串口的标准设备名这一点和 x86 上的ttyS0不同写错了就不生效。配好之后virsh console就能看到登录提示符了虚拟机出网络问题时也能靠这条通道进去救场——这是我强烈建议每个 arm64 虚拟机都配上的东西。我就遇到过一次客户机网络配置写错导致 SSH 全断全靠串口进去改回来的。5. 两套方案放在一起比成本、可控性与适用场景做完几轮之后我在团队里做了一张选型对照表新同事按这个表选基本不会走弯路维度手工 qemu-system-aarch64libvirt virt-install上手成本中等需要理解每个参数较低抽象掉了大部分细节参数控制粒度极高任何 QEMU 参数都能加高冷门参数需要手改 XML环境复现靠脚本和文档XML 文件即定义可直接版本管理快照能力需要qemu-img snapshot手工操作virsh snapshot-create-as一条命令日志排查终端输出或-serial file:统一的 libvirt 日志目录内核调试直接内核启动天然支持改-append即可需要在 XML 里写kernel和cmdline适合场景一次性验证、内核参数调试、精细控制长期使用的开发环境、多环境并存再补一句经验这两套方案不冲突。我现在的做法是用 libvirt 管着几台常驻的 arm64 环境同时保留一份命令行脚本用于快速起一个用完就删的临时环境。临时环境用命令行的原因是开得快、删得干净不会在 libvirt 里留一堆废弃定义。6. 装完只是开始TCG 环境下的性能与稳定性调优系统装好能登录这只是起点。arm64 虚拟机在 TCG 环境下的默认配置其实并不高效下面这几处调整我每次都会做。6.1 磁盘层cache、io、discard 的组合拳磁盘参数对体验的影响比想象中大。我在同一台机器上做过一轮对比客户机跑make -j4磁盘参数组合相对耗时说明cachewriteback,iothreads默认100%通用安全性和性能折中cachenone,ionative约 82%绕过宿主页缓存性能更好cacheunsafe,ionative约 70%最快但宿主崩溃可能丢数据追加discardunmap且客户机定期 fstrim约 78%长期使用后避免镜像虚拟盘无限膨胀cachenone配合ionative是我在宿主机磁盘是 SSD 时的首选收益明显而且数据安全。cacheunsafe只在一次性、可重建的环境里用比如做完就删的验证机。discardunmap这条容易被忽略qcow2 镜像有个特点客户机删除文件后宿主镜像不会自动变小长期跑下来能涨到几十 G。加上这个参数并在客户机里配好定期fstrim镜像增长就能控制住。另外镜像格式上如果客户机需要频繁大量写把系统盘从 qcow2 换成 raw 会快一些代价是失去快照能力和精简置备。我的习惯是开发环境用 qcow2 图方便跑 IO 密集型测试时临时转成 raw。6.2 内存与 CPU-smp、tb-size、内存超分CPU 这块有三条经验。第一vCPU 数量不要超过宿主机物理核数。TCG 多线程模式下每个 vCPU 对应一个宿主线程你给客户机 8 核而宿主只有 4 个物理核结果就是线程互相抢时间片整体反而更慢。我一般给物理核数的一半到三分之二给宿主机留出余量。第二CPU 型号选max还是cortex-a72有讲究。cortex-a72是较早的型号兼容性最好几乎所有 arm64 发行版都能跑max会暴露宿主 QEMU 支持的全部特性包括 LSE 原子指令这类扩展客户机内核如果支持内部锁操作的性能提升相当明显。我的判断方式是先用cortex-a72确保能装上装好之后再改成max试一次如果客户机启动正常并且dmesg里没有异常就保留max。第三调大 TCG 翻译缓存。默认 32MB 的翻译块缓存对于大程序来说偏小加上-accel tcg,threadmulti,tb-size1024后重复执行的代码命中缓存的概率更高。这条在编译类负载下能有 5% 到 10% 的改善属于低成本收益。内存方面-m给足是前提4GB 是舒适线2GB 是底线。宿主内存紧张时可以用-overcommit mem-lockoff允许超分但客户机实际使用量超过物理内存时表现会急剧恶化不建议在跑编译任务时开。6.3 时间漂移、串口日志与快照TCG 环境下客户机时钟漂移是必然的因为它依赖宿主提供的虚拟计时器而仿真执行的时间和真实时间对不上。表现是客户机里date命令显示的时间越跑越偏严重时会影响依赖时间戳的构建工具。解决方案很简单客户机里装chrony或systemd-timesyncd并启用网络通了之后自动校正。这个步骤我建议写进装机后的标准流程里。串口日志值得单独留一份配置。把上面命令里的-nographic换成-display none -serial file:serial.log客户机的所有 console 输出都会落到文件里包括崩溃时的内核栈回溯。相比在终端里往上翻文件更好搜、更好贴给同事看。这个习惯帮我定位过好几次内核 panic。快照方面qcow2 格式支持内部快照命令行方式是qemu-img snapshot -c before-upgrade arm64-root.qcow2 qemu-img snapshot -l arm64-root.qcow2 qemu-img snapshot -a before-upgrade arm64-root.qcow2libvirt 那边则用virsh snapshot-create-as和virsh snapshot-revert。升级内核、改 fstab、动网络配置之前先打一个快照翻车了能一秒回退这个习惯值得养成。我有一次在客户机里改/etc/fstab写错了分区 UUID重启直接进不去系统靠快照回退省了重装的两小时。6.4 补充一条捷径直接内核启动做内核与驱动调试如果你做的是内核模块开发或者驱动验证前面那套完整 UEFI 引导流程其实可以跳过。QEMU 支持直接指定内核和 initramfsqemu-system-aarch64 \ -machine virt,gic-version3 \ -accel tcg,threadmulti \ -cpu max -smp 2 -m 2048 \ -kernel /boot/vmlinuz-6.8.0-generic \ -initrd /boot/initrd.img-6.8.0-generic \ -append consolettyAMA0 root/dev/vda1 rw nokaslr \ -drive ifnone,idhd0,formatraw,file./rootfs.img \ -device virtio-blk-pci,drivehd0 \ -nographic内核和 initramfs 从哪来把它们从已装好的客户机镜像里拷出来就行挂载镜像或者从客户机里scp出来。这条路省掉了 UEFI 初始化和引导加载器的全部时间我实测下来启动快一半以上而且-append里想加什么内核参数随手就加做earlycon、nokaslr、init/bin/bash这类调试时特别顺手。有一点要注意root后面得写客户机里真实的根分区标识用PARTUUID最稳避免设备名变化导致找不到根分区。可以直接从客户机里blkid拿到。7. 报错排查实录几个最典型的翻车现场我把这几年遇到的报错按出现频率整理了一下前面那个是症状后面是实际原因。排查的关键是不要一上来就改参数先把症状和可能原因对照一遍。7.1 UEFI 起来了却直接掉进 EFI Shell这是我最常遇到的一个。现象是终端里能看到固件日志但接着没有出现 GRUB 菜单而是停在一个Shell提示符下。原因基本是三类。第一光驱设备加的方式不对。arm64 的virt机器没有 IDE 总线如果你用了-device ide-cd设备压根没挂上UEFI 自然找不到可引导介质。正确做法是virtio-scsi-pci加scsi-cd的组合。第二bootindex没写或者两个设备的 bootindex 相同UEFI 的启动顺序无法确定。第三ISO 文件里的引导文件路径是EFI/BOOT/BOOTAA64.EFI如果这个文件缺失比如镜像下载不完整也进不去。在 EFI Shell 里可以手工验证一下敲map看有哪些设备fs0:切过去ls EFI\BOOT\看文件在不在然后在那个目录下执行BOOTAA64.EFI试试能不能拉起来。能拉起来说明镜像没问题就是启动顺序的配置问题拉不起来说明镜像本身有问题去查第 7.3 条。7.2 GIC 版本不对导致内核卡在 earlycon 无输出症状是GRUB 之后屏幕一片黑或者只有一行很早期的内核输出然后就没有然后了。加了earlycon参数也看不到东西。这是中断控制器版本不匹配。virt机器默认可能给 GICv2但较新的 arm64 内核默认按 GICv3 初始化两者对不上内核在早期初始化阶段就挂住了。解决办法是显式指定-machine virt,gic-version3反过来也有情况某些使用旧内核的 arm64 发行版只支持 GICv2那你得写gic-version2。判断方法很简单两个都试一遍哪个能启动就用哪个。如果两个都试过还是卡住可以在 guest 内核参数里加earlyconpl011,0x9000000强制把 PL011 串口的早期输出打出来。0x9000000是 arm64virt机器上串口的固定地址这个值记一下调试时会反复用到。7.3 ISO 校验失败与源损坏伪装成安装报错这个坑最隐蔽因为它表现出来是各种各样的安装报错很容易让人误以为是参数问题。我遇到过一次GRUB 能起来安装器也能启动但走到解压 ramdisk 那一步报了个很含糊的镜像格式错误看着很像文件解压乱码那种问题。排查方式是回到根上做校验sha256sum ubuntu-24.04.2-live-server-arm64.iso # 与官网公布的校验值逐位比对对不上就是文件坏了。这种情况多半是下载中断后续传、或者从非官方渠道拿到的文件不完整导致的。重新下载并校验问题消失。所以我现在养成的习惯是镜像拿到手第一件事就是校验不校验不往下走。几分钟的校验能省掉几个小时的无效排查。另外还有一类相关的现象镜像文件挂载路径里有空格或者中文字符QEMU 解析参数时被截断表现是文件找不到。路径尽量用英文和短横线是个成本极低的防御性习惯。7.4 几个一句话能说完但很容易忘的坑最后补几条零碎的都是实际踩过的-cpu host在跨架构场景下会直接报错提示需要 KVM 支持。别怀疑 QEMU 装错了换成cortex-a72或max就行。显式写-accel kvm会报该目标架构不支持 KVM。这不是配置问题是硬件层面的限制跨架构没有硬件虚拟化可用老老实实用 TCG。内存给到 512MB 时 UEFI 可能根本引导不起来。arm64 的 UEFI 固件本身要占一部分加上内核解压需要空间1GB 是安全底线2GB 是舒适线。-nographic模式下以为卡死了其实是程序在等输入。Ctrl-A再按X是退出Ctrl-A再按C是切到监视器这两个快捷键记下来能省不少慌乱。我有一次以为虚拟机崩了等了十分钟才发现是它早就退出了终端只是停留在了监视器里。虚拟机里的时间一天能漂出去十几分钟。装上时间同步服务之后这个问题就没了且一定要在客户机里配在宿主机上怎么调参数都没用。我个人在实际操作中的体会是arm64 虚拟机的搭建难度其实不在命令本身而在于把跨架构仿真这个前提时刻记在脑子里。一旦接受了性能的量级、接受了 UEFI 固件必须自己准备、接受了显示和串口要显式配置这三件事剩下的就是按流程走。现在我建一台新的 arm64 验证环境从敲第一条命令到能 ssh 进去cloud image 路线大约十五分钟ISO 路线半小时到一小时其中大部分时间是等它自己跑。真正需要动脑子的是出问题的时候——而几乎所有问题的答案都藏在这套流程里哪一步和 x86 不一样这个问题里。