链接与装载---理解ELF文件

目录

elf文件整体透视

elf核心数据结构分析

1. ELF_Header

2. Program Header

3. Section Header Table

elf实例总结

细节说明

1. elf可执行文件的执行入口地址是怎么确定的?

2. 字符串表

3. ELF Header中e_shstrndx的理解

4. 符号表

5. elf常见section的含义

6. 重定位表

参考


 

        网络上有很多分析elf文件的文章, 大多文章一上来就码代码, 讲解elf文件的数据结构和含义, 到最后都没看明白elf文件是个什么东东, 本文按照总-分-总的结构来阐述笔者对elf文件的理解, 先整体分析elf文件结构,通过笔者对elf文件理解后绘制的一幅文件空间布局图来整体审视elf文件, 对elf文件有个整体的认识,  再解释elf核心数据结构的含义,最后实例分析elf文件,将elf各个部分关联起来,我们将能够绘制出elf空间布局图。

既然是分析elf文件, 就需要用到elf文件, 为了节省篇幅, 这里笔者将制作elf文件的程序放在另一篇文章中:示例程序demo.c

 

elf文件整体透视

elf文件内容划分为如下五大部分: 

  1. ELF_Header: ELF头部, 每个ELF文件都存在一个ELF_Header,它的作用是描述整个文件的组织,存放了很多重要的信息用来,如: 版本信息,程序执行入口地址,偏移信息等。程序执行也必须依靠其提供的信息。
  2. Program_Header_Table:程序头部表,用于告诉系统如何在内存中创建映像,是可选的一个表,文件中有段就必须有程序头部表,有程序头部表才有段。其中存放各个段的基本信息(包括地址指针,段偏移,大小等)。只需要记住一句话,Program_Header_Table存放各个Segment的基本信息,表中有多少个项,就对应多少个段
  3. Section_Header_Table:节区头部表, 类似与Program_Header_Table,但与其相对应的是节区(Section)。
  4. Section: 节区,将文件分成一个个节区,每个节区都有其对应的功能,如符号表,哈希表等,以及程序执行入口地址所在的.text也是一个节区。
  5. Segment: 段,就是将文件分成一段一段映射到内存中。段中通常包括一个或多个节区,而且同一个节区可能会存在于多个段中, 这里抛出一个疑问:  既然同一个节区可以位于不同的段中, 这难道不会导致同一份内容在文件中重复出现吗?

在详细说明之前,先来看一下elf文件的空间布局, 如下图所示, 这幅图是我根据示例程序demo.c编译得到的可执行elf文件绘制的(图中地址会随编译环境,代码不同而变化):

 

是不是很简单, 看似神秘的elf文件,从头到尾也就包含了五大块:ELF_Header, Program Header table, Section,Segment, Section Header table。 这幅图也直观的解决了上文中我们提出的一个疑问, elf文件中的各个Segment在空间中是交叉嵌套存在的(之所以会交叉、嵌套,是因为一个section可能会位于多个Segment中), 所以,是不会导致内容重复的。


elf文件其实是按照Section来组织的, 为什么还要划分为不同的Segment呢?另外,在很多资料中, 会将.text说成代码段,可是.text明明是Section,应该翻译为节才对,这里我们不得不说一下Segment与Section的区别:

事实上,Segment和Section提供了ELF文件的两种不同的视图,从Section角度来看ELF文件就是链接视图,从Segment角度来看就是执行视图(或装载视图),我们再谈ELF装载时,段专门指Segment, 而其他情况下, 段指的是Section。从上图中还可以看出, Segment2和Segment3包含了大部分的Section,是因为链接器会将相同属性权限的Section划分为同一个Segment中,我们看看readelf的执行结果:

# readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x8048310
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00644 0x00644 R E 0x1000
  LOAD           0x000f08 0x08049f08 0x08049f08 0x00114 0x00118 RW  0x1000
  DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x0004fc 0x080484fc 0x080484fc 0x0003c 0x0003c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .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

Program Headers Table用来保存Segment的信息, 共有9条,对应9个Segment, 其中Segment2和Segment3位LOAD类型, 而在装载时,只有类型为"LOAD"的Segment才会被映射到内存中,其他Segment都是起到辅助作用 ,于是, elf文件从文件偏移0x000000处的0x644字节将会被装载到0x8048000处, 这0x644字节,包含了elf header(52字节), program_header_talbe(9*32=288字节),以及segment2,而且这0x644字节数据是以0x1000对齐的。

 

