BusyBox 根文件系统移植主要可以分为两个部分:一是 BusyBox 的编译过程,二是 BusyBox 根文件系统的启动流程。
1、BusyBox是一个"多调用可执行程序",编译时会根据.config把ls、cat、mount、init、sh等多个applet功能链接进同一个busybox ELF;执行/bin/ls时,内核先沿着软链接加载/bin/busybox,BusyBox再从argv[0]取得命令名ls,查找applet表并调用ls_main(),并不是先查表再寻找软链接。
2、根文件系统则是Linux挂载为/的完整目录和文件集合,BusyBox只为它提供基础命令、Shell和可选的init,并不等同于根文件系统本身;其中/bin、/sbin保存命令及其BusyBox链接,/etc保存inittab、rcS等配置,/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.o和lib.a;最后由scripts/trylink调用交叉GCC链接出busybox_unstripped,经strip生成最终的busybox。
4、make install再将其安装到rootfs并创建ls、sh、init等软链接,运行时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.c 或 init.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
两个哈希相同即表示合并成功。