busybox根文件系统移植

BusyBox 根文件系统移植主要可以分为两个部分:一是 BusyBox 的编译过程,二是 BusyBox 根文件系统的启动流程。

1、BusyBox是一个"多调用可执行程序",编译时会根据.configlscatmountinitsh等多个applet功能链接进同一个busybox ELF;执行/bin/ls时,内核先沿着软链接加载/bin/busybox,BusyBox再从argv[0]取得命令名ls,查找applet表并调用ls_main(),并不是先查表再寻找软链接。

2、根文件系统则是Linux挂载为/的完整目录和文件集合,BusyBox只为它提供基础命令、Shell和可选的init,并不等同于根文件系统本身;其中/bin/sbin保存命令及其BusyBox链接,/etc保存inittabrcS等配置,/lib保存动态链接器和共享库,/dev保存设备节点,/proc/sys是内核虚拟文件系统的挂载点,/tmp/run/var保存运行时数据。若BusyBox采用静态链接,它自身可以不依赖/lib中的动态库,但rootfs里的其他动态程序仍可能需要这些库。

一、busybox编译

1、BusyBox编译首先由顶层Makefile确定版本、构建目录、目标架构、交叉工具链和构建目标;

2、再从源码中的特殊注释生成Config.in、Kbuild、配置头、applet注册表及帮助信息;

3、正式编译时,scripts/Makefile.build根据CONFIG_xxx递归地将各目录源码编译成.o,并聚合为built-in.olib.a;最后由scripts/trylink调用交叉GCC链接出busybox_unstripped,经strip生成最终的busybox。

4、make install再将其安装到rootfs并创建lsshinit等软链接,运行时BusyBox根据argv[0]查找applet表并调用对应的xxx_main()函数。

1、总体流程

复制代码
1. 读取顶层Makefile
        ↓
2. 生成.config配置
        ↓
3. 生成Kbuild、配置头和applet表
        ↓
4. 递归编译各功能目录
        ↓
5. 链接、strip并安装BusyBox

当前源码树还没有 .config 和编译产物,因此第一次编译前需要先执行 make defconfig 或导入项目自己的配置。

2、读取顶层Makefile,确定构建环境

执行 make 后,首先读取 BusyBox 顶层 Makefile

需要注意,Makefile不是像C程序一样从上到下逐行执行。make会先读取规则、计算变量、建立依赖关系,再选择目标执行。

顶层阶段可以概括为:

复制代码
make
  ↓ 读取Busybox_1.30.0/Makefile
确定BusyBox版本
├─ VERSION=1
├─ PATCHLEVEL=30
└─ SUBLEVEL=0
   【组合成版本1.30.0】
  ↓
确定源码目录和输出目录
├─ 没有设置O=
│  └─ srctree=objtree=当前目录
│     【源码和编译产物放在同一目录】
└─ 设置O=<输出目录>
   ├─ srctree=BusyBox源码目录
   └─ objtree=指定的输出目录
      【源码和编译产物分开】
  ↓
确定目标架构和交叉工具链
├─ ARCH=arm
│  【目标架构为ARM】
├─ CC=$(CROSS_COMPILE)gcc
│  【把.c编译为ARM .o,并负责最终ELF链接】
├─ AR=$(CROSS_COMPILE)ar
│  【把多个ARM .o归档为lib.a】
└─ STRIP=$(CROSS_COMPILE)strip
   【删除最终ELF中的符号等信息】
  ↓
分析用户要求的目标
├─ make defconfig/menuconfig
│  【进入配置流程】
├─ 裸make
│  【执行默认目标_all】
├─ make busybox
│  【只构建busybox及其依赖】
└─ make install
   【先保证busybox存在,再执行安装】

普通执行 make 时,默认依赖关系是:

复制代码
_all
【默认入口】
  ↓
all
├─ busybox
│  【构建BusyBox程序】
└─ doc
   【生成说明文档】

所以顶层 Makefile 的主要作用不是直接编译 ls.cinit.c,而是:

确定版本、目录、工具链和用户目标,然后把具体任务交给配置系统和各级构建规则。

3、解析配置系统,生成.config

BusyBox包含几百个命令,但一个产品通常不需要全部启用,所以必须用 .config 决定最终 busybox 包含哪些功能。例如:

复制代码
CONFIG_INIT=y       【包含init命令】
CONFIG_ASH=y        【包含ash命令】
CONFIG_MDEV=y       【包含mdev命令】
CONFIG_LS=y         【包含ls命令】
# CONFIG_HTTPD is not set
                    【不包含httpd命令】

第一次配置通常执行:

复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig

主要流程为:

复制代码
make defconfig
  ↓ 顶层Makefile匹配%config目标
scripts_basic
【生成fixdep等构建所需的主机工具】
  ↓
gen_build_files
【从源码注释生成各目录Config.in和Kbuild等文件】
  ↓
make -f scripts/Makefile.build obj=scripts/kconfig defconfig
【编译并运行Kconfig配置程序】
  ↓
