ELF文件详解

发布时间 2024-01-12 15:10:38作者: zxddesk

一、ELF概述

1、ELF的定义

ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的ELF格式文件包括:可执行文件、可重定位文件(.o)、共享目标文件(.so)、核心转储文件等。

ELF主要用于Linux平台,Windows下是PE/COFF格式。       

2、ELF文件的结构

一个完整的ELF文件一般会包括如下几个内容:ELF头、Section头、Program头和Section。

其中由Section头组成的集合称为Section头表,由Program头组成的集合称为Program头表。注意:数个连续的头称之为头表,头表是虚拟出来的定义,文件中不存在头表,只有头。

一个Section头指向一个Section,Section头中包括所指向Section的名字、类型、其在ELF文件中的偏移地址、大小等信息。

一个Program头指向一个Segment,Program头中包括所指向Segment的类型、其在ELF文件中的偏移地址、大小,映射到内存的虚拟地址等信息。一个Segment由一系列连续的Section构成,连续的Section拥有相同的权限,如只读、读写、可读可执行等;

一个ELF头内包含有:Section头表的在ELF文件中的偏移地址、单个Section头的大小、Section头表中Section头的个数;Program头表的在ELF文件中的偏移地址、单个Program头的大小、Program头表中Program头的个数;该ELF文件的类型,若是可执行文件的话,还包含的有程序的入口地址。

3、头的表示方法及其含义

1)变量及其大小:

2)ELF头

  1.  
    #define EI_NIDENT 16
  2.  
     
  3.  
    struct Elf32_Ehdr //共52个字节 //Ehdr表示ELF header
  4.  
    {
  5.  
      unsigned char  e_ident[EI_NIDENT];
  6.  
      Elf32_Half e_type; //类型包括:可执行文件、可重定向文件、共享目标文件等
  7.  
      Elf32_Half e_machine; //有X86、arm之类
  8.  
      Elf32_Word e_version;
  9.  
      Elf32_Addr e_entry; //可执行程序的入口地址
  10.  
      Elf32_Off e_phoff; //Program头表的偏移地址
  11.  
      Elf32_Off e_shoff; //Section头表的偏移地址
  12.  
      Elf32_Word e_flags;
  13.  
      Elf32_Half e_ehsize; //本结构体的size
  14.  
      Elf32_Half e_phentsize; //单个Program头的size
  15.  
      Elf32_Half e_phnum; //Segment头表中Segment头的个数
  16.  
      Elf32_Half e_shentsize; //单个Section头的szie
  17.  
      Elf32_Half e_shnum; //Section头表中Section头的个数
  18.  
      Elf32_Half e_shstrndx; //储存Section名字集合的Section的下标,指".shstrtab"的下标
  19.  
    };

2)Section头

  1.  
    struct Elf32_Shdr //共40个字节 //Shdl表示Section header
  2.  
    {
  3.  
        Elf32_Word sh_name; //所指向Section的名字,如".text"、".data"、".bss"等
  4.  
        Elf32_Word sh_type; //所指向Section的类型,如:符号表、字符串表等
  5.  
        Elf32_Word sh_flags;
  6.  
        Elf32_Addr sh_addr;
  7.  
        Elf32_Off sh_offset; //所指向Section在ELF文件中的偏移量
  8.  
        Elf32_Word sh_size; //所指向Section的size
  9.  
        Elf32_Word sh_link; //和其关联的Section头的下标索引
  10.  
        Elf32_Word sh_info;
  11.  
        Elf32_Word sh_addralign; //字节对齐
  12.  
        Elf32_Word sh_entsize;
  13.  
    };

3)Program头

  1.  
    struct Elf32_phdr //32个字节 //phdr表示Program header
  2.  
    {
  3.  
        Elf32_Word p_type; //如PT_LOAD表示,对应Segment可被加载到内存中
  4.  
        Elf32_Off p_offset; //Segment在ELF文件中的偏移量
  5.  
        Elf32_Addr p_vaddr; //Segment映射到内存后的虚拟地址
  6.  
        Elf32_Addr p_paddr; //Segment映射到内存后的物理地址,此时与虚拟地址相同
  7.  
        Elf32_Word p_filesz; //Segment在ELF文件中占用的size
  8.  
        Elf32_Word p_memsz; //Segment映射到内存后占用的size
  9.  
        Elf32_Word p_flage; //读、写、执行权限
  10.  
        Elf32_Word p_align; //字节对齐,p_vaddr和p_paddr对p_align取模后为0
  11.  
    };

更详细内容请参考:ELF文件格式解析

4、实例解析

可执行文件中Program头表是必须的,可重定向文件(.o)中Section头表是必须的,共享目标文件(.so)中两者都是必须的。

1)可重定向文件分析

ELF头信息如下所示:

在此文件中,可看到其类型为REL, 即可重定向文件。其中Program头的个数为0,Section头的个数为8个,没有程序入口地址。

下图是8个Section头的详细信息:

其中Addr在此处被填充为了0的原因是,其目前并不需要被加载到内存中,在链接的时候才会被填充。

根据上述各Section的偏移量及size可推断出其在该可重定向文件中空间布局,如下表所示:

偏移量(Off) 大小(size) Section 备注
0x0 0x34 ELF头 0x34表示十进制的52,刚好为ELF头的大小
0x34 0x2a .text  
0x60 0x38 .data  
0x98 0x0 .bss  
0x98 0x30 .shstrtab  
0xc8 0x140 Section头表 一个Section头的大小为40个字节,共8个头,大小为0x140
0x208 0x80 .symtab  
0x288 0x28 .strtab  
0x2b0 0x10 .rel.text  

下面详述上面几种类型的Section:

Ⅰ .shstrtab

.shstrtab中存放着各个Section的名字。

Ⅱ .strtab

.symtab中存放着程序中用到的符号的名字。

Ⅲ .bss

程序中未初始化的全局变量都会被归类到bss段,并在程序加载的时候被初始化为0。

在加载.bss的时候和.data一样,都属于可读可写的数据,但在ELF文件中.data需要占用一段内存空间来保存变量的初始化值,而.bss却不需要。

也就是说,.bss只占用一个Section头的大小,而不需要对应的Section。如上表中可以看出.bss所描述Section的size为0。

Ⅳ .rel.text

.rel.text用于告诉链接器,哪些地方需要重定向。

Ⅴ .symtab

.symtab内存放着程序中用到的符号,包括变量符号、函数符号,如printf、main等。

.symtab有如下定义:

  1.  
    struct Elf32_sym //
  2.  
    {
  3.  
        Elf32_Word st_name; //符号的名字
  4.  
        Elf32_Addr st_value; //符号相对于其所在Section偏移的相对地址
  5.  
        Elf32_Word st_size; //符号的size
  6.  
        unsigned char st_info; //低四位表示符号的作用范围(全局或局部),高四位表示符号的类型(变量、函数等)
  7.  
        unsigned char st_other;
  8.  
        Elf32_Half st_shndx; //该符号的值在哪个Section下存储
  9.  
    };

实例:

以上图中的data_items为例,其Ndx为3,表示其在第3个Section,即.data。data_items的value值为00000000,表示其相对于.data的偏移地址为0,即data_itms在.data的开头。

_start的value也为00000000,表示其在.text的开头,也即整个代码的入口是_start。

2)可执行文件分析

可执行文件的ELF头信息如下所示:

相对于可重定向文件来说,其类型变为了EXEC,少了两个Section header,多了两个Program头,并且有可执行程序的入口地址。

6个Section头如下所示:

从图中可以看出,.text和.data的Addr不再为0,有了实际的值,这便是在链接过程中装载上的。

.bss段因为没有使用到,所以被删除掉了。

.rel.text在链接之后,便完成了自己的使命,也就被删除掉了。

根据上述各Section的偏移量及size可推断出其在该可执行文件中空间布局,如下表所示:

偏移量(Off) 大小(size) Section 备注
0x0 0x34 ELF头 0x34表示十进制的52,刚好为ELF头的大小
0x34 0x40 Program头表 一个Program头的大小为32字节,共2个头,大小为0x40
0x74 0x2a .text  
0xa0 0x38 .data  
0xd8 0x27 .shstrtab  
0x100 0xf0 Section头表 一个Section头的大小为40个字节,共6个头,大小为0xf0
0x1f0 0xa0 .symtab  
0x290 0x40 .strtab  

2个Program头如下所示:

结合Program头和Section的空间布局表可以看出,ELF头、Program头表和Section头表共同组成了第一个Segment;.data单独组成了另一个Segment。

VirtAddr列指出第一个Segment加载到虚拟地址0x0804 8000(注意在x86平台上后面的PhysAddr列是没有意义的),第二个Segment加载到地址0x0804 90a0。

Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。

最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页,对应关系如下图所示:

这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。

此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,比如第二个Segment在文件中的偏移是0xa0,在内存页面0x0804 9000中的偏移仍然是0xa0,所以是从0x0804 90a0开始,这样规定是为了简化链接器和加载器的实现。

从上图也可以看出.text段的加载地址应该是0x0804 8074,这也正是程序的入口地址。
 

原来目标文件符号表中的Value都是相对地址,现在都改成绝对地址了。此外还多了三个符号__bss_start_edata_end,这些是在链接过程中添进去的,加载器可以利用这些信息把.bss段初始化为0。

5、可执行ELF文件的装载过程

 

 

附录

1)Section与Segment的英文含义

section和segment都有部分的意思,但

section指的“部分”是不同质的,如:The TOEFL is divided into three sectiond, namely listening, structure and reading.在这里托福考试是由三部分组成的,这三部分是不一样的,即不同质的。

而segment指的“部分”是同质的,如:I want the middle segment of the rope. 我想要中间那段绳...

2)段错误(Segment Error)

当程序试图访问不允许访问的内存位置,或试图以不允许的方式访问内存位置(例如尝试写入只读位置,或覆盖部分操作系统)时会发生段错误。

常见的段错误,包括:

1)使用未经初始化及或已经释放的指针地址

2)访问受系统保护的内存地址

3)写入只读的内存地址

4)数组越界

5)堆栈溢出

参考资料:

1.ELF文件解析和加载(附代码)

2.ELF文件格式解析(完)

3.ELF文件详解—初步认识