ELF:在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。
可以理解elf文件是一种固定数据结构的文件。我们只要把elf文件格式弄清楚。Linux有readelf工具可以读取elf文件,支持非常多的命令行参数。我们现在以某个.o开始分析。
假设我们写了非常简单的代码。
int flag;
int test()
{
int a = 0;
return a;
}
我们生成main.o
gcc -c main.c -o main.o
我们执行readelf -h main.o查看elf信息。
edward@Edward:~/workspace/binutils/binutils/src$ readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 496 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11
ELF 文件的作用有两个,一是用于程序链接(为了生成程序);二是用于程序执行(为了运行程序)。针对这两种情况,可以从不同的视角来看待同一个目标文件。对于同一个目标文件,当它分别被用于链接和用于执行的时候,其特性必然是不一样的,我们所关注的内容也不一样。从链接和运行的角度,可以将 ELF 文件的组成部分划分为 链接视图 和 运行试图 这两种格式。
下图(图1)展示了 ELF 文件的两种格式。
注:该图是官方文档中的原图:图1-1。
上图中的几个概念的简述。ELF 文件头 (ELF header):位于文件的最开始处,包含有整个文件的结构信息。节 (section):是专门用于链接过程而言的,在每个节中包含有指令数据、符号数据、重定位数据等等。程序头表 (program header table):在运行过程中是必须的,在连接过程中是可选的,因为它的作用是告诉系统如何创建进程的镜像。节头表 (section header table):ELF 格式解析包含有文件中所有 节 的信息。在链接视图中,节头表是必须存在的,文件里的每一个节都需要在节头表中有一个对应的注册项,这个注册项描述了节的名字、大小等等。
在上图中,程序头表紧跟在 ELF 文件头之后,节头表紧跟在节信息之后,但在实际的文件中,这个顺序并不是固定的。在 ELF 文件的各个组成部分中,只有ELF 文件头的位置是固定的,其它内容的位置全都可变。
产生两种视图的原因:看待同一个文件的不同角度。链接视图:文件结构的粒度更细,将文件按功能模块的差异进行划分,最小的意义块单位是:节,静态分析的时候一般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息。运行视图:根本目的是考虑 ELF 文件是如何为程序运行做准备,由于考虑内存装载过程的一些优化考虑,将 ELF 文件从装载的角度重新划分 ELF 文件,最小的意义块单位是:段。
ELF 文件结构逻辑十分清晰:无论从哪个角度来看 ELF 文件,文件中必然存在文件头,文件头中包含了文件的基本信息,更有关于程序头表和节头表的信息,而程序头表和节头表中分别存有有关段和节的信息。换言之,ELF 可以理解为是通过“分级管理”来达到管理文件内容的。
数据成员的命名规则:ELF 文件中涉及到的数据成员的名称的命名都极具规律性,基本上都是完整名称的缩写的组合的形式,非常简单就能够通过数据成员的名称猜出其代表的含义,在之后的介绍中我也会把完整的名称注释到数据成员的后面。
ELF 文件中所用到的数据表示方法:ELF 使用结构体的定义方式定义了数据结构,针对数据结构中的数据成员使用宏定义依次进行定义数据成员。数据并不依赖于所在机器的字长;其它数据使用目标处理器的数据格式,字长是在构建期间由编译器/连接器来指定的,与创建时所在的主机无关。在 ELF 格式中,出于可移植性的考虑,没有定义面向 比特(bit)的数据结构。在实际分析的时候:最让人信服的分析还是回归源码,数据成员具体的字长数需要到elf.h
源码中去求证,在之后的介绍中我会直接把准确的字节数注释到数据结构中每个成员的后面。
注:以分析Elf64_Addr
字长为例,展示分析数据成员字长的方法。Elf64_Addr
(typedef uint64_t Elf64_Addr;
)—>uint64_t
(typedef __uint64_t uint64_t;
)—>unsigned long int
(typedef unsigned long int __uint64_t;
)—>64位系统中占用 8 字节。源码中一层层寻找数据类型的定义,直到一些通用性数据类型。
至此,ELF 文件结构简述部分结束
ELF 文件的文件头(ELF Header)
ELF 文件头位于目标文件最开始的位置,含有整个文件的一些基本信息。文件头中含有整个文件的结构信息,包括一些控制单元的大小。
可以使用下面的数据结构来描述 ELF 文件的文件头。
注:(1)下面给出的源码片段截取自elf.h
,因为例子中生成的是 64 位的 ELF 文件,故在此只给出 64 位 ELF 的文件头的数据结构,这部分与 32 位 ELF 文件的文件头的结构一样,数据结构中成员的字节长度并不一定相同。(2)下方代码中//
符号后是我注释的具体对应的字节数目。(3)这里的数据结构非常简单,但初学理解容易混乱,文件头是由一个Elf64_Ehdr
的数据结构组成的,而其中的成员e_ident
是由16个1字节数据成员组成的数组,为了理解的连贯性,我在介绍e_ident
之后,直接介绍e_ident
组成成员。换言之,e_ident
是Elf64_Ehdr
的子项,而EI_ABIVERSION
又是e_ident
子项。
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ // 1 byte * 16
Elf64_Half e_type; /* Object file type */ // 2 bytes
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */ // 4 bytes
Elf64_Addr e_entry; /* Entry point virtual address */ // 8 bytes
Elf64_Off e_phoff; /* Program header table file offset */ // 8 bytes
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
各成员按照数据结构中定义的先后顺序,依次介绍意义。
e_ident
(ELF Header-Identification):
最开始处的这 16 个字节含有 ELF 文件的识别标志,作为一个数组,它的各个索引位置的字节数据有固定的含义,提供一些用于解码和解析文件内容的数据,是不依赖于具体操作系统的。
ELF 格式提供的目标文件框架可以支持多种处理器,以及多种编码方式。针对不同的体系结构和编码格式,ELF 文件的内容是会截然不同的。如果不知道编码格式,系统将无法知道怎么去读取目标文件;如果系统结构与本机不同,也将无法解析和运行。这些信息需要以独立的格式存放在一个默认的地方,所有系统都约定好从文件的同一个地方来读取这些信息,这就是 ELF 标识的作用。
ELF 文件最开始的这一部分的格式是固定并通用的,在所有平台上都一样。所有处理器都可能用固定的格式去读取这一部分的内容,从而获知这个 ELF 文件中接下来的内容应该如何读取和解析。
下表介绍e_ident
数组的结构。其中每个成员均占用 1 字节空间。
数据成员名称 | 数组下标的索引 | 意义 |
---|---|---|
EI_MAG0 | 0 | 文件标识 |
EI_MAG1 | 1 | 文件标识 |
EI_MAG2 | 2 | 文件标识 |
EI_MAG3 | 3 | 文件标识 |
EI_CLASS | 4 | 文件类别 |
EI_DATA | 5 | 编码格式 |
EI_VERSION | 6 | 文件版本 |
EI_OSABI | 7 | OS ABI 识别标志 |
EI_ABIVERSION | 8 | ABI 版本 |
EI_PAD | 9 | 补充字节开始的地址 |
EI_MAG0 ~ EI_MAG3(ELF Identification-Magic Number):文件的最前面 4 字节 e_ident[EI_MAG0] ~ e_ident[EI_MAG3] 的内容被称为“魔数”,用于标识这是一个 ELF 文件。这 4 个字节存放的 16 进制数值是固定的,依次为0x7f
,0x45
,0x4c
和0x46
,后三个数值对应的 ASCII码 为 “E”,“L” 和 “F”。
EI_CLASS(ELF Identification-Class):e_ident[EI_CLASS] 指明文件位数的标志,根据当前字节位置上的数值说明该文件是 32 位的还是 64 位的 ELF 文件。下面为可能的几个取值及其对应的含义。值为 1:32 位目标文件;值为 2:64 位目标文件。下方为源码中的定义、可取值及其对应的含义。
#define EI_CLASS 4 /* File class byte index */
#define ELFCLASSNONE 0 /* Invalid class */
#define ELFCLASS32 1 /* 32-bit objects */
#define ELFCLASS64 2 /* 64-bit objects */
#define ELFCLASSNUM 3
EI_DATA(ELF Identification-Data):e_ident[EI_DATA] 指明了目标文件中的数据编码格式,指明是小端编码还是大端编码。值为 1:补码编码(2's complement)且为小端编码(little endian);值为 2:补码编码且为大端编码(big endian)。下方为源码中的定义、可取值及其对应的含义。
注:低位数据存放在高位地址为大端编码,低位数据存放在低位地址为小端编码。
#define EI_DATA 5 /* Data encoding byte index */
#define ELFDATANONE 0 /* Invalid data encoding */
#define ELFDATA2LSB 1 /* 2's complement, little endian */
#define ELFDATA2MSB 2 /* 2's complement, big endian */
#define ELFDATANUM 3
EI_VERSION(ELF Identification-Version):e_ident[EI_VERSION] 指明 ELF 文件头的版本,目前这个版本号是 EV_CURRENT,即“1”。一般情况下,该位置对应的数值是 1。下方为源码中的定义、可取值及其对应的含义。
#define EI_ABIVERSION 8 /* ABI version */
EI_OSABI(ELF Identification-Operating System Application Binary Interface Identification):e_ident[EI_OSABI] 指明 ELF 文件操作系统的二进制接口的版本标识符。值为 0:指明 UNIX System V ABI。下方为源码中成员的定义及其中值为 0 代表的含义。
#define EI_OSABI 7 /* OS ABI identification */
#define ELFOSABI_NONE 0 /* UNIX System V ABI */
... ...
EI_ABIVERSION(ELF-Identification):e_ident[EI_ABIVERSION] 指明 ELF 文件的 ABI 版本。该位置一般值为零。下方为源码中的定义。
#define EI_ABIVERSION 8 /* ABI version */
EI_PAD(ELF Identification-Padding):e_ident[EI_PAD] 标记e_ident
中未使用字节的开始。这些字节被保留并设置为零;读取对象文件的程序应该忽略它们。如果当前未使用的字节被赋予意义,EI_PAD 的值将在将来发生变化。下方为源码中的定义。
#define EI_PAD 9 /* Byte index of padding bytes */
在官方文档中,e_ident
部分还提到一个成员 EI_NIDENT,位于下表索引 15 的位置(最后一个字节),表示的是e_ident
数组的大小。我之所以并没有将该成员放入上方进行介绍,我认为这一成员应该是已经被抛弃并不使用了。原因如下:首先在源码(elf.h
)中,我并没有发现该成员的宏定义;其次,如果按照该成员的定义,该字段出的值应该为 0x10(10 进制为 16),但是实际的值为 0,与定义本身存在矛盾;再者,如果成员 EI_PAD 被定义为未使用字节的开始,按照逻辑来说后方就不再会出现有意义的信息,否则成员 EI_PAD 的定义也存在问题。
e_type
(ELF Header-Type):该字段(共 2 字节)表明本目标文件属于哪种类型,是 可重定位文件 还是 可执行文件 亦或是 动态链接库文件。值为 1:重定位文件;值为 2:可执行文件;值为 3:动态链接库文件;值范围在:0xff00 ~ 0xffff 的文件类型是为特定处理器而保留的,如果需要为某种处理器专门设定文件格式,可以从这一范围内选取一个做为标识。在以上已定义范围外的文件类型均为保留类型,留做以后可能的扩展。下方为源码中的定义、可取值及其对应的含义。
/* Legal values for e_type (object file type). */
#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file */
#define ET_EXEC 2 /* Executable file */
#define ET_DYN 3 /* Shared object file */
#define ET_CORE 4 /* Core file */
#define ET_NUM 5 /* Number of defined types */
#define ET_LOOS 0xfe00 /* OS-specific range start */
#define ET_HIOS 0xfeff /* OS-specific range end */
#define ET_LOPROC 0xff00 /* Processor-specific range start */
#define ET_HIPROC 0xffff /* Processor-specific range end */
e_machine
(ELF Header-Machine):此字段(2 字节)用于指定该文件适用的处理器体系结构。在以上已定义范围外的处理器类型均为保留的,在需要的时候将分配给新出现的处理器使用。特别地,该字段对应的 16 进制数为 0x3e(对应 10 进制数为 62),代表的是 AMD x86-64 架构。下方为源码中的部分可取值及其对应的含义。
/* Legal values for e_machine (architecture). */
...
#define EM_X86_64 62 /* AMD x86-64 architecture */
...
e_version
(ELF Header-Version):此字段(4 字节)指明目标文件的版本。EV_CURRENT 是一个动态的数字,表示最新的版本。尽管当前最新的版本号就是“1”,但如果以后有更新的版本的话,EV_CURRENT 将被更新为更大的数字,而目前的“1”将成为历史版本。下方为源码中的可取值及其对应的含义。
/* Legal values for e_version (version). */
#define EV_NONE 0 /* Invalid ELF version */
#define EV_CURRENT 1 /* Current version */
#define EV_NUM 2
e_entry
(ELF Header-Entry Address):此字段(64 位 ELF 文件是 8 字节)指明程序入口的虚拟地址。即当文件被加载到进程空间里后,入口程序在进程地址空间里的地址。对于可执行程序文件来说,当 ELF 文件完成加载之后,程序将从这里开始运行;而对于其它文件来说,这个值应该是 0。但凡是地址信息,在分析时,务必注意文件的编码方式,后面内容涉及到不再赘述。
e_phoff
(ELF Header-Program Header Table Offset):此字段(8 字节)指明程序头表(program header table)开始处在文件中的偏移量,相对于 ELF 文件初始位置的偏移量。程序头表又称为段头表,上面介绍过 ELF 的执行试图中涉及到若干的段,而程序头表包含这些段的一个总览的信息。如果没有程序头表,该值应设为 0。e_phoff
与之后要介绍的e_phentsize
和e_phnum
这三个成员描述了 ELF 文件中关于程序头表部分的信息,e_phoff
:起始地址偏移,程序头表开始的位置;e_phentsize
:程序头表中每个表项的大小;e_phnum
:表项的数量。
e_shoff
(ELF Header-Section Header Table Offset):此字段(8 字节)指明节头表(section header table)开始处在文件中的偏移量。如果没有节头表,该值应设为 0。e_shoff
与之后要介绍的e_shentsize
和e_shnum
这三个成员描述了 ELF 文件中关于节头表部分的信息,e_shoff
:起始地址偏移,节头表开始的位置;e_shentsize
:节头表中每个表项的大小;e_shnum
:表项的数量。
e_flags
(ELF Header-Flags):此字段(4 字节)含有处理器特定的标志位。对于 Intel 架构的处理器来说,它没有定义任何标志位,所以 e_flags 应该值为 0。
e_ehsize
(ELF Header-ELF Header Size):此字段(2 字节)表明 ELF 文件头的大小,以字节为单位。分析文件时注意进制转换,后面内容涉及到不再赘述。
e_phentsize
(ELF Header-Program Header Table Entry Size):此字段(2 字节)表明在程序头表中每一个表项的大小,以字节为单位。在 ELF 文件的其他数据结构中也有相同的定义方式,如果一个结构由若干相同的子结构组成,则这些子结构就称为入口。
e_phnum
(ELF Header-Program Header Table Number):此字段(2 字节)表明程序头表中总共有多少个表项。如果一个目标文件中没有程序头表,该值应设为 0。
e_shentsize
(ELF Header-Section Header Table Entry Size):此字段(2 字节)表明在节头表中每一个表项的大小,以字节为单位。
e_shnum
(ELF Header-Section Header Table Number):此字段(2 字节)表明节头表中总共有多少个表项。如果一个目标文件中没有节头表,该值应设为 0。
e_shstrndx
(ELF Header-Section Header Table String Index):此字段(2 字节)表明节头表中与节名字表相对应的表项的索引。如果文件没有节名字表,此值应设置为 SHN_UNDEF。
引入该数据成员的原因:节头表中每个表项的大小是固定的,但它们的名称不可能一样长,如果将节头表的名字分别存入每个节头表中,节头表过短可能发生名字的失真,节头表过长就可能会出现大量地址未被利用,所以需要将每个节头表的名字统一存到一个专门存储节头表名字的一个节中,即为.shstrtab
节,而e_shstrndx
就表明了.shstrtab
节在所有的节中的索引值,也就是.shstrtab
节在所有的节中的位置。具体的数据关系的分析放在节的介绍的部分文档中。
上文说过.shatrtab一般放在最后,总共12个section,所以.shatrtab是11。如果不是放在最后,那么。shstrtab的ndx可能不是11.
edward@Edward:~/workspace/binutils/binutils/src$ readelf -S main.o
There are 12 section headers, starting at offset 0x1f0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000014 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000054
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000054
0000000000000004 0000000000000000 WA 0 0 4
[ 4] .comment PROGBITS 0000000000000000 00000054
000000000000002e 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000082
0000000000000000 0000000000000000 0 0 1
[ 6] .note.gnu.pr[...] NOTE 0000000000000000 00000088
0000000000000020 0000000000000000 A 0 0 8
[ 7] .eh_frame PROGBITS 0000000000000000 000000a8
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000170
0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000e0
0000000000000078 0000000000000018 10 3 8
[10] .strtab STRTAB 0000000000000000 00000158
0000000000000012 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 00000188
0000000000000067 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
这里需要注意,sections的偏移量是0x1f0。但是用-S读取的时候.shstrab是0x188。为什么对不上。这里需要注意就是这两个偏移根本就不是一回事。0x1fo并不是说.shstrtab的偏移,而是指sections的偏移。也就是0x1f0之后都保存的section的信息。每个section是64字节。64*12(section个数)就是整个section的大小。
所以我们第一步就是要根据elf header获取section的偏移。获取section的偏移之后,读取相关信息。就知道每个section的位置了。
添加调试,392就是从0x1f0偏移11个section得到的偏移。可以理解为section_header[11].offset。
通过计算section_header[11].offset是0x4c8、0x4c9正好对应188。验证了我们上述的分析。
所以分析elf文件的第一步就是分析section表。然后再从section表里逐步发掘更多的信息。
通过 ELF 文件头中提供的信息,完全可以掌握 ELF 文件各个部分之间的相对位置关系和字节数量大小,可构建出一张 ELF 文件的结构图,这也正是 ELF 文件头的作用。
执行readelf -S时会打印sections的名字。但是sh_name是一个int型的变量。存放不了sections name。
typedef struct elf_internal_shdr {
unsigned int sh_name; /* Section name, index in string tbl */
unsigned int sh_type; /* Type of section */
bfd_vma sh_flags; /* Miscellaneous section attributes */
bfd_vma sh_addr; /* Section virtual addr at execution in
octets. */
file_ptr sh_offset; /* Section file offset in octets. */
bfd_size_type sh_size; /* Size of section in octets. */
unsigned int sh_link; /* Index of another section */
unsigned int sh_info; /* Additional section information */
bfd_vma sh_addralign; /* Section alignment */
bfd_size_type sh_entsize; /* Entry size if section holds table */
/* The internal rep also has some cached info associated with it. */
asection * bfd_section; /* Associated BFD section. */
unsigned char *contents; /* Section contents. */
} Elf_Internal_Shdr;
为了更方便的管理sections,sections的结构体大小都是固定的。sh_name的值表示sections name在文件中的偏移。还是以.shstrtab为例。注意0x11这个值并不是最终的文件偏移。最终的文件偏移= 0x188+0x11 = 0x199。
我们到0x199偏移位置去找,可以发现正好就是字符串“.shstrtab”!
.shstrtab就是存放所有sections的name。所以可以看到从0x188开始,存放的都是字符串。
其他的sections名字查找都是一样的原理。
sections table和section在elf文件中的位置关系。
这样我们执行readelf -S就可以看到所有的信息了。目前不清楚section0是做什么用的,目前都是空。另外如果name过长显示不下会以[...]结尾。为什么要分成两行来表示,如果显示成一行不是更方便查看吗。
section | 含义 |
.shatrtab | 存放所有的sections的name |
.strtab | 存放所有的字符串 |
.symtab | 符号表 |
.symtab
接下来就对符号表进行分析。注意-S是查看sections表,-s是查看符号表。
readelf -s main.o
符号表结构体定义如下:
typedef struct {
uint32_t st_name;
unsigned char st_info;
unsigned char st_other;
uint16_t st_shndx;
Elf64_Addr st_value;
uint64_t st_size;
} Elf64_Sym;
怎么从sections table里找到符号表的呢。循环遍历所有的sections,然后判断TYPE是否是SYMTAB。这样就找到符号表了。
0x78表示符号表的大小。5 个 entries是这么计算的:0x78/sizeof(Elf64_Sym) = 120/24 = 5。符号表的名字也是通过偏移查找的。如果st_name不等于0,或者等于ABS,那么就是Link所指的section去找,可以看到link=10,对应.strtab。因为字符串本身就是放到.strtab里的。
name = .strtab的偏移+st_name的值就得到name在文件中的偏移了。如果st_name等于0,那么就去sections里找name。所以可以看到符号表的第2个.text就是从sections表里找到的。这样做的目的也可以理解。就是可以减小elf文件的大小,既然sections表里已经有字符串了,就没必要再在.strtab里存一份了。
flag是全局未初始化变量,Ndx为3,所在放在bss。testNdx是1,放在.text ,因为test是函数,肯定是放在.text的。
Ndx的显示规则如下:
char *get_symbol_ndx(Elf64_Section st_shndx) {
static char buf[10];
memset(buf, 0, 10);
int i = 8;
buf[9] = 0;
switch (st_shndx) {
case SHN_ABS:
return "ABS";
case SHN_COMMON:
return "COM";
case SHN_UNDEF:
return "UND";
default: {
// 正常情况
while (st_shndx) {
buf[i--] = '0' + st_shndx % 10;
st_shndx /= 10;
}
return buf + i + 1;
}
}
}
可以看到大小也一一对应,全局变量占用4个byte。test函数占用0x14=20个byte。
静态库
静态库就是将多个.o(elf)文件打包。就是linux里的归档tar命令。其实.a不会对各个.o的section进行合并等操作。reladelf在读取的时候先会解压.a,然后对.a里的文件逐个进行操作。
edward@Edward:~/workspace/binutils/binutils$ readelf -h libab.a
File: libab.a(a.o)
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 664 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
File: libab.a(b.o)
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 856 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
可以看到libab.a并不是elf文件,是arch类型。
动态库
制作动态库时会报错,是因为a.c和b.c里都定义了全局变量a。之所以静态库没有报错,是因为静态库只是将各个.o打开,不会检查.o之间的关系。而动态库是可以直接提供给别人使用的。所以需要检查是否有重定义等问题。可以理解为他和最终可执行程序没有什么区别,唯一的区别就是不需要main函数。
edward@Edward:~/workspace/lib$ gcc -shared -fPIC -o libab.so a.o b.o
/usr/bin/ld: b.o:(.data+0x0): multiple definition of `a'; a.o:(.data+0x0): first defined here
/usr/bin/ld: b.o: in function `func':
b.c:(.text+0x0): multiple definition of `func'; a.o:a.c:(.text+0x0): first defined here
/usr/bin/ld: b.o: warning: relocation against `a' in read-only section `.text'
/usr/bin/ld: a.o: relocation R_X86_64_PC32 against symbol `a' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: bad value
collect2: error: ld returned 1 exit status
edward@Edward:~/workspace/lib$
编译.o的时候也要加上fPIC
edward@Edward:~/workspace/lib$ gcc -fPIC -c a.c -o a.o
edward@Edward:~/workspace/lib$ gcc -fPIC -c b.c -o b.o
edward@Edward:~/workspace/lib$ gcc -shared -fPIC -o libab.so a.o b.o
edward@Edward:~/workspace/lib$ ls
a.c a.o a.s b.o libab.so main main.map
a.i a.out b.c libab.a '--library=gcc' main.c
edward@Edward:~/workspace/lib$
动态库的格式是elf。
010editor打开如下:
可执行文件
每个.o文件都有section,那么多个.o连接成可执行文件的时候,相同的sections是不是放在一起的呢。
a.c
int a = 1;
int funa()
{
return 0;
}
b.c
int b = 1;
int funb()
{
return 0;
}
main.c
int m = 2;
extern int a;
extern int b;
int main()
{
m = a + b;
printf("m = %d\r\n",m);
return 0;
}
readelf -s a.out
可以看到a、b、m三个变量 Ndx都是25。都位于.data。同时3个变量的地址都是连续的。所以对于多个.o链接成可执行文件后,相同的section会合并在一起。
假设我们将a变量设置到.mysect。从心编译。
int __attribute__((section(".mysect")))a = 1;
int funa()
{
return 0;
}
a变量确实被放到了.mysect。并且是 紧挨着.data。所以说编译器知道a是一个全局变量。就放在.data后面了。这是在linux上验证的。如果是mcu平台上,编译器可能会报警告或者错误,说在找不到.mysect被分配到哪里。所以需要在链接脚本里指定.mysect的分配情况。
可执行视图
前面分析的都是链接视图分析。现在从可执行视图分析下。前面说过,可执行文件多了program header。
所有的.o在链接成可执行文件之后还有一个变化就是。在.o里所有的变量、函数等地址都是0.
因为生成.o只是检查语法错误,并不知道变量、函数最终运行的地址。所以都是0。在链接阶段才会确定地址。查看a.out确实如此。
a.out的program header信息。
执行readelf -l a.out。
可以看到Section to Segment mapping。表示segment是有多个section组成的。组成的原理就是具有相同属性。然后也显示具体的13个segment的信息。
edward@Edward:~/workspace/lib/teset$ readelf -l a.out
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001b5 0x00000000000001b5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000144 0x0000000000000144 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000264 0x0000000000000268 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002010 0x0000000000002010 0x0000000000002010
0x0000000000000044 0x0000000000000044 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .mysect .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
virtualAddr表示运行地址。phyaddr表示加载地址。不清楚加载、运行地址的可以自行找资料了解。
Linux在执行程序的时候,会先运行elf加载器,加载器分析segment,然后将程序放到对应的地址,就可以运行了。
对于mcu来说,在上电后会先运行一段加载代码。将内容搬运到运行域,一般搬运的是全局初始化的变量。所以对于mcu来说,烧录ide主要也是分析segment header,sections header对于可执行文件来说其实已经用处不大了。