☰
ARM64嵌入式开发:从零构建Ubuntu Base rootfs全指南
2026/10/17 21:09:19 网站建设 项目流程

做 ARM64 嵌入式开发,绕不开一个问题:板子的文件系统到底从哪来。最早我也是直接下载现成的镜像烧进去,图省事,可用了几个项目之后问题越来越多——系统里塞了一堆用不上的软件包,内核版本和项目 SDK 对不上,想删点东西又不敢动,怕把系统搞坏。后来干脆自己从 Ubuntu Base 构建一个 ARM64 的根文件系统,整个流程一旦走通,你对系统的掌控力完全不一样。

这篇文章就记录我自己构建 Ubuntu Base ARM64 rootfs 的完整过程:从下载官方极简根文件系统开始,到 x86 宿主环境配置、chroot 进入系统安装必要组件、配置网络和账号,再到用 QEMU 在 PC 上提前验证启动,最后落到实机部署。内容偏实战,适合已经有内核和 bootloader、只缺用户空间的嵌入式开发者,也适合想给自己的产品定制一套干净根文件系统的人。

1. 为什么折腾:现成镜像不够用,才轮到自己拼 rootfs

1.1 一个 rootfs 里其实只装这几样东西

先把概念摆清楚。rootfs 就是根文件系统,是启动后内核挂载的那个“/”所指向的完整目录树,里面包含/bin、/sbin、/etc、/usr、/lib、/var这些基本目录,以及系统启动必需的一批程序:init 一号进程、shell、核心命令行工具、包管理器、网络配置工具等等。它不包含内核,也不包含 bootloader——这是很多人第一次接触时容易混的点。

而 Ubuntu Base 就是 Ubuntu 官方为各硬件架构发布的极简 rootfs 压缩包。它和我平时用的桌面版、服务器版镜像不一样:没有安装界面、没有内核、没有图形环境,只有能支撑起一个 Linux 系统的最少组件。里面已经预置了 dpkg、apt、coreutils 和一套默认的/etc配置,这意味着你拿到手之后可以马上用 apt 安装任何需要的包,从零开始重建出任何形态的系统。

我比较常用的是 22.04 版本的 Ubuntu Base,它同时提供 amd64、arm64、armhf、riscv64 等多个架构的压缩包。做嵌入式项目时,目标是 ARM64(aarch64)的最多,因为 64 位 ARM 既兼顾了功耗,又能跑现代 Linux 的完整特性,很多边缘计算、工控、车载设备都在这个平台。

1.2 现成系统镜像的三个代价

为什么不直接烧录一个现成镜像?我用一张表总结过几类痛点:

问题具体表现
黑盒镜像里预装了什么软件、启用了哪些服务,完全不可知
定制难想换成自己的 busybox、改启动流程、做极简系统,无从下手
版本绑定镜像的内核、库、工具链版本往往固定,和项目 SDK 对不上
臃肿动辄几个 GB,量产时烧录时间长、浪费存储

自己构建 rootfs 之后,这个问题就变成“我手里有什么是一清二楚的”,每个包都是亲手装进去的,出问题排查路径也很清晰。它的代价是需要自己处理网络、账号、启动参数这一整条链路——但反过来讲,这恰恰是嵌入式开发者应该完整走一遍的过程。

当然,如果只是想在板子上快速跑个 demo,那直接烧现成镜像没毛病。但只要是奔着产品化、定制化、长期维护去的,自建 rootfs 基本上是绕不开的一步。

2. 宿主环境准备:x86 PC 上模拟 ARM64 指令集的关键

2.1 binfmt_misc 与 qemu-user-static 的配合原理

我的开发机是 x86_64 架构,而目标 rootfs 是 ARM64,两者的可执行文件格式不同。x86 的 CPU 不能直接跑 ARM64 的二进制,所以我需要一个办法,让宿主机在执行 ARM64 程序时,把它翻译成 x86 指令。