要透彻理解elf文件, 就必须了解elf相关的一些数据结构, 涉及到的相关数据类型格式如下,这些对分析elf文件各个部分所占字节大小非常重要。

名称

大小

说明

Elf32_Addr

4

无符号程序地址

Elf32_Half

2

无符号中等整数

Elf32_Off

4

无符号文件偏移

Elf32_SWord

4

有符号大整数

Elf32_Word

4

无符号大整数

unsigned char

1

无符号笑整数

下面,我们就详细分析一下elf的核心数据结构吧。

 

elf核心数据结构分析

elf核心数据结构的定义位于:/usr/include/elf.h

1. ELF_Header

ELF_Header的结构定义:

#define EI_NIDENT 16
 
typedef struct{
 
  unsigned char  e_ident[EI_NIDENT];//16字节数据,包含魔数ELF,以及版本等信息
 
  Elf32_Half e_type;//e_type表示文件的类型,值为1时表示可重定位文件,2表示可执行我文件,3表示共享目标文件
 
  Elf32_Half e_machine;//
 
  Elf32_Word e_version;//e_machine和e_version表示文件的CPU体系结构和版本
 
  Elf32_Addr e_entry;//可执行程序入口地点
 
  Elf32_Off e_phoff;//e_phoff表示Program Header Table在文件中的偏移量,没有则为0
 
  Elf32_Off e_shoff;//e_shoff表示Section Header Table在文件中的偏移量,没有则为0
 
  Elf32_Word e_flags;//保存与文件相关的,特定于处理器的标志(好像用的不多)
 
  Elf32_Half e_ehsize;//ELF_Header Size,ELF头部的大小
 
  Elf32_Half e_phentsize;//e_phentsizze表示 Program Header Table的大小
 
  Elf32_Half e_phnum;//e_phnum表示Program Header的数量
 
  Elf32_Half e_shentsize;
 
  Elf32_Half e_shnum;//e_shentsize和e_shnum表示Section Header Table的大小和Section Header的数量
 
  Elf32_Half e_shstrndx;//e_shstrndx表示Section名称字符串表的索引,String Table Index,在节区表中有一个存储各节区名称的节区(通常是最后一个),这里表示名称表在第几个节区
 
}Elf32_Ehdr;

ELF_Header提供了elf文件的总览信息, 包括下文我们要分析的Program Header,Section Header的索引,数量和大小。

 

2. Program Header

在ELF_Header中,包含了Program Header的索引地址(e_phoff)、段数量(e_phnum)、表项大小(e_phentsize)。

Program Header 结构定义:

typedef struct{
 
    Elf32_Word p_type;//段的类型
 
    Elf32_Off p_offset;//段相对于文件的偏移地址
 
    Elf32_Addr p_vaddr;//段在内存中的虚拟地址
 
    Elf32_Addr p_paddr;//段的物理地址,在linux操作系统下,此段没有意义
 
    Elf32_Word p_filesz;//段在文件中所占的长度
 
    Elf32_Word p_memsz;//段在内存中所占的长度
 
    Elf32_Word p_flage;//段相关标志(read、write、exec)
 
    Elf32_Word p_align;//字节对其,p_vaddr 和 p_offset 对 p_align 取模后应该等于0。
 
} Elf32_phdr;

p_type的含义:定义此段的类型

取值

代表

含义

00

PT_NULL

此数组元素未用。结构中其他成员都是未定义的。

01

PT_LOAD

此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于 p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载的段在程序头部表格中根据 p_vaddr 成员按升序排列。

02

PT_DYNAMIC

数组元素给出动态链接信息。 

03

PT_INTERP

数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面。

04

PT_NOTE

此数组元素给出附加信息的位置和大小。

05

PT_SHLIB

此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符。

06

PT_PHDR

此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面。

0x70000000

PT_LOPROC

此范围的类型保留给处理器专用语义。

0x7fffffff

PT_HIPROC

此范围的类型保留给处理器专用语义。

0x6474e550

GNU_EH_FRAME

GNU自定义类型

0x6474e551GNU_STACK 
0x6474e552GNU_RELRO 

后面在分析elf文件时,会看到上面的这些类型。

Program Header Table在elf文件中是紧跟着ELF Header存在的。

