👀 『敬畏』、『热情』、『专注』、『分享』
👉在嵌入式系统构建中,Busybox可用于构建轻量级的根文件系统,本文从源码结构和源码入口角度分析busybox,了解其背后的运作机制。
busybox版本:1.35.0
一、Busybox简介
(1-1)开源项目
Busybox是一个开源项目,遵循GPL v2协议。Busybox将众多的UNIX命令集合进了一个很小的可执行程序中,可以用来替代GNU fileutils、shellutils等工具集。Busybox中各种命令与相应的GNU工具相比,所能提供的选项比较少,但是对于一般的应用场景也足够了,特别是在嵌入式系统的设计中。
(1-2)程序本体较小
Busybox在设计过程中对文件大小进行了优化,并考虑了系统资源有限(比如内存等)的情况。与一般的GNU工具集动辄几M的体积相比,动态链接的Busybox只有几百K,即使是采用静态链接也只有1M左右。除此之外,Busybox按模块设计,可以很容易地加入、去除某些命令,或增减命令的某些选项。
(1-3)使用简单
如果使用Busybox来创建根文件系统,使用起来比较方便,只需要在/dev目录下创建必要的设备节点,在/etc目录下增加一些配置文件即可,当然如果Busybox是动态链接的,那么还需要在/lib目录下包含相关的运行库文件。
二、Busybox源码目录结构
在较老版本的Busybox中,对于Busybox的多个程序是全部塞进了一个名为utility.c
的文件中,后来更改了Busybox的整体源码结构和设计,将这些程序拆分成了各个工具模块。
序号 | 目录名称 | 功能说明 |
---|---|---|
1 | applets | 实现applets框架的文件。目录中包含了几个main()的文件 |
2 | applets_sh | 此目录包含了几个作为shell脚本实现的applet示例。在“make install”时不会被自动安装,需要使用时,手动处理 |
3 | arch | 包含用于不同体系架构的makefile文件。约束busybox在不同架构体系下的编译构建过程 |
4 | archival | 与压缩相关命令的实现源文件。 |
5 | configs | busybox自带的默认配置文件 |
6 | console-tools | 与控制台相关的一些命令 |
7 | coreutils | 常用的一些核心命令。例如chgrp、rm等 |
8 | debianutils | 针对Debian的套件。 |
9 | e2fsprogs | 针对Linux Ext2 FS prog的命令。例如chattr、lsattr |
10 | editors | 常用的编辑命令。例如diff、vi等 |
11 | findutils | 用于查找的命令 |
12 | include | busybox项目的头文件 |
13 | init | init进程的实现源码目录 |
14 | klibc-utils | klibc命令套件 |
15 | libbb | 与busybox实现相关的库文件 |
16 | libpwdgrp | libpwdgrp相关的命令 |
17 | loginutils | 与用户管理相关的命令 |
18 | mailutils | 与mail相关的命令套件 |
19 | miscutils | 该文件下是一些杂项命令,针对特定应用场景 |
20 | modutils | 与模块相关的命令 |
21 | networking | 与网络相关的命令,例如arp |
22 | printutils | Print相关的命令 |
23 | procps | 与内存、进程相关的命令 |
24 | runit | 与Runit实现相关的命令 |
25 | shell | 与shell相关的命令 |
26 | sysklogd | 系统日志记录工具相关的命令 |
27 | util-linux | Linux下常用的命令,主要与文件系统操作相关的命令。 |
三、Busybox程序主体
Busybox是在linux内核启动后加载运行的用户空间程序,在源码设计上是基于C语言完成设计和开发的。与常规程序一样,Busybox的入口同样是main()
,定义在libbb/appletlib文件的末尾处。在函数开始处,使用ENABLE_BUILD_LIBBUSYBOX
对函数名称进行了条件分支处理:如果ENABLE_BUILD_LIBBUSYBOX
为真,则表示将Busybox以库的方式进行构建。
在函数体中,以条件宏定义进行代码的编译逻辑控制:
/* Tweak malloc for reduced memory consumption */
#ifdef M_TRIM_THRESHOLD
/* M_TRIM_THRESHOLD是释放的最顶层内存的最大数量
* 默认值太大,是256k
*/
mallopt(M_TRIM_THRESHOLD, 8 * 1024);
#endif
#ifdef M_MMAP_THRESHOLD
/* M_MMAP_THRESHOLD是使用mmap()的请求大小阈值。
* 默认值是256k
*/
mallopt(M_MMAP_THRESHOLD, 32 * 1024 - 256);
#endif
上述代码都调用了mallopt()
函数,该函数用于设置内存的分配参数,由于默认值太大(为256KB),故此处调整内存分配大小,让出多余的内存。
接着,是一个由#if -- #elif -- #else -- #endif
控制的条件宏多分支判断结构语句,此处以Busybox的一般运行情况为例(在Linux内核启动后期,加载并运行Busybox构建出的init
程序)。其执行逻辑如下:
首先Busybox是一个linux下的工具集合,本质则是一个个的命令,例如:ls、mv、cp等,在命令行我们输入想要执行的操作时,例如:mkdir iriczhao
,则会将参数传递给Busybox,然后由他完成对应的操作。
在源码中,使用char * applet_name
表示工具的名称(本质是字符串),首先会调用lbb_prepare()
函数:
lbb_prepare("busybox" IF_FEATURE_INDIVIDUAL(, argv));
将会设置applet_name
的值为“busybox“,用于执行ENABLE_FEATURE_INDIVIDUAL
为真时的逻辑操作:
void lbb_prepare(const char *applet
IF_FEATURE_INDIVIDUAL(, char **argv))
{
#ifdef bb_cached_errno_ptr
ASSIGN_CONST_PTR(&bb_errno, get_perrno());
#endif
applet_name = applet;
if (ENABLE_LOCALE_SUPPORT)
setlocale(LC_ALL, "");
#if ENABLE_FEATURE_INDIVIDUAL
/* Redundant for busybox (run_applet_and_exit covers that case)
* but needed for "individual applet" mode */
if (argv[1] && !argv[2] && strcmp(argv[1], "--help") == 0 && !is_prefixed_with(applet, "busybox"))
{
/* Special cases. POSIX says "test --help"
* should be no different from e.g. "test --foo".
*/
if (!(ENABLE_TEST && strcmp(applet_name, "test") == 0) && !(ENABLE_TRUE && strcmp(applet_name, "true") == 0) && !(ENABLE_FALSE && strcmp(applet_name, "false") == 0) && !(ENABLE_ECHO && strcmp(applet_name, "echo") == 0))
bb_show_usage();
}
#endif
}
接着,会解析命令行传递的第一个参数:
applet_name = argv[0];
if (applet_name[0] == '-')
applet_name++;
applet_name = bb_basename(applet_name);
例如,在命令行输入mkdir iriczhao
命令,则会解析到mkdir
命令传递给applet_name,至于后面的参数(此处是iriczhao)是如何传递的,后文会描述到。
如果配置了FEATURE_SUID_CONFIG
宏定义,在parse_config_file()
函数中还将从/etc/busybox.conf文件中解析关于busybox的配置参数。
在最后,则是busybox的重要函数:run_applet_and_exit()
,该函数定义如下:
static NORETURN void run_applet_and_exit(const char *name, char **argv)
{
#if ENABLE_BUSYBOX
//检查是否是带有busybox前缀的字符串,如果不是,则返回NULL。
//如果在命令行下输入具体的命令,则不是带有busybox前缀的命令字符串,则不会执行该条件下的语句
if (is_prefixed_with(name, "busybox"))
exit(busybox_main(/*unused:*/ 0, argv));
#endif
#if NUM_APPLETS > 0
/* find_applet_by_name() search is more expensive, so goes second */
{
int applet = find_applet_by_name(name);
if (applet >= 0)
run_applet_no_and_exit(applet, name, argv);
}
#endif
/*bb_error_msg_and_die("applet not found"); - links in printf */
full_write2_str(applet_name);
full_write2_str(": applet not found\n");
/* POSIX: "If a command is not found, the exit status shall be 127" */
exit(127);
}
如果NUM_APPLETS大于0,则会执行对应的命令操作,并退出;否则,busybox将会报错:
#if NUM_APPLETS > 0
/* find_applet_by_name() search is more expensive, so goes second */
{
int applet = find_applet_by_name(name);
if (applet >= 0)
run_applet_no_and_exit(applet, name, argv);
}
#endif
//正常情况下(NUM_APPLETS > 0),不会执行下述代码。
/*bb_error_msg_and_die("applet not found"); - links in printf */
full_write2_str(applet_name);
full_write2_str(": applet not found\n");
/* POSIX: "If a command is not found, the exit status shall be 127" */
exit(127);
从上述代码可知,在命令行键入命令后,实则起关键作者的函数是:find_applet_by_name()和run_applet_no_and_exit()
。下文将继续分析。
四、Busybox程序运行剖析
在上一小节中,已经知道当我们在busybox的命令行下,键入命令后,执行具体操作的函数是:find_applet_by_name()和run_applet_no_and_exit()
。
在编译构建源码并安装busybox后,在安装目录下的文件结构则是一个名为busybox的可执行程序和很多的链接,这些链接实则是我们在命令行键入的命令名称。如下图所示:
从源码角度看,busybox中的命令都有一一对应的执行函数,其函数命名格式为xxx_main()
,在源码设计上,其内部在/include/applet_tabls.h头文件中维护了一张命令表,定义如下(代码太长,有省略):
int (*const applet_main[])(int argc, char **argv) = {
test_main,
test_main,
acpid_main,
add_remove_shell_main,
addgroup_main,
adduser_main,
adjtimex_main,
uname_main,
arp_main,
arping_main,
ascii_main,
ash_main,
awk_main,
baseNUM_main,
baseNUM_main,
basename_main,
//省略大量内容
//...
}
上述函数指针数组中的元素则是分布于busybox源码各个目录下命令入口函数。在代码执行逻辑中,首先会调用find_applet_by_name()
函数,通过传入的命令名称获取在命令表中的数组下标。并将命令对应的下标applet、命令名称name和命令行参数字符串argv传递给run_applet_no_and_exit()
函数(注:解释了上一小节中,命令行对应命令后面的参数是如何传递的),该函数定义如下:
void FAST_FUNC run_applet_no_and_exit(int applet_no, const char *name, char **argv)
{
int argc;
/*
* We do not use argv[0]: do not want to repeat massaging of
* "-/sbin/halt" -> "halt", for example.
*/
applet_name = name;
show_usage_if_dash_dash_help(applet_no, argv);
if (ENABLE_FEATURE_SUID)
check_suid(applet_no);
argc = string_array_len(argv);
xfunc_error_retval = applet_main[applet_no](argc, argv);
/* Note: applet_main() may also not return (die on a xfunc or such) */
xfunc_die();
}
#endi
在上述代码中,执行命令下的对应具体操作函数的语句是:
xfunc_error_retval = applet_main[applet_no](argc, argv);
applet_main
是命令表数组,applet_no
是对应命令的数组下标,本质则是调用对应的applet_main
命令表数组中的元素(函数指针),并将argc
和argv
作为参数给了对应的命令执行函数。