ELF文件格式

0x01 ELF文件简介

常用的一般为下面三种:

  • 可重定位文件(.o):用于与其他目标文件连接生成可执行文件或共享目标文件(.so)的数据与代码;
  • 可执行文件(.exe):包含一个可执行程序,并且此文件规定exec()进程如何创建一个程序的进程映像;
  • 共享目标文件(.so):包含可在两种上下文中链接的代码与数据。首先链接编辑器可以将它与可重定位文件和其他共享目标文件一起处理,生成一个新的目标文件。另外,动态链接器可以将它与可执行文件和其他共享目标文件一起处理,创建程序镜像;

目标文件都是程序的二进制表示,目的是在处理器上直接执行;

这里再简单说一下编译原理的一些东西

一般来说由.c.cpp,直接生成的文件都应该是.o文件,如果在linux平台下的话,通过gcc编译来生成共享目标文件(动态链接库),和可执行文件,在linux平台下可执行文件是不带后缀的。

编译过程如下图:
image

关于编译过程推荐一下下面这一篇文章:

C语言的编译链接过程详解

另外关于链接这一块的东西,这一篇文章要更详细一点,特别是关于静态链接中的重定位与符号表介绍比较详细;动态链接中的PIC(地址无关代码)技术,GOT,PLT等知识有简单的介绍,对于动态链接的理解帮助很大;另外对静态链接可执行文件的生成与动态链接可执行文件的生成也介绍得很详细,对理解编译过程的链接阶段理解有很大帮助;

静态链接与动态链接在链接过程和文件结构上的区别

0x02 ELF文件格式

首先我们给一个ELF文件的格式图,对后面讲述的东西有个大致的模型,下面的每个部分以及段与节的联系区别我们都会在下面给出:

image

一、ELF文件头

使用readelf -h <filename>命令可以查看ELF文件头,另外可以用readelf -h查看其它命令。readelf对于分析ELF文件格式来说是很有用的一个命令。
下面给出ELF文件头的结构:

typedef struct elfhdr {
    unsigned char    e_ident[EI_NIDENT]; /* ELF Identification */
    Elf32_Half    e_type;        /* object file type */
    Elf32_Half    e_machine;    /* machine */
    Elf32_Word    e_version;    /* object file version */
    Elf32_Addr    e_entry;    /* virtual entry point */
    Elf32_Off    e_phoff;    /* program header table offset */
    Elf32_Off    e_shoff;    /* section header table offset */
    Elf32_Word    e_flags;    /* processor-specific flags */
    Elf32_Half    e_ehsize;    /* ELF header size */
    Elf32_Half    e_phentsize;    /* program header entry size */
    Elf32_Half    e_phnum;    /* number of program header entries */
    Elf32_Half    e_shentsize;    /* section header entry size */
    Elf32_Half    e_shnum;    /* number of section header entries */
    Elf32_Half    e_shstrndx;    /* section header table's "section 
                       header string table" entry offset */
} Elf32_Ehdr;

结构体中每个变量的值注释都给得比较清楚,值得说一下的就是最后一个变量e_shstrndx,它指的是存放每一个Section名字的SectionSection headerSection header table中的偏移量,如果对于Section不是很熟悉的朋友可以先不必在这纠结,我们后面讲到Section的时候,还会再说。另外可以通过查看ELF手册详细了解。

二、ELF程序头

ELF程序头是对二进制文件中段(Segment)的描述,是程序装载必须的部分。Segment是内核装载程序时被解析的,程序头描述了对应的段在内存中的布局以及如何映射到内存中。可以通过引用ELF头中的e_phoff(程序头表偏移量)来得到程序头表。程序头表由多个程序头条目组成,每一个程序头条目描述了其对应类型的段,存储了段相关的信息。下面给出32位ELF文件的程序头的结构体,在程序头表中就是存放了多个程序头结构体:

typedef struct
{
    Elf32_Word     p_type;      /* Segment type */
    Elf32_Off      p_offset;      /* Segment offset in file */
    Elf32_Addr     p_vaddr;      /* Segment virtual address in    memory*/                       
    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;

结构体中每个变量的值注释都说得比较清楚,我们就不解释了。接下来我们说一下4种常见类型的程序头。

  • PT_LOAD:一个可执行文件至少包含一个PT_LOAD段。这种类型的段将会被装载或映射到内存中。一个需要动态链接的可执行文件一般包括存放代码的test段与存放全局变量和动态链接信息的data段。
  • PT_DYNAMIC:这种类型的段是动态链接可执行文件特有的,包含了动态链接器所需要的一些信息。主要包括程序运行时所需要链接的共享库列表,全局偏移表(GOT)的地址和重定位条目的相关信息。这在后面讨论动态链接时会再说。
  • PT_INTERP:这个段存一般存放了动态链接器的位置,也就是程序解释器的位置。如:/lib/linux-ld.so.2
  • PT_PHDR:这个段保存了程序头表本身的位置和大小。程序头表保存了所有程序头对文件中段的描述。

可以通过查看ELF手册来详细了解段的相关信息。

我们可以通过readelf -l <filename>命令查看文件的程序头表,如下所示:

image

其中可以看见我们上面所介绍的几种常用类型的程序头,其中类型为LOAD,偏移量为0x0的条目即为text段的程序头,另一个类型为LOAD的条目即为data段的程序头。

三、ELF节头

这里我们先说一下节(Section)与段(Segment)的联系与区别,很多人将其混为一谈。段是每个程序执行的必要组成部分,在每个段中的代码或者数据,会划分为不同的节。也就是节相对于段来说,是对可执行文件在内存中更为详细的划分。后面我们介绍节的类型的时候,可以充分体现这一点。

每个节也和段一样,有自己的头,我们叫做节头(Section Header Table),许多节头构成了节头表。如果可执行文件存在节头表,可以通过引用ELF文件头中的e_shoff来得到其在文件中的偏移量。我们为什么要说如果存在呢,是因为节头表对于程序的执行不是必须的,没有节头表程序仍可以正常运行,节头的作用是描述相应的节的位置和大小,主要用于链接和调试。所以我们从内存中dump出来的.so文件,是可能没有节头的。

虽然二进制文件可能会缺少节头,但并不意味着节不存在。只是无法通过节头来引用节,对于调试器和反编译程序来说,少了许多参考信息。默认情况下,可执行程序是自带节头的,只是某些时候,为了程序不被黑客反编译,利用IDA,objdump等工具得到相关信息,厂商会自己将节头从中删除,或者填充垃圾信息。

下面我们给出一个32位ELF节头的结构:

typedef struct {
    Elf32_Word    sh_name;    /* name - index into section heade string table section */
    Elf32_Word    sh_type;    /* type */
    Elf32_Word    sh_flags;    /* flags */
    Elf32_Addr    sh_addr;    /* address */
    Elf32_Off     sh_offset;    /* file offset */
    Elf32_Word    sh_size;    /* section size */
    Elf32_Word    sh_link;    /* section header table index link */
    Elf32_Word    sh_info;    /* extra information */
    Elf32_Word    sh_addralign;    /* address alignment */
    Elf32_Word    sh_entsize;    /* section entry size */
} Elf32_Shdr;

我们可以利用readelf -l <filename>命令来得到每个段与节的关系,接上图,如下:

image

其中02对应那一行即为text段包含的不同类型的节,下面我们给出几种常见类型的节:

  • .text: .text节保存了程序代码指令,存在于text段,对应的类型为SHT_PROGBITS
  • .rodata: .rodata节保存了只读数据,因为它是只读的,所以只能放在只读段,存在于text段。比如c语言中printf("hello world\n")这条指令就是放在rodata节,对应的类型为SHT_PROGBITS
  • .plt: .plt节包含了动态链接器调入从共享库导入的函数所必须的相关代码。存在于text段。对应的类型为SHT_PROGBITS
  • .data: .data节保存了初始化的全局变量等数据。存在于data段,对应的类型为SHT_PROGBITS
  • .bss: .bss节保存了未初始化的全局变量等数据。存在于data段,对应的类型为SHT_NOBITS
  • .got.plt: .got节保存了全局偏移表。.got节与.plt节一起提供了对从共享库中导入的函数的访问入口,由动态链接器在运行时修改.got节。存在于data,对应的类型为SHT_PROGBITS
  • .dynsym: .dynsym节保存了从共享库导入的动态符号信息。存在于text段,对应的类型为SHT_DYNSYM
  • .dynstr: .dynstr节保存了动态符号字符串表。存在于text段
  • .rel*: 其保存了重定位的相关信息,这些信息描述了如何在链接或者运行时,对ELF目标文件的某一部分内容或者镜像进行补充或者修改。存在于text段,对应的类型为SHT_REL
  • .symbol: 其保存了二进制文件的所有符号,也就是我们所说的符号表。存在于text段,对应的类型为SHT_SYMTAB
  • .strtab: 其保存了二进制文件所以的字符串,也就是我们所说的字符串表。表中的内容会被符号表所引用。存在于text段,对应的类型为SHT_STRTAB
  • .shstrtab: 这个节就是保存了所有节头的名字字符串,在Elf文件头中的e_shstrndx这个变量就是指的这个节的节头在节头表中的偏移量,我们可以通过获取这个节的位置来得到每个节的名字字符串。存在于text段,对应的类型为SHT_STRTAB

一些主要类型的节我们就介绍,如果想了解更多,可以查看ELF手册

四、ELF符号

符号是对某些代码或者数据的符号引用,多个符号组成了一个二进制文件的符号表。比如,我们的代码中存在printf("hello world\n")这条语句,那么在符号就保存了它的名字字符串在字符表中的偏移,它的地址或者偏移量,大小之类的信息。

对于大多数共享库和动态链接可执行文件来说都存在两个符号表,.dynsym和.symtab。它们的区别在于.dynsym保存的是引用外部文件符号的全局/动态符号,如printf这样的库函数。而.symtab则是保存了程序所有的符号,除了.dynsym中的符号除外,还有本地函数之类的符号,即.dynsym是.symtab的子集。但为什么有.symtab符号表还要有.dynsym呢?是因为.dynsym在程序运行时会被装载进内存中,是程序运行所必须的,而.symtab不会。,dynsym中的符号只有在程序运行时被解析,是运行时动态链接器所需要唯一的符号。.symtab符号表只是用来链接和调试的,有时候为了节省空间,会将.symtab符号表从二进制文件中删除。

下面给出32位ELF文件符号项的结构,其构成了符号表:

typedef struct Elf32_Sym
{
  Elf32_Word    st_name;   /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;  /* Symbol value (addr or offset)*/
  Elf32_Word    st_size;   /* Symbol size */
  unsigned char st_info;   /* Symbol type and binding */
  unsigned char st_other;  /* Symbol visibility */
  Elf32_Section st_shndx;  /* Section index */
}Elf32_Sym;

ELF的文件格式到这里我们就说差不多了,讲得比较少,但本身ELF文件不是一两篇文章就可以说完的。需要花更多时间去理解,可以多多看一下ELF手册,这个讲得非常的细。最后给出一个ELF文件的解析器的代码,自己也可以实现以下,对理解很有帮助:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdint.h>
#include <elf.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc, char const *argv[])
{
    int fd,i;
    uint8_t *mem;
    struct stat st;
    char *stringSectionName,*interp;

    Elf32_Ehdr *ehdr;
    Elf32_Shdr *shdr;
    Elf32_Phdr *phdr;


    /* code */
    if (argc < 2)
    {
        /* code */
        printf("Usage: %s <excutable file>\n", argv[0]);
        exit(0);
    }

    if ((fd = open(argv[1],O_RDONLY)) < 0)
    {
        /* code */
        perror("open error");
        exit(-1);
    }

    if (fstat(fd,&st) < 0)
    {
        /* code */
        perror("fstat error");
        exit(-1);
    }

    mem = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,fd,0);
    if (mem == MAP_FAILED)
    {
        /* code */
        perror("mmap error");
        exit(-1);
    }

    ehdr = (Elf32_Ehdr *) mem;
    phdr = (Elf32_Phdr *) &mem[ehdr->e_phoff];
    shdr = (Elf32_Shdr *) &mem[ehdr->e_shoff];

    if (mem[0] != 0x7f && strcmp((char *)&mem[1], "ELF"))
    {
        /* code */
        fprintf(stderr, "%s is not ELF file\n", argv[1]);
        exit(-1);
    }

    if (ehdr->e_type != ET_EXEC)
    {
        /* code */
        fprintf(stderr, "%s is not excutable file\n", argv[1]);
        exit(-1);
    }

    printf("Entry Addr : 0x%x\n",ehdr->e_entry);
    stringSectionName = (char *)&mem[shdr[ehdr->e_shstrndx].sh_offset];
    printf("Section Header list :\n");
    printf("  SectionName\t\tVirAddr\n\n");
    for (i = 1; i < ehdr->e_shnum; ++i)
    {
        /* code */
        printf("  %s\t\t0x%x\t\t%d\n", &stringSectionName[shdr[i].sh_name],shdr[i].sh_addr,shdr[i].sh_type);
    }

    printf("Segment Header list :\n");
    printf("  SegmentName\t\tVirAddr\n\n");

    for (int i = 0; i < ehdr->e_phnum; ++i)
    {
        /* code */
        switch(phdr[i].p_type){
            case PT_LOAD:
                if (phdr[i].p_offset == 0)
                {
                    /* code */
                    printf("  Text Segment\t\t0x%x\n", phdr[i].p_vaddr);
                }else{
                    printf("  Data Segment\t\t0x%x\n", phdr[i].p_vaddr);
                }
                break;
            case PT_NOTE:
                printf("  NOTE Segment\t\t0x%x\n",phdr[i].p_vaddr );
                break;
            case PT_INTERP:
                interp = strdup(&mem[phdr[i].p_offset]);
                printf("  (Interpreter Path = %s)\n", interp);
                break;
            case PT_DYNAMIC:
                printf("  Dynamic Segment\t\t0x%x\n", phdr[i].p_vaddr);
                break;
            case PT_PHDR:
                printf("  Phdr Segment\t\t0x%x\n",phdr[i].p_vaddr);
                break;
            default:
                printf("  %s\t\t0x%x\n", "Other Segment",phdr[i].p_vaddr);

        }
    }

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ELF (Executable and Linkable Format)是一种用于存储可执行文件、目标文件和共享库的文件格式。它是Linux系统上常用的二进制文件格式之一,也是其他类UNIX系统所采用的标准格式。 ELF文件格式由三个主要部分组成:头部、段表和节表。 头部包含了一些重要的信息,如文件类型、系统架构、入口地址等。它还包含了段表和节表的偏移地址和大小等信息,以便系统可以正确解析文件。 段表记录了程序的运行时需要的各种段的信息。一个段可以是代码段、数据段、动态链接信息段等。每个段都有自己的虚拟地址和大小等属性。段表中的每个条目描述了一个段的属性和位置,用于程序的加载和运行。 节表类似于段表,不过节表记录了程序的编译时需要的各种节的信息。一个节可以是代码节、数据节、符号表等。每个节都有自己的虚拟地址和大小等属性。与段表类似,节表中的每个条目描述了一个节的属性和位置,用于编译、链接和调试。 ELF文件格式的优点是它的灵活性和可扩展性。由于ELF文件规范定义了头部、段表和节表等结构,因此可以通过添加新的段或节来实现文件的自定义属性和功能。这使得ELF文件格式非常适合于Linux系统的动态链接和加载机制。 总的来说,ELF文件格式是一种Linux系统中常用的二进制文件格式,它定义了文件的结构和属性,包括头部、段表和节表等部分。通过这个格式,可以实现文件的加载、链接和调试等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值