在探讨Linux挂载根文件系统之后的流程时,制作了一个小的根文件系统作为分析的示例,结果因为共享库的问题,好长时间没有解决。从网上搜索了些资料,初步理解下,后续有时间再深入。
1、理解什么是目标代码,什么可执行代码,什么是库。
编译之后生成的是二进制代码就是目标代码,它是不可以直接执行的。经过链接之后生成可执行代码,也就是可执行代码实际上是目标代码,操作系统的启动代码,库代码三者的总和。当然,不是简单的融合在一起,这个过程是比较复杂的,也就是链接器的作用了。
由于目标代码,可执行代码都是操作系统相关的文件格式,所以是不兼容的。比如,Linux下不支持windows的exe可执行文件格式,windows也不支持Linux下的ELF可执行文件格式。
库其实本质上也是可执行代码的二进制格式。库有三种使用形式:静态库、共享库、动态库。静态库供链接器使用,在链接时加载;共享库在链接时定位,在运行时加载;动态库是共享库的另一种变化形式,也是在运行时加载,不过并非在程序运行开始时加载,而是在程序中的语句需要使用该函数时才载入。动态库可以在程序运行期间释放动态库所占用的内存。
Linux下的库文件分为共享库和静态库两大类。它们之间的差别就在于加载时刻不同,如上所述。区分库类型的方法就是看文件后缀,通常共享库以.so(shared object)结尾,而静态库通常以.a结尾(archive)。在终端缺省情况下,共享库通常为绿色,而静态库为黑色。
2、库的命名规则
GNU的库的使用必须遵守Library GNU Public License(LGPL许可协议)。该协议和GNU许可协议略有不同,开发人员可以免费使用GNU库进行软件开发,但必须保证向用户提供所用的库函数的源代码。
系统中可用的库默认存在在/usr/lib和/lib目录中。库文件名由前缀lib和库名以及后缀组成。根据库的类型不同,后缀名也不一样。共享库的后缀名由.so和版本号组成,静态库的后缀名为.a。采用旧的a.out格式的共享库的后缀名为.sa.
libname.so.major.minor
libname.a
这里的name可以是任何字符串,用来唯一标识某个库。该字符串可以是一个单字、几个字符,甚至是一个字母。数学共享库的库名为libm.so.5,这里的标识字符为m,版本号为5.libm.a则是静态数学库。
3、自己的一些理解
初步理解了链接器和加载器的原理,可以明白下面的规则:
静态链接 --> 静态库 --> 链接时加载
动态链接 --> 共享库 --> 链接时定位,运行时加载
所以,很明显,如果是静态链接,则自动去寻找静态库。假定你指定了静态链接,而库路径下仅仅有相应的共享库文件,则依然提示找不到。对于静态链接程序而言,所有目标文件都集中在一起而成为可执行文件,它不需要其他的支持就可以到兼容的系统中运行。正是因为所有的都集中,所以,静态编译的文件特别大,占用空间就大。而嵌入式系统对空间要求比较苛刻,所以最好还是使用共享库。
共享库在链接时只是定位,知道库的位置,在运行时才会载入库。这样,问题就来了,它怎么载入库呢?是通过共享库加载器来完成的。对于ELF文件,使用的共享库加载器为ld-linux.so.2。所以,要想使用共享库,首先应该有ld-linux.so.2作为共享库加载器。那么,又产生的问题是:在运行时如何找到共享库的位置呢?无论何时载入程序打算运行时,共享库都应该位于以下位置:
(1)环境变量LD_LIBRARY_PATH列出的所有用分号分隔的位置
(2)文件/etc/ld.so.cache中找到的库的列表,由工具ldconfig维护
(3)目录/lib
(4)目录/usr/lib
如果我想要验证最小的根文件系统,那么最合适的方案就是:/bin /dev/ /lib /lost+found,bin下只有一个sh程序,dev下只有console,lib下存放sh的依赖共享库。只是实现了一个功能,就是打开一个shell终端程序,只是不能做任何工作。如果采用静态链接制作sh,那么lib文件夹也可以不需要。这个制作没有实际意义,但是可以理解初始的流程。
分析一下这个过程。大体的思路是,加载根文件系统,然后打开console,寻找第一个执行程序init,找到后就开始执行。具体看init/main.c中的init函数,基本初始化,完成命令行的解析后:
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) printk(KERN_WARNING "Warning: unable to open an initial console.\n");
|
所以,应该在/dev下包含console设备文件,否则的话,会出现无法打开初始化终端的提示。
/* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { run_init_process(execute_command); printk(KERN_WARNING "Failed to execute %s. Attempting " "defaults...\n", execute_command);
|
这里的execute_command是一个全局变量,在解析传递进内核的命令行参数时确定的。
static int __init init_setup(char *str) { unsigned int i;
execute_command = str; /* * In case LILO is going to boot us with default command line, * it prepends "auto" before the whole cmdline which makes * the shell think it should execute a script with such name. * So we ignore all arguments entered _before_ init=... [MJ] */ for (i = 1; i < MAX_INIT_ARGS; i++) argv_init[i] = NULL; return 1; } __setup("init=", init_setup);
|
这里的__setup是一个宏定义,比较复杂,定义在include/linux/init.h中。
#define __setup(str, fn) \ __setup_param(str, fn, fn, 0)
|
#define __setup_param(str, unique_id, fn, early) \ static char __setup_str_##unique_id[] __initdata = str; \ static struct obs_kernel_param __setup_##unique_id \ __attribute_used__ \ __attribute__((__section__(".init.setup"))) \ __attribute__((aligned((sizeof(long))))) \ = { __setup_str_##unique_id, fn, early }
|
这个利用了gcc的一些扩展用法,暂时忽略,来关注流程。就是如果你指定了init=filename,那么首先按照指定文件执行。如果没有指定,则要按照下列顺序依次执行:
run_init_process("/sbin/init"); run_init_process("/etc/init"); run_init_process("/bin/init"); run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
|
可见,如果这四个位置都没有找到,或者找到文件,但是不可执行,那么就会产生kernel panic。而这个kernel panic是最为常见的一个错误提示了。
分析到这里,也就清晰为什么最小的根文件系统会是这样了。具体制作步骤:
(1)制作一个ramdisk映象
dd if=/dev/zero of=if.img bs=1k count=15360 mke2fs -F -v -m0 if.img
|
其中,of指定生成的映象的名字,bs是块的大小,count是ramdisk的大小,单位是KB。mke2fs是初始化ramdisk中的文件系统,完成后挂载后会出现lost+found文件夹。
(2)添加内容
|-- new | |-- bin | | |-- bash | | `-- sh -> bash | |-- dev | | `-- console | |-- lib | | |-- ld-2.1.3.so | | |-- ld-linux.so.2 -> ld-2.1.3.so | | |-- libc-2.1.3.so | | |-- libc.so.6 -> libc-2.1.3.so | | |-- libtermcap.so.2 -> libtermcap.so.2.0.8 | | `-- libtermcap.so.2.0.8 | `-- lost+found
|
这是制作的根文件系统的最小部分。其中,bash和lib里面的共享库是从开发板的ramdisk中摘出来的。至于bash是用了那些共享库,可以用arm-linux-readelf查找,不过需要注意的是,前面工具没有列出ld-linux.so.2,而这个共享库加载器是必需的。在host下,可以使用ldd查看可执行文件依赖的共享库,比较全面。原来的时候,ld-linux.so.2出现了问题,结果不管怎么修改,都会出现上面提到的那个kernel panic,后来重新用了一个ld-linux.so.2,问题才消除。也说明,/etc/ld.so.conf和/etc/ld.so.cache并非是必须的,在嵌入式系统中,你完全可以把需要用到的共享库放到/lib或者/usr/lib下面,这样就不需要设定LD_LIBRARY_PATH,也不需要这两种/etc/下的配置文件了。不过应用多的情况下,为了区别,也可以使用上述两种方式添加库的查找路径。
(3)制作完成,并加载
[root@lqm fs]# umount new [root@lqm fs]# gzip -c -v9 ramdisk > ramdisk.gz
|
然后加载ramdisk.gz,最终效果:
RAMDISK: Compressed image found at block 0 VFS: Mounted root (ext2 filesystem). Freeing init memory: 84K bash#
|
可见挂载根文件系统是正确的,可以识别ext2文件系统。然后打开了console,并且执行了/bin/sh。
ramdisk映象备份如下,通过这个制作和分析过程,就比较清晰加载根文件系统后的初始过程。后面的完善功能定制,就完全依赖于shell脚本的配置和执行了。需要对这个过程进行更深入的分析和了解。
|
文件: |
ramdisk.rar |
大小: |
546KB |
下载: |
下载 | |
阅读(4076) | 评论(0) | 转发(0) |