这个环节由两部分完成:qemu-user-static和内核的binfmt_misc机制。

  • qemu-user-static是 QEMU 的用户态模拟版本,它的特点是编译成静态二进制,不依赖宿主机的动态库,放进 chroot 环境之后可以正常工作。
  • binfmt_misc是内核提供的一种机制,允许你为不同的二进制格式注册解释器。内核读取 ELF 文件头部,如果识别出是 aarch64 架构,就自动交给 qemu-aarch64-static 处理。

两者配合的效果就是:在 x86 宿主上进入 ARM64 的 chroot 环境后,执行apt、systemctl这些 ARM64 版本的命令时,系统会自动调用模拟器,看起来就像原生执行一样。这也是后面所有配置工作能进行的前提。

2.2 工具安装与注册检查

在 Ubuntu 宿主上安装这几样东西:

sudo apt update sudo apt install -y wget xz-utils qemu-user-static binfmt-support

安装完成后,检查解释器是否注册成功:

ls /proc/sys/fs/binfmt_misc/ | grep qemu

正常会看到qemu-aarch64这一项。如果没有,可以手动激活:

sudo systemctl restart systemd-binfmt

这一步经常有人忽略。我曾在某个没有 systemd 的宿主环境里折腾了半天,后来发现是/proc/sys/fs/binfmt_misc/根本没有注册项,ARM64 的二进制一执行就报Exec format error。所以建议开始构建前先跑一遍上面的检查,把这个变量排除掉。

另外,如果宿主内核比较老,或者宿主系统本身不支持 binfmt_misc,就需要改用完整系统模拟的方案,比如直接起一台 ARM64 虚拟机来构建。但绝大多数现代 Linux 发行版都默认支持,按上面步骤装完就能用。

3. 获取 Ubuntu Base 与搭建 rootfs 目录

3.1 下载与校验

Ubuntu 官方提供了单独的 Ubuntu Base 下载入口,在 cdimage 站点的 ubuntu-base 发布目录里,按发行版本和架构区分文件。我下载的是 22.04 对应 ARM64 的压缩包:

wget https://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04-base-arm64.tar.gz

下载完第一件事不是解压,而是校验。发布目录里同时提供 SHA256SUMS 文件,用它检查压缩包完整性:

echo "<SHA256SUMS 中对应的校验值> ubuntu-base-22.04-base-arm64.tar.gz" | sha256sum -c -

输出OK再继续。嵌入式构建最怕起源文件损坏,后面所有诡异的问题都可能是初始压缩包就不干净,这一步值得养成习惯。

3.2 解压、属主与特殊文件系统挂载

创建一个工作目录并解压:

sudo mkdir -p /opt/rootfs-arm64 sudo tar -xzf ubuntu-base-22.04-base-arm64.tar.gz -C /opt/rootfs-arm64

这里有两个需要特别注意的地方。

第一,解压必须用sudo,让文件属主保持为 root。如果普通用户解压,后面进入 chroot 之后各种 su、sudo、设备节点访问都会出权限问题。已经解压错了的话,可以用sudo chown -R root:root /opt/rootfs-arm64补救。

第二,工作目录不能放在 FAT、NTFS 这类不支持 Linux 权限和符号链接的文件系统上。以前我把 rootfs 放在移动硬盘的 NTFS 分区里构建,结果一堆符号链接全变成普通文件,系统直接起不来。

解压完成后,需要把宿主机的几个特殊文件系统挂载进去,这样 chroot 里的程序才能正常访问设备节点、获取内核信息:

sudo mount --bind /dev /opt/rootfs-arm64/dev sudo mount --bind /dev/pts /opt/rootfs-arm64/dev/pts sudo mount -t proc /proc /opt/rootfs-arm64/proc sudo mount -t sysfs /sys /opt/rootfs-arm64/sys

这些挂载的意义在于:/dev提供设备节点,安装 udev 时如果在 chroot 里没有设备节点,会报一堆 mknod 错误;/proc和/sys是内核虚拟文件系统,apt 安装脚本经常要读它们判断系统状态,不挂的话安装某些包时会报错。

3.3 DNS 配置与进入 chroot

chroot 之后要用 apt 联网下载,必须先让 DNS 解析正常。最直接的办法是把宿主机当前的 resolv.conf 复制进去:

sudo cp /etc/resolv.conf /opt/rootfs-arm64/etc/resolv.conf

然后进入 chroot 环境:

sudo chroot /opt/rootfs-arm64 /bin/bash

进入之后,第一件事检查环境:

uname -m cat /etc/os-release

如果uname -m显示 aarch64,说明 qemu 模拟已经生效,后续的 apt 安装才能正常进行。如果显示的还是 x86_64,那说明 chroot 进去的并不是 ARM64 rootfs,或者 binfmt 注册有问题,回到第二章检查。

4. chroot 里干正事:软件源、账号、串口一次配齐

4.1 软件源配置与基础包安装

Ubuntu Base 里已经自带了软件源配置,但 22.04 起,默认的源可能写在/etc/apt/sources.list.d/ubuntu.sources这种 deb822 格式文件里,而不是老的/etc/apt/sources.list。很多人习惯去改老的 sources.list,发现改了半天没生效,其实就是没注意格式变了。

ARM 架构的 Ubuntu 软件源有专门的端口源,以 22.04 为例:

deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse deb http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse deb http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse

写完先更新索引:

apt update

如果apt update报域名解析错误,优先检查/etc/resolv.conf是否还在。很多时候退出 chroot 后再次进入,这个文件可能已经被之前的安装脚本覆盖了。

接着安装一套最小可用系统需要的包:

apt install -y systemd-sysv ifupdown netplan.io openssh-server sudo kmod udev vim-tiny less

逐个说下为什么选这些:

  • systemd-sysv:把 systemd 设置为 PID 1 的 init。Ubuntu Base 默认可能只有最基本的 init,没有把 systemd 作为默认的一号进程,不装这个,后面 enable 服务都无法真正生效。
  • udev:设备管理,负责在启动时创建和管理/dev下的节点。嵌入式设备外设多,没有 udev 经常会遇到“明明接了设备,/dev 下却找不到”的问题。
  • kmod:内核模块加载工具,配合 udev 在内核加载驱动时使用。
  • netplan.io:Ubuntu 自 18.04 起的网络管理工具,负责生成后端网络配置。
  • ifupdown:提供 ifup、ifdown 和传统 interfaces 方式的支持,方便手工管理网络接口。
  • openssh-server:远程维护必备,嵌入式设备调试时大部分时间靠 SSH 登录。
  • sudo:给普通用户提权用,不然每次都要切 root。
  • vim-tiny、less:日常编辑和查看文件的工具,按需选装。

4.2 账号、密码与权限分组

根文件系统里默认只有一个 root 用户,没有密码,需要先设置:

passwd root

然后创建一个日常使用的普通用户,避免长期以 root 操作把系统搞坏:

adduser user usermod -aG sudo user

sudo组的作用是让普通用户可以用 sudo 提权。另外,嵌入式板子如果没有显示器只靠串口登录,要保证 root 用户在串口 getty 上的登录权限正常,否则某个 getty 服务会一直提示密码错误然后循环重启。

4.3 主机名、fstab 与串口终端服务

设置主机名:

echo "arm64-node" > /etc/hostname

fstab 是启动挂载的关键,我一般先写一个保守的模板:

/dev/mmcblk0p2 / ext4 defaults 0 1 /dev/mmcblk0p1 /boot vfat defaults 0 2 tmpfs /tmp tmpfs defaults 0 0

这里有个经验:如果板子的 bootloader 和内核偶尔会把 SD 卡识别成不同的设备节点,比如/dev/mmcblk0p2变成/dev/mmcblk1p2,单靠设备名挂载根分区就容易翻车。更稳的做法是用分区的 UUID:

blkid /dev/mmcblk0p2

然后把 fstab 里的设备名替换成UUID=xxxx-xxxx。这样无论设备节点怎么漂移,只要文件系统 UUID 唯一,内核都能找到正确的分区。

串口登录服务也要配好。ARM64 板子上最常见的串口是ttyAMA0(PL011 外设)或者ttyS0,具体可以看内核设备树。启用对应的 getty 服务:

systemctl enable serial-getty@ttyAMA0.service

