蓝天

从程序员角度看ELF

原文:http://xcd.blog.techweb.com.cn/archives/222.html

特殊说明(by jfo)

  对于static-linked或shared-linked的ELF可执行文件,他们的入口点都是 _start

  然后由 _start 函数调用 _init 执行相关的 .init 节中的初始化代码!(just disassemble the code)

  这说明内核在加载image后,在控制转入_start之前,_init 没有被调用;

  对于需要动态链接的可执行文件,内核将控制权转移给interpreter,

  interpreter 在完成链接工作后,将控制权转移给 _start ,也不会直接执行

  .init 节中的代码!(这里针对ELF可执行文件,对于共享库的.init段,还是由interpreter来调用的!!!see 启动过程::共享库的初始化)

  而dlopen 一个 .so 共享库后,_init 函数在返回前会被调用,.so 共享库

  是没有 _start 的!

  This is the example, which makes a shared .so file executable:

  http://hi.baidu.com/j%5Ffo/blog/item/1568184cf23d6dfad72afca3.html

  一个链接的example

  ld -o test -e_start -dynamic-linker=/lib/ld-linux.so.2 crt1.o crti.o crtbegin.o test.o -L /usr/lib/gcc/i386-redhat-linux/4.0.0/ -ldl -lc crtend.o crtn.o

  crt1.o中含有_start

  1 0×080480c0 : /* entry point in .text */

  2 call __libc_init_first /* startup code in .text */

  3 call _init /* startup code in .init */

  4 call atexit /* startup code in .text */

  5 call main /* application main routine */

  6 call _exit /* returns control to OS */

  7 /* control never reaches here */

  crti.o、test.o、crtn.o中的.init section中的代码共同组成了_init()函数,crti.o结尾代码会call 下一条指令,也就是说跳转到test.o中的.init section的代码继续执行,test.o中的.init代码不必ret,由crtn.o中的ret返回。

  crtend.o的.init代码含有对__do_global_ctors_aux()的调用,这说明C++构造函数是在前面所有.o文件(如 crti.o、crtbegin.o、test.o以及其他libc.a中的*.o)的.init代码执行之后才开始构造的,为什么放在最后,而不把对 __do_global_ctors_aux()的调用放在crtbegin.o中呢?那样可能更直观。

  其实也可 以理解,因为构造函数位于较高层次,很可能依赖于很多其他元素,如libc.a中的函数,因此先调用这些元素的.init代码也合情合理,就像C++构造子类时要先构造其父类一样。

  crtbegin.o的.fini代码含有对__do_global_dtors_aux()的调用,这说明C++析构函数是在后面所有.o文件(如 test.o、libc.a中的*.o、crtend.o、crtn.o)的.fini代码执行之前就开始析构了,同样也可以理解,应当先把位于较高层次的析构完成,再进行其他底层的析构代码,就像C++先析构子类再析构其其父类一样。

  crtbegin.o的.init代码还有一个对frame_dummy的调用,这个函数主要作用是注册exception frame(__register_frame_info_bases()函数),用于C++的异常处理机制(如回滚unwind),在 __do_global_dtors_aux()中析构对象后会unregister exception frame(__deregister_frame_info_bases()函数)。

  ———————–

  特殊参数

  ”-Wl,-Bstatic”参数,实际上是传给了****ld。指示它与静态库连接,如果系统中只有静态库当然就不需要这个参数了。 如果要和多个库相连接,而每个库的连接方式不一样,比如上面的程序既要和libhello进行静态连接,又要和libbye进行动态连接,其命令应为:

  $gcc testlib.o -o testlib -Wl,-Bstatic -lhello -Wl,-Bdynamic -lbye

  $gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 hello.o

  When creating an ELF shared object, set the internal DT_SONAME field to the

  specified name. When an executable is linked with a shared object which has a

  DT_SONAME field, then when the executable is run the dynamic linker will

  attempt to load the shared object specified by the DT_SONAME field rather than

  the using the file name given to the linker.

  ———————–

  启动过程 (linker and loader)

  启动动态链接器

  在操作系统运行程序时,它会像通常那样将文件的页映射进来,但注意在可执行程序

  中存在一个INTERPRETER区段。这里特定的解释器是动态链接器,即ld.so,它自己也是ELF

  共享库的格式。操作系统并非直接启动程序,而是将动态链接器映射到地址空间的一个合适

  的位置,然后从ld.so处开始,并在栈中放入链接器所需要的辅助向量(auxiliary vector)

  信息。向量包括:

  AT_PHDR,AT_PHENT,和AT_PHNUM:程序头部在程序文件中的地址,头部中每个表项的

  大小,和表项的个数。头部结构描述了被加载文件中的各个段。如果系统没有将程序映射到

  内存中,就会有一个AT_EXECFD项作为替换,它包含被打开程序文件的文件描述符。

  AT_ENTRY:程序的起始地址,当动态链接器完成了初始化工作之后,就会跳转到这个

  地址去。

  AT_BASE:动态链接器被加载到的地址。

  此时,位于ld.so起始处的自举代码找到它自己的GOT,其中的第一项(GOT[0])指向了ld.so文

  件中的DYNAMIC段。通过dynamic段,链接器在它自己的数据段中找到自己的重定位项表和

  重定位指针,然后解析例程需要加载的其它东西的代码引用(Linux ld.so将所有的基础例

  程都命名为由字串_dt_起头,并使用专门代码在符号表中搜索以此字串开头的符号并解析它

  们)。

  链接器然后通过指向程序符号表和链接器自己的符号表的若干指针来初始化一个符号

  表链。从概念上讲,程序文件和所有加载到进程中的库会共享一个符号表。但实际中链接器

  并不是在运行时创建一个合并后的符号表,而是将个个文件中的符号表组成一个符号表链。

  每个文件中都有一个散列表(一系列的散列头部,每个头部引领一个散列队列)以加速符号

  查找的速度。链接器可以通过计算符号的散列值,然后访问相应的散列队列进行查找以加速

  符号搜索的速度。

  库的查找

  链接器自身的初始化完成之后,它就会去寻找程序所需要的各个库。程序的程序头部

  有一个指针,指向dynamic段(包含有动态链接相关信息)在文件中的位置。在这个段中包

  含一个指针DT_STRTAB,指向文件的字串表,和一个偏移量表DT_NEEDED,其中每一个表项

  包含了一个所需库的名称在字串表中的偏移量。

  对于每一个库,链接器在以下位置搜索库:

  ● 是否dynamic段有一个称为DT_RPATH的表项,它是由分号分隔开的可以搜索库的目录列表。

  它可以通过一个命令行参数或者在程序链接时常规(非动态)链接器的环境变量来添加。它经

  常会被诸如数据库类这样需要加载一系列程序并可将库放在单一目录的子系统使用,

  ● 是否有一个环境符号LD_LIBRARY_PATH,它可以是由分号分隔开的可供链接器搜索库的目录

  列表。这就可以让开发者创建一个新版本的库并将它放置在LD_LIBRARY_PATH的路径中,这

  样既可以通过已存在的程序来测试新的库,或用来监测程序的行为。(因为安全原因,如果程

  序设置了set-uid,那么这一步会被跳过)

  ● 链接器查看库缓冲文件/etc/ld.so.conf,其中包含了库文件名和路径的列表。如果要查找的

  库名称存在于其中,则采用文件中相应的路径。大多数库都通过这种方法被找到(路径末尾的

  文件名称并不需要和所搜索的库名称精确匹配,详细请参看下面的库版本章节)。

  ● 如果所有的都失败了,就查找缺省目录/usr/lib,如果在这个目录中仍没有找到,就打印错

  误信息,并退出执行。

  一旦找到包含该库的文件,动态链接器会打开该文件,读取ELF头部寻找程序头部,它

  指向包括dynamic段在内的众多段。链接器为库的文本和数据段分配空间,并将它们映射进

  来,对于BSS分配初始化为0的页。从库的dynamic段中,它将库的符号表加入到符号表链

  中,如果该库还进一步需要其它尚未加载的库,则将那些新库置入将要加载的库链表中。

  在该过程结束时,所有的库都被映射进来了,加载器拥有了一个由程序和所有映射进

  来的库的符号表联合而成的逻辑上的全局符号表。

  共享库的初始化

  现在加载器再次查看每个库并处理库的重定位项,填充库的GOT,并进行库的数据段所

  需的任何重定位。

  在x86平台上,加载时的重定位包括:

  R_386_GLOB_DAT:初始化一个GOT项,该项是在另一个库中定义的符号的地址。

  R_386_32:对在另一个库中定义的符号的非GOT引用,通常是静态数据区中的指针。

  R_386_RELATIVE:对可重定位数据的引用,典型的是指向字串(或其它局部定义静态数

  据)的指针。

  R_386_JMP_SLOT:用来初始化PLT的GOT项,稍后描述。

  如果一个库具有.init区段,加载器会调用它来进行库特定的初始化工作,诸如C++的

  静态构造函数。库中的.fini区段会在程序退出的时候被执行。它不会对主程序进行初始化,

  因为主程序的初始化是有自己的启动代码完成的。当这个过程完成后,所有的库就都被完全

  加载并可以被执行了,此时加载器调用程序的入口点开始执行程序。

  静态的初始化

  如果一个程序存在对定义在一个库中的全局变量的引用,由于程序的数据地址必须在

  链接时被绑定,因此链接器不得不在程序中创建一个该变量的副本,如图4所示。这种方法

  对于共享库中的代码没有问题,因为代码可以通过GOT中的指针(链接器会调整它)来引用

  变量。但如果库初始化这个变量就会产生问题。为了解决问题,链接器在程序的重定位表

  (仅仅包含类型为R_386_JMP_SLOT、R_386_GLOB_DAT、R_386_32和R_386_RELATIVE的表项)

  中放入一个类型为R_386_COPY类型的表项,指向该变量在程序中的副本被定义的位置,并

  告诉动态链接器从共享库中将该变量被初始化的数值复制过来。

  —————————————————————————

  图10-4:全局数据初始化

  主程序中:

  extern int token;

  共享库中的例程:

  int token = 42;

  —————————————————————————

  虽然这个特性对于特定类型的代码是关键的,但在实际中很少发生。这是一种橡皮膏

  (译者注:权宜之计的意思),因为它只能用于单字的数据。好在初始化程序通常的对象是

  指向过程或其它数据的指针,所以这个橡皮膏够用了。

  库的版本

  动态链接库通常都会结合主版本和次版本号来命名,例如libc.so.1.1。但是应用程序

  只会和主版本号绑定,例如libc.so.1,次版本号是用于升级的兼容性的。

  为了保持加载程序合理的速度,系统会设法维护一个缓冲文件,保存最近用过的每一

  个库的全路径文件名,该文件会在一个新库被安装时有一个配置管理程序来更新。

  为了支持这个设计,每一个动态链接的库都有一个在库创建时赋予的称为SONAME的“

  真名”。例如,被称为libc.so.1.1的库的SONAME为libc.so.1(缺省的SONAME是库的名

  称)。当链接器创建一个使用共享库的程序时,它会列出程序所使用库的SONAME而不是库

  的真实名称。缓冲文件创建程序扫描包含共享库的所有目录,查找所有的共享库,提取每一

  个的SONAME,对于具有相同SONAME的多个库,除版本最高的外其余的忽略。然后它将SONAM

  E和全路径名称写入缓冲文件,这样在运行时动态链接器可以很快的找到每一个库的当前版

  本。

阅读(202) | 评论(2) | 转发(0) |
评论热议

posted on 2014-04-10 10:58  #蓝天  阅读(175)  评论(0编辑  收藏  举报

导航