3. Section Header Table

    我们可以从ELF Header中得到Section Header Table的索引地址(e_shoff)、节区数量(e_shnum)、表项大小(e_shentsize),还可以由名称节区索引(e_shstrndx)得到各节区的名称。

Section Header Table 结构定义:

typedef struct{
 
    Elf32_Word sh_name;//节区名称,此处是一个32位的索引值, 对应ELF_Header->e_shstrndx[]的一个字符串( e_shstrndx相当于一个字符串数组)
 
    Elf32_Word sh_type;//节区类型
 
    Elf32_Word sh_flags;//同Program Header的p_flags,代表(read、write、exec)
 
    Elf32_Addr sh_addr;//节区索引地址
 
    Elf32_Off sh_offset;//节区相对于文件的偏移地址
 
    Elf32_Word sh_size;//节区的大小
 
    Elf32_Word sh_link;//此成员给出节区头部表索引链接
 
    Elf32_Word sh_info;//此成员给出附加信息。
 
    Elf32_Word sh_addralign;//字节对齐,sh_addr 对sh_addralign 取模,结果必须为 0。目前仅允许取值为 0 和 2的幂次数。数值 0 和 1 表示节区没有对齐约束
 
    Elf32_Word sh_entsize;//某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。 如果节区中并不包含固定长度表项的表格,此成员取值为 0。
 
}Elf32_Shdr;
 

sh_type:节区类型,取值如下:

名称

取值

说明

SHT_NULL

0

此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。

SHT_PROGBITS

1

此节区包含程序定义的信息,其格式和含义都由程序来解释。

SHT_SYMTAB

2

此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。

SHT_STRTAB

3

此节区包含字符串表。目标文件可能包含多个字符串表节区。

SHT_RELA

4

此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。

SHT_HASH

5

此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。

SHT_DYNAMIC

6

此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。

SHT_NOTE

7

此节区包含以某种方式来标记文件的信息。

SHT_NOBITS

8

这种类型的节区不占用文件中的空间,其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移

SHT_REL

9

此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。

SHT_SHLIB

10

此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。

SHT_DYNSYM

11

作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。

SHT_LOPROC

0X70000000

这一段(包括两个边界),是保留给处理器专用语义的。

SHT_HIPROC

0X7FFFFFFF

这一段(包括两个边界),是保留给处理器专用语义的。

SHT_LOUSER

0X80000000

此值给出保留给应用程序的索引下界。

SHT_HIUSER

0X8FFFFFFF

此值给出保留给应用程序的索引上界。

 

sh_type

sh_link

sh_info

SHT_DYNAMIC

此节区中条目所用到的字符串表格的节区头部索引

0

SHT_HASH

此哈希表所适用的符号表的节区头部索引

0

SHT_REL、SHT_RELA

相关符号表的节区头部索引

重定位所适用的节区的节区头部索引

SHT_SYMTAB、SHT_DYNSYM

相关联的字符串表的节区头部索引

最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一

其它

SHN_UNDEF

0

Section Header Table在elf文件中,通常位于elf文件的末尾, 当然,它会在ELF Header中指示Section Header Table在elf文件中的偏移地址。

特别说明:

     1. 第一个节区(即节区索引为0)一般都是SHN_UNDEF,其各项值都固定为0, 通过readelf -a a.out能看出来

     2. 以“.”开头的节区名称是系统保留的。应用程序可以使用没有前缀的节区名称,以避免与系统节区冲突

     3. 保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写 + 节区名称

     4. 处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 街区是由 FOO 体系结构定义的 psect 节区

 

elf实例总结

明白了elf核心数据结构,我们就能够对实际的elf文件内容进行分析了,通过16进制打开elf文件(示例程序)。

在elf文件中,从0x154之后,存放的是31个Section的内容, 每个Section的内容是不一样的, 其中我们程序的执行入口.text就位于其中一个Section中。需要注意的是, 由于每个Section采用的字节对齐方式不同(有的是单字节对齐,有的是4字节对齐,还有的是0x1000字节对齐),会导致elf文件中存在空白区域,会自动补零。

而Section Header table则位于文件末尾, 它的起始偏移地址则记录在ELF Header中。

 

细节说明

