ELF文件格式详解-请查收

上一篇文章中主要介绍了ELF文件的基本定义和目标文件的分类,这篇文章中主要介绍下ELF文件格式

ELF文件主要提供了两个视图:链接视图及执行视图,分别针对程序运行过程的链接过程和执行过程。如下图所示:
在这里插入图片描述
单从相貌上来看,二者长得很像,除了肚子。可以看出链接视图采用的节(section)作为基本单位,而执行视图采用(segment)为基本单位。那么问题来了,这二者有什么区别?这是个设问句,区别如下:

Section与segment

在汇编语言中,代码段、数据段就是所谓的段(segment)。在可执行文件载入内存时,是以segment组织的,每个segment对应ELF文件中的program header中的一个条目,用来建立可执行文件映像。

在写汇编程序时,用.bss .data .txt或者data segment等这些段前指示,都是section,比如.text,告诉汇编器后面的代码放入.text section中。目标代码文件中的section和section header table中的条目是一一对应的。Section的信息用于链接器对代码的重定位。

可能还是不太懂,可以看下汇编程序的执行过程:

具体过程如下:

写一个汇编程序或通过编译器会产生文本文件hello.s。汇编器读取hello.s时会将其转换为目标文件hello.o,目标文件有若干个section组成,我们在汇编程序中声明的.section会成为目标文件中的section,此外汇编器还会自动添加一些section(比如符号表)。意思就是不同目标文件中相同的section会被合并成生成文件的一个section。

然后链接器把目标文件中的section合并成几个segment,生成可执行文件Hello.o。最后又加载器(loader)根据可执行文件中的segment信息加载运行这个程序。

大概是这样子的,吧?如果有不太对的地方欢迎指正。

然后针对两个视图中的每一部分进行详细解释,在此之前,我们熟悉一个命令readelf,其常用的命令格式如下

readelf -h [filename]         //查看文件头
readelf -S [filename]         //查看文件节头表
readelf -l [filename]         //查看程序头表
readelf -s [filename]         //查看符号表
readelf -a [filename]         //查看重定位表、符号表等信息
readelf -r [filename]         //只查看重定位表信息

/*注意这里的filename是elf文件的文件名,常见elf文件后缀有.o、.out等*/

1.ELF文件头解析

采用readelf -h xxx.o查看头部信息
我这里创建了一个简单的say hello的hello.o文件,头部信息如下
在这里插入图片描述我们看一下文件头在elf.h文件中的结构体定义,会发现其有14个成员



typedef struct

{
    unsigned char e_ident[EI_NIDENT];   /*Magic number and other info */
    Elf32_Half   e_type;        /*Object file type */
    Elf32_Half   e_machine;      /*Architecture */
    Elf32_Word   e_version;      /*Object file version */
    Elf32_Addr   e_entry;   /*Entry point virtual address */
    Elf32_Off   e_phoff;   /*Program header table file offset */
    Elf32_Off   e_shoff;   /*Section header table file offset */
    Elf32_Word   e_flags;   /*Processor-specific flags */
    Elf32_Half   e_ehsize;      /*ELF header size in bytes */
    Elf32_Half   e_phentsize;    /*Program header table entry size */
    Elf32_Half   e_phnum;   /*Program header table entry count */
    Elf32_Half   e_shentsize;    /*Section header table entry size */
    Elf32_Half   e_shnum;   /*Section header table entry count */
    Elf32_Half   e_shstrndx;     /*Section header string table index */ 
} Elf32_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;

类似于程序头表、节头表,elf文件头同时定义了32位及64位结构体,二者的区别只有地址位数的不同,其他成员的数据长度及类型都是相同的:

如下所示:

/* Type of addresses.  */
typedef uint32_t Elf32_Addr;        /*  32位  */
typedef uint64_t Elf64_Addr;       /*  64位  */

