cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
阅读:2685回复:0

[原创]ELF格式学习笔记

楼主#
更多 发布于:2009-04-05 16:35
Author: liucy

1.    ELF概念
1.1    ELF(Executable and Linking Format) 即可执行连接文件格式,是Linux,SVR4和Solaris2.0默认的目标文件格式,目前标准接口委员会TIS已将ELF标准化为一种可移植的目标文件格式。
1.2    ELF有三种文件类型格式。
    一个可重定位(relocatable)文件。该文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
    一个可执行(executable)文件。该文件保存着一个用来执行的程序,并指出了exec(BA_OS)如何来创建程序进程映象。
    一个共享object文件。该文件保存着代码和合适的数据,用来被下面的两个链接器链接:第一个是连接编辑器,可以和其他的可重定位和共享object文件来创建其他的objec;第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。

1.3    ELF目标格式不同视角关系。
Linking 视角    Execution 视角
ELF header    ELF header
Program header table (optional)    Program header table
Section 1    Segment 1
Section …    Segment …
Section n    Segment n
Section header table    Section header table (optional)

2.    ELF各部分介绍
2.1 ELF header
Elf头在程序的开始部位,作为引路表描述整个ELF的文件结构,其信息大致分为四部分:一是系统相关信息,二是目标文件类型,三是加载相关信息,四是链接相关信息。其中系统相关信息包括elf文件魔数(标识elf文件),平台位数,数据编码方式,elf头部版本,硬件平台e_machine,目标文件版本e_version,处理器特定标志e_ftags(这些信息的引入极大增强了elf文件的可移植性,使交叉编译成为可能);目标文件类型用e_type的值表示,可重定位文件为1,可执行文件为2,共享文件为3;加载相关信息有:程序进入点e_entry.程序头表偏移量e_phoff,elf头部长度e_ehsize,程序头表中一个条目的长度e_phentsize,程序头表条目数目e_phnum;链接相关信息有:节头表偏移量e_shoff,节头表中一个条目的长度e_shentsize,节头表条目个数e_shnum ,节头表字符索引e_shstmdx。
#define EI_NIDENT       16

 typedef struct {
      unsigned char       e_ident[EI_NIDENT];
      Elf32_Half          e_type;
      Elf32_Half          e_machine;
      Elf32_Word          e_version;
      Elf32_Addr          e_entry;
      Elf32_Off           e_phoff;
      Elf32_Off           e_shoff;
      Elf32_Word          e_flags;
      Elf32_Half          e_ehsize;
      Elf32_Half          e_phentsize;
      Elf32_Half          e_phnum;
      Elf32_Half          e_shentsize;
      Elf32_Half          e_shnum;
      Elf32_Half          e_shstrndx;
  } Elf32_Ehdr;

其中,数据类型大小如下:
Name    Size    Alignment    Purpose
Elf32_Addr    4    4    Unsigned program address
Elf32_Half    2    2    Unsigned medium integer
Elf32_Off    4    4    Unsigned file offset
Elf32_Sword    4    4    Signed large integer
Elf32_Word    4    4    Unsigned large integer
unsigned char    1    1    Unsigned small integer

程序为例:
#include <stdio.h>
 
int main(void)
{
        printf("hello world\n");
        return 0;
}
gcc  -o hello hello.c

hello文件内容:
 见图一

图片:pic1.JPG


使用readelf来读获取的信息:
ELF Header:
  Magic:   7f 45 4c 46 01 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:               0x80482f0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          2124 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         30
  Section header string table index: 27
下面详细列出内容:
(a). 前16个字节是属于unsigned char e_ident[EI_NIDENT],标识ELF的一些信息:
00000000-00000003 :7F 45 4C 46
    ELF文件以一个magic number开头(7F),接下来是’E’,’L’,’F’这三个字符。
00000004:01
    这位是标明文件的类型,如果是0为非法,1为32位目标,2为64位目标,当初属于预留的,呵呵。在这里为32位目标文件。
00000005:01
    这位是处理器的编码方式,01表示高位在前,02表示低位在前,0是非法数据编码。
00000006:01
    这位是头部版本号。