scripts/kconfig/conf -d Config.in
├─ conf
│  【BusyBox的Kconfig命令行配置程序】
├─ -d
│  【使用各配置项的默认值】
└─ Config.in
   【BusyBox全部配置项的总入口】
  ↓
生成配置结果
├─ .config
│  【供Makefile读取,例如CONFIG_LS=y】
├─ .kconfig.d
│  【记录各配置文件的依赖关系】
└─ include/autoconf.h
   【供C源码读取,生成ENABLE_xxx和IF_xxx等宏】

顶层 Config.in 会继续包含各功能目录的配置文件:

复制代码
Config.in
├─ coreutils/Config.in
├─ init/Config.in
├─ shell/Config.in
├─ networking/Config.in
├─ util-linux/Config.in
└─ 其他目录Config.in

ls 为例,配置描述中有:

复制代码
config LS
    bool "ls (14 kb)"
    default y

default y表示 make defconfig 默认启用ls,因此 .config 中生成:

复制代码
CONFIG_LS=y

如果在 make menuconfig 中关闭ls,则变为:

复制代码
# CONFIG_LS is not set

当前源码没有i.MX6ULL专用BusyBox defconfig,所以 make defconfig 得到的是通用默认配置,不是针对本板裁剪好的产品配置。

4、生成Kbuild、配置头文件和applet表

BusyBox有一个比较特别的设计:配置、编译、命令注册和帮助信息经常直接写在 .c 文件顶部的特殊注释中。

复制代码
//config:       【定义Kconfig配置项】
//kbuild:       【定义CONFIG_xxx对应哪些.o】
//applet:       【定义命令名、入口函数和安装位置】
//usage:        【定义--help帮助文本】

这些注释由 scripts/gen_build_files.sh 提取:

复制代码
各目录*.c中的特殊注释
  ├─ //config:
  │  └─ Config.src + 配置项
  │     → 各目录Config.in
  │     【生成Kconfig配置菜单】
  ├─ //kbuild:
  │  └─ Kbuild.src + 编译规则
  │     → 各目录Kbuild
  │     【生成CONFIG_xxx到目标文件的映射】
  ├─ //applet:
  │  └─ include/applets.src.h + 注册信息
  │     → include/applets.h
  │     【生成所有候选命令的注册描述】
  └─ //usage:
     └─ include/usage.src.h + 帮助文本
        → include/usage.h
        【生成命令帮助信息】

coreutils/ls.c 为例:

复制代码
//config:config LS
//config:    bool "ls (14 kb)"
//config:    default y

//kbuild:lib-$(CONFIG_LS) += ls.o

//applet:IF_LS(APPLET_NOEXEC(ls, ls, BB_DIR_BIN, ...))

分别生成:

复制代码
coreutils/ls.c
  ├─ //config: → coreutils/Config.in
  │              【在menuconfig中提供LS选项】
  ├─ //kbuild: → coreutils/Kbuild
  │              【CONFIG_LS=y时编译ls.o】
  └─ //applet: → include/applets.h
                 【注册命令ls,入口ls_main(),安装到/bin】

配置和applet生成的主要文件包括:

复制代码
.config
【Makefile使用的配置结果】

include/autoconf.h
【C源码使用的CONFIG_xxx、ENABLE_xxx、IF_xxx宏】

