前一阵我在群里看到有人问都 2026 年了根文件系统早就被 Buildroot、Yocto 这些工具封装成“点几下就出包”的流水线为什么还要自己编译 BusyBox我在刚入门那会儿也问过同样的问题。答案其实不复杂如果你只想要一个能启动的系统现成方案确实最高效但如果你想把手头这块国产 Cortex-A 开发板彻底吃透搞清楚内核引导之后用户空间那几百毫秒到底发生了什么BusyBox 就是最合适的切入点。它体积小、源码结构清晰、配置项覆盖面广特别适合从零开始手动撸一遍。这篇是“从 0 开始的 Rootfs 之路”系列的第二篇。开篇我们把交叉编译工具链、u-boot 和内核的启动链路都铺开了这篇就集中处理 rootfs 里的第一个核心成员——BusyBox。整篇思路是先讲为什么选 BusyBox、编译前要准备什么再走一遍从配置到安装的完整过程然后把 rootfs 目录补全到能启动的程度最后分享我实际踩过的一些坑。看完你就能动手生成一个最小可运行的 Linux 用户空间在串口里看到熟悉的 init 和 shell 提示符。我用的是一块很常见的国产 Cortex-A7 双核开发板SD 卡启动板级串口在系统里对应 ttyS0。这套流程不绑定具体板型ARM32、ARM64 都通用区别主要在于交叉编译前缀和 bootargs 里的设备名。如果你用的是 ARM64 板子把下文里的 arm-linux-gnueabihf 换成 aarch64-linux-gnu 就行其他逻辑基本一样。1. 手动搭建 Rootfs 之前先把账算明白1.1 现成方案这么香为什么还要自己搭Buildroot 和 Yocto 的最大优势是“确定性”一个配置文件下去工具链、库、rootfs、镜像一条龙全出来版本锁定也方便。但副作用是它们把中间过程都封装成了黑盒。你自己写 rootfs 时能很清楚 “系统启动后第一进程是谁”“/dev 下那些节点哪来的”“为什么 mdev 一跑tty 设备就自动冒出来了”。这些认知等你要移植驱动、裁剪系统、定位启动失败时会直接转换成解决问题的速度。其次国产开发板厂商给的出厂 rootfs 往往是 buildroot 或 distro 定制包里面带了不少你用不到的组件开机自启服务、调试脚本、一堆没注释的配置。手动搭的话你能清楚知道 rootfs 里每一个文件是从哪儿来的一个目录一个目录地抠体积。做量产项目的朋友应该都有体会rootfs 从 300MB 剪到 80MB往往靠的就是这种底层的掌控力。第三点比较实际你迟早会遇到需要同时管理多个板型、多套内核版本的情况。Buildroot 越做越重编译一次全量镜像的耗时和磁盘占用都不可忽略。手动构建的 rootfs 加 BusyBox配上简洁的打包脚本反而更灵活改动一条命令重新打包几十秒就完事。这也是很多干 BSP 的老工程师更愿意维护一套手工 rootfs 的原因。1.2 BusyBox 的“瑞士军刀”到底指什么BusyBox 的设计理念概括下来就是一句话把几百个常用命令塞进同一个文件用一个二进制对外提供所有 applet。你敲 ls实际上执行的是 /bin/busybox ls你敲 ps执行的是 /bin/busybox ps。BusyBox 通过 argv[0] 或者第一个参数来判断用户想调哪个功能所以在安装阶段会生成一大堆符号链接比如 /bin/ls、/bin/mv、/sbin/mdev 全部指向同一个 busybox 文件。这套设计的收益非常直接。第一rootfs 里只需要一个核心二进制省空间第二排障时只需要关心一个程序第三init 进程也能由 busybox 提供BusyBox 自带的 init 支持读取 /etc/inittab承担挂载、启动 rcS、生成 getty 这些职责。嵌入式系统从内核态切到用户态之后BusyBox 几乎是第一个也是唯一一个“扛把子”程序所以把它比作瑞士军刀并不夸张——你平时用它切螺丝、拧罐头、开酒瓶关键时刻它还能当主梁用。不过这里得提醒一句BusyBox 是“精简实现”不是完整 GNU coreutils 的替代品。很多命令的选项比桌面发行版里的少行为也可能有细微差异。比如 busybox 里的 ps 默认只显示本进程树相关的信息top 依赖 /proc 正确挂载mdev 也比 udev 简朴很多。理解“够用但简化”这层定位后面使用时就不容易产生预期落差。2. 编译前的准备交叉链、版本和目录规划2.1 先确认工具链别忙了一小时才发现用成了宿主 gccBusyBox 要跑在开发板上所以必须用交叉编译器而不是 PC 上的 gcc。我手上这块板子是 ARMv7 架构Cortex-A7 内核带硬件浮点所以我用的是 arm-linux-gnueabihf 工具链。开发机是 Ubuntu一条命令装上sudo apt install crossbuild-essential-armhf装完先验证一下避免 PATH 里混了多个工具链导致编出奇怪的东西arm-linux-gnueabihf-gcc -v which arm-linux-gnueabihf-gcc然后导出两个环境变量后续所有 make 命令都得带着它们export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-这里有个容易忽略的点CROSS_COMPILE 后面的横线不能省。Makefile 会拼出 arm-linux-gnueabihf-gcc 这条命令来调用编译器少了横线就会提示找不到工具。还有ARCH 必须和内核匹配如果你拿 ARM64 编译器去编 ARM32 源码链接阶段会报一堆 cpu 指令不支持的错而且这类错误出现得比较晚不好定位。2.2 BusyBox 版本怎么选BusyBox 的新版本迭代不算频繁但每年也会有若干 release。我手头用的是 1.36.1属于前两年的稳定版本文档全、网上问题总结多作为学习用途足够。2026 年如果项目比较新也可以选 1.38 系列差异主要体现在新增命令和部分功能增强上对基本 rootfs 影响不大。我的建议是选一个最新稳定版或官方标注的长期维护版别追太新的 nightly也别用十几年前的老版本。老版本虽然也能跑但面对现代内核的 sysfs、devtmpfs 和 cgroup 特性支持和兼容性会差一些。下载时顺便校验一下哈希防止下载到损坏或被人动过手脚的文件。BusyBox 官网 release 页面会给出 .sha256 文件命令行操作如下wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2.sha256 sha256sum -c busybox-1.36.1.tar.bz2.sha256解压之后建议把目录放到一个结构清晰的工作空间里比如 ~/bbsp/下面平行放 linux 内核源码、busybox 源码、rootfs 目录、工具链相关脚本。后面写打包脚本时引用相对路径会方便很多。2.3 rootfs 目录先铺好后面少改很多编译安装 BusyBox 之前我习惯先把目标 rootfs 目录建出来。虽然 make install 能自动创建部分目录但提前规划能避免装完后再补结构导致权限混乱。一个最小可启动的 rootfs 至少要有这些目录mkdir -p rootfs/{bin,sbin,etc/init.d,dev,proc,sys,tmp,var,root,usr/{bin,sbin,lib},lib}目录作用说明bin/sbin/usr/bin/usr/sbin命令与系统程序BusyBox 的符号链接主要落在这里etc配置文件init、挂载、用户配置都在这dev设备节点早期启动至少需要 console 和 nullproc/sys内核虚拟文件系统进程信息、内核参数必须挂载tmp/var临时与动态数据运行期产生的数据lib动态库如果选择静态编译则可以先留空rootroot 用户家目录部分程序需要登录环境目录的属主和权限也要注意。dev 下设备节点后面单独处理proc 和 sys 不需要在 rootfs 里创建实体内容它们靠运行时挂载。root 目录最好设成 0700tmp 设成 1777其他保持 0755 即可。很多人习惯在宿主机上用 root 用户直接创建文件导致 rootfs 里所有文件属主都变成 root:root这在嵌入式里问题不大但养成用普通用户打包的习惯更安全。3. 动真格BusyBox 配置、编译与安装3.1 配置阶段defconfig 打底menuconfig 做关键调整BusyBox 的配置风格和 Linux 内核一样基于 Kconfig。先跑一次默认配置把所有命令的全集打开后续再根据需求裁剪比一个一个勾选省事得多make defconfigdefconfig 生成的配置已经覆盖了常见嵌入式命令init、shell、mdev、mount、vi、ping 等都在里面。接着打开图形配置界面调整两个关键项make menuconfig进入 menuconfig 后操作和内核配置完全一致方向键移动空格键勾选回车进入子菜单。我每次必改的第一项是“Settings → Build static binary (no shared libs)”。把这项选中BusyBox 会编译成静态二进制不依赖 rootfs 里的动态链接器和 libc。这么做的收益是 rootfs 少了 lib 目录的牵挂随便拷进任何分区都能跑不用担心 glibc 版本不一致。代价是二进制体积大几百 KB对现代 Flash 容量来说完全可以接受。第二项是确认 init 和 mdev 都已经启用。默认 defconfig 里 CONFIG_INITy 和 CONFIG_MDEVy 一般是开着的但最好进去看一眼。一个常见问题是有人把 mdev 不小心关掉导致 rootfs 起来后 /dev 下始终没有 tty、mmc 等设备节点板子看起来像死机实际是 shell 都分配不到终端。3.2 启动成败往往取决于这三个细节menuconfig 里的配置项很多但真正影响首次启动成败的我认为就三块静态编译、init 启用、devtmpfs/mdev 策略。我们展开说。静态编译在上面已经提过这里补充一个判断方法编译安装后用 file 命令查看 busybox 文件的类型。如果输出是 ELF 32-bit ARM 且带 “static” 字样说明编译对了如果显示 “dynamically linked”说明你忘了在 menuconfig 里选静态或者之前保存的 .config 没生效。静态版本第一次跑的时候少了动态库依赖排障范围小很多。init 启用这一点不用担心太多因为默认就开着。但要注意 BusyBox 的 init 和内核参数 rdinit/init 的配合。内核在挂载 rootfs 后会按顺序寻找 /init、/sbin/init、/etc/init、/bin/init。BusyBox 安装后会在 /sbin/init 创建指向 busybox 的符号链接所以只要 rootfs 里存在这个链接内核就能找到 init。如果自己手动改过安装路径务必确认 /sbin/init 存在。devtmpfs/mdev 策略决定 /dev 下的设备节点从哪来。内核开启 CONFIG_DEVTMPFS 后会在挂载 rootfs 时自动挂一个 devtmpfs 到 /dev并由内核维护设备节点。BusyBox 的 mdev 可以在此基础上做补充也可以替代 devtmpfs 独立工作。开发板上最省心的组合是内核开 devtmpfsrootfs 里静态创建 console 和 null启动 rcS 里再跑一次 mdev -s 扫一遍。这样既有兜底节点又能覆盖内核没创建的杂项设备。3.3 make 编译与 make install装到哪里去配置保存后就开始编译。开发机核心数多的话直接并行编译make -j$(nproc)交叉编译 BusyBox 很快正常情况下一两分钟就出结果编译器的输出会滚动得很快。如果中途报错先检查 ARCH 和 CROSS_COMPILE 有没有正确设置再看一下是不是宿主机缺少 make、gcc 这些基础工具。别急着怀疑源码九成编译失败都是环境问题。接着是安装。BusyBox 不像普通软件那样装到宿主机的 / 下而是通过 CONFIG_PREFIX 指定安装到我们规划的 rootfs 目录make install CONFIG_PREFIX/home/you/bbsp/rootfs这里的 CONFIG_PREFIX 建议写绝对路径。写相对路径不是不行但如果你在别的目录再次执行 make install很容易装到意想不到的位置。安装完成后检查一下ls -l rootfs/bin ls -l rootfs/sbin你会看到 bin 目录下出现一个 1MB 左右的 busybox 文件以及 ash、sh、ls、cat、mount 等一串符号链接sbin 下则有 init、mdev、reboot、poweroff 等。这些链接统一指向 /bin/busybox这就是它作为“一把瑞士军刀”的实际形态。确认这些链接存在说明安装阶段正常。4. 补全 Rootfs让系统真正能启动4.1 /etc/inittab 与 /etc/init.d/rcSinit 的路线图内核找到 /sbin/init 后BusyBox 的 init 进程会读取 /etc/inittab按表格里的动作依次执行。inittab 的格式不复杂每条记录用冒号分成四段id:runlevel:action:process。BusyBox 的 init 不看 runlevel所以中间那段留空即可。我的一份最小 inittab 长这样::sysinit:/etc/init.d/rcS console::respawn:/sbin/getty -L console 115200 vt100 ::shutdown:/bin/umount -a -r第一行的 sysinit 动作表示内核初始化阶段只执行一次 /etc/init.d/rcS所有系统初始化都放在这个脚本里。第二行的 respawn 动作是指当 console 终端上的进程退出后自动重新拉起 getty。这个 getty 会打开 /dev/console 并启动登录逻辑所以你才能看到 login: 提示符。第三行的 shutdown 动作负责关机时卸载文件系统。然后是 rcS 脚本属于 rootfs 里你第一个真正亲手写的“系统启动剧本”#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t tmpfs tmpfs /tmp mkdir -p /dev/pts mount -t devpts devpts /dev/pts if [ -x /sbin/mdev ]; then echo /sbin/mdev /proc/sys/kernel/hotplug mdev -s fi /bin/hostname -F /etc/hostname写完之后记得加执行权限否则 init 执行到 rcS 时会报 Permission deniedchmod x rootfs/etc/init.d/rcSmount proc 和 sysfs 是最基本的两步不挂载它们ps、top、mdev 这些依赖内核信息接口的命令都会表现异常。devpts 是给伪终端用的以后要 ssh、开多个终端会话时会需要。mdev 相关的那段是运行时设备节点自动管理把 mdev 注册成内核热插拔助手再手动扫一次已有设备。最后 hostname 设置主机名其实不影响启动但设一下能避免部分程序因主机名异常而报错。4.2 设备节点提前准备 console 与 null即使内核开了 devtmpfs也建议在 rootfs 里静态创建两个节点console 和 null。这是 init 进程最早要用到的设备不能等 devtmpfs 慢慢补。创建方法用 mknod主次设备号要写对sudo mknod -m 666 rootfs/dev/console c 5 1 sudo mknod -m 666 rootfs/dev/null c 1 3console 的主设备号是 5、次设备号是 1null 是 1、3。权限设成 666避免权限问题干扰启动判断。为什么手动建这两个因为内核在初始化 console 驱动之前init 打开 /dev/console 写入消息会失败devtmpfs 虽然会自动建节点但可能等不到根文件系统挂载完成。先把这两个节点放进 rootfs能保证从 init 到 shell 整个链路的输入输出都落在正确的设备上。4.3 打包与启动SD 卡分区和 bootargsrootfs 目录里有了 busybox、inittab、rcS、设备节点之后理论上就能启动了。实际操作时还得把 rootfs 装进启动介质。我是用 SD 卡做两分区方案第一个分区放 u-boot 的引导文件、内核 zImage 和设备树 dtb第二个分区放 rootfs格式化成 ext4。以 Linux 宿主机上操作 /dev/sdX 为例分区表和格式化命令大致如下sudo fdisk /dev/sdX # 建第一个分区 128M类型 c(W95 FAT32) # 建第二个分区占剩余空间类型 83(Linux) sudo mkfs.vfat /dev/sdX1 sudo mkfs.ext4 /dev/sdX2然后把内核镜像和 dtb 拷到第一个分区把 rootfs 内容拷到第二个分区。拷贝时注意保留属主和符号链接sudo mkdir /mnt/rootfs sudo mount /dev/sdX2 /mnt/rootfs sudo cp -a rootfs/. /mnt/rootfs/ sync sudo umount /mnt/rootfscp -a 会保留符号链接、权限和时间戳这点必须注意。用普通 cp 会因为符号链接变成复制目标文件而让整个 rootfs 无比膨胀init 链接也可能失效。最后是 u-boot 环境变量也就是 bootargs。我用的板子大体是这样setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw rootwait fatload mmc 0:1 0xc2000000 zImage fatload mmc 0:1 0xc4000000 yoursoc.dtb bootz 0xc2000000 - 0xc4000000这里 console 要和板级串口对应root 设备要写成 rootfs 所在分区。加 rootwait 是为了让内核等待 mmc 设备完全初始化后再尝试挂载根分区。如果你用 NFS 启动只需要把 root/dev/nfs nfsroot... 写进 bootargsrootfs 逻辑完全一样。5. 第一次启动的现场记录常见问题与排查思路5.1 按症状定位一张排查表先存着首次上电不成功是常态关键是别慌。我整理了一个按串口输出症状对应排查方向的速查表以后遇到类似现象可以直接对照。现象直接原因排查方向Kernel panic提示 VFS: Unable to mount root fs内核找不到 rootfs检查 bootargs 中 root 设备、文件系统类型是否被内核支持卡在 Waiting for root devicemmc 驱动初始化慢或未识别分区加 rootwait检查 dtb 里 mmc 节点状态确认分区表正确输出 /sbin/init 后无反应init 路径不对或 rcS 有问题确认 /sbin/init 链接存在确认 rcS 有执行权限/bin/sh: cant access ttyconsole 节点缺失或 bootargs 设备名不对确认 rootfs/dev/console 存在确认 console 参数匹配mdev: cant open /syssysfs 没挂载在 rcS 里确保 mount -t sysfs 在 mdev -s 之前执行这些症状大多不是单一原因造成的但按“先排除 rootfs再排查 bootargs最后检查内核配置”的顺序走一般能快速收敛。5.2 三个特别容易犯的低级错误第一个是 rcS 脚本里的换行符问题。如果你在 Windows 下编辑过脚本再拷进 Linux文件会带着 CRLF 换行执行时会出现 “bad interpreter” 或 “not found” 之类的诡异报错但脚本本身明明是存在的。排查方法是用 file 命令看脚本类型如果显示 “with CRLF line terminators”就执行 sed -i s/\r$// 清理。这个坑我至少见人踩过五次值得单独强调。第二个是忘掉 rcS 或 inittab 的执行权限。很多人把文件内容写好就扔进 rootfs结果 init 启动到一半静默退出串口刷不出任何有效信息。记着 chmod x 三件套rcS、inittab 不需要执行权限但 rcS 必须有。如果 inittab 里指定的命令路径写错了也会出现类似现象所以写完把路径通读一遍别凭空猜测。第三个是 rootfs 复制时把符号链接弄丢。用 cp 不加 -a、或者用 tar 打包时没解开链接都会导致 /bin/sh 变成普通文件或直接缺失然后你会在串口看到 “sh: not found” 或者 init 反复重启。拷完 rootfs 后建议立刻在宿主机上 ls -l 检查 bin 目录下的 sh 和 sbin/init 是否都显示为箭头链接。5.3 一个几乎每次都管用的检查习惯除了临时排查我更建议你在把 SD 卡插进开发板之前先在宿主机上对 busybox 做一次体检。最快的一步是file rootfs/bin/busybox如果看到架构和你板子一致、并且是静态链接就可以放心上电。如果架构不对多半是 CROSS_COMPILE 没设置如果是动态链接说明 CONFIG_STATIC 没生效。这一步只需要一秒钟但能规避掉后面至少半小时的莫名其妙。再进一步可以用 readelf 查看关键头信息readelf -h rootfs/bin/busybox看 Machine 字段是否对应 ARM或者 aarch64 对应板子架构。没有任何一条命令能保证系统一定启动成功但这两条检查能把工具链层面的大坑提前排除。接下来才是真正上电看串口日志。上电之后如果一切正常你会在串口工具里看到内核启动日志刷刷滚动接着是 BusyBox 的 init 启动最后出现 login: 提示符。默认情况下 BusyBox 的 root 用户不需要密码输入 root 回车就能进入 shell。到这一刻前面所有编译、补文件、调 bootargs 的工作才算真正闭环。我个人在移植 BusyBox 早期最大的一个体会是第一次做的时候千万别急着追求最小体积和完美配置。先用静态编译、保留默认配置、把 devtmpfs 和 mdev 都打开能启动是第一目标。系统跑起来以后再逐项裁剪命令、移除不需要的 applet、换动态链接库每一步都有反馈、可回滚。这个“先跑通再瘦身”的顺序比一上来就抠配置项要顺得多也更能让你理解 BusyBox 每个开关到底在控制什么。下一阶段等这套 rootfs 稳定工作后就可以往里面加我们的业务程序、第三方库和内核模块从“最小可启动”一步步走向“产品可交付”。