ELF文件格式解析

ELF文件是什么?

ELF是Executable and Linkable Format的缩写,字面上看就是可执行和可连接文件。在Linux下可重定位文件(.o)、可执行文件、共享目标文件(.so)、核心转储问文件(core dump) 都是使用ELF文件格式。

ELF 通常由编译器或者连接器产生,并且是二进制格式,使用一些工具可以更好的观察它的结构,如readelf、objdump

ELF由什么组成

ELF文件由ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)4个部分组成。
ELF文件格式解析_第1张图片
除了ELF头,ELF文件不一定包含其他部分,且位置分布也不一定和上图一致。

分析ELF前置准备

int printf(const char *, ...);
  
int global_init_var = 84;
int global_uninit_var;

void func1(int i){
    printf("func1 %d", i);
}

int main(){
    static int static_var = 5;
    static int static_var2;

    int a = 1;
    int b;

    func1(11 + 22 + 33 + a - b);

    return a;
}

后续会使用上面的代码产生的可重定位文件进行ELF文件的分析,产生可重定位文件的方式是gcc -c

ELF Header

ELF头的结构定义在elf.h的ELF32_Ehdr/ELF64_Ehdr中,这边以Elf64_Ehdr为例子

typedef struct
{
  unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
  Elf64_Half    e_type;         /* Object file type */
  Elf64_Half    e_machine;      /* Architecture */
  Elf64_Word    e_version;      /* Object file version */
  Elf64_Addr    e_entry;        /* Entry point virtual address */
  Elf64_Off e_phoff;        /* Program header table file offset */
  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;

使用readelf对之前的.o文件其进行分析

>> 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:          1048 (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:         13
  Section header string table index: 12

将上述输出与Elf64_Ehdr结构进行对应

字段 对应Elf64_Ehdr的结构 描述
Magic e_ident 前4个字节固定是 7F 45 4C 46; 45 4C 46 是ELF的ascii码;
Class e_ident[4] 01为32位,02为64位
Data e_ident[5] 01 是小端序, 02 是大端序
Version e_ident[6] 表示ELF的版本号,不过一般都为1,因为ELF1.2版本后到目前都没有更新
OS/ABI e_ident[7] 表示使用的ABI类型,不过一般情况下是0, 也就是UNIX - System V
ABI VERSION e_ident[8] 表示使用的ABI的版本,一般情况下是0
Type e_type 表示ELF的文件类型,ET_REL(1)为可重定位文件,一般是.o文件;ET_EXEC(2)为可执行文件;ET_DYN(3)一般为.so文件; ET_CORE(4) 为core file,也就是core dump 产生的文件
Machine e_machine 常量定义也在elf.h中,数量得有点多,其中EM_x86_64 为62
Version e_version 意义和上面的Version一致
Entry point address e_entry 表示程序执行的入口地址,因为这边是.o文件所以该值为0
Start of program headers e_phoff 表示Program Header的入口偏移量
Start of section headers e_shoff 表示Section Header的入口偏移量
Flags e_flags 表示ELF文件相关的特定处理器的flag
Size of this header e_ehsize 表示ELF Header大小, 当前header的大小就是64 字节
Size of program headers e_phentsize 表示Program Header大小
Size of section headers 实际上这里指的是Elf64_Shdr这个结构的大小
Number of section headers e_shnum 表示Section Header的个数
Section header string table index e_shstrndx 段表字符串表所在段在段表中的下标

这边还可以使用hexdump 来查看.o文件的前64个字节来对比readelf的输出

>>hexdump -C -n 64 main.o 
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  18 04 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0d 00 0c 00  |....@.....@.....|
00000040

段表Section Header Table

ELF中包含各种各样的段,而段表就是描述这些段的基本属性的结构,如段名、段长、段在文件中的偏移、读写权限等。而段表所在的位置由Elf64_Ehdr中的e_shoff来决定。换句话说编译器、加载器、链接器等都是依靠ELF文件的段表来访问各个段的。

这边先看段表的结构,实际上段表是一个以Elf64_Shdr为元素的数组,而Elf64_Shdr结构如下:

typedef struct
{
  Elf64_Word    sh_name;        // section_name段名,实际上名字存储在字符串表'.shstrtab'中,这边存的是段名在字符串表中的下标
  Elf64_Word    sh_type;        //section type 段的类型,具体枚举后续会有
  Elf64_Xword   sh_flags;       //section flag 段的标志位,具体枚举后续会有
  Elf64_Addr    sh_addr;        //section addr  段的虚拟地址,如果该段可被加载,则会被加载到地址空间的对应虚拟地址中
  Elf64_Off sh_offset;      // section offset 段在文件中的偏移
  Elf64_Xword   sh_size;        // section size 段的长度
  Elf64_Word    sh_link;        //  section Link 段的链接信息
  Elf64_Word    sh_info;       //   section Info 段的信息
  Elf64_Xword   sh_addralign;     // Section alignment 段对齐长度,某些段会对地址对齐有要求
  Elf64_Xword   sh_entsize;     // Entry size if section holds table 某些段会有固定大小的项,这些项所占用的大小是一样的。如果为0,则不没有包含这种项
} Elf64_Shdr;

section type

段的类型相关常量都是以SHT_开头,以下是常用的段以及对应的描述

常量 描述
SHT_NULL 0 无效
SHT_PROGBITS 1 代码段、数据段都是这个类型
SHT_SYMTAB 2 该段内容是符号表
SHT_STRTAB 3 该段内容是字符串表
SHT_RELA 4 重定位表,该段包含重定位信息
SHT_HASH 5 符号表的哈希表
SHT_DYNMIC 6 动态链接信息表
SHT_NOTE 7 提示性信息
SHT_NOBITS 8 表示该段在文件中不存在内容,.bss段就是该类型的
SHT_REL 9 重定位表,该段包含重定位信息
SHT_SHLIB 10 保留
SHT_DNYSYM 11 动态链接符号表

还有很多段类型的常量没展现, 具体可以看elf.h;SHT_REL(Relocation entries, no addends) 和 SHT_RELA(Relocation entries with addends) 都是重定位表,但有少许区别,对于静态编译的重定位而言,重定位地址总时相对于某个参照物进行偏移,而SHT_REL 将偏移放在了需要重定位的内存地址处,而SHT_RELA 将偏移放在了重定位表中,而重定位内存处则填0.

section flag

段的标志位相关常量是以SHF_开头,以下是常用的段的标志位

常量 描述 readelf 的标志
SHF_WRITE 1<< 0 表示该段可写入 W
SHF_ALLOC 1 << 1 表示该段在执行时需要分配空间,如代码段、数据段、.bss段都会有这个标志 A
SHF_EXECINSTR 1 << 2 表示该段可以被执行, 一般是指代码段 X
SHF_MERGE 1 << 4 表示可以合并以消除重复的数据的section M
SHF_STRINGS 1 << 5 标识由NULL结尾的字符串组成的section,每个字符串的大小在section header的sh_entsize指定 S
SHF_INFO_LINK 1 << 6 表示字段sh_info中包含section headers的某个section的下标 I
SHF_LINK_ORDER 1 << 7 表示链接器添加了特殊的排序要求 L
SHF_OS_NONCONFORMING 1 << 8 表示section需要链接标准之外的基于特定操作系统的处理 O
SHF_GROUP 1 << 9 表示section 是section Group的成员,该标志只能为包含在可重定位对象中的section设置 G
SHF_TLS 1 << 10 表示sction包含线程本地存储 T
SHF_COMPRESSED 1 << 11 包含压缩数据,不能与SHF_ALLOC一起使用 C

还有一些其他的标志没展现,具体可以看elf.h。

section link && section info

如果段的类型是与连接相关的,那么sh_link、sh_info这两个成员就会有一些意义。

sh_type sh_link sh_info
SHT_DYNAMIC 该段所使用的字符串表在段表的下标 0
SHT_HASH 该段所使用的符号表在段表的下标 0
SHT_REL/SHT_RELA 该段所使用的符号表在段表的下标 重定位表作用的表在段表中的下标
SHT_SYMTAB/SHT_DYNSY 操作系统相关 操作系统相关
other SHN_UNDEF 0

分析目标文件的section header Table

这边可以使用objdump 或者readelf来观察段表的结构

>> objdump -h main.o 

main.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000048  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000088  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  00000090  2**2
                  ALLOC
  3 .rodata       00000009  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000001d  0000000000000000  0000000000000000  00000099  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000b6  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000b8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


>> readelf -S main.o
There are 13 section headers, starting at offset 0x418:

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
       0000000000000048  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000338
       0000000000000048  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000088
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  00000090
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  00000090
       0000000000000009  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000099
       000000000000001d  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000b6
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000380
       0000000000000030  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000110
       0000000000000198  0000000000000018          11    11     8
  [11] .strtab           STRTAB           0000000000000000  000002a8
       000000000000008c  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000003b0
       0000000000000061  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),
  l (large), p (processor specific)

可以看到readelf 输出的内容会比objdump更多,因为objdump会省略一些辅助性的段。在前文elf header中可以看到main.o中的Number of section headers确实是13,与readelf输出段的个数匹配,不过ELF段表的第一个元素是无效的描述符,所以真正有效的描述符只有12个

name Type flag 描述
.text PROGBITS SHF_ALLOC && SHF_EXECINSTR 代码段,加载时需要分配空间且可以被执行
.rela.text RELA SHF_INFO_LINK 代码段的重定位段,sh_link字段包含符号表在段表的下标,sh_info可以确定作用于.text
.data PROGBITS SHF_WRITE && SHF_ALLOC 数据段,可写入且加载时需要分配空间
.bss NOBITS SHF_WRITE && SHF_ALLOC Block Started by Symbol,存储没有初始化或者初始化为0的全局变量,文件中不存在内容,但加载后可写入且需要分配空间
.rodata PROGBITS SHF_ALLOC 只读数据段存储常量数据,比如程序中定义为const的全局变量,#define定义的常量。 加载后需要分配空间
.comment PROGBITS SHF_MERGE && SHF_STRINGS 注释信息段,在连接的时候可以合并该段,并且段中存储的是以NULL为结尾的字符串
.note.GNU-stack PROGBITS 堆栈提示段
.eh_frame PROGBITS SHF_ALLOC 这个段在GCC生成处理异常的代码时,描述如何展开堆栈 。加载时需要分配空间
.rela.eh_frame RELA SHF_INFO_LINK .eh_frame的重定位段,由sh_info可以确定作用于.eh_frame
.symtab SYMTAB 符号表
.strtab STRTAB 字符串表
.shstrtab STRTAB 段表字符串表,通常存储段名

重定位表

重定位表表项的结构回在静态连接中总结

字符串表

因为字符串的长度一般都不固定,所以常见的做法是存放到统一的表里
ELF文件格式解析_第2张图片
让偏移和字符串可以一一对应
ELF文件格式解析_第3张图片
这样在ELF文件中只需要引用一个字符串表的偏移就可以了。常见的字符串表就是.strtab(字符串表) 和 .shstrtab(段表字符串表)。字符串表用来保存普通的字符串,比如符号名字等。而段表字符串表用来保存段表中会用到的字符串,比如段表名字等。

回到最开头的mian.o文件中,在ELF Header里由一个e_shstrndx,这个变量是段表字符串表在段表中的下标,即.shstrtab在段表中的下标,可以对比ELF headr的输出和readelf -S 的输出,发现e_shstrndx确实是12。

符号表

.symtab 是由 Elf32_Sym/Elf64_Sym 够成的一个数组,其定义如下:

typedef struct
{
  Elf64_Word    st_name;        // 符号名字在字符串表中的下标
  unsigned char st_info;        //  符号类型和绑定信息
  unsigned char st_other;      // 貌似没用
  Elf64_Section st_shndx;     // 符号所在段,但部分特殊符号意义稍微特殊一点
  Elf64_Addr    st_value;       // 符号值,不同符号这个字段的意义不一致
  Elf64_Xword   st_size;      // 符号大小,如果为0则大小为未知
} Elf64_Sym;

st_info

st_info 的低4位用来代表符号类型,高28位代表符号绑定信息。
绑定信息的部分宏定义如下:

宏定义 描述
STB_LOCAL 0 局部符号,目标文件之外不可见
STB_GLOBAL 1 全局符号,所有目标文件都可见
STB_WEAK 2 弱符号,类似于全局符号,但定义具有较低优先级

符号类型的部分宏定义如下

宏定义 描述
STT_NOTYPE 0 未知类型
STT_OBJECT 1 符号是个数据对象,比如变量、数组等
STT_FUNC 2 符号是个函数
STT_SECTION 3 表明该符号是一个段,这个符号必须是STB_LOCA的
STT_FILE 4 该符号是文件名字,一般是该目标文件对应的源文件名, 一定是STB_LOCAL类型,且st_shndx一定是SHN_ABS
STT_COMMON 5 该符号是一个未初始化的公共块,处理方式会与 STB_LOCAL 相同

st_shndx

符号如果定义在本文件中,则该字段就是 所在段 在 段表中的下标。如果不是定义在目标文件以及对于有些特殊的符号,则是按以下定义来。

宏定义 描述
SHN_UNDEF 0 符号未定义,这个符号在该目标文件中有引用,但定义不在该目标文件中
SHN_ABS 1 表示该符号包含一个绝对的值,如文件名称这种符号
SHN_COMMON 2 表示该符号属于未初始化的公共块,比如mina.o中定义的gloabal_uninit_var这个变量
还有一些特殊定义没展现。

st_value

这个变量对于不同情况有不同的定义:

  • 如果符号在目标文件中,且符号的st_shndx不是SHN_COMMON,则st_value表示符号在段中的偏移,最常见的就是全局变量的符号
  • 如果符号在目标文件中,且符号的st_shndx是SHN_COMMON,则st_value是对齐属性
  • 如果符号在可执行文件中,则st_value表示符号的虚拟地址,这个对于动态连接十分有用。

分析main.o的符号表

>>readelf -s main.o 
Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 _ZZ4mainE10static_var
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 _ZZ4mainE11static_var2
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    13: 0000000000000000    36 FUNC    GLOBAL DEFAULT    1 _Z5func1i
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z6printfPKcz
    16: 0000000000000024    36 FUNC    GLOBAL DEFAULT    1 main

>>>objdump -t main.o 
main.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 main.cpp
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000004 l     O .data  0000000000000004 _ZZ4mainE10static_var
0000000000000004 l     O .bss   0000000000000004 _ZZ4mainE11static_var2
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     O .data  0000000000000004 global_init_var
0000000000000000 g     O .bss   0000000000000004 global_uninit_var
0000000000000000 g     F .text  0000000000000024 _Z5func1i
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 _Z6printfPKcz
0000000000000024 g     F .text  0000000000000024 main

符号表的第一项是无效的,所以main.o中16个有效符号,然后Name被readelf直接读取字符串表给翻译出来出来了。

符号下标 描述
1 该符号是文件名字, 内容是绝对的值,符号名字是main.cpp
6 该符号是个数据对象,是个本地符号,定义在该目标文件中,所在段的下标是3,也就是.data段,名字是_ZZ4mainE10static_var,长度是4,在段中的偏移是4
7 该符号是个数据对象,是个本地符号,定义在该目标文件中,所在段的下标是4,也就是.bss段,名字是_ZZ4mainE11static_var2, 长度是4,在段中的偏移是4
11 该符号是个数据对象,是个全局符号,定义在该目标文件中,所在段的下标是3,也就是.data段,名字是global_init_var, 长度是4, 在段中偏移是0
12 该符号是个数据对象,是个全局符号,定义在该目标文件中,所在段的下标是4,也就是.bss段,名字是global_uninit_var, 长度是4, 在段中偏移是0
13 该符号是个函数对象,是个全局符号,所在段的下标是1,也就是.textt段,名字是_Z5func1i, 长度是36
14 该符号类型未定义,是个全局符号,在该文件中只是引用,名字是_GLOBAL_OFFSET_TABLE_,实际上这个是一个由编译器生成的符号,用于定位全局地址无关的变量的真实地址,素材GOT
15 该符号是个未定义,是个全局符号,在该文件中只是引用,名字是_Z6printfPKcz,实际上这个是printf函数,文件中确实对printf没有进行定义
16 该符号是个函数对象,是个全局符号,定义在该目标文件中,所在段的下标是1,也就是.text段,函数名字是main,长度36,在段中偏移是0X24

对于其他Type是section的符号,实际上都是对应Ndx的段的段名,readelf没有显示,但objdump 可以看到这些符号的名字。

然后通过objdump 可以查看代码段,对上述处于代码段的符号偏移进行验证

>>> objdump -d main.o 
main.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z5func1i>:
  ......
  23:   c3                      retq   

0000000000000024 
: ...... 47: c3 retq

可以看到_Z5func1i从0X00开始长度为36(0X24), 从0x24开始是main函数的内容,长度为36(0x2f)。

然后可以通过hexdump来对数据段的内容进行验证:

>> hexdump -n 8 -s 0x88 main.o
0000088 0054 0000 0005 0000                    
0000090

数据段在文件中偏移为0x88,长度为8, 前4个字节是global_init_var,内容是采用小端序存储,其值为0x00000054,十进制下位84.第4到7个字节为_ZZ4mainE10static_var,内容是0X00000005.

总结

这边较为深入的介绍了ELF Header 、section Header 、以及secton的结构,并使用了《程序员的自我修养-链接、装载与库》这本书上的例子作为实验的内容,详细展现了目标文件中的这些信息应该如何解读。
在知道了目标文件中的结构后,接下来的问题就是如何在链接的时候将他们组合起来,形成一个可执行文件或者是.so文件。

引用

  • 程序员的自我修养-链接、装载与库.pdf
  • https://sp4n9x.github.io/2021/05/27/ELF_FileFormat_Analysis/ ELF文件格式分析
  • https://blog.csdn.net/wyzworld/article/details/114805643 ELF文件详解
  • https://zhuanlan.zhihu.com/p/286088470
  • http://nicephil.blinkenshell.org/my_book/ch04.html
  • https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-79797/index.html

你可能感兴趣的:(编译与链接,linux,服务器)