include/config/*.h
【拆分后的配置头,便于增量编译】

include/applet_tables.h
【命令名称与入口函数表】

include/NUM_APPLETS.h
【本次配置实际启用的applet数量】

include/usage_compressed.h
【压缩后的帮助信息】

例如 CONFIG_LS=y 时,生成的表中会建立:

复制代码
命令名"ls"
   ↕ 相同数组索引
入口函数ls_main()

如果配置发生变化,普通 make 会通过 silentoldconfig重新校验 .config 并更新 autoconf.h,保证Makefile和C源码看到的配置一致。

5、递归编译各功能目录

配置和生成文件准备好后,普通 make 开始构建:

复制代码
_all
  ↓
all
  ↓
busybox
  ↓
busybox_unstripped
  ↓
递归编译各功能目录

主要目录包括:

复制代码
applets/          【BusyBox入口和applet生成工具】
libbb/            【BusyBox公共函数库】
coreutils/        【ls、cat、cp等基础命令】
init/             【init、reboot等命令】
shell/            【ash、hush等shell】
networking/       【ifconfig、ping、udhcp等】
util-linux/       【mount、mdev等系统工具】

顶层对每个目录调用统一规则:

复制代码
make -f scripts/Makefile.build obj=<目录>

scripts/Makefile.build读取该目录的Kbuild,然后完成:

复制代码
目录Kbuild
【读取obj-y、lib-y等目标列表】
  ↓
.c/.S → .o
【用目标交叉编译器生成ARM目标文件】
  ↓
obj-y → built-in.o
【使用-r聚合为可继续链接的目标文件】
  ↓
lib-y → lib.a
【使用ar归档为静态目标库】

ls 为例,如果 .config 中有:

复制代码
CONFIG_LS=y

那么 coreutils/Kbuild 中:

复制代码
lib-$(CONFIG_LS) += ls.o

展开为:

复制代码
lib-y += ls.o

于是执行:

复制代码
coreutils/ls.c
  ↓ arm-linux-gnueabihf-gcc -c
coreutils/ls.o
  ↓ arm-linux-gnueabihf-ar rcs
coreutils/lib.a

其他目录的过程相同:

复制代码
init/init.c
  → init/init.o
  → init/lib.a

shell/ash.c
  → shell/ash.o
  → shell/lib.a

libbb/*.c
  → libbb/*.o
  → libbb/lib.a

applets/applets.c
  → applets/applets.o
  → applets/built-in.o

如果 CONFIG_LS没有启用,ls.o不会进入正常的 lib-y,同时applet表也不会注册 ls

6、最终链接、strip并安装BusyBox

各目录完成后,顶层开始最终链接:

复制代码
applets/built-in.o
各目录built-in.o
各目录lib.a
目标C库和必要外部库
  ↓ scripts/trylink
交叉gcc最终链接
  ↓
busybox_unstripped
【未strip的ARM用户空间ELF,保留较多符号信息】
  ↓ 交叉strip
busybox
【最终用于目标rootfs的精简ELF】

这里最终不是像Linux内核那样用内核链接脚本生成镜像,而是由 scripts/trylink 调用交叉GCC,把目标文件、静态库和目标C库链接成普通用户空间ELF。

安装时执行:

复制代码
make ARCH=arm \
     CROSS_COMPILE=arm-linux-gnueabihf- \
     CONFIG_PREFIX="$PWD/_install" \
     install

安装依赖关系为:

复制代码
make install
  ├─ busybox
  │  【保证最终BusyBox ELF已经生成】
  ├─ busybox.links
  │  【记录已启用命令的安装路径】
  └─ applets/install.sh
     【复制busybox并创建命令链接】

默认安装结果类似:

复制代码
_install/
├─ bin/
│  ├─ busybox
│  ├─ ls → busybox
│  ├─ cat → busybox
│  └─ sh → busybox
└─ sbin/
   ├─ init → ../bin/busybox
   └─ mdev → ../bin/busybox

运行 /bin/ls 时,实际加载的是 /bin/busybox

复制代码
执行/bin/ls
  ↓ /bin/ls是busybox的链接
加载/bin/busybox
  ↓ argv[0]表示当前命令名ls
查找applet表
  ↓
调用ls_main()

因此,BusyBox通过"一个ELF + applet表 + 大量命令链接"提供众多用户空间命令。

make install只安装BusyBox主程序和命令链接,并不会自动生成完整rootfs。真正启动系统还需要准备 /etc/inittab/etc/init.d/rcS/dev/proc/sys以及动态链接时所需的共享库。

二、roofs根文件构建实操

1、编译BusyBox

复制代码
cd ~/100ask_imx6ull-sdk/Busybox_1.30.0

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 导入100ask配置
cp ../DevelopmentEnvConf/100ask_imx6ull_busybox_config .config

# 检查或修改配置
make menuconfig

# 编译
make -j4

# 安装到_install
make CONFIG_PREFIX="$PWD/_install" install

检查结果:

复制代码
file _install/bin/busybox
ls -l _install/bin/busybox
ls -l _install/bin/sh
ls -l _install/sbin/init

期望得到:

复制代码
_install/bin/busybox
_install/bin/sh    → busybox
_install/sbin/init → ../bin/busybox

2、创建完整rootfs

进入DevelopmentEnvConf

复制代码
cd ~/100ask_imx6ull-sdk/DevelopmentEnvConf

sudo bash busybox_build_config.sh

该脚本会自动:

复制代码
复制BusyBox _install
  ↓
复制ARM动态库
  ↓
创建/etc/inittab、rcS、fstab
  ↓
创建/dev/console、/dev/null
  ↓
生成rootfs/rootfs.tar.bz2

注意检查脚本中的动态库路径是否与实际工具链一致:

复制代码
LIB_PATH=/home/book/100ask_imx6ull-sdk/ToolChain/
gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/
arm-linux-gnueabihf/libc/lib/

3、解压为NFS根文件系统

复制代码
sudo mkdir -p /home/book/nfs_rootfs

sudo tar -xjf \
  ~/100ask_imx6ull-sdk/DevelopmentEnvConf/rootfs/rootfs.tar.bz2 \
  -C /home/book/nfs_rootfs

检查:

复制代码
ls -l /home/book/nfs_rootfs/bin/busybox
ls -l /home/book/nfs_rootfs/bin/sh
ls -l /home/book/nfs_rootfs/sbin/init
ls -l /home/book/nfs_rootfs/etc/inittab
ls -l /home/book/nfs_rootfs/etc/init.d/rcS

验证NFS rootfs使用的是刚编译的BusyBox:

复制代码
sha256sum \
  ~/100ask_imx6ull-sdk/Busybox_1.30.0/_install/bin/busybox \
  /home/book/nfs_rootfs/bin/busybox

两个哈希相同即表示合并成功。