/* Type of file offsets.  */
typedef uint32_t Elf32_Off;        /*  32位  */
typedef uint64_t Elf64_Off;        /*  64位  */
名称大小对齐代表
Elf32_Addr44uint32_t
Elf64_Addr88uint64_t
Elf32_Half/Elf64_Half22uint16_t
Elf32_Off44uint32_t
Elf64_Off88uint64_t
Elf32_Sword/Elf64_Sword44int32_t
Elf32_Word/Elf64_Word44uint32_t

其14个成员如下图所示:
在这里插入图片描述

2.ELF节头表

采用readelf -S xxx.o查看节头表信息
在这里插入图片描述

查看源文件(以Elf32_Shdr为例)

/* Section header.  */
typedef struct
{ 
Elf32_Word  sh_name;      /* Section name (string tbl index) */ 
Elf32_Word  sh_type;      /* Section type */ 
Elf32_Word  sh_flags;     /* Section flags */ 
Elf32_Addr  sh_addr;       /*Section virtual addr at execution */
Elf32_Off   sh_offset;    /* Section file offset */
Elf32_Word  sh_size;      /* Section size in bytes */ 
Elf32_Word  sh_link;      /* Link to another section */
Elf32_Word  sh_info;      /* Additional section information */
Elf32_Word  sh_addralign;     /* Section alignment */ 
Elf32_Word  sh_entsize;   /* Entry size if section holds table */
} Elf32_Shdr;

在节头表中,包含10个成员,如图所示:
在这里插入图片描述
成员说明如下:

成员名称说明
sh_name给出节的名称,本身不是字符串,而是数值,代表目标文件字符串表中的一个索引值。
sh_type指出节类型
sh_flags标志位
sh_addr如果节将出现在进程的内存映像中,此成员指定该节数据在内存中的起始地址,否则,如该节数据不需要映射到内存中时,此字段为0
sh_offset该节数据在目标文件中的偏移量。需要注意的是:该节类型是SHT_NOBITS(如.bss节),说明该节在目标文件中并不占用空间大小,此时,该成员的数值只是概念上的偏移
sh_size节的大小(字节数)。当该节类型不是SHT_NOBITS时,成员数值就应该是该节在目标文件的实际大小。
sh_link节头部表的索引链接
sh_info附加信息,其会因为节类型的不同而不同。特别的,类型为SHT_REL或SHT_RELA的节,此时成员代表的是一个节头表索引值,指代需要做重定位操作的节
sh_addralign地址对齐的约束条件,数值0、1没有对其约束
sh_entsize如果节的内容是固定大小的表,例如符号表,此时成员sh_entsize给出的是每个表项的大小。如果该成员的数值为0,说明该节的数据内容并不是一张表

节类型(sh_type)说明:

类型说明
SHT_NULL说明节头表表项无效,目标中没有它对应的节
SHT_PROGBITS表明该节有程序定义的信息,这些信息的格式与意义完全由程序所定义
SHT_SYMTAB/SHT_DYNSYM说明节的数据内容为符号表,SHT_SYMTAB:为静态链接器提供符号信息,SHT_DYNSYM:为动态连接器提供符号信息
SHT_STRTAB说明该节是字符串表
SHT_RELA说明该节的内容是重定位表,表项带Addend
SHT_HASH说明该节包含符号哈希表
SHT_DYNAMIC说明该节内容是用来提供给动态链接器使用的
SHT_NOTE包含系统构建人员需要使用的特殊信息
SHT_NOBITS在文件中不占空间,其他属性类似于SHT_PROGBITS
SHT_REL说明该节内容是一个重定位表,表项不带Addend
SHT_INIT_ARRAY说明该节存储的是一个函数指针数组
andso on…

节类型中的SHT_RELA和SHT_REL的区别其实不太大,知道他们都是重定位表,应该就可以了。
原文中是这么说的,英语好的同学可以琢磨一下>

SHT_RELA
The section holds relocation entries with explicit addends, such as type Elf32_Relafor the 32-bit class of object files. An object file may have multiple relocation sections.
SHT_REL
The section holds relocation entries without explicit addends, such as type Elf32_Rel for the 32-bit class of object files. An object file may have multiple relocation sections. See ‘‘Relocation’’ below for details.

