1、ELF文件简介

  ELF(Executable and Linkable Format):一种对可执行文件、目标文件和库使用的文件格式。它在Linux下成为标准格式已经很长时间。由于ELF文件的存在,对所有体系结构而言,程序本身的相关信息以及程序的各个部分在二进制文件中编码的方式都是相同的。

  ELF文件包括:可执行文件、可重定位的目标文件(包括.o和.a文件)、core文件和共享对象(.so文件)。

  ELF文件的基本布局:

android elf文件格式 elf文件详解_objdump

 

除了用于标识ELF文件的几个字节之外,ELF头还包含了有关文件类型和大小的有关信息,以及文件加载后程序执行的入口点信息等。

  程序头表(仅在可执行文件和.so文件中才有意义):向系统提供了可执行文件的数据在进程虚拟地址空间中组织方式的有关信息。它还表示了文件可能包含的段数目、段的位置和用途。各个段保存了与文件相关的各种形式的数据。例如,符号表、实际的二进制码、固定值(如字符串)或程序使用的数值常数。

  节头表:管理文件的各个节。

 

  2、示例源程序和ELF文件

// test.c
int add(int a, int b)
{
    printf("Numbers are added together\n");
    return a + b;
}

int main()
{
    int a, b;
    a = 3;
    b = 4;
    int ret = add(a, b);
    printf("Result: %u\n", ret);
    exit(0);
}

  产生test.o和test:gcc test.c -c -o test.o && gcc test.c -o test。使用file命令查看它们的文件类型:

test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped  # 可重定位的目标文件
test:   ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32 ...

 

  3、相关命令

  1)readelf:Displays information about ELF files

  (1)选项:-h:显示ELF头中的信息;-l:显示程序头表的信息;-S:显示节头表的信息;-r:显示文件重定位节的内容;-a:相当于指定-h -l -S -s -r -d -n -V。

  (2)示例:

# 查看ELF头中的信息(忽略某些输出行)
[root@localhost ~]# readelf -h test
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00  # 45 4c 46即ELF
  Class:                             ELF64  # 64位机器
  OS/ABI:                            UNIX - System V
  Type:                              EXEC (Executable file)  # 可执行文件
  Entry point address:               0x4004d0  # 程序执行的入口点
  Start of program headers:          64 (bytes into file)  # 各个部分的长度和索引位置
  Start of section headers:          4472 (bytes into file)
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27

[root@localhost ~]# readelf -h test.o
ELF Header:
  Type:                              REL (Relocatable file)  # 可重定位文件
  Size of program headers:           0 (bytes)  # 没有程序头表
# 查看程序头表中的信息(忽略某些输出行)
[root@localhost ~]# readelf -l test

Program Headers:  # 9个程序头/段。显示各段在虚拟地址空间中的大小、位置、标志和访问授权等信息
  Type           Offset             VirtAddr           PhysAddr    ...    Flags   ...
  PHDR           ...  # 保存程序头表
  INTERP         ...  # 指定在程序已从可执行文件映射到内存后必须调用的解释器(通过链接其他库满足未解决引用的程序)
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]  # 通常ld用于在虚拟地址空间中插入程序运行所需的动态库
  LOAD           ...  # 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等
  LOAD           ...
  DYNAMIC        ...  # 保存了由动态链接器(INTERP中指定的解释器)使用的信息
  NOTE           ...
  GNU_EH_FRAME   ...
  GNU_STACK      ...
  GNU_RELRO      ...

 Section to Segment mapping:  # 节到段的映射(哪些节载入到哪些段)。一个段包含一个或多个节。节信息参考readelf -S的输出
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got
# 查看节头表中的信息(忽略某些输出行)。节信息无须复制到在虚拟地址空间中为可执行文件创建的最终的进程映像
[root@localhost ~]# readelf -S test.o 
There are 13 section headers, starting at offset 0x1c0:

