ELF文件格式

目标代码(Object Code)指编译器和汇编器处理源代码后所生成的机器语言目标代码
目标文件(Object File)指包含目标代码的文件
最早的目标文件格式是自有格式,非标准的。标准的几种目标文件格式:
– DOS操作系统(最简单) :COM格式,文件中仅包含代码和数据,且被加载到固定位置
– System V UNIX早期版本:COFF格式,文件中不仅包含代码和数据,还包含重定位信息、调试信息、符号表等其他信息,由一组严格定义的数据结构序列组成
– Windows:PE格式(COFF的变种),称为可移植可执行(Portable Executable,简称PE)
– Linux等类UNIX:ELF格式(COFF的变种),称为可执行可链接(Executable and Linkable Format,简称ELF)
 
ELF文件格式提供了两种视图,分别是:
链接视图(被链接):可重定位目标文件 (Relocatable object files) 
执行视图(被执行):可执行目标文件(Executable object files)
(1)链接视图——可重定位目标文件
扩展名为.o(相当于Windows中的 .obj文件),包含代码、数据、定位信息(指出哪些符号引用处需要重定位)等
可被链接合并,生成可执行文件或共享目标文件(.so文件);若干个可重定位目标文件组成静态链接库文件(.a文件)。
节(section)是ELF 文件中具有相同特征的最小可处理单位:
  • ELF 头:
分32位系统对应结构和64位系统对应结构(32位版本、64位版本)。32位系统中:
ELF头位于ELF文件开始,共52字节,包括16字节标识信息、文件类型 (.o, exec, .so)、机器类型(如 IA-32)、节头表的偏移、节头表的表项大小以及表项个数等
#define EI_NIDENT 16
typedef struct{
    unsigned char e_ident[EI_NIDENT]; //16个字节的数组,定义了一些标识信息。最开头4B称为魔数,标识目标文件的类型或者格式。加载或读取文件时,可用魔数确认文件类型是否正确。再后面12字节,标识32位还是64位、大端还是小端、ELF头的版本号。
    Elf32_Half e_type;     //目标文件类型,是可重定位文件、可执行文件、共享库文件还是其它
    Elf32_Half e_machine;  //机器结构类型,是IA32、AMD64、SPARC V9还是其它
    Elf32_Word e_version; //目标文件版本
    Elf32_Addr e_entry;  //程序执行的入口地址,可重定位目标文件应该为0,且没有程序头表(段头表)等执行视图
    Elf32_Off  e_phoff; //程序头表(段头表)的起始位置
    Elf32_Off  e_shoff;   //节头表的起始位置
    Elf32_Word e_flags;   //处理器特定标志
    Elf32_Half e_ehsize;    //ELF头结构大小
    Elf32_Half e_phentsize;   //程序头表(段头表)的表项长度
    Elf32_Half e_phnum;       //程序头表(段头表)的表项数目
    Elf32_Half e_shentsize;    //节头表的表项长度
    Elf32_Half e_shnum;   //节头表的表项数目
    Elf32_Half e_shstrndx;  //字符串表在节头表中的索引,即节头表中第多少项是字符串表(String Table)。
}Elf32_Ehdr;
ELF头是二进制存储的,需要使用命令查看内容
$ readelf -h main.o
Magic:   7f 45 4c 4601 01 01 00 00 00 00 00 00 00 00 00    //ELF文件的魔数是7f 45 4c 46。
Class:    ELF32   
Data:     2's complement, little endian    //负数用2进制补码形式表示、小端方式存放
Version: 1 (current)
OS/ABI:  UNIX - System V  
ABI Version:   0
Type:    REL (Relocatable file) 
Machine:   Intel 80386 
Version:    0x1
Entry point address:  0x0     //可重定位文件程序执行入口地址为0
Start of program headers:  0 (bytes into file)    //没有程序头表
Start of section headers:   516 (bytes into file) //节头表起始位置
Flags:    0x0
Size of this header:   52 (bytes)          
Size of section headers:    40 (bytes)  //节头表每项40B
Number of section headers:  15       //节头表共15项
Section header string table index: 12        //.strtab在节头表中的索引
  • .text 节
编译后的代码部分
  • .rodata节
只读数据,如 printf 格式串、switch跳转表等
  • .data 节
已初始化全局变量和静态成员变量,存放具体的初始值,需要占磁盘空间。
区分初始化和非初始化是为了空间效率
  • .bss 节
未初始化全局变量和局部静态变量,默认初始值为0,.bss节中无需存放初始值,只要说明.bss中的每个变量将来在执行时占用几个字节即可,因此,.bss节实际上不占用磁盘空间,
通过专门的节头表(Section header table)来说明应该为.bss节预留多大的空间
  • .symtab 节
