elf文件格式与动态链接库大地小神之个人收藏.docx
《elf文件格式与动态链接库大地小神之个人收藏.docx》由会员分享,可在线阅读,更多相关《elf文件格式与动态链接库大地小神之个人收藏.docx(9页珍藏版)》请在冰豆网上搜索。
elf文件格式与动态链接库大地小神之个人收藏
elf文件格式与动态链接库(非常之好)
机器执行的是机器指令,而机器指令就是一堆二进制的数字。
高级语言编写的程序之所以可以在不同的机器上移植就因为有为不同机器设计的编译器的存在。
高级语言的编译器就是把高级语言写的程序转换成某个机器能直接执行的二进制代码。
以上的知识在我们学习CS(ComputerScience)的初期,老师都会这么对我们讲。
但是我就产生疑问了:
既然机器都是执行的二进制代码,那么是不是说只要硬件相互兼容,不同操作系统下的可执行文件可以互相运行呢?
答案肯定是不行。
这就要谈到可执行文件的格式问题。
每个操作系统都会有自己的可执行文件的格式,比如以前的Unix?
是用a.out格式的,现代的Unix?
类系统使用elf格式,WindowsNT?
是使用基于COFF格式的可执行文件。
那么最简单的格式应该是DOS的可执行格式,严格来说DOS的可执行文件没有什么格式可言,就是把二进制代码安顺序放在文件里,运行时DOS操作系统就把所有控制计算机的权力都给了这个程序。
这种方式的不足之处是显而易见的,所以现代的操作系统都有一种更好的方式来定义可执行文件的格式。
一种常见的方法就是为可执行文件分段,一般来说把程序指令的内容放在.text段中,把程序中的数据内容放在.data段中,把程序中未初始化的数据放在.bss段中。
这种做法的好处有很多,可以让操作系统内核来检查程序防止有严重错误的程序破坏整个运行环境。
比如:
某个程序想要修改.text段中的内容,那么操作系统就会认为这段程序有误而立即终止它的运行,因为系统会把.text段的内存标记为只读。
在.bss段中的数据还没有初始化,就没有必要在可执行文件中浪费储存空间。
在.bss中只是表明某个变量要使用多少的内存空间,等到程序加载的时候在由内核把这段未初始化的内存空间初始化为0。
这些就是分段储存可执行文件的内容的好处。
下面谈一下Unix系统里的两种重要的格式:
a.out和elf(ExecutableandLinkingFormat)。
这两种格式中都有符号表(symboltable),其中包括所有的符号(程序的入口点还有变量的地址等等)。
在elf格式中符号表的内容会比a.out格式的丰富的多。
但是这些符号表可以用strip工具去除,这样的话这个文件就无法让debug程序跟踪了,但是会生成比较小的可执行文件。
a.out文件中的符号表可以被完全去除,但是elf中的在加载运行时起着重要的作用,所以用strip永远不可能完全去除elf格式文件中的符号表。
但是用strip命令不是完全安全的,比如对未连接的目标文件来说如果用strip去掉符号表的话,会导致连接器无法连接。
例如:
代码:
$:
gcc-chello.c
$:
lshello.chello.o
用gcc把hello.c编译成目标文件hello.o
代码:
$:
striphello.o
用strip去掉hello.o中的符号信息。
代码:
$:
gcchello.o/usr/lib/crt1.o/usr/lib/crti.o/usr/lib/crtn.o–ohello
/*
$:
gcchello.o/usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o7
:
Infunction`_start'
:
init.c:
(.text+0x18)
:
undefinedreferenceto`main'collect2
:
ldreturned1exitstatus
*/
再用gcc连接时,连接器ld报错。
说明在目标文件中的符号起着很重要的作用,如果要发布二进制的程序的话,在debug后为了减小可执行文件的大小,可以用strip来除去符号信息但是在程序的调试阶段还是不要用strip为好。
在接下去讨论以前,我们还要来讲讲relocations的概念:
首先有个简单的程序hello.c
代码:
$:
cathello.c
main()
{
printf("HelloWorld\n");
}
当我们把hello.c编译为目标文件时,我们并没有在源文件中定义printf这个函数,所以汇编器也不知道printf这个函数的具体的地址,所以在目标文件中就会留下printf这个符号。
以下的工作就交给连接器了,连接器会找到这个函数的入口地址然后传递给这个文件最终形成可执行文件,这个过程就叫做relocations。
a.out格式的可执行文件是没有这种relocation的功能的,内核不会执行其中还有未知函数的入口地址的可执行文件的。
在目标文件中当然可以relocation,只不过连接器需要把未知函数的入口地址完全找到,生成可执行文件才行。
这样就有一个很尴尬的问题,在a.out格式中极其难以实现动态连接技术。
要知道为什么现在的Unix几乎都是用的elf格式的可执行文件就要了解a.out格式的短处。
a.out的符号是极其有限的,在/usr/include/linux/asm/a.out.h中定义了一个结构exec就是:
代码:
structexec{
unsignedlonga_info;/*UsemacrosN_MAGIC,etcforaccess*/unsigneda_text;/*lengthoftext,inbytes*/
unsigneda_data;/*lengthofdata,inbytes*/
unsigneda_bss;/*lengthofuninitializeddataareaforfile,inbytes*/unsigneda_syms;/*lengthofsymboltabledatainfile,inbytes*/unsigneda_entry;/*startaddress*/
unsigneda_trsize;/*lengthofrelocationinfofortext,inbytes*/unsigneda_drsize;/*lengthofrelocationinfofordata,inbytes*/
};
在这个结构中更本没有指示每个段在文件中的开始位置,内核加载器具有一些非正式的方法来加载可执行文件的。
明显的,a.out是不支持动态连接的。
(在内部不支持动态连接,用某些技术也是可以实现a.out的动态连接)
要了解elf可执行文件的运行方式,我们有必要讨论一下动态连接技术。
很多人对动态连接技术十分熟悉,但是很少有人真正了解动态连接的内部工作方式。
回想没有动态连接的日子,程序员写程序时不用什么都从头开始,他们可以调用定义的很好的函数,然后再用连接器与函数库连接。
这样的话使得程序员更加有效率,但是一个十分重要的问题出现了:
这样产生的可执行文件就会很大。
因为连接器把程序需要用的所有函数的代码都复制到了可执行文件中去了。
这种连接方式就是所谓的静态连接,与之相对的就是动态连接。
连接器在可执行文件中标记出程序调用外部函数的位置,并不把代码复制进去,只是标出函数在动态连接库中的位置。
用这样的方式生成的特殊可执行文件就是动态连接的。
在运行这种动态程序时,系统在运行时把该程序调用的外部函数地址映射到程序地址,这就是所谓的动态连接,系统就有一个程序叫做动态连接器,在动态连接的程序执行前都要先把地址映射好。
很显然的,必须有一种机制保证动态连接的程序中的函数地址正确地指向了动态连接库的某个函数地址。
这就需要讨论一下elf可执行文件格式处理动态连接的机制了。
elf的动态连接库是内存位置无关的,就是说你可以把这个库加载到内存的任何位置都没有影响。
这就叫做positionindependent。
而a.out的动态连接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。
在编译内存位置无关的动态连接库时,要给编译器加上-fpic选项,让编译器产生的目标文件是内存位置无关的还会尽量减少对变量引用时使用绝对地址。
把库编译成内存位置无关会带来一些花费,编译器会保留一个寄存器来指向全局偏移量表(globaloffsettable(orGOTforshort)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。
Elf的另一个特点是它的动态连接库是在运行时处理符号的,这是通过用符号表和再布置(relocation)表来实现的。
在载入文件时并不能立即执行,要在处理完符号表把所有的地址都relocation完后才可以执行。
这个听起来有点复杂而且可能导致文件运行慢,不过对elf做了很大的优化后,这种减慢已经是微不足道的了。
理论上说不是用-fpic选项编译出来的目标文件也可以用作动态连接库,但是在运行时会需要做数目极大的relocation,这是对运行速度有极大影响的。
这样的程序性能是很差的,几乎没有可用性。
当从动态连接库中读一个全局变量时与从非-fpic编译的目标文件读是不同的。
读动态连接的库中的变量是通过GOT来寻找到目标变量的,GOT已经由某一个寄存器指向了。
GOT本生就是一个指针列表,找到GOT中的某一个指针就可以读到所要的全局变量了,有了GOT我们要读出一个变量只要做一次relocation。
下面我们来看看elf文件中到底有些什么信息:
代码:
$:
cathello.c
main()
{
printf("HelloWorld\n");
}
$:
gcc-elf-chello.c
还是这个简单的程序,用gcc把它编译成目标文件hello.o。
然后用readelf工具来探测一下elf文件的内容。
(readelf是在binutils软件包里的一个工具,大多数Linux发行版都包含它)
下面用-h选项列出ELF的头信息:
代码:
$:
readelf-hhello.o(查看ELF头部信息)
ELFHeader:
Magic:
7f454c46010101000000000000000000Class:
ELF32
Data:
2'scomplement,littleendianVersion:
1(current)OS/ABI:
UNIX-SystemVABIVersion:
0Type:
REL(Relocatablefile)
Machine:
Intel80386Version:
0x1Entrypointaddress:
0x0Startofprogramheaders:
0(bytesintofile)Startofsectionheaders:
256(bytesintofile)Flags:
0x0Sizeofthisheader:
52(bytes)Sizeofprogramheaders:
0(bytes)Numberofprogramheaders:
0Sizeofsectionheaders:
40(bytes)Numberofsectionheaders:
11Sectionheaderstringtableindex:
8
-h选项是列出elf文件的头信息。
Magic:
字段是一个标识符,只要Magic字段是7f454c46010101000000000000000000的文件都是elf文件。
Class:
字段是表示elf的版本,这是一个32位的elf。
Machine:
字段是指出目标文件的平台信息,这里是I386兼容平台。
其他的字段可以从其字面上看出它的意义,这里就不一一解释了。
下面用-S选项列出段的头信息:
代码:
$:
readelf-Shello.o(查看ELF的SectionHeaders信息)
Thereare11sectionheaders,startingatoffset0x100:
SectionHeaders:
[Nr]NameTypeAddrOffSizeESFlgLkInfAl
[0]NULL0000000000000000000000000
[1].textPROGBITS0000000000003400002a00AX004
[2].rel.textREL0000000000037000001008914
[3].dataPROGBITS0000000000006000000000WA004
[4].bssNOBITS0000000000006000000000WA004
[5].rodataPROGBITS0000000000006000000e00A001[6].note.GNU-stackPROGBITS0000000000006e00000000001[7].commentPROGBITS0000000000006e00003e00001[8].shstrtabSTRTAB000000000000ac00005100001
[9].symtabSYMTAB000000000002b80000a0101084
[10].strtabSTRTAB0000000000035800001500001KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings)I(info),L(linkorder),G(group),x(unknown)O(extraOSprocessingrequired)o(OSspecific),p(processorspecific)
Name字段显示的是各个段的名字,Type显示段的属性,Addr是每个段载入虚拟内存的位置,Off是每个段在目标文件中的偏移位置,Size是每个段的大小,后面的一些字段是表示段的可写,可读,或者可执行。
用-r可以列出elf文件中的relocation:
代码:
$:
readelf-rhello.o
Relocationsection'.rel.text'atoffset0x370contains2entries:
OffsetInfoTypeSym.ValueSym.Name0000001f00000501R_386_3200000000.rodata0000002400000902R_386_PC3200000000printf
在.text段中有两个relocation,其中之一就是printf函数的relcation。
Offset指出当relocation时要把printf函数的入口地址贴到离.text段开头00000024处。
下面我们可以看一下连接过后的可执行文件中的内容:
代码:
$:
gcchello.o
$:
readelf-Sa.out
Thereare32sectionheaders,startingatoffset0xbc4
:
SectionHeaders:
[Nr]NameTypeAddrOffSizeESFlgLkInfAl[0]NULL0000000000000000000000000
[1].interpPROGBITS0804813400013400001300A001
[2].note.ABI-tagNOTE0804814800014800002000A004
[3].hashHASH0804816800016800002c04A404
[4].dynsymDYNSYM0804819400019400006010A514[5].dynstrSTRTAB080481f40001f400006000A001[6].gnu.versionVERSYM0804825400025400000c02A402[7].gnu.version_rVERNEED0804826000026000002000A514[8].rel.dynREL0804828000028000000808A404
[9].rel.pltREL0804828800028800001008A4114
[10].initPROGBITS0804829800029800001700AX004
[11].pltPROGBITS080482b00002b000003004AX004
[12].textPROGBITS080482e00002e00001b400AX0016
[13].finiPROGBITS0804849400049400001a00AX004
[14].rodataPROGBITS080484b00004b000001600A004[15].eh_framePROGBITS080484c80004c800000400A004[16].ctorsPROGBITS080494cc0004cc00000800WA004
[17].dtorsPROGBITS080494d40004d400000800WA004
[18].jcrPROGBITS080494dc0004dc00000400WA004[19].dynamicDYNAMIC080494e00004e00000c808WA504[20].gotPROGBITS080495a80005a800000404WA004
[21].got.pltPROGBITS080495ac0005ac00001404WA004[22].dataPROGBITS080495c00005c000000c00WA004
[23].bssNOBITS080495cc0005cc00000400WA004
[24].commentPROGBITS000000000005cc0001b200001[25].debug_arangesPROGBITS0000000000078000005800008[26].debug_infoPROGBITS000000000007d800016400001[27].debug_abbrevPROGBITS0000000000093c00002000001[28].debug_linePROGBITS0000000000095c00015a00001[29].shstrtabSTRTAB00000000000ab600010c00001
[30].symtabSYMTAB000000000010c40005101031564
[31].strtabSTRTAB000000000015d400032200001KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings)I(info),L(linkorder),G(group),x(unknown)O(extraOSprocessingrequired)o(OSspecific),p(processorspecific)
这里的段比目标文件hello.o的段要多的多,这是因为这个程序需要elf的一个动态连接库libc.so.1。
在这里需要简单的介绍一下内核加载elf可执行文件。
内核先是把整个文件加载到用户的虚拟内存空间,如果程序是与动态连接库连接的,则程序中就会包含动态连接器的名称,可能是/lib/elf/ld-linux.so.1。
(动态连接器本身也是一个动态连接库)
在文件的尾部的一些段的Addr值是00000000,因为这些都是符号表,动态连接器并不把这些段的内容加载到内存中。
.interp段中只是储存这一个ASCII的字符串,它就是动态连接器的名字(路径)。
.hash,.dynsym,.dynstr这三个段是用于动态连接器执行relocation时的符号表。
.hash是一个哈希表,可以让我们很快的从.dynsym中找到所需的符号。
.plt段中储存着我们调用动态连接库中的函数入口地址,在默认状态下,程序初始化时,.plt中的指针并不是指向正确的函数入口地址的而是指向动态连接器本身,当你在程序中调用某个动态连接库中的函数时,连接器会找到那个函数在动态连接库中的位置,再把这个位置连接到.plt段中。
这样做的好处是如果在程序中调用了很多动态连接库中的函数,会花费掉连接器很长时间把每个函数的地址连接到.plt段中。
所以就可以采用连接器只是把要用的函数地址连接进去,以后要用的再连接。
但是也可以设置环境变量LD_BIND_NOW=1让连接器在程序执行前把所有的函数地址都连接好,这主要是方便调试程序。
readelf工具还有很多选项,具体内容可以查看man手册。
在文章的开头就说elf文件格式很方便运用动态连接技术,下面我就写一个就简单的动态连接库的例子:
代码:
$:
catDyn_hello.c
intmain(void)
{
hi();
}
$:
cathi.c
#include
hi()
{
printf("Helloworld\n");
}
两个简单的文件,在mian函数中调用hi()函数,下面并不是把两个文件一起编译,而是把hi.c编译成动态连接库。
(注意Dyn_hello.c中并没有包含任何头文件。
)
代码:
$:
gcc-fPIC-chi.c
$:
gcc-shared-olibhi.sohi.o
现在在当前目录下有一个名字为libhi.so的文件,这就就是仅含有一个函数的动态连接库。
代码:
$:
gcc-cDyn_hello.c
$:
gcc-oDyn_helloDyn_hello.o-L.-lhi
在当前目录下有了一个Dyn_hello可执行文件,现在就可以执行它了。
代码:
$:
./Dyn_hello
./Dyn_hello:
errorwhileloadingsharedlibraries:
libhi.so:
cannotopensharedobjectfile:
Nosuchfileordirectory
执行不成功,这就表明