Section Headers:  # 各节都指定了大小和在二进制文件内部的偏移量
  [Nr] Name              Type             Address           Offset  # 偏移量是相对于二进制文件的(0x1c0)
       Size              EntSize          Flags  Link  Info  Align  # Address指定节加载到虚拟地址空间中的位置(.o文件为0)
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000068  0000000000000000  AX       0     0     4  # A标志控制装载文件时是否将节的数据复制到虚拟地址空间
  [ 2] .rela.text        RELA             0000000000000000  00000678  # REL:重定位信息
       0000000000000090  0000000000000018          11     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000a8  # PROGBITS:程序必须解释的信息,如二进制代码
       0000000000000000  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a8
       0000000000000000  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a8
       0000000000000027  0000000000000000   A       0     0     1
  [10] .shstrtab         STRTAB           0000000000000000  00000158  # STRTAB:存储与ELF格式有关的字符串(如.text)。与程序无直接关联
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  00000500  # SYMTAB:符号表
       0000000000000150  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  00000650
       0000000000000022  0000000000000000           0     0     1

[root@localhost ~]# readelf -S test
There are 30 section headers, starting at offset 0x1178:

Section Headers:
  [Nr] Name              Type             Address           Offset  # Address现在是有效的,因为相应代码必须映射到虚拟地址空间中
       Size              EntSize          Flags  Link  Info  Align
  [ 1] .interp           PROGBITS         0000000000400238  00000238  # .interp:保存解释器文件名(ASCII字符串)
       000000000000001c  0000000000000000   A       0     0     1

  一些常用的节的作用(可结合下文nm命令输出的各个符号的类型来帮助理解):

  .data:保存初始化过的数据。

  .bss:保存未初始化的数据。在程序执行前,内核将.bss中的数据初始化为0或空指针。如全局的long sum[1000];和int a3 = 0;将存放到.bss中。另外,.bss的内容并不存放在磁盘上的程序文件中:分别定义全局的int a[10 * 1024 * 1024] = {3};和int a[10 * 1024 * 1024];,可以发现a.out大小的显著不同。

  .rodata:保存只读数据,如字符串常量。一个字符串常量在同一个程序中只有一个拷贝。

  .text:保存二进制代码。由CPU执行的机器指令部分。它通常是可共享的,所以即使是频繁执行的程序(如文本编辑器)在存储器中也只需有一个副本。另外,它常常是只读的,以防止指令被意外修改。

  .rel.text:保存.text节的重定位信息。

  .*debug_*(如.debug_info):指定-g编译时出现的调试相关的节。

  .strtab/.shstrtab:字符串表,用于管理字符串。其中.shstrtab存放文件中各个节的文本名称(如.text)。

  .symtab:符号表。它确定了符号的名称与其值之间的关联。每一项由两个元素组成:符号名在字符串表中的位置和符号的值。

  这些节最终被加载到进程的虚拟地址空间后,它们和其他部分共同形成的C程序典型的存储空间布局如下:

android elf文件格式 elf文件详解_android elf文件格式_02

  这里顺便介绍栈和堆的特性:

  栈:存放自动变量以及每次函数调用时所需保存的信息。每次调用函数时,其返回地址以及调用者的环境信息(如寄存器的值)都存放在栈中。然后,最近被调用的函数在其栈帧上为其自动和临时变量分配存储空间。

  栈的分配依赖内置于处理器的指令集,效率很高,但容量有限(如8M)。

  Linux将虚拟地址空间划分为内核空间和用户空间。每个用户进程自身的虚拟地址范围从0到TASK_SIZE,用户空间之上的区域(从TASK_SIZE到2^32或2^64)保留给内核专用。如IA-32系统中地址空间在3GB处划分,因此栈底在3GB处开始从高地址向低地址方向增长。

  另外,函数参数一般是从右至左入栈的。可通过以下例子验证:

int main()
{
    int a[3] = {0, 1, 2};
    int i = 0;
    printf("%d, %d\n", a[i], a[i++]);    // 结果为1, 0

    return 0;
}

  堆:通常用于动态存储空间的分配。堆的大小主要受限于系统中有效的虚拟内存(超出会抛出std::bad_alloc异常)。堆的分配使用较为复杂的算法(如伙伴系统),效率比栈要低,且容易产生内存碎片。

 

  2)size:list section sizes and total size

  (1)作用:列出文件中各个节的大小以及它们的总大小。

  (2)示例:

# 忽略某些行
[root@localhost ~]# size --format=sysv test
test  :
section              size      addr
.text                 468   4195536
.rodata                55   4196016
.data                   4   6295616
.bss                    4   6295620
Total                2131

 

  3)objdump:display information from object files

  (1)选项:-d/--disassemble:显示文件中机器指令对应的汇编器助记符(反汇编)。只反汇编那些包含指令的节(如.text);-D:类似于-d,但反汇编所有节;-h:显示文件节头表的信息;-t:显示符号表条目;-S:尽可能“反汇编”出源代码(隐式指定了-d)。该选项在编译指定-g时效果比较明显。

  (2)示例:

# 查看重定位节中的信息(忽略某些输出行)
[root@localhost ~]# readelf -r test.o 

Relocation section '.rela.text' at offset 0x678 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend  # 偏移量指定了需要修改的项的位置
00000000000f  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
000000000014  000a00000002 R_X86_64_PC32     0000000000000000 puts - 4  # 18
000000000043  000900000002 R_X86_64_PC32     0000000000000000 add - 4  # 47
000000000050  00050000000a R_X86_64_32       0000000000000000 .rodata + 1b
00000000005a  000c00000002 R_X86_64_PC32     0000000000000000 printf - 4  # 5e
000000000064  000d00000002 R_X86_64_PC32     0000000000000000 exit - 4  # 68

# 在printf和add函数的地址已经确定之后,必须将其插入到指定的偏移量处,以便生成能够正确运行的可执行代码
[root@localhost ~]# objdump --disassemble test.o
Disassembly of section .text:

0000000000000000 <add>:
   0:   55                      push   %rbp
  13:   e8 00 00 00 00          callq  18 <add+0x18>  # 调用puts,18与上文注释对应
  21:   c3                      retq   

0000000000000022 <main>:
  22:   55                      push   %rbp
  42:   e8 00 00 00 00          callq  47 <main+0x25>  # 调用add,47与上文注释对应
  59:   e8 00 00 00 00          callq  5e <main+0x3c>  # 调用printf,5e与上文注释对应
  5e:   bf 00 00 00 00          mov    $0x0,%edi
  63:   e8 00 00 00 00          callq  68 <main+0x46>  # 调用exit,68与上文注释对应

  重定位是将ELF文件中未定义符号关联到有效值的处理过程。如test.o对printf和exit的未定义引用必须替换为该进程的虚拟地址空间中适当的机器代码所在的地址。

 

  4)nm:list symbols from object files

  (1)选项:-a:列出所有符号(包括调试符号);-A:在每个符号前显示其所在的文件名;-l:利用调试信息找出符号所在的文件名和行号;-u(--undefined-only):只列出未定义符号,相对的有--defined-only;-n:将符号按它们的地址排序。

  (2)nm列出每个符号的值、类型和名字。

  对于类型字段,小写通常表示符号是local的(有几个例外情况),大写则表明是global/external的。例如,R/r:只读符号;N:调试符号;B/b:未初始化数据节(.bss)中的符号;D/d:初始化数据节(.data)中的符号;T/t:.text节中的符号;U:未定义符号。

  (3)示例

// gcc -c nm.c生成nm.o
// gcc nm.c -o nm.out生成nm.out
static int a;  // 类型为b
int b = 1;  // 类型为D
static int c = 1;  // 类型为d
const int d = 10;  // 类型为R
extern int e;
int f;  // 类型为C

void function() { printf("Hello"); }  // function的类型为T,printf的类型为U

int foo()  // foo的类型为T
{
    static int g = 0;  // g的符号名为g.2186,类型为b
    static int h = 1;  // h的符号名为h.2187,类型为d
    int i = 33;  // 栈上的变量
    return i;
}

int bar()  // bar的类型T
{
    static int g = 0;  // g的符号名为g.2191,类型为b
    static int h = 1;  // h的符号名为h.2192,类型为d
}

int main() {}

  使用nm查看符号:

# 忽略某些输出行
[root@localhost ~]# nm nm.o
0000000000000000 b a
0000000000000000 D b
0000000000000025 T bar
0000000000000004 d c
0000000000000000 R d
0000000000000004 C f
0000000000000015 T foo
0000000000000000 T function
0000000000000004 b g.2186
0000000000000008 b g.2191
0000000000000008 d h.2187
000000000000000c d h.2192
                 U printf  # 未定义符号(程序引用的自身代码未定义的符号)没有“符号值”
[root@localhost ~]# nm nm.out
                 U printf@@GLIBC_2.2.5  # 未定义符号现在有了进一步信息

 

 

  参考资料:

  《深入Linux内核架构》

  《UNIX环境高级编程》