【zz】从程序员角度看ELF


my

特殊说明(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    0x080480c0 <_start>:         / 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和全路径名称写入缓冲文件,这样在运行时动态链接器可以很快的找到每一个库的当前版
本。


my



http://www.xfocus.net/articles/200109/260.html

ELF: From The Programmer’s Perspective
http://linux4u.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html


从程序员角度看ELF[z

★3 .init和.fini sections

在ELF系统上,一个程序是由可执行文件或者还加上一些共享object文件组成。
为了执行这样的程序,系统使用那些文件创建进程的内存映象。进程映象
有一些段(segment),包含了可执行指令,数据,等等。为了使一个ELF文件
装载到内存,必须有一个program header(该program header是一个描述段
信息的结构数组和一些为程序运行准备的信息)。

一个段可能有多个section组成.这些section在程序员角度来看更显的重要。

每个可执行文件或者是共享object文件一般包含一个section table,该表
是描述ELF文件里sections的结构数组。这里有几个在ELF文档中定义的比较
特别的sections.以下这些是对程序特别有用的:

.fini
该section保存着进程终止代码指令。因此,当一个程序正常退出时,       
系统安排执行这个section的中的代码。
.init
该section保存着可执行指令,它构成了进程的初始化代码。
因此,当一个程序开始运行时,在main函数被调用之前(c语言称为
main),系统安排执行这个section的中的代码。

.init和.fini sections的存在有着特别的目的。假如一个函数放到
.init section,在main函数执行前系统就会执行它。同理,假如一
个函数放到.fini section,在main函数返回后该函数就会执行。
该特性被C++编译器使用,完成全局的构造和析构函数功能。

当ELF可执行文件被执行,系统将在把控制权交给可执行文件前装载所以相关
的共享object文件。构造正确的.init和.fini sections,构造函数和析构函数
将以正确的次序被调用。
从程序员角度看ELF[z

★3.1 在c++中全局的构造函数和析构函数

在c++中全局的构造函数和析构函数必须非常小心的处理碰到的语言规范问题。
构造函数必须在main函数之前被调用。析构函数必须在main函数返回之后
被调用。例如,除了一般的两个辅助启动文件crti.o和crtn.o外,GNU C/C++
编译器–gcc还提供两个辅助启动文件一个称为crtbegin.o,还有一个被称为
crtend.o。结合.ctors和.dtors两个section,c++全局的构造函数和析构函数
能以运行时最小的负载,正确的顺序执行。


.ctors
该section保存着程序的全局的构造函数的指针数组。

.dtors
该section保存着程序的全局的析构函数的指针数组。

ctrbegin.o
有四个section:
1 .ctors section
local标号
CTOR_LIST指向全局构造函数的指针数组头。在
ctrbegin.o中的该数组只有一个dummy元素。

[译注:
# objdump -s -j .ctors                
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o

/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o:
file format elf32-i386
Contents of section .ctors:
0000 ffffffff                             ….
这里说的dummy元素应该就是指的是ffffffff
]

2 .dtors section
local标号
DTOR_LIST指向全局析构函数的指针数组头。在
ctrbegin.o中的该数组仅有也只有一个dummy元素。

3 .text section
只包含了
do_global_dtors_aux函数,该函数遍历DTOR_LIST
列表,调用列表中的每个析构函数。
函数如下:
[code]
Disassembly of section .text:

00000000 <do_global_dtors_aux>;:
0: 55                   push %ebp
1: 89 e5                mov %esp,%ebp
3: 83 3d 04 00 00 00 00 cmpl $0x0,0x4
a: 75 38                jne 44 <
do_global_dtors_aux+0x44>;
c: eb 0f                jmp 1d <do_global_dtors_aux+0x1d>;
e: 89 f6                mov %esi,%esi
10: 8d 50 04             lea 0x4(%eax),%edx
13: 89 15 00 00 00 00    mov %edx,0x0
19: 8b 00                mov (%eax),%eax
1b: ff d0                call *%eax
1d: a1 00 00 00 00       mov 0x0,%eax
22: 83 38 00             cmpl $0x0,(%eax)
25: 75 e9                jne 10 <
do_global_dtors_aux+0x10>;
27: b8 00 00 00 00       mov $0x0,%eax
2c: 85 c0                test %eax,%eax
2e: 74 0a                je     3a <do_global_dtors_aux+0x3a>;
30: 68 00 00 00 00       push $0x0
35: e8 fc ff ff ff       call 36 <
do_global_dtors_aux+0x36>;
3a: c7 05 04 00 00 00 01 movl $0x1,0x4
41: 00 00 00
44: c9                   leave
45: c3                   ret
46: 89 f6                mov %esi,%esi
[/code]
从程序员角度看ELF[z

esi


4 .fini section
它只包含一个do_global_dtors_aux的函数调用。请记住,它仅是
一个函数调用而不返回的,因为crtbegin.o的.fini section是这个
函数体的一部分。
函数如下:
Disassembly of section .fini:

00000000 <.fini>;:
0: e8 fc ff ff ff       call 1 <.fini+0x1>;


crtend.o
也有四个section:

1 .ctors section
local标号
CTOR_END指向全局构造函数的指针数组尾部。

2 .dtors section
local标号
DTOR_END指向全局析构函数的指针数组尾部。

3 .text section
只包含了
do_global_ctors_aux函数,该函数遍历CTOR_LIST
列表,调用列表中的每个构造函数。
函数如下:
[code]
00000000 <do_global_ctors_aux>;:
0: 55                   push %ebp
1: 89 e5                mov %esp,%ebp
3: 53                   push %ebx
4: bb fc ff ff ff       mov $0xfffffffc,%ebx
9: 83 3d fc ff ff ff ff cmpl $0xffffffff,0xfffffffc
10: 74 0c                je     1e <
do_global_ctors_aux+0x1e>;
12: 8b 03                mov (%ebx),%eax
14: ff d0                call %eax
16: 83 c3 fc             add $0xfffffffc,%ebx
19: 83 3b ff             cmpl $0xffffffff,(%ebx)
1c: 75 f4                jne 12 <do_global_ctors_aux+0x12>;
1e: 8b 5d fc             mov 0xfffffffc(%ebp),%ebx
21: c9                   leave
22: c3                   ret
23: 90                   nop
[/code]
从程序员角度看ELF[z

nop

4 .init section
它只包含一个
do_global_ctors_aux的函数调用。请记住,它仅是
一个函数调用而不返回的,因为crtend.o的.init section是这个函
数体的一部分。
函数如下:
Disassembly of section .init:

00000000 <.init>;:
0: e8 fc ff ff ff       call 1 <.init+0x1>;
从程序员角度看ELF[z

crti.o
在.init section中仅是个_init的函数标号。
在.fini section中的_fini函数标号。

crtn.o
在.init和.fini section中仅是返回指令。

Disassembly of section .init:

00000000 <.init>;:
0: 8b 5d fc             mov 0xfffffffc(%ebp),%ebx
3: c9                   leave
4: c3                   ret
Disassembly of section .fini:

00000000 <.fini>;:
0: 8b 5d fc             mov 0xfffffffc(%ebp),%ebx
3: c9                   leave
4: c3                   ret
从程序员角度看ELF[z

编译产生可重定位文件时,gcc把每个全局构造函数挂在CTOR_LIST上
(通过把指向构造函数的指针放到.ctors section中)。
它也把每个全局析构函挂在
DTOR_LIST上(通过把指向析构函的指针
放到.dtors section中)。

连接时,gcc在所有重定位文件前处理crtbegin.o,在所有重定位文件后处理
crtend.o。另外,crti.o在crtbegin.o之前被处理,crtn.o在crtend.o之后
被处理。

当产生可执行文件时,连接器ld分别的连接所有可重定位文件的ctors 和
.dtors section到CTOR_LISTDTOR_LIST列表中。.init section
由所有的可重定位文件中_init函数组成。.fini由_fini函数组成。

运行时,系统将在main函数之前执行_init函数,在main函数返回后执行
_fini函数。

从程序员角度看ELF

★5.2 共享C++库 Shared C++ Library

在共享c++库中主要的困难是如何对待构造函数和析构函数。
在SunOS下,构造和使用一个共享的ELF C库是容易的,但是在SunOS下不能
构造共享的C++库,因为构造函数和析构函数有特别的需求。为止,在ELF
中的.init和.init section提供了完美的解决方法。

当构造共享C++库时,我们使用crtbegin.o和crtend.o这两个特殊的版本(crtbeginS.o和crtendS.o),
(它们已经是经过-fPIC的)。对于连接器(link editor)来说,构造共享
的C++库几乎是和一般的可执行文件一样的。全局的构造函数和析构函数
被.init和.fini section处理(在上面3.1节中已经讨论过)。

但一个共享库被映射到进程的地址空间时,动态连接器将在传控制权给程序
之前执行_init函数,并且将为_fini函数安排在共享库不再需要的时候被
执行。

连接选项-shared是告诉gcc以正确的顺序放置必要的辅助文件并且告诉它将
产生一个共享库。-v选项将显示什么文件什么选项被传到了连接器
(link editor).
[code]
[alert7@redhat62 dl]# gcc -v -shared -o libbar.so libbar.o
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs
gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/collect2 -m elf_i386
-shared -o libbar.so /usr/lib/crti.o /usr/lib/gcc-lib/i386-redhat
-linux/egcs-2.91.66/crtbeginS.o
-L/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66
-L/usr/i386-redhat-linux/lib libbar.o -lgcc -lc –version-script
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/libgcc.map
-lgcc /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtendS.o
/usr/lib/crtn.o
[/code]
crtbeginS.o和crtendS.o用-fPIC编译的两个特殊的版本。带上-shared
创建共享库是重要的,因为那些辅助的文件也提供其他服务。我们将在
5.3节中讨论。

从程序员角度看ELF

★5.3 扩展的GCC特性

GCC有许多扩展的特性。有些对ELF特别的有用。其中一个就是attribute
使用attribute可以使一个函数放到CTOR_LIST或者DTOR_LIST里。
例如:

[code]
]# cat miss.c

#include <stdio.h>;
#include <stdlib.h>;

static void foo(void) attribute ((constructor));
static void bar(void) attribute ((destructor));


int main(int argc, char
argv[])
{
printf("foo == %pn", foo);
printf("bar == %pn", bar);

exit(EXIT_SUCCESS);
}

void foo(void)
{
printf("hi dear njlily!n");
}

void bar(void)
{
printf("missing u! goodbye!n");
}

[alert7@redhat62 dl]# gcc -o miss miss.c
[alert7@redhat62 dl]# ./miss
hi dear njlily!
foo == 0x8048434
bar == 0x8048448
missing u! goodbye!

[/code]
从程序员角度看ELF[z

我们来看看是否加到了.ctors和.dtors中。
[code]]# objdump -s -j .ctors miss

miss:     file format elf32-i386

Contents of section .ctors:
8049504 ffffffff 34840408 00000000           ….4…….

[alert7@redhat62 dl]# objdump -s -j .dtors miss

miss:     file format elf32-i386

Contents of section .dtors:
8049510 ffffffff 48840408 00000000           ….H…….

[/code]

已经把foo和bar地址分别放到了.ctors和.dors,显示34840408只是因为
x86上是LSB编码的,小端序。

attribute ((constructor))促使函数foo在进入main之前会被自动调用。
attribute ((destructor))促使函数bar在main返回或者exit调用之后
会被自动调用。foo和bar必须是不能带参数的而且必须是static void类型的
函数。在ELF下,这个特性在一般的可执行文件和共享库中都能很好的工作。