漫谈兼容内核之九ELF映像的装入二.docx

上传人:b****8 文档编号:9290523 上传时间:2023-02-04 格式:DOCX 页数:23 大小:32.71KB
下载 相关 举报
漫谈兼容内核之九ELF映像的装入二.docx_第1页
第1页 / 共23页
漫谈兼容内核之九ELF映像的装入二.docx_第2页
第2页 / 共23页
漫谈兼容内核之九ELF映像的装入二.docx_第3页
第3页 / 共23页
漫谈兼容内核之九ELF映像的装入二.docx_第4页
第4页 / 共23页
漫谈兼容内核之九ELF映像的装入二.docx_第5页
第5页 / 共23页
点击查看更多>>
下载资源
资源描述

漫谈兼容内核之九ELF映像的装入二.docx

《漫谈兼容内核之九ELF映像的装入二.docx》由会员分享,可在线阅读,更多相关《漫谈兼容内核之九ELF映像的装入二.docx(23页珍藏版)》请在冰豆网上搜索。

漫谈兼容内核之九ELF映像的装入二.docx

漫谈兼容内核之九ELF映像的装入二

漫谈兼容内核之九:

ELF映像的装入

(二).txt我们用一只眼睛看见现实的灰墙,却用另一只眼睛勇敢飞翔,接近梦想。

男人喜欢听话的女人,但男人若是喜欢一个女人,就会不知不觉听她的话。

漫谈兼容内核之九:

ELF映像的装入

(二)

[align=center][size=4][b]漫谈兼容内核之九:

ELF映像的装入

(二)[/b][/size][/align]

[align=center]毛德操[/align]

上一篇漫谈介绍了在通过execve()系统调用启动一个ELF格式的目标映像时发生于Linux内核中的活动。

简而言之,内核根据映像头部所提供的信息把目标映像映射到(装入)当前进程用户空间的某个位置上;并且,如果目标映像需要使用共享库的话,还要(根据映像头部所提供的信息)将所需的“解释器”的映像也映射到用户空间的某个位置上,然后在从系统调用返回用户空间的时候就“返回”到解释器的入口,下面就是解释器的事了。

如果目标映像不使用共享库,那么问题就比较简单,返回用户空间的时候就直接“返回”到目标映像的入口。

现代的应用软件一般都要使用共享库,所以我们把这当作常态,而把不使用共享库的应用软件作为一种简化了的特例。

映像装入用户空间的位置有些是固定的、在编译连接时就确定好了的;有些则是“浮动”的、可以在装入时动态决定;具体要看编译时是否使用了-fPIC选项。

一般应用软件主体的映像都是固定地址的,而共享库映像的装入地址都是浮动的。

特别地,解释器映像的装入地址也是浮动的。

2.ELF映像的结构

每个操作系统对于在其内核上运行的可执行程序二进制映像都有特定的要求和规定,包括例如映像的格式,映像在用户空间的布局(程序段、数据段、堆栈段的划分等等),映像装入用户空间的地址是否可以浮动、以及如何浮动,是否支持动态连接、以及如何连接,如何进行系统调用,等等。

这些要求和规定合在一起就构成了具体操作系统的“应用(软件)二进制界面(ApplicationBinaryInterface)”,缩写成ABI。

显然,ABI是二进制映像的“生产者”即编译/连接工具和使用者即映像装入/启动手段之间的一组约定。

而我们一般所说的二进制映像格式,实际上并不仅仅是指字面意义上的、类似于数据结构定义那样的“格式”,还包括了跟映像装入过程有关的其它约定。

所以,二进制映像格式是ABI的主体。

目前的LinuxABI是在Unix系统5的时期(大约在1980年代)发展起来的,其主体就是ELF,这是“可执行映像和连接格式(ExecutableandLnkingFormat)”的缩写。

读者已经看到,ELF映像文件的开始是个ELF头,这是一个数据结构,结构中有个指针(位移量),指向文件中的一个“程序头”数组(表)。

各个程序头表项当然也是数据结构,这是对映像文件中各个“节(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

Version:

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]

0x00000001(NEEDED)Sharedlibrary:

[libpthread.so.0]

0x00000001(NEEDED)Sharedlibrary:

[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

:

8048ce0:

55push%ebp

8048ce1:

89e5mov%esp,%ebp

......

8048cef:

6820910408push$0x8049120

8048cf4:

e847f9ffffcall8048640<_init+0x5

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 解决方案 > 学习计划

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1