如果内核打印的 console 参数配的是ttyS0,这里就要改成serial-getty@ttyS0.service,两者必须对应,否则系统起来后串口上没有任何登录提示。

5. 内核与 bootloader 的衔接:rootfs 之外的事

5.1 rootfs 不装内核,那内核放哪

Ubuntu Base 是纯用户空间,它里面没有任何内核镜像和 bootloader。你要自己解决内核、设备树、bootloader 的存放位置。常见做法有两种。

一种是单独划一个/boot分区(通常是 FAT),把内核镜像 Image、设备树文件 dtb 都放进去,bootloader 从分区读取后引导。另一种是把内核放在根文件系统所在分区的/boot目录,由 bootloader 直接访问文件系统读取。

无论哪种,rootfs 本身只需要做好一件事:让内核挂载根分区之后能找到/sbin/init。这也是为什么我在上一节特意安装了systemd-sysv,如果 rootfs 里没有可用的 init,内核起来后就会卡在 systemd 无法启动的阶段,表现得像死机一样。

5.2 root 参数、rootwait 与 console 的配合

内核启动参数里,跟 rootfs 最相关的是这几个:

root=/dev/mmcblk0p2 rootwait rw console=ttyAMA0,115200
  • root=指定根分区,可以用设备节点,也可以用PARTUUID=,我的经验是用 PARTUUID 更稳。
  • rootwait一定要加。SD 卡、U 盘这类设备初始化需要时间,不加 rootwait,内核可能在内核驱动还没就绪时就尝试挂载根分区,然后直接 panic。
  • rw表示根分区以读写方式挂载。调试阶段忘了加的话,系统起起来是只读的,改什么都报错。
  • console=要和 getty 服务的 tty 对应,否则内核日志看不到,串口登录也没有提示。

如果你的内核不打算用 initramfs,那 rootfs 里必须包含挂载根分区所需的内核驱动,比如 ext4 支持要编进内核而不是模块,virtio 或对应的存储控制器驱动同理。这也是我自己做定制系统时不加 initramfs 的一个重要前提:提前确认哪些驱动是启动必需的,然后编进内核,rootfs 这边就省去了每次都要配套 initramfs 的麻烦。

6. QEMU 验证:在 PC 上先把启动链路跑通

6.1 制作一个可启动的 ext4 磁盘镜像

rootfs 目录配置得差不多了,不要直接拿板子试错,先在 PC 上用 QEMU 验证一遍。第一步是把目录做成一个磁盘镜像:

dd if=/dev/zero of=arm64-rootfs.img bs=1M count=2048 mkfs.ext4 arm64-rootfs.img sudo mkdir -p /mnt/arm64-rootfs sudo mount arm64-rootfs.img /mnt/arm64-rootfs sudo cp -a /opt/rootfs-arm64/* /mnt/arm64-rootfs/ sudo umount /mnt/arm64-rootfs

这里注意cp -a会保留文件属主、权限、符号链接和时间戳。强烈建议不要用cp -r,别问为什么,等你在镜像里看到一个目录下全是损坏的软链接时就明白了。

6.2 QEMU 启动命令

验证时还需要一个 ARM64 内核镜像。可以用你板子对应的内核,也可以临时用一个通用 ARM64 内核。QEMU 的 virt 平台不需要真实板子的 dtb,它自带设备树。启动命令大致这样:

qemu-system-aarch64 \ -M virt \ -cpu cortex-a53 \ -smp 4 \ -m 2048 \ -kernel Image \ -drive file=arm64-rootfs.img,format=raw,if=virtio \ -append "root=/dev/vda rw console=ttyAMA0,115200" \ -nographic

几个参数的含义:

  • -M virt:QEMU 提供的虚拟 ARM 平台,不需要具体板型的设备树。
  • -cpu cortex-a53:如果内核镜像不带更新的 ARM 指令集特性,用这个兼容性比较稳。
  • -drive ... if=virtio:把磁盘镜像当作 virtio 块设备,内核里需要 virtio_blk 支持。
  • -nographic:把串口重定向到当前终端。

我这里没有加-initrd,前提是内核已经编入了 ext4 和 virtio 驱动。如果你手上的内核必须配 initramfs,那就加上-initrd initrd.img。

如果启动后能看到串口登录提示、能输入 root 密码登录,那这套 rootfs 的基础链路基本就算通过了。我对这个环节的态度是:QEMU 验证不是可选项,而是必选项。板子调试一次的成本高得多,PC 上把逻辑问题排干净,上板之后的问题往往只剩硬件相关的那几个。

6.3 常见启动失败与排查链路

我整理过几个高频启动问题,对照现象看原因是最快的路径:

现象常见原因检查方向
Kernel panic: VFS: Unable to mount root fsroot 参数错误、缺少存储驱动、根文件系统类型不支持核对 append 里的 root=,确认内核是否编入 ext4/virtio
No working init found没有 PID 1 可执行程序、systemd 没装好、/usr 损坏检查 /sbin/init、/usr/lib/systemd 是否存在
can't open /dev/ttyAMA0console 与实际串口不匹配、getty 服务没启查看设备树、确认 getty 服务名
网络接口没有 IPnetplan 配置缺失、后台网络服务没起来检查 /etc/netplan、systemctl 状态

排查时有个原则:先把内核日志停在最早报错的那一行,而不是只看最后的 panic 状态。内核打印里Kernel panic - not syncing前面的几行往往才是真正出问题的原因。

7. 构建过程中的翻车点与瘦身交付建议

7.1 我踩过的几个典型坑位

第一是/etc/resolv.conf。第一次构建时,我明明复制了宿主机的 resolv.conf,apt update 也正常,但安装某些包时突然解析失败。后来发现是某个包安装时把 resolv.conf 覆盖成了空文件。解决办法是安装完成后重新复制一次,或者进入 chroot 后第一时间把它设为只读。但要记得,chroot 退出、准备交付时要换回目标板适用的真实 DNS 配置。

第二是 chroot 之后随手执行apt upgrade的风险。Ubuntu Base 本身包很少,升级通常没问题,但一旦升级了某些基础库,而你的板子内核版本比较老,可能起不来。我的习惯是开发阶段只apt update和安装指定包,不随手 upgrade,等系统整体验证通过后再决定是否升级。

第三是 udev 和 systemd 在 chroot 里的坑。安装 systemd 时如果没挂载/proc和/sys,有的安装脚本会报错或者无法正确创建服务链接。遇到这类问题先检查挂载,而不是盲目重装。

还有一个多架构的问题:如果你的嵌入式系统里同时存在 32 位和 64 位程序,需要配置 multiarch,例如在 ARM64 rootfs 里执行dpkg --add-architecture armhf后再装兼容库。这个操作最好在系统还干净时就做,等软件装了一堆再切架构会相当痛苦。

7.2 rootfs 瘦身与最终交付

做产品时 rootfs 体积直接关系到烧录时间,我有几个常规操作:

apt clean rm -rf /var/cache/apt/archives/* rm -rf /usr/share/doc rm -rf /usr/share/man

另外,如果确信用不到多语言 locale,可以只保留 C.UTF-8,省下的空间和 inode 都不少。Ubuntu Base 里默认带的一些服务也要逐一关掉,例如用不上云初始化功能的话,直接卸载掉相关软件包,能省出几十 MB。

交付的方式一般有两种:

  • 目录打包成 tar 压缩包,方便别人解压后用tar -xzf覆盖到已有分区,也便于版本管理和 diff。
  • 直接做成 ext4 镜像,烧录工具能够直接写。我用得比较多的是先把目录同步到挂载的镜像文件里,再卸载镜像,这样每次出包都是同一套流程。
sudo tar -czf rootfs-arm64.tar.gz -C /opt/rootfs-arm64 .

最后再说一个个人习惯:构建完成之后,我会把整个过程的关键命令整理成一个 SHELL 脚本放进项目的 docs 目录。因为 rootfs 构建这种事,隔三个月再捡起来往往已经忘了当初是怎么配的。脚本化之后,换一块板子、换一个系统版本,只需要改几个参数就能复现,比翻聊天记录和笔记靠谱得多。这套方法我用了很多个项目,每次重新出包都相当省心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询