从零构建ARM64 Ubuntu 20.04最小系统:QEMU模拟与实战指南
1. 为什么要在QEMU里折腾一个ARM64的Ubuntu最小系统?
你可能正在用一台普通的x86电脑,也许是Intel的,也许是AMD的。但你的项目、你的代码,最终要跑在一块ARM架构的开发板上,比如树莓派、比如NVIDIA Jetson系列,或者各种国产的AIoT芯片。这时候问题就来了:你总不能每次调试都抱着那块开发板,或者频繁地往SD卡里烧写系统吧?效率太低,也容易把硬件折腾坏。
这就是我们今天要聊的场景:在你自己熟悉的x86电脑上,用QEMU模拟器,凭空“造”出一个ARM64架构的Ubuntu 20.04最小系统。这个系统是“最小”的,意味着它只包含最核心的运行环境,没有图形界面,没有多余的软件,非常轻量。但它又是一个“完整”的Linux系统,你可以在里面安装软件、编译代码、调试程序,甚至配置网络服务。
我这些年做嵌入式开发和AI模型部署,这种“模拟环境”是我的日常。它就像一个安全的沙盒,让你可以大胆地尝试各种系统配置、软件安装,甚至搞崩了也无所谓,反正只是一个虚拟的文件。等一切调试完美了,再把整个系统镜像“烧录”到真实的硬件上,成功率会高很多。对于想学习ARM Linux系统构建、或者为特定硬件定制轻量级系统的朋友来说,这绝对是性价比最高的入门和实践方式。接下来,我就手把手带你走一遍完整的流程,把过程中容易踩的坑也一并告诉你。
2. 准备工作:理清思路与备齐工具
在动手之前,我们得先搞清楚整个过程的“地图”。核心思想其实就两步:获取一个ARM64架构的Ubuntu基础文件系统(rootfs),然后在x86主机上通过QEMU的用户态模拟(qemu-user-static)来运行这个ARM64环境。听起来有点绕?我打个比方:你的x86电脑是主会场(Host),QEMU用户态模拟器就像一个精通ARM64语言的同声传译。当ARM64程序(Guest)想说话(执行指令)时,这位“同声传译”会实时翻译给x86 CPU听,让它能理解并执行。
所以,你需要准备以下东西:
- 一台x86_64的Linux主机:Ubuntu 20.04/22.04或者Debian都可以,我实测下来最稳。如果你是Windows或macOS用户,建议先装个WSL2或者虚拟机跑个Ubuntu,因为后续很多操作在原生Linux环境下最顺畅。
- 一个干净的ARM64 Ubuntu基础文件系统:我们将从Ubuntu官方获取最小化的
ubuntu-base镜像。 - QEMU用户态静态二进制文件:这是实现跨架构运行的关键魔法棒,名字叫
qemu-aarch64-static。
2.1 安装必要的宿主系统工具
首先,我们更新一下系统并安装最基础的依赖。打开你的终端,执行以下命令:
sudo apt update sudo apt upgrade -y sudo apt install -y wget curl tar xz-utils binfmt-support这些工具是下载、解压和后续处理的基础,先装上准没错。
2.2 获取ARM64 Ubuntu基础根文件系统
Ubuntu官方提供了专门用于构建自定义系统的基础镜像,叫做ubuntu-base。它非常精简,是我们打造最小系统的完美起点。
# 创建一个专门的工作目录,保持环境整洁 mkdir -p ~/qemu_arm64_ubuntu && cd ~/qemu_arm64_ubuntu # 从Ubuntu官方镜像站下载20.04 LTS版本的ARM64基础包 # 注意:这里我使用了清华大学的镜像源,速度会快很多。你也可以替换成其他国内源或官方源。 wget https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cdimage/ubuntu-base/releases/20.04.5/release/ubuntu-base-20.04.5-base-arm64.tar.gz这里有个小细节:原始文章里用的20.04.3版本,我这里直接用了最新的20.04.5小版本。用哪个都行,核心是20.04这个LTS版本。下载完成后,你应该能看到一个大约50MB左右的压缩包。
接下来,我们创建并解压根文件系统:
# 创建根文件系统的目录 sudo mkdir rootfs # 使用sudo解压到rootfs目录,保留文件权限至关重要! sudo tar -xzf ubuntu-base-20.04.5-base-arm64.tar.gz -C rootfs解压后,rootfs目录里就是未来我们ARM64 Ubuntu系统的全部家当了。现在它还只是一个“骨架”,没有灵魂(无法运行)。
3. 注入灵魂:配置QEMU用户态模拟与Chroot环境
现在,我们要让这个ARM64的“骨架”能在我们x86的身体里动起来。关键就是qemu-aarch64-static这个静态编译的模拟器。它不需要在目标系统(rootfs)里安装任何依赖,直接拷贝进去就能用。
3.1 安装并配置QEMU静态模拟器
在你的x86宿主系统上安装qemu-user-static包,它包含了多种架构的静态模拟器。
sudo apt install -y qemu-user-static安装完成后,我们需要把这个模拟器“植入”到ARM64的文件系统里:
# 将aarch64架构的静态模拟器复制到目标根文件系统的/usr/bin目录下 sudo cp /usr/bin/qemu-aarch64-static rootfs/usr/bin/这一步是核心魔法。当你在chroot环境(即rootfs)里尝试运行ARM64程序时,系统内核会识别出这是一个ARM64的可执行文件,然后自动调用rootfs/usr/bin/qemu-aarch64-static来翻译执行它。整个过程对用户是透明的,感觉就像直接在运行ARM64程序一样。
3.2 配置网络与挂载脚本
在进入这个虚拟系统之前,还有几项准备工作。首先是网络,我们需要让chroot环境能访问外部网络来安装软件。
# 备份目标系统自带的resolv.conf(通常是空的或指向本地) sudo mv rootfs/etc/resolv.conf rootfs/etc/resolv.conf.bak 2>/dev/null || true # 将宿主机的DNS配置复制进去,这样chroot里才能解析域名 sudo cp /etc/resolv.conf rootfs/etc/然后是挂载。Linux系统运行时依赖/proc,/sys,/dev等虚拟文件系统。我们需要在chroot前,把这些宿主机的关键目录“绑定挂载”到目标系统的对应路径下。
手动挂载卸载太麻烦,我习惯写一个脚本。在~/qemu_arm64_ubuntu目录下创建ch-mount.sh:
#!/bin/bash # 这是一个非常实用的chroot挂载/卸载脚本 function mnt() { echo "正在挂载必要的虚拟文件系统到 $1" sudo mount -t proc /proc "$1/proc" sudo mount -t sysfs /sys "$1/sys" sudo mount -o bind /dev "$1/dev" sudo mount -o bind /dev/pts "$1/dev/pts" # /run 有时会有问题,如果后续服务启动异常,可以尝试取消注释下面这行 # sudo mount -o bind /run "$1/run" } function umnt() { echo "正在卸载虚拟文件系统从 $1" sudo umount "$1/proc" sudo umount "$1/sys" sudo umount "$1/dev/pts" # sudo umount "$1/run" sudo umount "$1/dev" } # 脚本的使用方法 if [ "$1" = "-m" ] && [ -n "$2" ]; then mnt "$2" elif [ "$1" = "-u" ] && [ -n "$2" ]; then umnt "$2" else echo "用法:" echo " $0 -m <rootfs路径> # 挂载并chroot进入" echo " $0 -u <rootfs路径> # 卸载" echo "" echo "示例:" echo " $0 -m ./rootfs # 进入ARM64环境" echo " 在chroot中执行操作后,输入'exit'退出" echo " $0 -u ./rootfs # 退出后清理挂载" fi给脚本加上执行权限:
chmod +x ch-mount.sh这个脚本比我之前见过的很多版本更清晰,它把挂载(-m)和卸载(-u)分开了,这样你可以在chroot环境里工作很久,退出后再统一卸载,逻辑更清楚。
4. 首次进入系统:换源与基础配置
激动人心的时刻到了,我们要第一次“启动”这个ARM64的Ubuntu系统了。
4.1 进入Chroot环境
使用我们刚写的脚本挂载并进入:
sudo ./ch-mount.sh -m ./rootfs如果一切顺利,你的命令提示符会变成类似root@ubuntu:/#的样子。注意,这个root和ubuntu主机名是镜像里默认的,你现在已经身处ARM64的虚拟系统中了!你可以用uname -m验证一下,应该输出aarch64。
4.2 更换APT软件源
系统自带的官方源在国外,速度慢到怀疑人生。第一步必须是换源。在chroot环境里执行:
# 先安装一个编辑器,原系统里连nano都没有 apt update apt install -y nano备份原来的源列表文件,然后用清华大学的ARM64专用源(ubuntu-ports)替换。这里我提供一个更简洁的20.04(代号focal)配置:
cat > /etc/apt/sources.list << 'EOF' # 清华大学 Ubuntu-ports 镜像源 (ARM64) deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal-updates main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal-backports main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ focal-security main restricted universe multiverse EOF更新软件包列表:
apt update如果看到一大串包列表在快速滚动,恭喜你,换源成功了,网络也是通的。
4.3 安装最核心的系统组件
最小系统真的非常“最小”,连sudo、bash-completion、systemd的一些必要组件都可能不完整。我们安装一些让系统更可用的基础包:
# 安装系统基础工具和网络工具 apt install -y sudo bash-completion systemd-sysv net-tools iputils-ping curl wget # 安装locales,避免后续软件安装出现语言环境警告 apt install -y locales # 生成en_US.UTF-8 locale,这是开发环境的常用设置 locale-gen en_US.UTF-85. 系统个性化:用户、主机名与基础服务
一个能用的系统,得有用户,得有主机名,一些基础服务也得安排上。
5.1 创建新用户并设置Root密码
一直用root用户不安全,我们创建一个普通用户,并赋予其sudo权限:
# 创建一个名为‘ubuntu’的用户,主目录为/home/ubuntu,默认shell为bash useradd -m -s /bin/bash -G sudo ubuntu # 为ubuntu用户设置密码(比如设置为‘ubuntu’) echo "ubuntu:ubuntu" | chpasswd # 也为root用户设置一个强密码(务必修改!) echo "root:YourStrongRootPassword123!" | chpasswd重要提醒:上面的root密码是示例,在实际使用中,尤其是在任何可能联网的环境里,一定要设置一个复杂且唯一的密码。
5.2 设置主机名与时区
# 将主机名设置为‘qemu-arm64’ echo "qemu-arm64" > /etc/hostname # 设置时区为上海(亚洲/上海),如果你在其他地区,可以搜索/usr/share/zoneinfo下的名称 ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime5.3 配置网络(Netplan)
Ubuntu 20.04默认使用Netplan管理网络。我们配置一个简单的DHCP客户端,让系统启动后能自动获取IP(在QEMU虚拟网络或桥接网络中)。
cat > /etc/netplan/01-netcfg.yaml << 'EOF' network: version: 2 renderer: networkd ethernets: eth0: dhcp4: yes dhcp6: no optional: true EOF这个配置告诉系统,对于网卡eth0,使用DHCPv4自动获取IP地址。networkd是systemd自带的网络管理器,非常轻量,适合服务器和无图形界面环境。
6. 安装更多实用软件与开发环境
基础系统有了,但要做开发还远远不够。我们可以根据需求安装更多软件。这里我分成“通用工具”和“开发环境”两类。
6.1 通用工具集
这些工具能极大提升你在终端下的工作效率:
apt install -y vim htop tmux git rsync build-essential pkg-config apt install -y software-properties-common apt-transport-https ca-certificatesvim/htop/tmux:分别是编辑器、进程监控和终端复用神器。build-essential:包含了gcc, g++, make等编译套件,是编译任何C/C++项目的基础。ca-certificates:更新CA证书,避免HTTPS访问出错。
6.2 针对嵌入式开发的常用库
如果你打算在这个系统里交叉编译或运行一些嵌入式常见的软件,可能需要这些库:
apt install -y libssl-dev libffi-dev python3-dev python3-pip apt install -y device-tree-compiler u-boot-tools bcdevice-tree-compiler和u-boot-tools对于玩开发板的朋友应该不陌生,它们是处理设备树和U-Boot引导程序的重要工具。
6.3 清理与退出
安装完所有需要的软件后,最好做一次清理,减小系统体积:
# 清理下载的软件包缓存 apt clean # 删除不需要的文档和手册页(对于最小系统可选) rm -rf /usr/share/doc/* /usr/share/man/*现在,输入exit退出chroot环境。然后回到宿主机的终端,运行脚本卸载挂载点:
sudo ./ch-mount.sh -u ./rootfs7. 制作可引导的磁盘镜像
到目前为止,我们有了一个可以chroot进去玩的文件系统目录。但要想让它在QEMU里像一个真正的机器一样独立启动,或者烧录到SD卡,我们需要把它打包成一个完整的磁盘镜像文件。
7.1 创建空白镜像并格式化
首先,估算一下rootfs目录的大小,然后创建一个稍大一点的镜像文件,留出增长空间。
# 回到宿主机的工作目录 cd ~/qemu_arm64_ubuntu # 查看rootfs目录实际大小(单位:MB) du -sh rootfs # 假设显示为350M,我们创建一个600M的镜像,留出250M空间 dd if=/dev/zero of=ubuntu-arm64.img bs=1M count=600接下来,在这个空镜像上创建分区表和一个Linux分区,并格式化为ext4文件系统。这里我们用parted和mkfs.ext4工具:
# 给镜像文件建立MS-DOS分区表并创建一个主分区 sudo parted ubuntu-arm64.img --script mklabel msdos sudo parted ubuntu-arm64.img --script mkpart primary ext4 1MiB 100% # 找到新创建的循环设备(注意:你的设备名可能不同,通常是loop0, loop1等) LOOP_DEV=$(sudo losetup --find --show --partscan ubuntu-arm64.img) echo "镜像挂载在: $LOOP_DEV" # 格式化第一个分区为ext4 sudo mkfs.ext4 "${LOOP_DEV}p1"7.2 将rootfs写入镜像分区
现在,挂载这个镜像分区,并把我们精心配置好的rootfs目录整个拷贝进去。
# 创建一个临时挂载点 mkdir -p /tmp/mnt-rootfs # 挂载镜像的第一个分区 sudo mount "${LOOP_DEV}p1" /tmp/mnt-rootfs # 使用rsync同步文件,保留所有权限、链接等信息,比cp -r更可靠 sudo rsync -aHAX rootfs/ /tmp/mnt-rootfs/ # 同步完成后卸载 sudo umount /tmp/mnt-rootfs # 卸载并释放循环设备 sudo losetup -d "$LOOP_DEV"7.3 镜像优化与检查
最后,我们可以检查并尝试修复镜像文件系统的错误,还可以压缩一下未使用的空间。
# 检查ext4文件系统 e2fsck -p -f ubuntu-arm64.img # (可选)调整文件系统大小以完全填充分区(如果之前分区留的空间太大) # resize2fs -M ubuntu-arm64.img现在,你就得到了一个名为ubuntu-arm64.img的磁盘镜像文件。这个文件可以直接被QEMU虚拟机使用,也可以使用dd或balenaEtcher等工具烧写到真实的ARM64开发板的SD卡或eMMC中。
8. 使用QEMU系统模式完整启动你的ARM64 Ubuntu
前面我们一直用的是用户态模拟(qemu-user-static + chroot),这适合在宿主机环境下“嵌入”一个目标架构的环境。而系统模式模拟则是用QEMU虚拟出一台完整的ARM64计算机(包括CPU、内存、磁盘、网络),然后从这个磁盘镜像启动。这更接近真实硬件环境。
8.1 安装QEMU系统模拟器
我们需要安装qemu-system-arm,它包含了模拟ARM架构完整机器所需的组件。
sudo apt install -y qemu-system-arm qemu-utils8.2 准备启动内核与设备树
一个完整的系统启动需要内核(Kernel)。我们可以为ARM64虚拟机编译一个内核,更简单的方法是直接使用现成的。例如,Debian/Ubuntu提供了针对QEMU的ARM64内核包:
# 下载适用于ARM64 QEMU的内核和initrd sudo apt install -y linux-image-generic-arm64 # 安装后,内核通常位于/boot/vmlinuz-*,但我们需要专门为ARM64的。 # 一个更直接的方法是下载QEMU官方提供的“virt”机器固件,它包含了可启动的内核。 # 这里我们使用一个更通用的方法:从Ubuntu Cloud镜像获取内核。 # 创建一个boot目录存放启动文件 mkdir -p boot cd boot # 下载Ubuntu Cloud镜像的内核(这是一个通用的ARM64内核) wget https://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-arm64-vmlinuz-generic wget https://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-arm64-initrd-generic # 重命名以便使用 mv focal-server-cloudimg-arm64-vmlinuz-generic vmlinuz mv focal-server-cloudimg-arm64-initrd-generic initrd.img cd ..8.3 编写启动脚本并运行
创建一个启动脚本run_qemu.sh,配置虚拟机的参数:
#!/bin/bash # 定义变量 IMAGE="ubuntu-arm64.img" KERNEL="boot/vmlinuz" INITRD="boot/initrd.img" # QEMU启动参数 # -machine virt: 模拟ARM的“virt”通用机器,支持PCI、USB等。 # -cpu cortex-a57: 指定CPU模型为Cortex-A57,这是ARMv8-A架构的经典款。 # -smp 2: 给虚拟机2个CPU核心。 # -m 1024: 分配1GB内存。 # -drive file=${IMAGE},format=raw,if=virtio: 使用我们的系统镜像,以virtio设备形式提供,性能更好。 # -kernel 和 -initrd: 指定内核和初始内存盘。 # -append "root=/dev/vda1 console=ttyAMA0": 内核参数,告诉内核根文件系统在第一个virtio磁盘,控制台串口。 # -netdev user,id=net0 -device virtio-net-device,netdev=net0: 配置一个用户模式网络,虚拟机可以通过NAT上网。 # -nographic: 不使用图形界面,直接输出到当前终端。按Ctrl+A,然后按X可以退出QEMU。 qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -smp 2 \ -m 1024 \ -drive file="${IMAGE}",format=raw,if=virtio \ -kernel "${KERNEL}" \ -initrd "${INITRD}" \ -append "root=/dev/vda1 console=ttyAMA0" \ -netdev user,id=net0 \ -device virtio-net-device,netdev=net0 \ -nographic给脚本执行权限并运行:
chmod +x run_qemu.sh ./run_qemu.sh如果一切配置正确,你将看到内核启动信息滚动,最终进入系统登录提示符。你可以用之前创建的ubuntu用户(密码ubuntu)或root用户登录。登录后,你就拥有了一台完全独立运行的、ARM64架构的Ubuntu 20.04虚拟机了!你可以在这里面进行更彻底的测试,比如启动SSH服务,安装Docker,编译你的应用程序,所有操作都和在一台真实的ARM64服务器上一样。
9. 进阶技巧与避坑指南
走完上面的流程,你应该已经成功构建并运行了系统。但在实际项目中,你可能会遇到更多需求。这里分享几个我踩过坑后总结的进阶技巧。
9.1 如何让镜像更小?
最小系统的追求就是“小”。除了安装时只选必要的包,还可以在制作镜像后进一步“瘦身”:
- 使用
fstrim或zerofree:在chroot环境里,安装zerofree,然后在卸载镜像前对根分区运行它,可以将未使用的块清零,便于后续压缩。# 在chroot环境中 apt install -y zerofree exit # 回到宿主机,挂载镜像后,对分区运行zerofree(需要卸载后以只读方式重新挂载) - 转换为压缩格式:将raw镜像转换为qcow2格式,它支持稀疏存储和动态扩容,实际占用的磁盘空间更小。
qemu-img convert -f raw -O qcow2 ubuntu-arm64.img ubuntu-arm64.qcow2 - 清理APT缓存和日志:如前所述,
apt clean和删除/var/log/*.log、/var/log/*.gz能释放不少空间。
9.2 网络配置的更多玩法
我们之前用的-netdev user是用户模式网络,简单但有限(虚拟机可以访问外网,宿主机不能直接访问虚拟机)。如果你需要更复杂的网络,比如桥接网络让虚拟机和宿主机在同一局域网,可以这样:
- 在宿主机上创建Linux桥接设备(如
br0)。 - 使用TAP设备将QEMU虚拟机连接到桥接设备。
- QEMU参数改为:
同时需要提前用-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \ -device virtio-net-device,netdev=net0sudo ip tuntap add tap0 mode tap创建tap设备并加入桥接。
9.3 内核配置的坑
如果你打算为真实开发板编译自定义内核,并在这个QEMU环境里测试,有两点内核配置至关重要,这也是原始文章末尾提到的:
CONFIG_DEVTMPFS=y:这个选项会启用devtmpfs,它在系统启动早期自动创建/dev下的设备节点。没有它,你的/dev目录可能是空的,导致很多设备无法访问。CONFIG_DEVTMPFS_MOUNT=y:这个选项让内核在启动时自动挂载devtmpfs到/dev。通常和上一个选项一起开启。
在真实硬件启动时,如果缺少这些配置,你可能会卡在“Waiting for /dev to be fully populated...”这类信息上。在QEMU里测试内核时,同样需要注意。
9.4 关于Systemd和串口控制台
我们使用console=ttyAMA0作为内核参数,这是ARM“virt”机器默认的串口。在系统内,你需要确保有一个getty服务在这个串口上运行,才能看到登录提示。在Ubuntu 20.04中,systemd通常会通过serial-getty@ttyAMA0.service自动处理。如果没出现登录提示,可以尝试在chroot环境里启用它:
systemctl enable serial-getty@ttyAMA0.service构建一个最小系统并让它跑起来,就像搭乐高一样,从一堆散件开始,按照正确的顺序和逻辑组装,最终得到一个能运行的整体。这个过程里最享受的,就是那种完全掌控的感觉——你知道系统里每一个主要组件是怎么来的,为什么要装它。下次当你在真实的ARM开发板上遇到诡异的问题时,这个在QEMU里构建的、纯净可控的最小系统,就是你最好的调试和验证基地。我自己的项目里,但凡涉及到系统层面的改动,一定会先在QEMU的虚拟环境里跑通一遍,确认无误再上真机,这个习惯帮我省下了无数个小时的折腾时间。希望这份指南也能帮你建立起自己的ARM64开发沙盒,玩得开心。