存放函数和全局变量的 (符号表)信息 ,它不包括局部变量
  • .rel.text 节
.text节的重定位信息,用于重新修改代码段的指令中的地址信息
  • .rel.data 节
.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息
  • .debug 节
调试用符号表
  • strtab 节
字符串表,包括.symtab节和.debug节中的符号以及节头表中的节名。
字符串表就是以null结尾的字符串序列。
  • Section header table(节头表)
节头表的起始位置、表项数目、长度在ELF头中给出。
以下是32位系统对应的节头表数据结构(每个表项占40B),说明了每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等
typedef struct {
    Elf32_Word sh_name;   //节名字符串在.strtab节(字符串表)中的偏移
    Elf32_Word sh_type;   //节类型:无效/代码或数据/符号/字符串/...
    Elf32_Word sh_flags;  //节标志:该节在虚拟空间中的访问属性
    Elf32_Addr sh_addr;   //虚拟地址:若可被加载,则对应虚拟地址
    Elf32_Off  sh_offset; //在文件中的偏移地址,对.bss节而言则无意义
    Elf32_Word sh_size;   //节在文件中所占的长度
    Elf32_Word sh_link;   //sh_link和sh_info用于与链接相关的节(如 .rel.text节、.rel.data节、.symtab节等)
    Elf32_Word sh_info;
    Elf32_Word sh_addralign; //节的对齐要求
    Elf32_Word sh_entsize;   //节中每个表项的长度,0表示无固定长度表项
} Elf32_Shdr;
使用readelf命令命令查看节头表内容
$ readelf -S test.o
可重定位目标文件中,每个可装入节的起始地址总是0
.bss节应占00000c大小,但只有装入内存时才会分配。
这11个节在装入内存时,只有.text节、.data节、.bss节和.rodata节会分配存储空间
 
(2)执行视图——可执行目标文件
与可重定位文件的不同
  1. ELF头中字段e_entry给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0
  2. 多一个程序头表,也称段头表(segment header table) ,是一个结构数组
  3. 多一个.init节,用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作
  4. 少两个.rel节(无需重定位)
使用readelf命令查看ELF头的内容:
$ readelf -h main.o
Magic:   7f 45 4c 4601 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:  x8048580
Start of program headers:  52 (bytes into file)   //程序头表在ELF头后
Start of section headers:  3232 (bytes into file)
Flags:    0x0
Size of this header:   52 (bytes)  
Size of program headers:    32 (bytes)   //程序头表每项32B
Number of program headers:   8          //程序头表共8项
Size of section headers:    40 (bytes)       
Number of section headers:  29       
Section header string table index: 26        //.strtab在节头表中的索引
装入内存时,ELF头、程序头表、.init节、.rodata节会被装入只读代码段
.data节和.bss节会被装入读写数据段
描述可执行文件中的节与虚拟空间中的存储段之间的映射关系。
一个表项32B,说明虚拟地址空间中一个连续的段或一个特殊的节。
以下是32位系统对应的程序头表数据结构:
typedef struct {
    Elf32_Word p_type;   //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
    Elf32_Off p_offset;  //此成员给出从文件头到该段第一个字节的偏移
    Elf32_Addr p_vaddr;  //此成员给出段的第一个字节将被放到内存中的虚拟地址
    Elf32_Addr p_paddr;  //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
    Elf32_Word p_filesz; //此成员给出段在文件映像中所占的字节数。可以为0。
    Elf32_Word p_memsz;  //此成员给出段在内存映像中占用的字节数。可以为0。
    Elf32_Word p_flags;  //此成员给出与段相关的标志。
    Elf32_Word p_align;  //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;
使用readelf命令某可执行目标文件的程序头表
$ readelf –l main
程序头表信息有8个表项,其中两个为可装入段(即Type=LOAD):
第一可装入段:第0x00000~0x004d3的长度为0x4d4字节的ELF头、程序头表、.init、.text和.rodata节,映射到虚拟地址0x8048000开始长度为0x4d4字节的区域 ,按0x1000=2^12=4KB对齐,具有只读/执行权限(Flg=RE),是只读代码段。
第二可装入段:第0x00f0c~0x01014的长度为0x108字节的.data节和磁盘中不占存储空间的.bss节,映射到虚拟地址0x8049f0c开始长度为0x110字节的存储区域,在0x110=272B存储区中,前0x108=264B用.data节内容初始化,后面272-264=8B对应.bss节,初始化为0 ,按0x1000=4KB对齐,具有可读可写权限(Flg=RW),是可读写数据段。
由此看出.bss节在文件中不占用磁盘空间,但在存储器中需要给它分配相应大小的空间。
posted @ 2018-05-25 10:53  扬羽流风  阅读(2914)  评论(0编辑  收藏  举报