4.2.2 Linking View下的ELF
由图4-1可知,从Linking View角度来观察,一个ELF文件会包含若干个Section,并且还有一个Section Header Table来集中描述该文件中所有Section的头信息。请读者注意,Section Header Table描述的是Section的Header信息,并不是Section本身的内容。
ELF这个Section Header Table就是本节要介绍的知识。图4-6所示为Section Header Table中各元素对应的数据结构。
图4-6 Section Header Table元素的数据结构示意图
图4-6为是Section Header Table表元素对应的数据结构。
·sh_name:每个section都有一个名字。ELF有一个专门存储Section名字的Section(Section Header String Table Section,简写为shstrtab)。这里的sh_name指向shstrtab的某个位置,该位置存储了本Section名字的字符串。
·sh_type:section的类型,不同类型的Section存储不同的内容。比如.shstrtab的类型就是SHT_STRTAB,它存储字符串。
·sh_flags:Section的属性。下文将详细介绍sh_type和sh_flags。
·sh_addr:如果该Section被加载到内存的话(可执行程序或动态库),sh_addr指明应该加载到内存什么位置(进程的虚拟地址空间)。
·sh_offset:表明该Section真正的内容在文件什么位置。
·sh_size:section本身的大小。不同类型的Section分别对应不同的数据结构。
下面将以main.o(通过图4-4示例中的"make obj"命令得到一个Obj文件。Obj文件将参与链接,所以它是Linking View研究的绝好试验品)为例来看看它的Section Header Table包含什么内容,如图4-7所示。
图4-7 readelf--sections main.o结果
在图4-7中:
·第一行内容说明main.o文件的Section Header Table包含13项,而Table的起始位置在文件的0x148(十进制为328)字节处(读者可利用readelf工具同时查看main.o的ELF文件头结构的e_shoff,这两个值必须严格相等)。
·接下来的内容为Table中各表项的内容,一共13项。根据ELF规范,sh table表第0项是占位用的,所以其值全为0。图4-7展示了每项的内容,包括名字、类型等。
Section命名规则提示:
图4-7中Name列展示了Section的名字。理论上Section的名字可以随意取,但实际上Section的命名是有一些“潜规则”的。比如,系统定义的名字一般以“.xxxx”为名。另外,像“.text”“.bss”这样的名字(包括这些Section的类型等)也由系统预定义的。所以,自定义的Section尽量不要和它们重名。
下面来认识几个比较重要的Section。
4.2.2.1 .shstrtab section
图4-6中曾提到,Section的名字是字符串,这些字符串信息存储在Section Header String Table中,而Section Header String Table自身也是一个Section:
·它的名字叫“.shstrtab”,是Section Header String Table的简写。
·其类型(sh_type)为SHT_STRTAB(取值为3)。注意,ELF中大部分标记符的命名都有一定含义,比如SHT_STRTAB中的SHT是Section Header Type的缩写,STRTAB则为String Tab的缩写。
.shstrtab section是如何存储字符串的呢?来看图4-8。
图4-8 .shstrtab内容介绍
图4-8中首先展示了一个.shstrtab section的内容示例。它其实就是一块存储区域,里边包含所有section名字的字符。根据规范,该存储区域第一个元素为“\0”。那么,其他Section该如何使用.shstrtab中的字符串呢?答案是通过索引来找到自己想要的字符串,来看图4-8中最下方的图。
·如果index为1,表示从索引1开始,到其后的“\0”结束,如此可得到字符串"name."。
·如果index为11,表示从11开始,到其后的“\0”结束,如此可得到字符串"able"。
图4-9 .shtrtab和.strtab节的内容
ELF中,类型为SHT_STRTAB的Section都是按图4-8所示的结构来组织的。图4-9所示为main.o文件中两个同为SHT_STRTAB类型的section的内容。
"readelf-p[section名|section索引]main.o"可将指定名字或索引的section的内容转换成字符信息打印出来。
4.2.2.2 .text和.bss等section
本节接着介绍几个关键的section。
·.text section:用于存储程序的指令。简单点说,程序的机器指令就放在这个section中。根据规范,.text section的sh_type为SHT_PROGBITS(取值为1),意为Program Bits,即完全由应用程序自己决定(程序的机器指令当然是由程序自己决定的),sh_flags为SHF_ALLOC(当ELF文件加载到内存时,表示该Section会分配内存)和SHF_EXECINSTR(表示该Section包含可执行的机器指令)。
·.bss section:bss是block storage segment的缩写(bss一词很有些历史,感兴趣的读者可自行了解)。ELF规范中,.bss section包含了一块内存区域,这块区域在ELF文件被加载到进程空间时会由系统创建并设置这块内存的内容为0。注意,.bss section在ELF文件里不占据任何文件的空间,所以其sh_type为SHF_NOBITS(取值为8),它只是在ELF加载到内存的时候会分配一块由sh_size指定大小的内存。.bss的sh_flags取值必须为SHF_ALLOC和SHF_WRITE(表示该区域的内存是可写的。同时,因为该区域要初始化为0,所以要求该区域内存可写)。什么样的数据应该属于.bss section呢?本例的main.o中并没有.bss数据(图4-7中.bss section的Size为0),但如果读者在main.c中定义一个全局的"int a=0"之后,生成的main.o就包含有效的.bss section(Size将变成4)了。读者不妨一试。
·.data section:.data和.bss类似,但是它包含的数据不会初始化为0。这种情况下就需要在文件中包含对应的信息了。所以.data的sh_type为SHF_PROGBITS,但sh_flags和.bss一样。读者可以尝试在main.c中定义一个比如"char c='f'"这样的变量就能看到.data section的变化了。
·.rodata section:包含只读数据的信息,比如main.c中printf里的字符串就属于这一类。它的sh_flags只能为SHF_ALLOC。
假设我们为main.c添加"int a=0"和"char c='f'"这两个全局变量,然后利用工具来打印上述各section的内容,则结果如图4-10。
图4-10中上图为.text内容的示例,下图为.bss、.data、.rodata的内容。其中:
·通过命令"objdump-S-d main.o"可反编译.text的内容。"-S"参数表示结合源码进行反汇编。这要求编译main.o的时候使用gcc-g参数。
·"readelf-x section名main.o"打印指定section的内容。.bss由于在文件中没有数据,所以无法显示。.data有一个值为'f'的字符,而.rodata包含"this is elf test"字符串。
4.2.2.3 .symtab section
.symtab section是ELF中非常重要的一个section,里边存储的是符号表(Symbol Table)。.symtab section的类型为SHT_SYMTAB。一般而言,符号表主要用于编译链接,也可以参与动态库的加载。
.dynsymtab section
.symtab section往往包含了全部的符号表信息,但不是其中所有符号信息都会参与动态链接,所以ELF还专门定义一个.dynsym section(类型为SHT_DYNSYM),这个section存储的仅是动态链接需要的符号信息。
.symtab section存储的是符号表,其元素的数据结构如图4-11所示。
图4-10 .text等section内容示例
图4-11 symbol table表元素数据结构
图4-11所示的Elf64_Sym为Symbol Table表元素对应的数据结构。
·st_name:该符号的名称,指向.strtab section某个索引位置。注意,ELF文件可能包含多个String Table Section,常见的有:.shstrtab section(专门存储section名)、.strtab section(存储.symtab符号表用到的字符串)、.dynstr section(存储.dynsym符号表用到的字符串)。
·st_info:说明该符号的类型和绑定属性(binding attributes)。
·st_other:说明该符号的可见性(Visibility)。它往往和st_info配合使用,用法见图4-11中所示的三个宏。
·st_shndx:symbol table中每一项元素都和其他section有关系。st_shndx就是这个相关section的索引号
·st_value:符号的值,不同类型的ELF文件该变量的含义不同。比如:对于relocatable类型,st_value表示该符号位于相关section(索引号为st_shndx)的具体位置。而对于shared和executable类型,st_value为该符号的虚拟内存地址。
·st_size:和这个符号相关联的数据的长度。
提示 由图4-11可知,st_name代表位于某个string table section里的字符串,但是它并没有说明到底是哪个string table section(请读者注意,ELF文件可包含多个string table section)。根据规范,这是由Section Header数据结构中sh_link决定的。图4-7中.symtab的Link列取值为12,这表明它的符号表里的字符串应该使用索引号为12的String Table Section,这恰好就是.strtab section。
图4-12所示为用readelf查看main.o得到的symbol table信息。
图4-12 readelf-s main.o结果示意
要真正看懂图4-12,需要先对Type和Bind有所了解,它们的解释如表4-2和表4-3所示。
表4-2 符号表项Type介绍
表4-3 符号表Bind属性介绍
图4-12中索引编号列出现了ABS(Num=1的行)和UNDEF(Num=16的行),它们代表ELF定义的一些特殊的索引编号。
·SHN_ABS:取值为0xFFF1,ABS是Absolute之意,表示这个符号的值是固定不变的。
·SHN_UNDEF:取值为0。UNDEF是Undefine的意思,表示该符号的定义在别的ELF文件中,此处只是引用它,程序在链接时会处理UNDEF符号项。图4-12最后一行的printf,其对应的索引编号就是UNDEF。显然,printf不是在main.c中定义的。
最后,图4-12中所有符号的可见性(Vis列)都是DEFAULT,这意味着该符号的可见性将由Bind属性来决定。
4.2.2.4 .rel和.rela section
本节介绍和重定位有关的Section。重定位最主要的作用就是将符号的使用之处和符号的定义之处关联起来。比如前面示例代码里的printf,这个函数符号是在别的ELF文件中定义的。如何将符号的使用之处和它的定义之处关联起来呢,方法有两种,
·编译链接过程中,最终生成可执行文件或动态库文件时,编译链接器将根据ELF文件中的重定位表计算最终的符号的位置。
·加载动态库时,加载器也会根据重定位信息修改对应的符号使用之处,使得动态库能正常工作。
根据ELF规范,重定位信息包含在重定位表中,而重定位表则由特定Section描述。根据重定位表项所用数据结构的不同,重定位表Section的命名略有不同。
·section名字形如“.relname”:这种类型的重定位表Section以“.rel”开头,后跟其他常见的section名,比如.rel.text、.rel.data等。
·section名字形如“.relaname”:这种类型的重定位表Section以“.rela”开始,后面也跟其他常见的section名,比如.rela.text、.rela.data等。
之所以命名方式不同,源于这两种重定位表中元素内容的细微差别,如图4-13所示。
图4-13中定义了Elf64_Rel和Elf64_Rela两个数据结构及三个宏。
·r_offset:是一个偏移量,具体用法和ELF类型有关。
·r_info:r_info由两个信息组成(组成方式参考图4-13中的宏),分别是,该重定位项针对符号表哪一项(即目标项的索引号,sym)和重定位的类型(type,不同处理器有不同的类型)。
·Elf64_Rela还多了一个成员域r_addend,它代表一个常量值,用于计算最终的重定位信息的位置。
图4-13 Elf64_Rel和Elf64_Rela数据结构
接下来我们通过一个例子来研究重定位数据结构中各成员的作用。先来看示例代码,如图4-14所示。
图4-14 示例代码
示例代码依然很简单:
·包含一个main.c和一个test.c文件。
·test.c中定义了test函数,而main.c调用了这个函数。
·编译完之后得到test.o和main.o。
我们重点研究main.o,如图4-15所示。
图4-15解释了.rela.text各参数的含义。不过还留了一个小尾巴,即"offset=000000000015"的作用。这需要通过反汇编main.o来解释,如图4-16所示。
请读者留意图4-16中左边标记"14"对应的指令码"e8 00 00 00 00"。
·"e8":intel汇编指令,表示call(代表函数调用)。e8位于.text section第0x14个字节。
·"00 00 00 00":接下来的4个值为0的字节为call的参数,即目标函数离下一条指令的偏移量。注意,这里的偏移量是相对于call指令的下一条指令(即mov$0x0,%eax)的偏移量。由于偏移量是0,所以,反编译后得到call的参数是19<main+0x19>。0x19就是下一条指令的起始地址。但我们知道call的真正目标应该是test函数,绝对不可能是这里的0x19!
图4-15 main.o解析
图4-16 objdump-r-d main.o结果
由上述内容可知,图4-16反编译得到的调用test函数居然将call指令的目标函数地址设置为全0。显然这是有问题的。不过这个问题的解决也正是体现重定位作用的地方。
·读者还记得图4-15中offset=000000000015以及.rela.text的sh_info=1(图4-15中右边的小圈)这两处地方吗?这个offset=0x15表明重定位表中test那一项将修改.text section(索引号为1,与sh_info=1对应)中0x15字节之处!恰恰就是call指令后面参数对应的地址。
objdump也意识到这一点,它正确得在call指令的下一行展示了该指令对应的重定位信息,即图4-16中的"15:R_X86_64_PC32 test-0x4"。
·15是offset的对应值。注意,这里讨论的都是16进制。
·R_X86_64_PC32:是重定位的类型,和目标机器有关,主要作用是告诉编译器如何计算真正的地址值。
·test-0x4是重定位对应的符号信息和r_addend的值。
最终,编译器会根据重定位表里的信息进行处理,使得call指令能调用到正确的目标函数。不过,很多情况下在编译期间是无法得到符号的最终目标地址的,只能在运行时处理。这部分内容我们留待下文再来介绍。
提示 关于ELF Section的内容就先介绍到这。从笔者个人经验来看,了解section的组成、各种section对应数据结构的含义以及它们的作用是非常关键的。
接下来,我们将从执行的角度再一次观察ELF文件。