标志位说明如下:
在这里插入图片描述

特殊节如下所示:
在这里插入图片描述

如果节sh_link的值为1,则其可链接到本程序的相关节

在这里插入图片描述

若sh_info的值为1,则其可链接到符号表内的索引+1

查看符号表:readelf -s hello.o

在这里插入图片描述
学习的时候要笑!接下来笑着>_<看程序头表

3.程序头表

采用readelf -l xxx.o查看文件的程序头表
在这里插入图片描述
查看定义

/* Program segment header.  */

typedef struct
{
 Elf32_Word  p_type;       /* Segment type */ 
 Elf32_Off   p_offset;     /* Segment file offset */ 
 Elf32_Addr  p_vaddr;      /* Segment virtual address */
 Elf32_Addr  p_paddr;      /* Segment physical address */
 Elf32_Word  p_filesz;     /* Segment size in file */
 Elf32_Word  p_memsz;      /* Segment size in memory */
 Elf32_Word  p_flags;      /* Segment flags */
 Elf32_Word  p_align;      /* Segment alignment */
} Elf32_Phdr;

有8个成员
在这里插入图片描述
P_type类型描述:
在这里插入图片描述

4.符号表

符号:对数据、函数或者代码(如全局变量或者函数)的引用

符号表:简单理解就是通过索引对数据、函数、变量的调用

例如:在使用printf()函数时,就会调用动态符号表.dynsym,因为在符号表.dynsym中存在一个指向该函数的符号条目。

在大多数共享库和可执行文件中,存在两个符号表:.symtab、.dynsym。

.dynsym保存了引用来自外部文件的全局符号,如printf这样的库函数。可以说.dynsym是.symtab的一个子集,因为.symtab中还存有可执行文件的本地符号,如全局变量或者代码定义的本地函数等,因此.symtab保存了所有的符号,而.dynsym只保存了动态/全局符号。

但存在一个问题,为什么同时定义包含相同部分的两个符号表呢?在采用readelf -S 查看可执行文件的输出时,我们发现:.dynsym被标记了ALLOC,即运行时会被内存调用,而.symtab并没有。可以看出,在程序运行的时候,.symtab并不是必需的。因此不会被装载到内存中。而.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。可以说:.dynsym符号表对于动态链接可执行文件的执行来说是必需的,而.symtab符号表只是用来调试和链接的,有时候为了节省空间,会将.symtab符号表删掉

符号表查看,readelf -s hello.o,如下图所示:

在这里插入图片描述

符号类型说明:
在这里插入图片描述

符号绑定说明:
在这里插入图片描述

5.重定位表

静态链接的过程分为两步:第一步是空间与地址的分配,第二步是符号解析与重定位;第二步是链接过程的核心。在重定位过程中,重定位表和符号表起着至关重要的作用,重定位表确定了需要被重定位的地址,符号表决定了其需要被替换成什么值。

可采用readelf -a test.o或readelf -r test.o查看文件重定位表,不同的是,后者只显示重定位表,如图所示:
在这里插入图片描述

静态链接中,目标文件中包含有用于重定位的表:代码段重定位表.rel.text;数据段重定位表.rel.data

动态链接中,目标文件的重定位表:.rel.dyn对数据引用的修正,修正的位置位于.got和数据段;.rel.plt对函数引用的修正,修正位置位于.got.plt。

如上图所示,我们可以观察到几种重定位入口类型

R_X86_64_RELATIVE, R_X86_64_GLOB_DAT,
R_X86_64_JUMP_SLO

R_X86_64_GLOB_DAT(.rel.dyn中针对.got)和R_X86_64_JUMP_SLO(.rel.plt中针对.got.plt)表示被修正的位置只需要直接将符号的地址填入。

而在重定位表中的Offset表明了当前符号在.got或.got.plt中的偏移,可以根据该值在GOT表中寻找对应的位置。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值