00000007-0000000f:00 00 00 00 00 00 00 00 00
    标记 e_ident 中未使用字节的开始。初始化为 0。
(b) e_type,用来指明文件类型
00000010-00000011:02 00
    2代表可执行文件,1是可重定位的目标文件(gcc –c得到的是这样的文件),而3表示共享目标文件(gcc –shared得到的文件如此),同样,0是指未知的目标文件。其它的值含义如下:
        ET_CORE         4  Core file
        ET_LOPROC  0xff00  Processor-specific
        ET_HIPROC  0xffff  Processor-specific
(c) e_machine,用以指定系统的体系结构
00000012-00000013:03 00
    这里的3是指Intel 80386结构,据某篇文档说,ia32的结构上这位是必须指定为EM_386的,即值为3。其它取值含义如下:0是未指定,1是EM_M32,即AT&TWE 32100,2是EM_SPARC,即SPARC结构,4和5分别指EM_68K(Motorola 68000)和EM_88K(Motorola 88000),7指EM_860(Intel 80860),8指EM_MIPS(MPIS RS3000)。
(d) e_version,指明目标文件版本
00000014-00000017:01 00 00 00
取1为当前版本EV_CURRENT,取0为非法版本EV_NONE
(e) e_entry,程序入口的虚拟地址。如果目标文件没有程序入口,可以为 0
00000018-0000001b:F0 82 04 08
(f) e_phoff和e_shoff,指程序头部表格和节区表格的偏移量,以字节计算。如果没有,则可以为0。
0000001c-00000023:34 00 00 00 4C 08 00 00
(g) e_flags,是与文件和处理器相关的标志。
00000024-00000027:00 00 00 00
(h)e_ehsize了,说明了ELF头部的大小(字节)
00000028-00000029:34 00
(i) e_phentsize,e_phnum,和e_shentsize,e_shnum,表明程序头部表格的表项大小和项目数以及节区头部表格的表项大小和数目,每个2字节长。
0000002a-00000031:20 00 08 00 28 00 1E 00
(j) e_shstrndx是节区头部表格中与名称字串相关的表项索引。如果没有节区名称字符串表,此参数可为SHN_UNDEF。
00000032-00000033:1B 00
2.2 program header table
程序头表告诉系统如何建立一个进程映像.它是从加载执行的角度来看待elf文件.从它的角度看.elf文件被分成许多段,elf文件中的代码、链接信息和注释都以段的形式存放。每个段都在程序头表中有一个表项描述,包含以下属性:段的类型,段的驻留位置相对于文件开始处的偏移,段在内存中的首字节地址,段的物理地址,段在文件映像中的字节数.段在内存映像中的字节数,段在内存和文件中的对齐标记。
typedef struct elf32_phdr{
Elf32_Word p_type; //段的类型
Elf32_Off p_offset; //段的位置相对于文件开始处的偏移
Elf32_Addr p_vaddr; //段在内存中的首字节地址
Elf32_Addr p_paddr;//段的物理地址
Elf32_Word p_filesz;//段在文件映像中的字节数
Elf32_Word p_memsz;//段在内存映像中的字节数
Elf32_Word p_flags;//段的标记
Elf32_Word p_align;,/段在内存中的对齐标记
)Elf32_Phdr;
hello 的例子:
 见图2

图片:pic2.JPG


使用readelf来读获取的HELLO信息:
Elf file type is EXEC (Executable file)
Entry point 0x80482f0
There are 8 program headers, starting at offset 52
 
Program Headers:
  Type      Offset   VirtAddr   PhysAddr   FileSiz MemSiz Flg Align
  PHDR   0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP  0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD   0x000000 0x08048000 0x08048000 0x00540 0x00540 R E 0x1000
  LOAD   0x000540 0x08049540 0x08049540 0x000fc 0x00104 RW  0x1000
  DYNAMIC   0x000554 0x08049554 0x08049554 0x000c8 0x000c8 RW  0x4
  NOTE       0x000148 0x08048148 0x08048148 0x00044 0x00044 R   0x4
 GNU_EH_FRAME 0x0004cc 0x080484cc 0x080484cc 0x0001c 0x0001c R   0x4
 GNU_STACK   0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
 
 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 .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07    
