以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
参考博客
前言
uboot给内核传参的bootargs中有“init=/linuxrc”这个项目。
由参考博客(2)可知 /linuxrc 这个二进制文件位于根文件系统中。
由参考博客(1)可知它是一个软连接,指向了根文件系统中的/bin/busybox这个命令。
接下来先总结性地介绍 /linuxrc 这个文件,然后再分析busybox的源码。
一、/linuxrc文件简介
1.1 /linuxrc文件的本质
(1)/linuxrc是一个应用层的程序,和内核没有关系。
(2)/linuxrc在开发板当前内核系统下是可执行的。如果在ARM SoC的linux系统上运行,那么这个程序就是用arm-linux-gcc编译链接的;如果是在PC机linux系统下运行,那么这个程序就是用gcc编译连接的。
(3)/linuxrc如果是静态编译连接的,则直接可以运行;如果是动态编译连接的,必须提供必要的库文件才能运行。但实际上/linuxrc由内核直接调用执行,因此用户没有机会去导出库文件的路径,因此/linuxrc没法动态连接,一般都是静态连接的。
1.2 /linuxrc文件的作用
(1)执行时引出用户界面。/linuxrc应用程序,也就是我们认识中的进程1,是其他所有应用程序进程的祖宗进程。比如用户操作界面由/linuxrc引出,应用程序是直接或者间接地被/linuxrc调用执行的。用户界面程序、其他的应用程序就是进程2、3、4等等。
(2)负责系统启动后的配置。为了让操作系统用起来更方便,更具实用性,操作系统启动后还需要进行配置(一般叫做运行时配置,英文简写是etc),/linuxrc负责系统启动后的配置。
1.3 /linuxrc文件的种类
在嵌入式linux中,/linuxrc文件一般就是busybox(执行busybox),下面是它的简单介绍
(1)busybox是一个C语言写出来的项目,里面包含了很多.c文件和.h文件。此项目可以被配置编译成各个平台下面可以运行的应用程序。如果用arm-linux-gcc来编译busybox就会得到一个可以在我们开发板linux内核上运行的应用程序。
(2)busybox是为了在嵌入式环境下构建rootfs而开发的,换言之,它是为了构建根文件系统而专门开发的init进程应用程序。比如海思SDK提供的文件夹形式的根文件系统,其目录中的/linuxrc文件,它里面只有一行代码“bin/busybox”。
(3)busybox同时也为当前系统提供了一整套的shell命令程序集,比如vi、cd、ls等。在linux发行版(比如ubuntu、redhat、centOS等)中,vi、cd、ls等命令都是一个个单独的应用程序,但是在嵌入式linux中,为了省事,把vi、cd等所有常用的shell命令集合到一起,构成一个shell命令包,起名叫busybox。如下所示,嵌入式linux中的shell命令都是busybox的软连接。
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh/bin$ ls
ash chown dmesg fdflush hush ln mktemp nice reformime setarch touch
base64 conspy dnsdomainname fgrep ionice login more pidof rev setserial true
busybox cp dumpkmap fsync iostat ls mount ping rm sh umount
cat cpio echo getopt ipcalc lsattr mountpoint ping6 rmdir sleep uname
catv cttyhack ed grep kbd_mode lzop mpstat pipe_progress rpm stat usleep
chattr date egrep gunzip kill makemime mt printenv run-parts stty vi
chgrp dd false gzip linux32 mkdir mv ps scriptreplay su watch
chmod df fatattr hostname linux64 mknod netstat pwd sed tar zcat
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh/bin$ ls -l
total 600
lrwxrwxrwx 1 root root 7 十一 9 16:32 ash -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 base64 -> busybox
-rwxr-xr-x 1 root root 613584 十二 6 00:03 busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 cat -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 catv -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 chattr -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 chgrp -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 chmod -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 chown -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 conspy -> busybox
lrwxrwxrwx 1 root root 7 十一 9 16:32 cp -> busybox
###忽略部分输出
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh/bin$
(4)集成后busybox程序的大小比这些常用的命令的大小加起来要小很多。这是因为busybox本身提供的shell命令是阉割版的。busybox中的命令支持的参数选项比发行版中要少,比如ls在发行版中可以有几十个参数选项,但是在busybox中只保留了几个常用的选项,不常用的都删除掉了。另外busybox中所有命令的实现代码都在一个程序中实现,而各个命令中有很多代码函数都是通用的,通用会降低重复代码出现的次数,从而减少总的代码量和体积。比如ls、cd、mkdir等命令都会操作目录,因此在busybox中实现目录操作的函数就可以被这些命令通用。busybox的体积优势是嵌入式系统本身的要求和特点造成的。
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh$ du -h ./bin/ #嵌入式linux的
604K ./bin/
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh$ du -h /bin #ubuntu的
9.3M /bin
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh$ du -h /sbin/
13M /sbin/
xjh@ubuntu:~/iot/embedded_basic/rootfs/rootfs_xjh$ du -h /usr/sbin/
15M /usr/sbin/
二、busybox源码分析
上面提到,/linuxrc在嵌入式linux中一般就是busybox,因此接下来我们分析busybox的源码。
2.1 源码目录结构
busybox的源码目录如下:
xjh@ubuntu:~/iot/embedded_basic/rootfs/busybox/busybox-1.24.1$ ls
applets configs editors libbb Makefile.custom printutils selinux util-linux
applets_sh console-tools examples libpwdgrp Makefile.flags procps shell
arch coreutils findutils LICENSE Makefile.help qemu_multiarch_testing sysklogd
archival debianutils include loginutils miscutils README testsuite
AUTHORS docs init mailutils modutils runit TODO
Config.in e2fsprogs INSTALL Makefile networking scripts TODO_unicode
xjh@ubuntu:~/iot/embedded_basic/rootfs/busybox/busybox-1.24.1$
我们进入coreutils目录,发现该目录下的文件都是一些shell命令的实现:
xjh@ubuntu:~/iot/embedded_basic/rootfs/busybox/busybox-1.24.1$ cd coreutils/
xjh@ubuntu:~/iot/embedded_basic/rootfs/busybox/busybox-1.24.1/coreutils$ ls
basename.c comm.c du.c hostid.c ls.c od.c shuf.c tail.c uname.c yes.c
cal.c Config.src echo.c id.c md5_sha1_sum.c printenv.c sleep.c tee.c uniq.c
cat.c cp.c env.c id_test.sh mkdir.c printf.c sort.c test.c unlink.c
catv.c cut.c expand.c install.c mkfifo.c pwd.c split.c test_ptr_hack.c usleep.c
chgrp.c date.c expr.c Kbuild.src mknod.c readlink.c stat.c touch.c uudecode.c
chmod.c dd.c false.c length.c.disabled mv.c realpath.c stty.c tr.c uuencode.c
chown.c df.c fold.c libcoreutils nice.c rm.c sum.c true.c wc.c
chroot.c dirname.c fsync.c ln.c nohup.c rmdir.c sync.c truncate.c whoami.c
cksum.c dos2unix.c head.c logname.c od_bloaty.c seq.c tac.c tty.c who.c
xjh@ubuntu:~/iot/embedded_basic/rootfs/busybox/busybox-1.24.1/coreutils$
2.2 源码的入口
我们对程序进行分析时,需要按照程序运行时的逻辑顺序来分析,因此要找到入口地址。
在uboot和kernel这两个大的C语言项目中,入口地址由链接脚本指定。而操作系统下的应用程序,它们的入口地址一般是main函数。busybox是linux启动后运行的第一个应用程序,因此其中必然有main函数,而且main就是入口地址。
对busybox源码建立SI工程,搜索main符号时,发现它出现在不同文件中(条件编译的缘故),哪个文件的main函数才是入口地址?我们可以在编译之后再查看这些文件是否有对应的.o文件,进而得知该文件是否被编译。如何编译busybox源码,见博客从零开始构建根文件系统。
我们发现这些包含main函数的文件中,只有/busybox-1.24.1/libbb/appletlib.c这个文件被编译(严谨地说,这些文件中也有其他几个文件被编译生成.o文件,但是它们对busybox这体系只是起到辅助作用,不会被链接。具体过程可以研究busybox的配置与编译体系)。因此busybox的入口地址就是/busybox-1.24.1/libbb/appletlib.c的main函数。
#if ENABLE_BUILD_LIBBUSYBOX
int lbb_main(char **argv)
#else
int main(int argc UNUSED_PARAM, char **argv)
#endif
{
/* Tweak malloc for reduced memory consumption */
#ifdef M_TRIM_THRESHOLD
/* M_TRIM_THRESHOLD is the maximum amount of freed top-most memory
* to keep before releasing to the OS
* Default is way too big: 256k
*/
mallopt(M_TRIM_THRESHOLD, 8 * 1024);
#endif
#ifdef M_MMAP_THRESHOLD
/* M_MMAP_THRESHOLD is the request size threshold for using mmap()
* Default is too big: 256k
*/
mallopt(M_MMAP_THRESHOLD, 32 * 1024 - 256);
#endif
#if !BB_MMU
/* NOMMU re-exec trick sets high-order bit in first byte of name */
if (argv[0][0] & 0x80) {
re_execed = 1;
argv[0][0] &= 0x7f;
}
#endif
#if defined(SINGLE_APPLET_MAIN)
/* Only one applet is selected in .config */
if (argv[1] && is_prefixed_with(argv[0], "busybox")) {
/* "busybox <applet> <params>" should still work as expected */
argv++;
}
/* applet_names in this case is just "applet\0\0" */
lbb_prepare(applet_names IF_FEATURE_INDIVIDUAL(, argv));
return SINGLE_APPLET_MAIN(argc, argv);
#else
lbb_prepare("busybox" IF_FEATURE_INDIVIDUAL(, argv));
applet_name = argv[0]; //在命令行输入命令时,该命令存储在argv[0]中
if (applet_name[0] == '-')
applet_name++;
applet_name = bb_basename(applet_name);
parse_config_file(); /* ...maybe, if FEATURE_SUID_CONFIG */
run_applet_and_exit(applet_name, argv);//寻找输入的命令所对应的函数并执行
/*bb_error_msg_and_die("applet not found"); - sucks 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);
#endif
}
2.3 xxx_main函数
我们注意到,busubox中有很多 xxx_main 函数,它们是busybox所支持的xxx命令的真正入口。
当执行xxx命令时,busybox内部都是先执行appletlib.c中的main函数,然后通过参数argv[0]判别要执行xxx函数,接着调用xxx_main函数来具体实现这个命令。这过程见上面第2点的代码注释。
比如执行pwd命令时,先执行/busybox-1.24.1/libbb/appletlib.c的main函数,然后调用pwd_main函数来完成pwd的命令。pwd_main函数位于busybox-1.24.1\coreutils\pwd.c文件中。
int pwd_main(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
{
char *buf;
if (ENABLE_DESKTOP) {
/* TODO: assume -L if $POSIXLY_CORRECT? (coreutils does that)
* Rationale:
* POSIX requires a default of -L, but most scripts expect -P
*/
unsigned opt = getopt32(argv, "LP");
if ((opt & 1) && logical_getcwd())
return fflush_all();
}
buf = xrealloc_getcwd_or_warn(NULL);//buf中的内容是pwd命令的结果
if (buf) {
puts(buf);//输出pwd命令的结果到屏幕
free(buf);
return fflush_all();
}
return EXIT_FAILURE;
}
2.4 对inittab文件进行解析与执行
busybox-1.24.1/init/init.c中的init_main函数,负责对根文件系统的/etc/inittab文件进行解析与执行。
(1)init_main函数首先调用parse_inittab函数对inittab文件进行解析,这里只是将inittab文件中的各个action和process解析出来并放在链表中,并没有执行。
(2)init_main函数的后面部分,先执行sysinit、wait和once(注意这里只执行一遍),接着在while(1)死循环中去执行respwan和askfirst。
int init_main(int argc UNUSED_PARAM, char **argv)
{
if (argv[1] && strcmp(argv[1], "-q") == 0) {
return kill(1, SIGHUP);
}
#if DEBUG_SEGV_HANDLER //设置信号处理函数?
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handle_sigsegv;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
}
#endif
####忽略部分代码
/* Figure out where the default console should be */ //初始化控制台
console_init();
set_sane_term();
xchdir("/");
setsid();
####忽略部分代码
/* Check if we are supposed to be in single user mode */
if (argv[1]
&& (strcmp(argv[1], "single") == 0 || strcmp(argv[1], "-s") == 0 || LONE_CHAR(argv[1], '1'))
) {
/* ??? shouldn't we set RUNLEVEL="b" here? */
/* Start a shell on console */
new_init_action(RESPAWN, bb_default_login_shell, "");
} else {
/* Not in single user mode - see what inittab says */
/* NOTE that if CONFIG_FEATURE_USE_INITTAB is NOT defined,
* then parse_inittab() simply adds in some default
* actions (i.e., INIT_SCRIPT and a pair
* of "askfirst" shells) */
parse_inittab();//解析inittab文件
}
####忽略部分代码
/* Now run everything that needs to be run */ //执行各活动
/* First run the sysinit command */
run_actions(SYSINIT);
check_delayed_sigs();
/* Next run anything that wants to block */
run_actions(WAIT);
check_delayed_sigs();
/* Next run anything to be run only once */
run_actions(ONCE);
/* Now run the looping stuff for the rest of forever.
*/
while (1) {
int maybe_WNOHANG;
maybe_WNOHANG = check_delayed_sigs();
/* (Re)run the respawn/askfirst stuff */
run_actions(RESPAWN | ASKFIRST);
maybe_WNOHANG |= check_delayed_sigs();
/* Don't consume all CPU time - sleep a bit */
sleep(1);
maybe_WNOHANG |= check_delayed_sigs();
/* Wait for any child process(es) to exit.
*
* If check_delayed_sigs above reported that a signal
* was caught, wait will be nonblocking. This ensures
* that if SIGHUP has reloaded inittab, respawn and askfirst
* actions will not be delayed until next child death.
*/
if (maybe_WNOHANG)
maybe_WNOHANG = WNOHANG;
while (1) {
pid_t wpid;
struct init_action *a;
/* If signals happen _in_ the wait, they interrupt it,
* bb_signals_recursive_norestart set them up that way
*/
wpid = waitpid(-1, NULL, maybe_WNOHANG);
if (wpid <= 0)
break;
a = mark_terminated(wpid);
if (a) {
message(L_LOG, "process '%s' (pid %d) exited. "
"Scheduling for restart.",
a->command, wpid);
}
/* See if anyone else is waiting to be reaped */
maybe_WNOHANG = WNOHANG;
}
} /* while (1) */