1. elf可执行文件的执行入口地址是怎么确定的?

    在本例中,执行入口地址是0x8048310(  0x8048000 + 0x310),  0x8048000是装载地址,是通过编译时链接脚本指定的,关于这方面的知识可以查看另一篇文章; 0x310是代码段(.text)在elf文件中的偏移地址, 代码段也就是程序执行的起始。

    .text位于第14个Section,所以.text的偏移地址

                 = sizeof(ELF_Header) + sizeof( Program_Header_table)+ sizeof(0~13 Section)

                 = 52 + 9*32 + 下图中红色方框中各个Section大小的和

                 = 0x310  ( 细心的网友会发现,以上数值加起来要小于0x310,是因为没有考虑字节对齐)

 

2. 字符串表

  elf中会存在一个或多个字符串表, 字符串表就是内容为一个个字符串的表,可以理解为字符串数组,在本例中,类型为STRTAB的Section都是字符串表。

.dynstr: 符号名称表, 为符号表提供服务

.shstrtab:节区名称表,存放所有Section的名字。

 

3. ELF Header中e_shstrndx的理解

   e_shstrndx代表了'Section名称字符串表'位于第几个节区,本例中是位于第28个节区,它的偏移地址是0x16fc,也就是从0x16fc开始,之后的0x10a字节,存放的是'Section名称字符串表'。

可以看出,这个'Section名称字符串表'中存放的是诸如:  .symtab, .strtab,.bss等之类的字符串, 这个就和Section_Header_Table->sh_name对应起来了, sh_name是一个整形, 代表着'Section名称字符串表'中第几个字符串。

4. 符号表

符号表有自己的数据结构:

typedef struct {
 
    Elf32_Word st_name;//符号名称,给出的是一个在符号名称表(.dynstr)中的索引
 
    Elf32_Addr st_value;//一般都是函数地址,或者是一个常量值
 
    Elf32_Word st_size;//从 st_value 地址开始,共占的长度大小
 
    unsigned char st_info;//用于标示此符号的属性,占一个字节(2个字),两个标示位,第一个标示位(低四位)标志作用域,第二个标示位(高四位)标示符号类型
 
    unsigned char st_other;
 
    Elf32_Half st_shndx;
 
} Elf32_sym;

取值

代表

含义

0

STB_LOCAL

局部符号在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响。

1

STB_GLOBAL

全局符号对所有将组合的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。

2

STB_WEAK

弱符号与全局符号类似,不过他们的定义优先级比较低。

 

取值

代表

含义

1

STT_OBJECT

符号与某个数据对象相关,比如一个变量、数组等等

2

STT_FUNC

符号与某个函数或者其他可执行代码相关

3

STT_SECTION

符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。

4

STT_FILE

传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号(如果有的话)

eg: 0x12,那么他就对应着 STB_GLOBAL 和 STT_OBJECT,代表一个全局变量。

 

5. elf常见section的含义

数据段包括.bss,.data,.rodata

  • .data 存放已初始化的全局变量、常量。
  • .bss 存放未初始化的全局变量、局部静态变量,所以此段数据均为0,仅作占位。
  • .rodata 是只读数据段,此段的数据不可修改,存放常量。

 

.interp:  和动态链接相关, 静态链接的程序是没有这个段的, 该段用来设置动态链接器路径,如下图所示:

 

6. 重定位表

通常,在编译过程中产生的目标文件(*.o)中, 会有一个叫做“.rel.text”的段, 它的类型(sh_type) 为“SHT_REL”, 也就是说它 

是一个重定位表(Relocation Table) 。链接器在处理目标文件时, 须要对目标文件中某些部位进行重定位, 即代码段和数据段

中那些对绝对地址的引用的位置。 这些重定位的信息都记录在ELF文件的重定位表里面, 对于每个须重定位的代码段或数据段,

都会有一个相应的重定位表。 其中“.rel.text”就是针对“.text”段的重定位表, 因为“.text”段中至少有一个绝对地址的引用, 比如

对“printf”函数的调用; 而“.data”段则没有对绝对地址的引用, 它只包含了几个常量, 所以没有针对“.data”段的重定位表

“.rel.data”。一个重定位表同时也是ELF的一个段, 那么这个段的类型(sh_type) 就是“SHT_REL”类型的, 它的“sh_link”表

示符号表的下标, 它的“sh_info”表示它作用于哪个段。 比如“.rel.text”作用于“.text”段, 而“.text”段的下标为“1”, 那么“.rel.text”的

“sh_info”为“1”。

重定位表在链接过程中起着非常重要的作用, 可参考另一篇文章:链接

参考

 感谢博友们的无私奉献

 ELF格式解析

 ELF文件格式


 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值