2.3 section header table
节头表描述程序节,为编译器和链接器服务。它把elf文件分成了许多节.每个节保存着用于不同目的的数据.这些数据可能被前面的程序头重复使用,完成一次任务所需的信息往往被分散到不同的节里。由于节中数据的用途不同,节被分成不同的类型,每种类型的节都有自己组织数据的方式。每一个节在节头表中都有一个表项描述该节的属性,节的属性包括小节名在字符表中的索引,类型,属性,运行时的虚拟地址,文件偏移,以字节为单位的大小,小节的对齐等信息。
typedef struct{
Elf32_Word sh_name;//小节名在字符表中的索引
E1t32_Word sh_type;//小节的类型
Elf32_Word sh_flags;//小节属性
Elf32_Addr sh_addr; //小节在运行时的虚拟地址
Elf32_Off sh_offset;//小节的文件偏移
Elf32_Word sh_size;//小节的大小.以字节为单位
Elf32_Word sh_link;//链接的另外一小节的索引
Elf32 Word sh_info;//附加的小节信息
Elf32 Word sh_addralign;//小节的对齐
Elf32 Word sh_entsize; //一些sections保存着一张固定大小入口的表。就像符号表
}Elf32_Shdr;

以hello 为例:
见图二
There are 30 section headers, starting at offset 0x84c:
 
Section Headers:
  [Nr] Name     Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]          NULL         00000000 000000 000000 00      0   0  0
  [ 1] .interp    PROGBITS     08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag    NOTE    08048148 000148 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE     08048168 000168 000024 00   A  0   0  4
  [ 4] .gnu.hash   GNU_HASH   0804818c 00018c 000020 04   A  5   0  4
  [ 5] .dynsym    DYNSYM     080481ac 0001ac 000050 10   A  6   1  4
  [ 6] .dynstr     STRTAB      080481fc 0001fc 00004a 00   A  0   0  1
  [ 7] .gnu.version VERSYM     08048246 000246 00000a 02   A  5   0  2
  [ 8] .gnu.version_r  VERNEED  08048250 000250 000020 00   A  6   1  4
  [ 9] .rel.dyn     REL          08048270 000270 000008 08   A  5   0  4
  [10] .rel.plt      REL         08048278 000278 000018 08   A  5  12  4
  [11] .init        PROGBITS   08048290 000290 000017 00  AX  0   0  4
  [12] .plt       PROGBITS     080482a8 0002a8 000040 04  AX  0   0  4
  [13] .text      PROGBITS     080482f0 0002f0 0001a8 00  AX  0   0 16
  [14] .fini      PROGBITS     08048498 000498 00001c 00  AX  0   0  4
  [15] .rodata     PROGBITS    080484b4 0004b4 000018 00   A  0   0  4
  [16] .eh_frame_hdr  PROGBITS  080484cc 0004cc 00001c 00   A  0   0  4
  [17] .eh_frame   PROGBITS   080484e8 0004e8 000058 00   A  0   0  4
  [18] .ctors      PROGBITS    08049540 000540 000008 00  WA  0   0  4
  [19] .dtors     PROGBITS    08049548 000548 000008 00  WA  0   0  4
  [20] .jcr      PROGBITS    08049550 000550 000004 00  WA  0   0  4
  [21] .dynamic  DYNAMIC   08049554 000554 0000c8 08  WA  6   0  4
  [22] .got      PROGBITS   0804961c 00061c 000004 04  WA  0   0  4
  [23] .got.plt    PROGBITS   08049620 000620 000018 04  WA  0   0  4
  [24] .data      PROGBITS   08049638 000638 000004 00  WA  0   0  4
  [25] .bss       NOBITS     0804963c 00063c 000008 00  WA  0   0  4
  [26] .comment  PROGBITS   00000000 00063c 000114 00      0   0  1
  [27] .shstrtab   STRTAB     00000000 000750 0000fc 00      0   0  1
  [28] .symtab   SYMTAB     00000000 000cfc 000420 10     29  46  4
  [29] .strtab    STRTAB      00000000 00111c 00020b 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

