在嵌入式系统构建中,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 "); /* 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 "); /* 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作为参数给了对应的命令执行函数。
站在巨人的肩膀上,『敬畏』、『热情』
评论
查看更多