漫谈兼容内核之九ELF映像的装入二Word格式文档下载.docx
《漫谈兼容内核之九ELF映像的装入二Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《漫谈兼容内核之九ELF映像的装入二Word格式文档下载.docx(23页珍藏版)》请在冰豆网上搜索。
各个程序头表项当然也是数据结构,这是对映像文件中各个“节(Segment)”的(结构性)描述。
从映像装入的角度看,一个映像是由若干个Segment构成的。
有些Segment需要被装入、即被映射到用户空间,有些则不需要被装入。
在前一篇漫谈中读者已经看到,只有类型为PT_LOAD的Segment才需要被装入。
所以,映像装入的过程只“管”到Segment为止。
而从映像的动态连接、重定位(即浮动)、和启动运行的角度看,则映像是由若干个“段(Section)”构成的。
我们通常所说映像中的“代码段”、“数据段”等等都是Section。
所以,动态连接和启动运行的过程所涉及的则是Section。
一般而言,一个Segment可以包含多个Section。
其实,Segment和Section都是从操作/处理的角度对映像的划分;
对于不同的操作/处理,划分的方式也就可以不同。
所以,读者在后面将会看到,一个Segment里面也可以包含几个别的Segment,这就是因为它们是按不同的操作/处理划分的、不同意义上的Segment。
Section也是一样。
在Linux系统中,(应用软件主体)目标映像本身的装入是由内核负责的,这个过程读者已经看到;
而动态连接的过程则由运行于用户空间的“解释器”负责。
这里要注意:
第一,“解释器”是与具体的映像相连系的,其本身也有个映像,也需要被装入。
与目标映像相连系的“解释器”也是由内核装入的,这一点读者也已看到。
第二,动态连接的过程包括了共享库映像的装入,那却是由“解释器”在用户空间实现的。
本来,看了内核中与装入目标映像有关的代码以后,应该接着看“解释器”的代码了。
但是后者比前者复杂得多,也繁琐得多,原因是牵涉到许多ELF和ABI的原理和细节,所以有必要先对ELF动态连接的原理作一介绍。
明白了有关的原理和大致的方法以后,具体的代码实现倒在其次了。
前面讲过,Linux提供了两个很有用的工具,即readelf和objdump。
下面就用这两个工具对映像/usr/local/bin/wine进行一番考察,以期在此过程中逐步对ELF和ABI有所了解和理解,这也是进一步阅读、理解“解释器”的代码所需要的。
我们用命令行“readelf–a/usr/local/bin/wine”和“objdump–d/usr/local/bin/wine”产生两个文件(把结果重定向到文件中),然后察看这两个文件的部分内容。
首先是目标映像的ELF头:
ELFHeader:
Magic:
7f454c46010101000000000000000000
Class:
ELF32
Data:
2'
scomplement,littleendian
Version:
1(current)
OS/ABI:
UNIX-SystemV
ABIVersion:
0
Type:
EXEC(Executablefile)
Machine:
Intel80386
0x1
Entrypointaddress:
0x8048750
Startofprogramheaders:
52(bytesintofile)
Startofsectionheaders:
114904(bytesintofile)
Flags:
0x0
Sizeofthisheader:
52(bytes)
Sizeofprogramheaders:
32(bytes)
Numberofprogramheaders:
6
Sizeofsectionheaders:
40(bytes)
Numberofsectionheaders:
36
Sectionheaderstringtableindex:
33
这就是映像文件开头处的ELF头,其最初4个字节为‘0x7f’、和‘E’、‘L’、‘F’。
从其余字段中我们可以看出:
●OS是Unix、其实是Linux、而ABI是系统5的ABI。
ABI的版本号为0。
●CPU为x86。
●映像的类型为EXEC,即带有主函数main()的应用软件映像(若是共享库则类型为DYN、即动态连接库)。
●映像的程序入口地址为0x8048750。
如前所述,EXEC映像的装入地址是固定的、不能浮动。
●程序头数组起点在文件中的位移为52(字节),而ELF头的大小正好也是52,所以紧接ELF头的后面就是程序头数组。
数组的大小为6,即映像中有6个Segment。
●Section头的数组则一直在后面位移位114904的地方,映像中有36个Section。
于是,我们接下去看程序头数组:
ProgramHeaders:
TypeOffsetVirtAddrPhysAddrFileSizMemSizFlgAlign
PHDR0x0000340x080480340x080480340x000c00x000c0RE0x4
INTERP0x0000f40x080480f40x080480f40x000130x00013R0x1
[Requestingprograminterpreter:
/lib/ld-linux.so.2]
LOAD0x0000000x080480000x080480000x011cc0x011ccRE0x1000
LOAD0x0011cc0x0804a1cc0x0804a1cc0x001580x00160RW0x1000
DYNAMIC0x0011d80x0804a1d80x0804a1d80x000d80x000d8RW0x4
NOTE0x0001080x080481080x080481080x000200x00020R0x4
一个程序头就是关于一个Segment的说明,所以这就是6个Segment。
第一个Segment的类型是PHDR,在文件中的位移为0x34、即52,这就是程序头数组本身。
其大小为0xc0、即192。
前面说每个程序头的大小为32字节,而6X32=192。
第二个Segment的类型是INTERP,即“解释器”的文件/路径名,是个字符串,这里说是“/lib/ld-linux.so.2”。
下面是两个类型为LOAD的Segment。
如前所述,只有这种类型的Segment才需要装入。
但是,看一下前者的说明,其起点在文件中的位移是0,大小是0x011cc,显然是把ELF头和前两个Segment也包含在里面了。
再看后者,其起点的位移是0x011cc,所以是和前者连在一起的;
其大小为0x158,这样两个Segment合在一起是从0到0x1324。
计算一下就可知道,实际上是把所有的Segment都包括进去了。
所以,对于这个特定的映像,说是只装入类型为LOAD的Segment,实际上装入的却是整个映像。
那么,映像中的什么内容可以不必装入呢?
例如bss段,那是无初始内容的数据段,就不用装入;
还有(与动态连接无关的)符号表,那也不需要装入。
注意两个LOAD类Segment的边界(Alignment)都是0x1000,即4KB,那正好是存储页面的大小。
还有个问题,既然两个LOAD类的Segment是连续的,那为什么不合并成一个呢?
看一下它们的特性标志位就可以知道,第一个Segment的映像是可读可执行、但是不可写;
第二个则是可读可写、但是不可执行,这当然不能合并。
再往下看,下一个Segment的类型是DYNAMIC,那就是跟动态连接有关的信息。
如上所述,这个Segment其实是包含在前一个Segment中的,所以也会被装入。
最后一个Segment的类型是NOTE,那只是注释、说明一类的信息了。
当然,跟动态连接有关的信息是我们最为关心的,所以我们看一下这个Segment的具体内容:
Dynamicsegmentatoffset0x11d8contains22entries:
TagTypeName/Value
0x00000001(NEEDED)Sharedlibrary:
[libwine.so.1]
[libpthread.so.0]
[libc.so.6]
0x0000000c(INIT)0x80485e8
0x0000000d(FINI)0x8049028
0x00000004(HASH)0x8048128
0x00000005(STRTAB)0x8048368
0x00000006(SYMTAB)0x80481d8
0x0000000a(STRSZ)301(bytes)
0x0000000b(SYMENT)16(bytes)
0x00000015(DEBUG)0x0
0x00000003(PLTGOT)0x804a2c4
0x00000002(PLTRELSZ)160(bytes)
0x00000014(PLTREL)REL
0x00000017(JMPREL)0x8048548
0x00000011(REL)0x8048538
0x00000012(RELSZ)16(bytes)
0x00000013(RELENT)8(bytes)
0x6ffffffe(VERNEED)0x80484c8
0x6fffffff(VERNEEDNUM)3
0x6ffffff0(VERSYM)0x8048496
0x00000000(NULL)0x0
这个Segment中有22项数据,开头几项类型为NEEDED的数据是我们此刻最为关心的,因为这些数据告诉了我们目标映像要求装入那一些共享库,例如libwine.so.1。
读者已经看过内核怎样装入用户空间映像,解释器只不过是在用户空间做同样的事,所以共享库的装入对于读者并不复杂,问题是怎样实现动态连接,这是我后面要着重讲的。
前面说过,Segment是从映像装入角度考虑的划分,Section才是从连接/启动角度考虑的划分,现在我们就来看Section。
先看Section与Segment的对应关系:
SectiontoSegmentmapping:
SegmentSections...
00
01.interp
02.interp.note.ABI-tag.hash.dynsym.dynstr.gnu.version.gnu.version_r
.rel.dyn.rel.plt.init.plt.text.fini.rodata.eh_frame
03.data.dynamic.ctors.dtors.jcr.got.bss
04.dynamic
05.note.ABI-tag
Section的名称都以’.’开头,例如.interp;
名称中间也可以有’.’,例如rel.dyn。
这说明,Segment0不含有任何Section,因为这就是程序头数组。
Segment1只含有一个Section,那就是.interp,即解释器的文件/路径名。
而Segment2所包含的Section就多了。
而且,这个Segment还包含了前面两个Segmrnt,所以.interp又同时出现在这个Segment中。
余类推。
前面ELF头中说一共有36个Section,下面就是一份清单:
SectionHeaders:
[Nr]NameTypeAddrOffSizeESFlgLkInfAl
[0]NULL0000000000000000000000000
[1].interpPROGBITS080480f40000f400001300A001
[2].note.ABI-tagNOTE0804810800010800002000A004
[3].hashHASH080481280001280000b004A404
[4].dynsymDYNSYM080481d80001d800019010A514
[5].dynstrSTRTAB0804836800036800012d00A001
[6].gnu.versionVERSYM0804849600049600003202A402
[7].gnu.version_rVERNEED080484c80004c800007000A534
[8].rel.dynREL0804853800053800001008A404
[9].rel.pltREL080485480005480000a008A4b4
[10].initPROGBITS080485e80005e800001700AX004
[11].pltPROGBITS0804860000060000015004AX004
[12].textPROGBITS080487500007500008d800AX004
[13].finiPROGBITS0804902800102800001b00AX004
[14].rodataPROGBITS0804906000106000016600A0032
[15].eh_framePROGBITS080491c80011c800000400A004
[16].dataPROGBITS0804a1cc0011cc00000c00WA004
[17].dynamicDYNAMIC0804a1d80011d80000d808WA504
[18].ctorsPROGBITS0804a2b00012b000000800WA004
[19].dtorsPROGBITS0804a2b80012b800000800WA004
[20].jcrPROGBITS0804a2c00012c000000400WA004
[21].gotPROGBITS0804a2c40012c400006004WA004
[22].bssNOBITS0804a32400132400000800WA004
[23].stabPROGBITS000000000013240048780c2404
[24].stabstrSTRTAB00000000005b9c014cd400001
[25].commentPROGBITS0000000001a87000016500001
[26].debug_arangesPROGBITS0000000001a9d800007800008
[27].debug_pubnamesPROGBITS0000000001aa5000002500001
[28].debug_infoPROGBITS0000000001aa75000a9800001
[29].debug_abbrevPROGBITS0000000001b50d00013800001
[30].debug_linePROGBITS0000000001b64500028400001
[31].debug_framePROGBITS0000000001b8cc00001400004
[32].debug_strPROGBITS0000000001b8e00006be01MS001
[33].shstrtabSTRTAB0000000001bf9e00013a00001
[34].symtabSYMTAB0000000001c67800089010355c4
[35].strtabSTRTAB0000000001cf080005db00001
KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings)
I(info),L(linkorder),G(group),x(unknown)
O(extraOSprocessingrequired)o(OSspecific),p(processorspecific)
这是按Section的名称列出的,其中跟动态连接有关的Section也出现在前面名为Dynamic的Segment中,只是在那里是按类型列出的。
例如,前面类型为HASH的表项说与此有关的信息在0x8048128处,而这里则说有个名为.hash的Section,其起始地址为0x8048128。
还有,前面类型为PLTGOT的表项说与此有关的信息在0x804a2c4处,这里则说有个名为.got的Section,其起始地址为0x804a2c4,不过Section表中提供的信息更加详细一些,有些信息则互相补充。
在Section表中,只要类型为PROGBITS,就说明这个Section的内容都来自映像文件,反之类型为NOBITS就说明这个Section的内容并非来自映像文件。
有些Section名是读者本来就知道的,例如.text、.data、.bss;
有些则从它们的名称就可猜测出来,例如.symtab是符号表、.rodata是只读数据、还有.comment和.debug_info等等。
还有一些可能就不知道了,这里择其要者先作些简略的介绍:
(1).hash。
为便于根据函数/变量名找到有关的符号表项,需要对函数/变量名进行hash计算,并根据计算值建立hash队列。
●.dynsym。
需要加以动态连接的符号表,类似于内核模块中的INPORT符号表。
这是动态连接符号表的数据结构部分,须与.dynstr联用。
●.dynstr。
动态连接符号表的字符串部分,与.dynsym联用。
●.rel.dyn。
用于动态连接的重定位信息。
●.rel.plt。
一个结构数组,其中的每个元素都代表着GOP表中的一个表项GOTn(见下)。
●.init。
在进入main()之前执行的代码在这个Section中。
●.plt。
“过程连接表(ProcedureLinkingTable)”,见后。
●.fini。
从main()返回之后执行的代码在这个Section中,最后会调用exit()。
●.ctors。
表示“Constructor”,是一个函数指针数组,这些函数需要在程序初始化阶段(进入main()之前,在.init中)加以调用。
●.dtors。
表示“Distructor”,也是一个函数指针数组,这些函数需要在程序扫尾阶段(从main()返回之后,在.fini中)加以调用。
●.got。
“全局位移表(GlobalOffsetTable)”,见后。
●.strtab。
与符号表有关的字符串都集中在这个Section中。
其中我们最关心的是“过程连接表(ProcedureLinkingTable)”PLT和“全局位移表(GlobalOffsetTable)”GOT。
程序之间的动态连接就是通过这两个表实现的。
下面我们通过一个实例来说明程序之间的动态连接。
目标映像/usr/local/bin/wine的main()函数中调用了一个库函数getenv(),这个函数在C语言共享库libc.so.6中。
下面是main()经编译/连接以后的汇编代码:
08048ce0<
main>
:
8048ce0:
55push%ebp
8048ce1:
89e5mov%esp,%ebp
......
8048cef:
6820910408push$0x8049120
8048cf4:
e847f9ffffcall8048640<
_init+0x5