今天在看ELF文件格式的过程中,想起了之前浩哥在coolshell上贴出的一篇文章:C语言全局变量那些事儿,另外在StackOverFlow上也有相应的讨论,无私的大神们给出了详尽的答案,突然发现在学习ELF的过程中可以顺道窥探出其中的奥秘,将知识连贯起来的兴奋劲不言而喻。
ELF是Executable and Linking Format的缩写,即可执行链接格式,最初由UNIX实验室开发发布的,作为应用程序二进制接口的一部分。简单的来说,ELF就是描述了可执行文件的格式规范,也就是说我们的执行文件到底该是什么样的格式。
在理解具体的文件格式之前,我们需要知道ELF文件格式可以用来描述三种类型的文件:
可执行文件:这个就很熟悉了,经常运行的程序便是,这个文件规定了exec()如何来创建一个程序的进程映像。
可重定位文件:包含适合于与其他文件链接来创建可执行文件或者共享目标文件的这么一个文件。
共享目标文件:就是我们常提到的静态库或者动态库了。链接编辑器可以将此文件和其他可重定位文件和共享目标文件一起处理生成另外的一个文件,另外,动态链接器可能将它与某个可执行文件以及其他共享目标一起组合,创建进程映像。
ELF文件格式提供了两种并行视图,来分别描述链接和执行过程,从不同的角度可以有不同的含义。可以仔细的看下面的图,然后我们辅以实际的文件来讲解其中的格式。
从图中可以知道,左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:
我们还是使用上一篇中的代码编译出的执行文件来看。
#include<iostream>
using namespace std;
int global_a = 100;
int global_b;
static int global_static_a = 200;
int main(){
static int main_static_b = 200;
static int main_static_c;
cout << "Hello World" << endl;
return 0;
}
将上面的代码编译之后我们可以得到可执行文件a.out,然后查看该文件的头部,使用readelf -h a.out,结果如下:
choudan@ubuntu:~/coding/cpp$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048560
Start of program headers: 52 (bytes into file)
Start of section headers: 4456 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 31
Section header string table index: 28
从上面的内容中可以看出文件头主要描述了文件的格式,操作系统的大小端特征,系统位数,硬件相关的体系架构等等,具体的头文件格式我们可以查看后面参考文档列出的第一篇文章。在此头文件中,我们关注以下几个比较重要的点:
在前面提到了section是从链接的角度出发来理解的,文件末尾存在一个section header tables 描述了文件中存在的每一个section,我们可以通过readelf命令查看文件中到底有哪些section,我们之前理解的代码段,数据段和bss段与section有什么关系?
choudan@ubuntu:~/coding/cpp$ readelf -S a.out
There are 31 section headers, starting at offset 0x1168:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000030 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481dc 0001dc 0000c0 10 A 6 1 4
[ 6] .dynstr STRTAB 0804829c 00029c 00014b 00 A 0 0 1
[ 7] .gnu.version VERSYM 080483e8 0003e8 000018 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048400 000400 000050 00 A 6 2 4
[ 9] .rel.dyn REL 08048450 000450 000010 08 A 5 0 4
[10] .rel.plt REL 08048460 000460 000040 08 A 5 12 4
[11] .init PROGBITS 080484a0 0004a0 00002e 00 AX 0 0 4
[12] .plt PROGBITS 080484d0 0004d0 000090 04 AX 0 0 16
[13] .text PROGBITS 08048560 000560 0001fc 00 AX 0 0 16
[14] .fini PROGBITS 0804875c 00075c 00001a 00 AX 0 0 4
[15] .rodata PROGBITS 08048778 000778 000014 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 0804878c 00078c 000044 00 A 0 0 4
[17] .eh_frame PROGBITS 080487d0 0007d0 000104 00 A 0 0 4
[18] .init_array INIT_ARRAY 08049ef8 000ef8 000004 00 WA 0 0 4
[19] .ctors PROGBITS 08049efc 000efc 000008 00 WA 0 0 4
[20] .dtors PROGBITS 08049f04 000f04 000008 00 WA 0 0 4
[21] .jcr PROGBITS 08049f0c 000f0c 000004 00 WA 0 0 4
[22] .dynamic DYNAMIC 08049f10 000f10 0000e0 08 WA 6 0 4
[23] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 08049ff4 000ff4 00002c 04 WA 0 0 4
[25] .data PROGBITS 0804a020 001020 000014 00 WA 0 0 4
[26] .bss NOBITS 0804a040 001034 0000a0 00 WA 0 0 32
[27] .comment PROGBITS 00000000 001034 00002a 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 00105e 000108 00 0 0 1
[29] .symtab SYMTAB 00000000 001640 000500 10 30 52 4
[30] .strtab STRTAB 00000000 001b40 0003c3 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
从该命令的第一行可以知道section header tables在文件中的偏移量为0x1168,转换成十进制就是elf header中描述的4456。此文件中存在31个section,仔细看看,就可以找到熟悉的.text,.data,.bss。这三个section就是我们常提到的代码段数据段等,除了这些段之外,还有很多其他的,譬如前面提到过的28,是字符串表的header,编码是29的section则是符号表的header。
section header table中的每一个section都由十个属性描述,例如section的名字,类型,标志,地址,在文件中的偏移,section的大小等等,具体每个含义大家参考文章末尾给出的ELF文件格式分析一文。
其中关键的属性解释下: * type: section有多种类型,例如PROGBITS,表示此section的内容由程序来解释,.text和.data都是,还有NOBITS,表示此段不占用文件空间,STRTAB是表示字符串表,SYMTAB表示符号表等等。 * flags: 可以在该命令的最末看出不同flag标志的含义,其中Alloc,表示该section在进程执行过程中占用内存。 * addr : 如果section将出现在进程的内存映像中,此成员给出section的第一个字节应处的位置。
我们提到了第29项是字符串表,即索引为28的项,当然还有其他的section也是字符串表,例如索引为30的section,那么字符串表中存的是哪些内容了。我们同样可以使用readelf读取出来。
choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out
String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .interp
[ 23] .note.ABI-tag
[ 31] .note.gnu.build-id
[ 44] .gnu.hash
[ 4e] .dynsym
[ 56] .dynstr
[ 5e] .gnu.version
[ 6b] .gnu.version_r
[ 7a] .rel.dyn
[ 83] .rel.plt
[ 8c] .init
[ 92] .text
[ 98] .fini
[ 9e] .rodata
[ a6] .eh_frame_hdr
[ b4] .eh_frame
[ be] .init_array
[ ca] .ctors
[ d1] .dtors
[ d8] .jcr
[ dd] .dynamic
[ e6] .got
[ eb] .got.plt
[ f4] .data
[ fa] .bss
[ ff] .comment
在该section中存取的就是这些字符串,变长的,每个字符串都以0结尾。在section header tables中每一项都有一个name,那么只需要将name设置成其中shstrtab中的索引就可以了,不需要真的存储实际的字符串。
同样,在strtabsection中,也是字符串,只是其中的内容是程序中各种符号的名字,我们截取部分看看:
choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out
String dump of section '.strtab':
[ 1] crtstuff.c
[ 9b] __do_global_ctors_aux
[ b1] hello.cpp
[ bb] _ZStL8__ioinit
[ ca] _ZL15global_static_a
[ 10f] _GLOBAL__sub_I_global_a
[ 127] _ZZ4mainE13main_static_c
[ 140] _ZZ4mainE13main_static_b
[ 159] _GLOBAL_OFFSET_TABLE_
[ 1d8] __gmon_start__
[ 202] _fini
[ 22d] __libc_start_main@@GLIBC_2.0
[ 2c3] __data_start
[ 311] global_a
[ 31a] __bss_start
[ 326] _end
[ 398] global_b
[ 3b8] main
[ 3bd] _init
上面的内容只是部分字符串,删除了一些,我们看到一些很熟悉的符号,例如在程序中定义的变量,global_a,函数的符号main等等。
符号表包含用来定位,重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。例如在下面将看到的符号_ZL15global_static_a,就是由global_static_a变量名经过修饰而来。
我们先来看符号表的结构,依然可以使用readelf命令读取文件中的符号表section,具体如下:
choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out
Symbol table '.dynsym' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@GLIBC_2.1.3 (2)
2: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
8: 0804877c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
Symbol table '.symtab' contains 80 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
27: 00000000 0 SECTION LOCAL DEFAULT 27
31: 08049f0c 0 OBJECT LOCAL DEFAULT 21 __JCR_LIST__
32: 08048590 0 FUNC LOCAL DEFAULT 13 __do_global_dtors_aux
36: 00000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
39: 08049f0c 0 OBJECT LOCAL DEFAULT 21 __JCR_END__
41: 00000000 0 FILE LOCAL DEFAULT ABS hello.cpp
43: 0804a02c 4 OBJECT LOCAL DEFAULT 25 _ZL15global_static_a
45: 08048688 28 FUNC LOCAL DEFAULT 13 _GLOBAL__sub_I_global_a
46: 0804a0dc 4 OBJECT LOCAL DEFAULT 26 _ZZ4mainE13main_static_c
48: 08049ff4 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
50: 08049ef8 0 NOTYPE LOCAL DEFAULT 18 __init_array_start
51: 08049f10 0 OBJECT LOCAL DEFAULT 22 _DYNAMIC
55: 08048560 0 FUNC GLOBAL DEFAULT 13 _start
57: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
70: 0804a028 4 OBJECT GLOBAL DEFAULT 25 global_a
73: 00000000 0 FUNC GLOBAL DEFAULT UND _ZNSolsEPFRSoS_E@@GLIBCXX
76: 0804a0d4 4 OBJECT GLOBAL DEFAULT 26 global_b
78: 08048614 52 FUNC GLOBAL DEFAULT 13 main
此文件中存在两张符号表,内容都较多,删除了一些,留下了一些有代表性的数据。从上面的结果知道,符号表包含很多项,每项都由7个属性值描述。注意Bind一列,描述了变量的链接可见性和行为。
LOCAL: 对于local类型的符号,表示该符号在包含该符号定义的文件之外不可见。相同名称的多个local符号可以存在于多个文件中,互不影响。
GLOBAL: 全局符号对所有将组合的文件都可见,即定义在a文件中的全局符号,可以在b文件中引用。
WEAK : 弱符号与全局符号类似,不过他们的定义优先级较低。我们再此处可以看文章头贴出的coolshell上关于弱符号和强符号的讨论。
在每个符号表中,绑定为local的符号优先级都高于弱符号和全局符号。那么到底什么样的符号会成为弱符号,什么样的会成为全局符号?weak和global都是针对全局变量来说的,若全局变量初始化了,则是global绑定,若没有初始化则是weak绑定。我们还知道,若全局变量没有初始化,则会被编译器放到.bss段,置为0。大家还可以参考这篇文章来加深理解,GCC中的弱符号和强符号,还有参考文档中的第四条,这个文档给出了更详细清楚的解释。
那么,WEAK符号和GLOBAL符号之间的又存在哪些差别了,上面的链接中给出了详细的解释,这儿再将主要的三点区别贴出来:
当连接器处理若干可重定位的文件时,不允许对同名的GLOBAL符号给出多个定义,即我们不能再多个文件中对同样名字的变量给与多个定义。
如果存在一个全局符号,出现一个同名的弱符号,此时链接器只会关心全局符号,忽略弱符号。
如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
在符号表中,除了Bind这一列之外,还可以看到符号的类型,有OBJECT,FUNC,FILE等等。Ndx列给出了该符号表项关联的section,即该符号在section header tables中索引,例如第70条,符号是global_a,在section header tables的索引是25,查看section headers table索引值是25的表项,就是我们熟悉的.data段。除了能给出具体的索引值之外,我们发现还有其他的几种情况存在,例如看到的ABS,UND。其实还存在三种特别的索引:
重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。
我们可以使用readelf -r 命令查看该文件中的重定位表,这个系统存在两张,如下所列出来的。
choudan@ubuntu:~/coding/cpp$ readelf -r a.out
Relocation section '.rel.dyn' at offset 0x450 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a040 00000b05 R_386_COPY 0804a040 _ZSt4cout
Relocation section '.rel.plt' at offset 0x460 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 __cxa_atexit
0804a004 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a008 00000407 R_386_JUMP_SLOT 00000000 _ZNSt8ios_base4InitC1E
0804a00c 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main
0804a010 00000a07 R_386_JUMP_SLOT 08048520 _ZNSt8ios_base4InitD1E
0804a014 00000607 R_386_JUMP_SLOT 00000000 _ZStlsISt11char_traits
0804a018 00000707 R_386_JUMP_SLOT 00000000 _ZNSolsEPFRSoS_E
0804a01c 00000907 R_386_JUMP_SLOT 08048550 _ZSt4endlIcSt11char_tr
这张表描述了哪些符号在链接过程中需要进行重定位,这个表中给出了足够的信息告诉链接器采用什么方的方式计算符号的最终地址。关于这张表的具体描述大家可以参考这个链接,ELF重定位理解。
越是对ELF深究,越发现这一块的知识点很多,想通过这一篇博客来描述清楚ELF文件格式几乎是不可能的,但是在尝试理清ELF格式的过程中,发现了不少值得学习的书籍或者有意思的点,充满了趣味。其中最想推荐给大家的是《程序员的自我修养》一书,可以找到关于程序链接装载的所有知识点,并且详细易懂。
对于ELF的理解,我觉的可以尝试顺着这样的思路。一般的,可执行文件是由多个源码文件和各种库文件构成的完整源码,经过编译汇编生成一堆的目标文件,然后目标文件在链接器的辅助下链接成我们期待的可执行文件。那么在此过程中,每个源码文件都单独编译成目标文件,并且目标文件生成的代码都是从地址0开始,目标文件还可能引用了其他目标文件的变量或者函数,这就意味着在单独编译一个源码文件时,该文件引用的其他目标文件的变量和函数地址都是不知道的,所以需要重定位操作,并且将变量和函数统一成符号一种概念来处理了。在最终的链接过程中,还需要将代码都映射到正确的虚拟地址上,所以ELF格式必须提供完成链接过程所需要的全部信息了。当然,上面描述的过程只是简化了的过程,还有许多其他的问题也需要考虑,例如程序的装载,如何完成从磁盘到内存,完成运行。依然推荐程序猿的自我修养,可以加深对这一切的理解。