Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1087523
  • 博文数量: 414
  • 博客积分: 10030
  • 博客等级: 上将
  • 技术积分: 4440
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-05 21:42
文章分类

全部博文(414)

文章存档

2011年(1)

2009年(1)

2008年(412)

我的朋友

分类: LINUX

2008-10-13 09:44:31

在探讨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
下载: 下载
阅读(470) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~