这里可以参照前面一节中Section to Segment mapping:内容,可以看出节与段的映射关系。
3.    ELF的一些特点(下面是摘录内容)
3.1 平台相关性
    平台相关信息可在编译由编译器决定,定义再ELF.h头文件中。
   linux系统加载ELF可执行文件时,首先做一些简单一致性检查.其代码如下
if(memcmp(elf_ex.e_ident,ELFMAG,SELFMAG)!=0)
goto out; //检查文件头开始四个字符是否为ELF魔数
if(elf_ex.e_type!=ET_EXEC&&elf_ex.e_type!=ET_DYN)
goto out;//检查文件类型是否为可执行文件或共享目标文件
if(!elf_check_arch(&elf_ex))
goto out;//检查硬件平台是否一致
    其中的elf_check_arch(x)在不同的硬件平台上有不同的定义,其由系统的硬件平台决定。这样,在硬件平台相同的系统上,ELF可以不作修改的执行。因此,它可以支持不同平台上的交叉编译(cross_compilation)和交叉链接(cross_linking)。
3.2 PIC
ELF可以生成位置无关的代码(position-independent code,PIC)。用户对gcc使用-fPIC指示GNU编译系统生成PIC代码,可以加载到内存地址空间的任何地址执行,它是实现共享库或共享可执行代码的基础.
PIC代码是通过GOT间接的引用每个全局变量来实现的。其原理就是使用的转移指令都是相对于当前程序计数器(IP)的偏移量;代码中引用变量、函数的地址都是相对于某个基地址的偏移量,总之,从不引用一个绝对地址。为了这些相对地址能够再加载后正确的位置,编译器在数据段开始的地方创建了一个全局偏移量表(global offset table.GOT)。GOT表中每一项都是本运行模块要引用的一个全局变量或函数
的地址,并且编译器还为GOT每个表目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个表目,使得它包含正确的绝对地址。在x86体系结构上,本运行模块的GOT表首地址始终保存在%ebx寄存器中。
PLT(Procedure Linkage Table)表每一项都是一小段代码,对应于本运行模块要引用
的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下:

.PLTfun: jmp *fun@GOT(%ebx)
pushl $offset
jmp .PLT0@PC

其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当
于nop空指令。

如一个elf可执行文件需要调用共享库中的函数,那么它就有自己的GOT和PLT(procedure linkage table,过程链接表),这两个节之间的交互实现了延迟绑定(lazy binging),这种方法将过程地址的绑定推迟到第一次调用该函数。为了实现延迟绑定,GOT的头三条表目是特殊的:GOT[0]包含.dynamic段的地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;GOT[1]包含动态链接器的标识;GOT[2]包含动态链接器的延迟绑定代码的入口点。GOT的其他表目为本模块要引用的一个全局变量或函数的地址。PLT是一个以16字节(32位平台中)表目的数组形式出现的代码序列。其中PLT[0]是一个特殊的表目,它跳转到动态链接器中执行;每个定义在共享库中并被本模块调用的函数在PLT中都有一个表目,从PLT[1]开始.模块对函数的调用会转到相应PLT表目中执行,这些表目由三条指令构成:一条指令是跳转到相应的GOT存储的地址值中,二条指令把函数相应的ID压入栈中,第三条指令跳转到PLT[O]中调用动态链接器解析函数地址,并把函数真正地址存入相应的GOT表目中。被调用函数GOT相应表目中存储的最初地址为相应PLT表目中第二条指令的地址值,函数第一次被调用后GOT表目中的值就为函数的真正地址。因此,第一次调用函数时开销比较大.但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。

用户程序中对fun的直接调用经编译连接后生成一条call fun@PLT指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到.PLT0。该PLT项保留给编译器生成的额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入fun@GOT表项。图示如下:

user program
--------------
call fun@PLT
|
v
DLL PLT table loader
-------------- -------------- -----------------------
fun: <-- jmp*fun@GOT --> change GOT entry from
| $loader to $fun,
v then jump to there
GOT table
--------------
fun@GOT:$loader
第一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT 表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才 要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转
的代码进行修补,所以整个代码段都能在进程间共享。

本来有两幅图片,可不会传。就当附件来传了。

最新喜欢:

qian357549004qian35...
走走看看开源好 Solaris vs Linux
游客

返回顶部