c-目标文件的三魂七魄
本文基于一个简单的目标文件例子,来窥探目标文件的三魂七魄。
首先,展示一下例子的源码,了解一下这段代码做了些什么。代码中的注释部分进行了详细说明。
然后,通过gcc将main.c编译成main.o
注意:绿色部分的main.o文件大小是1896(0x768)。
由于目标文件涉及的知识点非常的多,本文不可能讲解所有细节,主要讲解目标文件的主*分---“三魂七魄”。"三魂七魄"到底是什么鬼!!!
目标文件的整体轮廓
通过readelf命令先来感受一下main.o里面到底长啥样!
上面两张图中的ELF Header、Section Headers、Symbol table就是目标文件的"三魂"
ELF Header
ELF文件类型
ELF(Executable Linkable Format)是LINUX系统中的一种文件格式。通常我们见到的目标文件(.o)、可执行文件、动态库(.so)都是ELF文件类型。
可以通过file命令查看文件类型
从上图可以看出,main.o 是ELF relocatable文件,可重定位文件能被链接成可执行文件或动态库;main.c是c源文件;/bin/ls是ELF executable文件,可执行文件;/usr/lib/libcpupower.so.4.4.0-154是ELF shared object文件,即动态库。从这里其实可以看出(如果你对gcc编译的流程熟悉的话),relocatable file通常被链接成 executable file或 shared object file。
了解了什么是ELF,接下来再看看什么是ELF Header。
ELF Header描述
为什么我将elf header看作是三魂之一,说明了它非常重要。elf header描述了整个目标文件的基础属性,如:
ELF Magic
ELF Magic由16个字节组成,前四个字节(第0到第3个字节)是所有ELF文件都相同的标识码0x7f、0x45、0x4c、0x46,如果你对ASCII码有所了解的话,你会发现,0x7f刚好对应DEL字符,0x45、0x4c、0x46分别对应大写的’E’ ‘L’ ‘F’。第4个字节(0x02)表示文件CLASS,也即文件位数,因为我的机器是64位的,所以0x02表示64位,如果是32位的机器,第4个字节的值应该是0x01。第5个字节(0x01)表示字节序,0x01表示小端字节序,0x02表示大端字节序。第6个字节表示ELF的主版本号,ELF标准从1.2版本后就再没有更新了,所以一般是1。剩下的9个字节通常都为0。
其实可以发现,ELF Header的16字节的魔数包含了Class(机器位数)、Data(字节序)、Version(ELF版本)信息。
OS/ABI 表示运行的平台为unix-system v。
ABI Version 表示ABI版本。
ELF Type
REL 表示relocatable file,说明该文件是目标文件(可重定位文件),即.o文件
EXEC 表示executable file,说明该文件是可执行文件
DYN 表示shared object file,说明该文件是动态库文件,即.so文件
可分别执行readelf -h /bin/ls 和 readelf -h /usr/lib/libcpupower.so.4.4.0-154 查看ELF Type。
Machine
表示该ELF文件的可运行的平台属性。
Entry point address
入口地址,也就是ELF程序的虚拟入口地址,OS在加载完程序后从这个地址开始执行。relocatable文件因为还没有链接,所以一般没有入口地址,其值为0。
Start of program headers
program header描述了ELF文件是怎样被操作系统加载到进程的虚拟地址空间的。由于目标文件(relocatable file)没有program header(可通过readelf -l main.o查看program header),所以这里不展开讲解program header。
Start of section headers
段表在文件中的偏移。main.o的start of section headers值为1064(0x428),表示段表从文件的1065个字节开始。section headers将在后面详细讲解。
Flags
描述平台架构相关的属性。可以查看/usr/include/elf.h 中以EF_开头的宏。
Size of this header
表示elf文件头本身的大小。main.o的文件头大小为64字节。
Size of program headers
由于目标文件没有program header,所以这里暂时不管。
Number of program headers
由于目标文件没有program header,所以这里暂时不管。
Size of section headers
表示段表描述符的大小。一般等于sizeof(Elf64_Shdr)。64字节。
Number of section headers
表示段表中段的个数。main.o中段的个数为13,说明有13个段
Section header string table index
表示段表字符串表 所在段的下标。
说明:本文中,用中文表达的段都是指section,因为还有一个概念叫segment,但在中文表达上不好区分,所以这里特别说明一下,以免产生混淆。
我个人觉得,可以将section翻译为"节",将segment翻译为"段"。但是由于书籍或已经约定俗成的叫法中已经将section表达为段了,例如,代码段、数据段等,这里的段是指section。
为了在中文上尽量表达清晰而不产生混淆,我后面的系列文章会将section表达成"节",例如,代码节、数据节;而将segment表达成"段"。"段"是比"节"更大一点的概念。当然,如果你习惯用英文表达,就无所谓了。
section headers(节表头)
首先,我们来看看main.o的节表信息
可以看到 section(节)的个数是13个,和我们前面分析elf header的number of section headers的值一致。starting at offset 0x428 也就是1064,也就是前面分析elf header的start of section headers。在代码层面,每个section是通过Elf64_Shdr来描述的,所以Elf64_Shdr也叫做节描述符(section descriptor)
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
可以发现,Elf64_Shdr的10个成员刚好和readelf命令显示的每个section的属性字段相对应。接下来,详细分析每个section及其相应字段的含义。
Name
节名,通常 节名的字符串是存放在.shstrtab中的,sh_name是节名字符串在.shstrtab中的偏移。
Type
NULL 表示 无效节
PROGBITS 表示程序节,代码节、数据节都是这种类型
RELA 表示重定位表
NOBITS 表示该节在目标文件中没有内容,如 .bss节
STRTAB 表示该节的内容为字符串表
SYMTAB 表示该节的内容为符号表
Address
表示节被加载后的进程虚拟地址。这里暂不做过多说明。
Off
表示节偏移,即节在文件中的偏移,如果节的内容不在文件中,那么off就没有意义
Size
表示节的大小
Es(section entry size)
有些节包含了一些固定大小的项(entry),而Es就表示这种项的大小。如果Es为0表示该节不包含固定大小的项。例如,符号表,它包含的每个符号所占的大小都是一样的。
Flg
节的标志位 表示 节在进程虚拟地址空间中的属性。
A 表示该节在进程空间中需要分配相应的空间。
E 表示该节在进程空间中可以被执行,一般都是指代码节。
W 表示该节在进程空间中可写。
Lk Inf
表示节的链接信息。一般节的类型与链接相关,这两个成员才有意义。例如节的类型是重定位表、符号表等。
Al
表示节地址对齐。
.strtab 字符串表,用来保存普通字符串
.shstrtab 段表字符串表,用来保存节表中用到的字符串,最常见的就是节名(section name)
Symbol(符号表)
在目标文件的链接过程中,本质上是对“地址”的引用,而“地址”即是函数或变量的地址。同时,为了方便引用,函数或变量都有自己的名字,这个名字就是所以为的符号(symbol),函数名或变量名就是符号名(symbol name)。
我们可以将符号看作是链接过程的粘合剂,整个链接过程正是基于符号才能够完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应符号表(symbol table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是他们地址。
符号的分类:
- 定义在本目标文件中的全局符号,可以被其他目标文件引用;如main.c中的g_var, func, main;
- 在目标文件中引用的全局符号,却没有定义在本目标文件中,一般叫做外部符号(external symbol);如,printf
- 节名,这种符号通常由编译器产生,它的值就是该段的起始地址;如,.data,.text;
- 局部符号,这类符号只在编译单元内可见,这种符号通常对于链接过程没有作用,链接器往往会忽略他们;如,l_static_var,l_static_var2;
- 行号信息,目标文件指令与源代码中代码行的对应关系,可选;
在链接过程中,"可见的"还是全局符号和外部符号。
接下来,先展示一下main.o里面的符号表到底长得是什么样子,然后再详细解释里面的内容。
符号表通过readelf -s main.o
命令查看,符号表的section name 一般叫做’.symtab’。接下来依次解释符号表资格字段的含义。
Num
表示符号表数组的索引。
### Size
表示相应符号的内容的大小
Type
NOTYPE
表示未知类型符号
FILE
该符号为文件名
SECTION
该符号表示一个节,这种符号的Bind一定是LOCAL,他们的Name没有显示,起始默认就是指相应的section name,所以没有必要显示出来。
OBJECT
表示一个数据对象,如,变量、数组等
FUNC
一般表示函数名
Bind
LOCAL
表示局部符号,目标文件外部不可见
GLOBAL
表示全局符号,目标文件外部可见
Ndx
一般表示该符号所在节的节索引(查看节索引命令:readelf -S -W main.o),如,g_static_val,g_var在.data节。
ABS
表示该符号包含了一个绝对值。文件名的符号就是这一类
COM
“COMMON块”,一般未初始化的全局变量就是这种
UND
表示该符号未定义,说明该符号不在本目标文件中定义,但在其他文件中定义
Value
- 如果符号在目标文件内定义,则value表示该符号所在节的偏移值。
- 如果符号是
COM
,value表示对齐属性 - 在可执行文件中,value表示符号的虚拟地址
了解了前面的5个字段的含义,再来解读符号表的内容就显得更加的从容啦!
main和func都是函数名,显然属于.text,所以Ndx为1,类型为FUNC;Bind为GLOBAL表示全局可见;value表示相对于.text起始位置的偏移量
g_static_val、l_static_val.2813、g_var,初始化的静态存储的外部或内部链接变量 都属于.data。
g_static_val2、l_static_val2.2814 未初始化的静态存储的内部或空链接的变量 都属于.bss。
g_var2 Ndx为COM
,它的value为对齐属性,外部可见。
从这里例子,可以发现,我们平常所说的未初始化的全局变量(g_var2)放在.bss中是不准确的。