侧边栏壁纸
博主头像
胜星的博客博主等级

行动起来,活在当下

  • 累计撰写 23 篇文章
  • 累计创建 38 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Linux ELF文件格式

胜星
2021-03-06 / 0 评论 / 0 点赞 / 288 阅读 / 55334 字

ELF文件

01 什么是ELF文件?

  • 现在PC平台流行的可执行文件格式(Executable) 主要是Windows下的PE (PortableExecutable)和Linux的ELF ( Executable Linkable Format),它们都是COFF (Common fileformat)格式的变种。

  • COFF(通用对象文件格式)是由UnixSystemVRelease3首先提出并且使用的格式规范,后来微软公司基于COFF格式,制定了PE格式标准,并将其用于当时的WindowsNT系统。SystemV Release 4在COFF的基础上引入了ELF格式,目前流行的Linux系统也以ELF作为基本可执行文件格式。这也就是为什么目前PE和ELF如此相似的主要原因,因为它们都是源于同一种可执行文件格式COFF。

  • 在linux下ELF文件格式包括了

    • 可重定位文件(.obj, xxxx.o)
    • 可执行文件(.exe, /bin/bash)
    • 共享目标文件(.dll, .so)
    • 核心转储文件(core dump)
  • 通过file命令可以查看文件格式

    命令结果
    file fun.ofun.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
    file funlibfun.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d9fb2a4d3debed0a19452dbd13e10f7411ff72cb, not stripped
    file libfun.sofun: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=bd6a4c4f0375be6f0a889b84f48f4baa110ee860, not stripped

02 目标文件

  • 目标文件(.o)中的内容至少有编译后的机器指令代码、数据,还包括了链接时所须要的一-些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”( Segment)。

  • 源码(.c)编译成机器指令后(.o),机器指令经常被放到代码段中(.text .code),全局变量被放到数据段(.data)

  • 程序员的自我修养

程序员的自我修养-19f6e81035a64380aa2c3d2cfe5c61ba

  • 来看看一个目标文件

    // fun.c
    int global_init_var =100;
    int fun(int a, int b)
    {
        return 0;
    }
    int main()
    {
        fun();
        retrun 0;
    }
    // gcc -c fun.c -o fun.o
    
  • 这里可以使用linux的两个工具查看可执行文件的objdump和readelf他们都属于binutils的工具集

    objdump -h fun.o
    

微信截图_20210306151530-72710b2704a04c5d9c7e9d2e6a9b4532

微信截图_20210306151732-6d3945dcc1a8479a928346e698f879e8

  • 查看目标文件中区段信息,“-s”打印所有区段中16进制数据,"-d"显示所有指令的反汇编代码

    objdump -s -d fun.o
    

微信截图_20210306152024-a3c89ea97fdb41c78658a89619329094

  • 常见的区段名

    常用的段名说明
    .rodataRead only Data,这种段里存放的是只读数据,比如字符串常量、全局const
    .comment变量。跟“,rodata” 一样存放的是编译器版本信息,比如字符串: "GCC: (GNU) 4.2.0"
    .debug调试信息
    .dynamic动态链接信息
    .hash符号哈希表
    .line调试时的行号表,即源代码行号与编译后指令的对应表
    .note额外的编译器信息。比如程序的公司名、发布版本号等
    .strtabStringTable.字符串表,用于存储ELF文件中用到的各种字符串
    .symtabSymbol Table.符号表
    .shstrtabSection String Table.段名表
    .plt动态链接的跳转表
    .got全局入口表
    .init程序初始化与终结代码段常用的段名
  • 最后通过ld连接器将目标文件和其他目标文件,so文件,相同属性的区段合并,最后生成一个可执行文件

03 ELF文件结构

  • ELF文件格式结构

微信截图_20210306152117-0723b85d8cde4620bbd573df3994e9d1

  • 链接视图,执行视图

微信截图_20210306152204-1e60cbef9b7e42b5a02a80d8232652a1

04 ELF工具分析

  • readelf命令是binutils工具集中的一个查看elf文件格式的工具。

  • readelf文件常用参数

    常用参数说明
    -a显示文件所有内容
    -h显示ELF文件头
    -S显示ELF文件区段头表(Section Header Table)信息
    -l显示ELF文件程序头表(Program Header Table)信息
    -s显示ELF文件符号表
    -r显示ELF文件重定位表
    -d显示ELF文件动态加载信息
  • 下载readelf源码,辅助我们学习elf文件

    • 1.找到安装路径 which readelf
    • 2.查看源码包dpkg -S /usr/bin/readelf
    • 3.下载源码apt-get source binutils
  • 搜索main函数

    • 在./binutils/readelf.c文件最底下
      • **process_file()**整个解析elf文件的开始
      • process_object()
      • get_file_header()
      • process_file_headers()
      • process_program_headers()
      • process_dynameic_section()
      • process_relocs
      • process_symbol_table()
  • 010edit分析工具

05 ELF头部

  • readelf -h main

    微信截图_20210306152332-146fdaea8eb94c498629454353029d39

  • 微信截图_20210306152519-dc26fcf846fc43ac99197d9312e6dc88

  • ELF文件结构体定义都在/usr/include/elf.h 文件中

    typedef struct
    {
      unsigned char	e_ident[EI_NIDEN] /*16  文件标识以及其他信息*/
      Elf32_Half	e_type;	       /* 文件类型 */
      Elf32_Half	e_machine;	       /* CPU体系 */
      Elf32_Word	e_version;	       /* 文件版本 */
      Elf32_Addr	e_entry;	       /* 入口点地址 */
      Elf32_Off	e_phoff;	       /* 程序头表文件偏移 */
      Elf32_Off	e_shoff;	       /* 区段头表文件偏移*/
      Elf32_Word	e_flags;	       /* 处理器标志 */
      Elf32_Half	e_ehsize;	       /* ELF header 大小 */
      Elf32_Half	e_phentsize;     /* 一项程序头表大小 */
      Elf32_Half	e_phnum;	       /* 程序头表格个数 */
      Elf32_Half	e_shentsize;     /* 一项区段头表大小 */
      Elf32_Half	e_shnum;	       /* 区段头表个数 */
      Elf32_Half	e_shstrndx;      /* 区段头的字符串表 下标 */
    } Elf32_Ehdr;
    
  • ELF文件魔术:e_ident 前4字节

    // 文件:usr/include/elf.h 
    #define EI_MAG0  0  /* File identification byte 0 index */ 
    #define ELFMAG0  0x7f /* Magic number byte 0 */ 
     
    #define EI_MAG1  1  /* File identification byte 1 index */ 
    #define ELFMAG1  'E'  /* Magic number byte 1 */ 
     
    #define EI_MAG2  2  /* File identification byte 2 index */ 
    #define ELFMAG2  'L'  /* Magic number byte 2 */ 
     
    #define EI_MAG3  3  /* File identification byte 3 index */ 
    #define ELFMAG3  'F'  /* Magic number byte 3 *
    
  • 文件类型:e_ident 第4字节

    • 第 4 字节表示文件类别,0 表示无效文件,1 表示 32 位 ELF 文件,2,表示 64 位 ELF 文 件,分别对应一个宏。
    #define EI_CLASS	4		/* File class byte index */
    #define ELFCLASSNONE 0	/* Invalid class */
    #define ELFCLASS32	1		/* 32-bit objects */
    #define ELFCLASS64	2		/* 64-bit objects */
    #define ELFCLASSNUM	3
    
  • 字节序:e_ident 第5字节

    • 第 5 字节表示字节序,0 表示无效格式、1 表示小端字节序、2 表示大端字节序,源码 中可见以下宏
    #define EI_DATA		5		/* Data encoding byte index */
    #define ELFDATANONE	0		/* Invalid data encoding */
    #define ELFDATA2LSB	1		/* 2's complement, little endian */
    #define ELFDATA2MSB	2		/* 2's complement, big endian */
    #define ELFDATANUM	3
    
  • ELF版本:e_ident 第6字节

    • 第 6 字节表示 ELF 版本,默认必须是 1
    #define EI_VERSION	6		/* File version byte index */
    					/* Value must be EV_CURRENT */
    
  • 系统版本:e_ident 第7字节

    • 第 7 字节表示系统版本,默认是 ELFOSABI_NONE ,即 0,其他很多的宏可以见源
    #define EI_OSABI	7		/* OS ABI identification */
    #define ELFOSABI_NONE		0	/* UNIX System V ABI */
    #define ELFOSABI_SYSV		0	/* Alias.  */
    #define ELFOSABI_HPUX		1	/* HP-UX */
    #define ELFOSABI_NETBSD		2	/* NetBSD.  */
    //...
    
  • ABI版本:e_ident 第8字节

    • 第 8 字节表示 ABI 版本,默认为 0,相关的宏
    #define EI_ABIVERSION	8		/* ABI version */
    
  • e_type

    • e_type 表示 ELF 文件类型,0 表示无效文件类型,1 表示可重定位目标文件、2 表 示可执行文件、3 表示共享目标文件、4 表示核心转储文件。汇编器输出的是可重定位文 件,静态链接器输出的是可执行文件或是共享目标文件。 相关的宏
    #define ET_NONE		0		/* No file type */
    #define ET_REL		1		/* Relocatable file */
    #define ET_EXEC		2		/* Executable file */
    #define ET_DYN		3		/* Shared object file */
    #define ET_CORE		4		/* Core file */
    #define	ET_NUM		5		/* Number of defined types */
    #define ET_LOOS		0xfe00		/* OS-specific range start */
    #define ET_HIOS		0xfeff		/* OS-specific range end */
    #define ET_LOPROC	0xff00		/* Processor-specific range start */
    #define ET_HIPROC	0xffff		/* Processor-specific range end */
    
  • e_machine

    • e_machine,表示 ELF 文件所在的机器类型,例如 3 表示 Intel386,40 表示 ARM。 源码中对应的宏
  • e_version:

    • 表示ELF文件的版本,一般取值为1
  • e_entry :

    • 表示程序入口点线性地址,一般用于ELF可执行文件。对应可重定位目标文件,该字段设置为0
  • e_phoff :

    • e_phoff,表示程序头表在ELF文件内的偏移地址,标识了程序头表在文件内的位置。
  • e_shoff:

    • e_shoff,表示段表在ELF文件内的偏移地址,标识了段表在文件内的位置。
  • e_flags

    • 表示ELF文件平台相关的属性,一般默认为0.
  • e_ehsize

    • e_ehsize表示ELF文件头的大小。
  • e_phentsize

    • e_phentsize表示程序头表项的大小。
  • e_phnum

    • e_phnum表示程序头表项的个数。可以使用e_phoff(偏移)、e_phentsize(表项大小)、e_phnum(表项个数)计算出程序头表占据空间。
  • e_shentsize

    • e_shentsize表示段头表项的大小。
  • e_shnum

    • e_shnum表示段头表项的个数。与程序头表类似,段头表相关的字段也有e_shoff(偏移)、e_shentsize(表项大小)、e_shnum(表项数量)。
  • e_shstrndx

  • e_shstrndx表示段表字符串表(.shstrtab)所在段在段表中的索引。

  • 分析代码

    // 解析elf头部
    void parse_elf_head(char *pbuff)
    {
        // 获取elf头部
        Elf32_Ehdr *phdr = get_elf_head(pbuff);
        // elf Migic 魔术
        for (int i = 0; i < 16; i++)
        {
            printf("%2x ", phdr->e_ident[i]);
        }
        // elf ident Class 文件类型
        printf("\n class:");
        if(phdr->e_ident[EI_CLASS] == 1)
        {
            printf("ELF32");
        }else if(phdr->e_ident[EI_CLASS] == 2){
            printf("ELF64");
        }else{
            printf("Unknow");
        }
        // elf type 类型 4 core shared exec reload
        printf("\n type:");
        char * sztype[5]={"unknow","可重定位","可执行","动态共享","核心转储"};
        if(phdr->e_type>4 || phdr->e_type == 0 )
        {
            printf("Unknow");
        }else{
            printf(sztype[phdr->e_type]);
        };
        // 判断机器
        printf("\n machine:");
        if(phdr->e_machine == EM_386)
        {
            printf("intel 80386 ");
        }else if(phdr->e_machine == EM_X86_64)
        {
            printf("amd x86_64 ");
        }
        // 入口点
        printf("\n entry: %08x",phdr->e_entry);
        // 程序头
        printf("\n program: offset=%08x entsize=%08x num=%d",
        phdr->e_phoff,phdr->e_phentsize,phdr->e_phnum);
        // 区段头 
        printf("\n section: offset=%08x entsize=%08x num=%d\n",
        phdr->e_shoff,phdr->e_shentsize,phdr->e_shnum);
        // 字符表位置
        printf("\n string index : %d",phdr->e_shstrndx);
        printf("\n");
    }
    

06 ELF区段头表(section)

  • 区段头表在解析(编译)时候用到,运行程序时区段头表没有意义,系统只会加载程序表

    typedef struct
    {
      Elf32_Word	sh_name;		/* 区段字符串表的索引  sh_name + .strtab */
      Elf32_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;		/* 区段每个元素大小,对于重定位和符号表有用 */
    } Elf32_Shdr;
    
  • 字段说明

    字段说明
    sh_name是一个4字节偏移,记录了段名字符串在段表字符串表(“.shstrtab” 段)内的偏移 ,".shstrta"段由Elf32_ehdr->e_shstrndx指定。
    sh_type表示段的类型。段的类型有很多,常见的有SHT_PROGBITS,表示程序数据,有代码段.text、数据段.data, SHT_SYMTAB表示符号表,有符号表段.symtab,SHT_STRTAB表示字符串表,有段表字符串表段.shstrtab,串表段.strtab。还有专门存放构造函数数组的段SHT_INIT_ARRAY,析构函数数组段SHT_FINI_ARRAY。 具体类型见下列
    sh_flags表示段标志,记录段的属性。其中0表示默认属性。1表示段可写,取值SHF_WRITE。2表示段加载后需要为之分配内存空间,取值SHF_ALLOC。4表示段可以执行,取值SHF_EXECINSTR。段标志属性可以进行复合叠加,比如代码段“.text”属性,具有可分配、可执行属性,数据段“.data”具有可写可分配属性。
    sh_addr表示段加载后的线性地址。在可重定位目标文件内,无法确定段的虚拟地址,故设置默认值为0。
    sh_offset表示段在文件内的偏移,根据此偏移可以确定段的位置,读取段的内容 
    sh_size表示段的大小,单位为字节。需要注意的是,如果段类型为SHT_NOBITS,段内没有数据,那么段大小并非指文件块的大小,而是指段加载后占用内存的大小。比如bss段
    sh_linksh_link记录的是符号表使用的串表所在段(一般是.strtab)对应段表项在段表内的索引
    sh_info此成员给出附加信息,其解释依赖于节区类型。
    sh_addralign表示段对齐方式,对齐规则为 sh_offset%sh_addralign=0,即段的文件偏移必须是sh_addralign的整数倍。sh_addralign取值必须是2的整数幂,如1、2、4、8等。常见对齐值
    sh_entsize一般用于保存诸如符号表段、重定位表段时,表示段内保存表的表项大小。例如符号表段“.symtab”内保存的符号表的表项大小为sizeof(Elf32_Sym)=16字节,重定位表段“.rel.plt”内保存的重定位表的表项大小为sizeof(Elf32_Rel)=8字节。
  • 类型说明sh_type

    类型说明
    SHT_NULL0此值标志节区头部是非活动的,没有对应的节区。此节区头部 中的其他成员取值无意义
    SHT_PROGBITS1此节区包含程序定义的信息,其格式和含义都由程序来解释,通常数据和代码段都是这个属性
    SHT_SYMTAB2节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。
    SHT_STRTAB3此节区包含字符串表。目标文件可能包含多个字符串表节区。
    SHT_RELA4此节区包含重定位表项,其中可能会有补齐内容(addend),例 如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多 个重定位节区
    SHT_HASH5此节区包含符号哈希表。所有参与动态链接的目标都必须包含 一个符号哈希表。目前,一个目标文件只能包含一个哈希表, 不过此限制将来可能会解除
    SHT_DYNAMIC6此节区包含动态链接的信息。目前一个目标文件中只能包含一 个动态节区,将来可能会取消这一限制。
    SHT_NOTE7此节区包含以某种方式来标记文件的信息
    SHT_NOBITS8这种类型的节区不占用文件中的空间,其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员 sh_offset 中还是会包含概念性的文件偏移
    SHT_REL9此节区包含重定位表项,其中没有补齐(addends),例 如 32 位 目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定 位节区。
    SHT_SHLIB10此节区被保留,不过其语义是未规定的。包含此类型节区的程 序与 ABI 不兼容。
    SHT_DYNSYM11作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节 区,其中保存动态链接符号的一个最小集合,以节省空间。
    SHT_LOPROC0X70000000这一段(包括两个边界),是保留给处理器专用语义的。
    SHT_HIPROCOX7FFFFFFF这一段(包括两个边界),是保留给处理器专用语义的。
  • 段类标志sh_flags

    类型说明
    SHF_WRITE0节区包含进程执行过程中将可写的数据。
    SHF_ALLOC2此节区在进程执行过程中占用内存。某些控制节区并不出现于目标 文件的内存映像中,对于那些节区,此位应设置为 0
    SHF_EXECINSTR4节区包含可执行的机器指令。
    SHF_MASKPROC0xF0000000所有包含于此掩码中的四位都用于处理器专用的语义
  • sh_link和sh_info字段

    • 根据节区类型的不同,sh_link 和 sh_info 的具体含义也有所不同:
    sh_typesh_linksh_info
    SHT_DYNAMIC此节区中条目所用到的字符串表格 的节区头部索引0
    SHT_HASH0
    SHT_REL相关符号表的节区头部索引重定位所适用的节区的 节区头部索引
    SHT_RELA相关符号表的节区头部索引重定位所适用的节区的 节区头部索引
    SHT_SYMTAB相关联的字符串表的节区头部索最后一个局部符号(绑 定 STB_LOCAL)的符 号表索引值加1
    SHT_DYNSYM相关联的字符串表的节区头部索最后一个局部符号(绑 定 STB_LOCAL)的符 号表索引值加1
  • 解析代码

    // 解析elf 区段
    void parse_elf_section(char *pbuff){
       Elf32_Shdr *pshdr =  get_elf_section(pbuff);
       Elf32_Ehdr *pehdr = get_elf_head(pbuff);
       char *section_name_tables = get_elf_section_name_table(pbuff);
        printf("name        type  addr  off  size  al  es  flag  link info \n");
        //遍历所有区段
        for(int i=0;i<pehdr->e_shnum;i++){
            // 区段名
            char *section_name = section_name_tables + pshdr[i].sh_name;
            printf("%-20s",section_name);
            printf("  %08x",pshdr[i].sh_type);
            printf("  %08x",pshdr[i].sh_addr);
            printf("  %08x",pshdr[i].sh_offset);
            printf("  %08x",pshdr[i].sh_size);
            printf("  %04d",pshdr[i].sh_addralign);
            printf("  %04d",pshdr[i].sh_entsize);
            printf("  %04d",pshdr[i].sh_flags);
            printf("  %04d",pshdr[i].sh_link);
            printf("  %04d\n",pshdr[i].sh_info);
        }
    }
    

07 ELF符号表

  • 链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起,或者说像玩具积木- -样, 可以拼装形成一个整体。 为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行,就像积木模块必须有凹凸部分才能够拼合。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的用,即对函数和变量的地址的引用。比如目标文件B要用到了目标文件A中的函数“foo”,, 那么我们就称目标文件A定义(Define)了函数“foo”,称目标文件B引用(Reference)了目标文件A中的函数“foo”。这两个概念也同样适用于变量。每个函数或变最都有自己独特的名字,才能避免链接过程中不同变量和的数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name )。 我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。

  • 链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表( Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变最和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

    • 定义在本目标文件的全局符号,可以被其他目标文件引用。

    • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(Extemal Symbol),也就是我们前面所讲的符号引用。

    • 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如SimpleSection.o里面的“.text”.“.data”等。.

    • 局部符号,这类符号只在编译单元内部可见。比如SimpleSection.o里面的static. _var和“static_var2"。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。

    • 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。

  • 全局符号表

    • 区段类型为 SHT_SYMTAB是全局符号表
    • 区段类型为 SHT_DYNSYM动态符号表
    typedef struct
    {
      Elf32_Word	st_name;		/* Symbol name (string tbl index) */
      Elf32_Addr	st_value;		/* Symbol value */
      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;
    
  • 字段说明

    字段说明
    st_name包含目标文件符号字符串表的索引,其中包含符号名的字符串表示。如 果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没 有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。由所在区段link指向符号表
    st_value此成员给出相关联的符号的取值。依赖于具体的上下文,它可能是一个 绝对值、一个地址等等。
    st_size很多符号具有相关的尺寸大小。例如一个数据对象的大小是对象中包含 的字节数。如果符号没有大小或者大小未知,则此成员为 0
    st_info此成员给出符号的类型和绑定属性。下面给出若干取值和含义的绑定关 系。
    st_otherst_other 该成员当前包含 0,其含义没有定义。
    st_shndx每个符号表项都以和其他节区间的关系的方式给出定义(符号所在区段下标)。此成员给出相 关的节区头部表索引。某些索引具有特殊含义。
  • st_info字段

    • st_info 中包含符号类型和绑定信息,操纵方式如:
    #define ELF32_ST_BIND(i) ((i)>>4) 
    #define ELF32_ST_TYPE(i) ((i)&0xf) 
    #define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf)
    
  • 绑定类型 ELF32_ST_BIND(st_info)

    名称取值说明
    STB_LOCAL0局部符号在包含该符号定义的目标文件以外不可见。相同名 称的局部符号可以存在于多个文件中,互不影响。
    STB_GLOBAL1全局符号对所有将组合的目标文件都是可见的。一个文件中 对某个全局符号的定义将满足另一个文件对相同全局符号的 未定义引用。
    STB_WEAK2弱符号与全局符号类似,不过他们的定义优先级比较低。
    STB_LOPROC13处于这个范围的取值是保留给处理器专用语义的。
    STB_HIPROC15处于这个范围的取值是保留给处理器专用语义的。
  • 符号类型 ELF32_ST_TYPE(st_info)

    名称取值说明
    STT_NOTYPE0符号的类型没有指定
    STT_OBJECT1符号与某个数据对象相关,比如一个变量、数组等等
    STT_FUNC2符号与某个函数或者其他可执行代码相关
    STT_SECTION3符号与某个节区相关。这种类型的符号表项主要用于重定 位,通常具有 STB_LOCAL 绑定。
    STT_FILE4传统上,符号的名称给出了与目标文件相关的源文件的名 称。文件符号具有 STB_LOCAL 绑定,其节区索引SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号 (如果有的话
    ......
  • 解析符号

    #include "tools/elf_opation.h"
    
    void parse_elf_symbole(char *pbuff)
    {
        // 区段类型为 SHT_SYMTAB  全局符号表   SHT_DYNSYM为动态符号表
        // link指向  字符串符号表 
        // off 指向  符号表数组  Elf32_Sym
        Elf32_Shdr * psymshdr  =  get_elf_symble_section(pbuff);
        Elf32_Shdr *pshdr = get_elf_section(pbuff);
        // 获取字符串符号表
        Elf32_Shdr *psym_string_table_shdr =  &pshdr[psymshdr->sh_link];
        // 获取字符串表首地址
        char * psym_string_table = (char *)(psym_string_table_shdr->sh_offset + pbuff);
    
        // 获取符号表数组
        Elf32_Sym * psym = (Elf32_Sym *)(psymshdr->sh_offset+ pbuff);
        // 获取符号表个数
        int sym_count= psymshdr->sh_size/psymshdr->sh_entsize; 
        // 遍历符号表
        for(int i=0;i<sym_count;i++)
        {
    
            char *sym_name = (char *)(psym[i].st_name + psym_string_table);
            if(*sym_name == 0 ){
                printf("[%d]: 未知 \n",i);
            }else{
                printf("[%d]:%s\n",i,sym_name);
            }
            // st_value符号的值,一般在可重定位目标文件内,该值记录了符号相对于所在 段基地址的偏移量,而在可执行文件内,该值记录了符号的线性地址。
            psym[i].st_value;
            // st_info 符号属性,
            //    1. 其低 4 位表示符号的类型使用宏 ELF32_ST_TYPE 获取,
            //          STT_NOTYPE (0):未知 | STT_OBJECT (1):对象 | STT_FUNC(2):函数 |STT_SECTION(3):区段
            //    2. 高 4 位表示符号的绑定信息,使用宏 ELF32_ST_BIND 获取
            //          STB_LOCAL(0):局部符号| STB_GLOBAL(1):全局符号 。。。。
            psym[i].st_info;
            // st_shndx 符号所在段索引,
            //      当该字段 为 0 时,表示符号未定义,取值 SHN_UNDEF。
            //      该字段为 0Xfff1 时,表示符号为绝对值, 比如文件名,取值 SHN_ABS。
            //      该字段为 0xfff2 时,表示符号在 COMMON 块内,取值为 SHN_COMMON,特别的,此时符号的 st_value 表示符号的对齐属性
            psym[i].st_shndx;
    
            printf("\t\t %08x\n",psym[i].st_value);
            printf("\t\t %08x\n",psym[i].st_size);
    
            const char *attrs[20]={"未知","对象","函数","区段","文件"};
            printf("\t\t %s\n",attrs[ELF32_ST_TYPE( psym[i].st_info)]);
    
            const char *binds[20]={"局部符号","全局符号","弱引用"};
            printf("\t\t %s\n",binds[ELF32_ST_BIND( psym[i].st_info)]);
            
        }
    
    }
    

08 ELF重定位表

  • 重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时, 相关的调用指令必须把控制传输到适当的目标执行地址,例如在main函数中使用到printf函数,那么会在重定位表中记录printf函数位置(rel.plt)

  • 重定位表类型 SHT_REL、SHT_RELA

  • 重定位可能有多个

    typedef struct
    {
      Elf32_Addr	r_offset;		/* Address */
      Elf32_Word	r_info;			/* Relocation type and symbol index */
    } Elf32_Rel;
    
    typedef struct {  
        Elf32_Addr r_offset;  
        Elf32_Word r_info;  
        Elf32_Word r_addend;
    } Elf32_Rela;
    
  • 重定位字段

    成员说明
    r_offset此成员给出了重定位动作所适用的位置。对于一个可重定位文件而言, 此值是从节区头部开始到将被重定位影响的存储单位之间的字节偏 移。对于可执行文件或者共享目标文件而言,其取值是被重定位影响 到的存储单元的虚拟地址。
    r_info此成员给出要进行重定位的符号表索引,以及将实施的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果 索引是 STN_UNDEF,那么重定位使用 0 作为“符号值”。重定位类型是和处理器相关的。当程序代码引用一个重定位项的重定位类型或 者符号表索引,则表示对表项的 r_info 成员应用 ELF32_R_TYPE 或 者 ELF32_R_SYM 的结果。 #define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i) ((unsigned char)(i)) #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
    r_addend此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值
  • 重定位类型

类型名说明
R_386_NONE0
R_386_32132重定位数据,ld链接时使用
R_386_PC32232重定位数据,ld链接时使用
R_386_GOT323此重定位类型计算从全局偏移表基址到符号 的全局偏移表项之间的距离。它会通知连接编 辑器构造一个全局偏移表。
R_386_PLT324此重定位类型计算符号的过程链接表项的地 质,并通知链接编辑器构造一个过程链接表。
R_386_JMP_SLOT7链接编辑器创建这种重定位类型主要是为了 支持动态链接。其偏移地址成员给出过程链接 表项的位置。动态链接器修改过程链接表项的 内容,把控制传输给指定符号的地址
R_386_RELATIVE8链接编辑器创建这种重定位类型是为了支持 动态链接。其偏移地址成员给出共享目标中的 一个位置,在该位置包含了代表相对地址的一 个数值。动态链接器通过把共享目标被加载到 的虚地址和相对地址相加,计算对应的虚地 址。这种类型的重定位项必须设置符号表索引 为 0。
  • 需要重定位类型

    void parse_elf_reloc(char *pbuff){
        // 获取重定位表
        Elf32_Shdr *preloc_shdr = get_elf_reloc_section(pbuff);
        // sh_offset: 指向重定位表数组
        // sh_link  : 指向符号表下标      (Elf32_Sym)
        // sh_info  : 指向符号所在区段下标 (Elf32_Shdr)
        while (preloc_shdr){
            Elf32_Shdr *pshdr = get_elf_section(pbuff);
            // 重定位数据所在区段名称
            char * rel_section_name = 
               (char *)(preloc_shdr->sh_name +  get_elf_section_name_table(pbuff));
            printf("%s \n",rel_section_name);
            // 获取重定位数组
            Elf32_Rel *prelocs = (Elf32_Rel *)(preloc_shdr->sh_offset + pbuff);
            // 获取重定位个数
            int reloc_cont = preloc_shdr->sh_size / preloc_shdr->sh_entsize;
            // 获取重定位对应的符号表
            Elf32_Sym *preloc_sym_table = (Elf32_Sym *)(pshdr[preloc_shdr->sh_link].sh_offset + pbuff);
            char *preloc_sym_string_table = (char *)(pshdr[pshdr[preloc_shdr->sh_link].sh_link].sh_offset + pbuff);
            printf("offset      info        type              sym.value      sym.name\n");
            //遍历重定位表
            for (int i = 0; i < reloc_cont; i++){
                // r_info,描述了重定位类型和符号
                //      1.低 8 位表示重定位类型,使用宏 ELF_32_R_TYPE 获取,
                //      2.高 24 位表示重定位符号对应符号表项在符号表内的索引,使用宏 ELF_32_R_SYM 获取。
                //      3.不同的处理器体系结构都属于自己的一套重定位类型,对于 x86 体 系结构而言,静态链接中常见的重定位类型有两种:
                //          3.1 绝对地址重定位(取值 R_386_32)
                //          5.2 相对地址重定位 R_386_PC32。
                //     每条重定位信息的含义为使用 r_info 记录的符号的线性地 址,根据重定位类型更新 r_offset 处的内存信息。
                prelocs[i].r_info;
                //r_offset,表示重定位地址.
                //       对应可重定位目标文件来说,表示重定位位置相对于 被重定位段的基地址的偏移。
                //       而对于可执行文件或共享目标文件来说,表示重定位位置对 应的线性地址,这个与动态了解相关
                prelocs[i].r_offset;
                printf("%08x    ", prelocs[i].r_offset);
                printf("%08x    ", prelocs[i].r_info);
                const char *types[20] = {
                    "R_386_NONE", "R_386_32", "R_386_PC32", "R_386_GOT32", 
                    "R_386_PLT32", "R_386_COPY", "R_386_GLOB_DAT","R_386_JMP_SLOT"};
                printf("%-15s   ", types[ELF32_R_TYPE(prelocs[i].r_info)]);
                int sym_index = ELF32_R_SYM(prelocs[i].r_info);
                printf("%08x    ", preloc_sym_table[sym_index].st_value);
                char *reloc_sym_name = (char *)(preloc_sym_table[sym_index].st_name + preloc_sym_string_table);
                printf("%15s    ", reloc_sym_name);
                printf("\n");
            }
            printf("\n");
            // 遍历下一个重定位表
            preloc_shdr = get_elf_reloc_next_section(preloc_shdr);
        }
    }
    
    

09 ELF动态链接表

  • 如果一个目标文件参与动态链接,它的程序头部表将包含类型为 PT_DYNAMIC 的元素。此“段”包含.dynamic 节区。该节区采用一个特殊符号_DYNAMIC 来标记。

  • 段类型是 SHT_DYNAMIC (PT_DYNAMIC)

  • 通常我们可以从区段(程序段)中去解析,它们两个是一样的位置。当程序运行时应该从程序段解析。

    typedef struct {   
        Elf32_Sword d_tag;   
        union {    
            Elf32_Word  d_val;
            Elf32_Addr  d_ptr;   
        } d_un;  
    } Elf32_Dyn;    
    extern Elf32_Dyn  _DYNAMIC[];
    
    • d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。
    • d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。
    • 如前所述,文件的 虚拟地址可能与执行过程中的内存虚地址不匹配。在解释包含于动态 结构中的地址时,动态链接程序基于原来文件值和内存基地址计算实 际地址。为了保持一致性,文件中不包含用来“纠正”动态结构中重定位项地址的重定位项目。
  • 动态d_tag包括

    名称数值说明
    DT_NULL0标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数 组的末端
    DT_NEEDED1此元素包含一个 NULL 结尾的字符串的字符串表 偏移,该字符串给出某个需要的库的名称。所使用 的字符串表根据 DT_STRTAB 项目中记录的内容确 定。所谓的偏移即是指在该表中的下标。动态数组中可以包含多个这种类型的条目。这些条目的相对 顺序很重要,尽管他们与其他类型条目间的顺序没 有很大关系。 在link中保存字符串表的index (这个程序中所有使用的so文件名)
    DT_PLTRELSZ2此元素给出了与过程链接表(PLT)相关联的重定 位项的总计大小(按字节)。如果存在 DT_JMPREL 类型的条目,必须有与之配合的 DT_PLTRELSZ 条 目。
    DT_PLTGOT3此元素给出一个与过程链接表(PLT)与/或全局偏 移表相关联的一个地址
    DT_HASH4此元素包含符号哈希表的地址。此哈希表指的是被 DT_SYMTAB 元素引用的符号表
    DT_STRTAB5此元素包含字符串表的地址,符号名、库名、和其 他字符串都包含在此表
    DT_SYMTAB6此元素包含符号表的地址。对 32 位的文件而言, 这个符号表中的条目是 Elf32_Sym 类型。
    DT_REL17此元素与 DT_RELA 类似,只是其表格中包含隐式 的补齐,对 32 位文件而言,就是 Elf32_Rel。如 果文件中包含此元素,那么动态结构中也必须包含 DT_RELSZ 和 DT_RELENT 元素。
    DT_RELSZ18此元素包含 DT_REL 重定位表的总计大小,按字节 数计算
    DT_JMPREL23如果存在这种成员,则表示条目的 d_ptr 成员包 含了某个重定位项的地址,并且该重定位项仅与过 程链接表相关。把重定位项分开有利于让动态链接 器在进程初始化时忽略它们,当然后期绑定必须可 行。如果存在此成员,相关的 DT_PLTRELSZ 和 DT_PLTREL 必须也存在
    .......
  • 解析代码

    void parse_elf_dynamic_link(char *pbuff){
        Elf32_Shdr *pshdr = get_elf_section(pbuff);
        // 获取动态连接区段
        Elf32_Shdr *pdynamic_shdr = get_elf_dynamic_link_section(pbuff);
        // 动态连接区段的字符串表
        char * dynamic_string_tables = (char *)(pshdr[pdynamic_shdr->sh_link].sh_offset + pbuff);
    
        // 获取动态连接数组
        Elf32_Dyn * pdynamic_links = (Elf32_Dyn *)(pdynamic_shdr->sh_offset + pbuff);
        // 获取动态连接信息个数
        int dynamic_count =  pdynamic_shdr->sh_size / pdynamic_shdr->sh_entsize;
    
        // d_val 标记,用于控制d_un含义
        //      d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。 „ 
        //      d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。
        //      如前所述,文件的 虚拟地址可能与执行过程中的内存虚地址不匹配。
        //      在解释包含于动态 结构中的地址时,动态链接程序基于原来文件值和内存基地址计算实 际地址。
        //      为了保持一致性,文件中不包含用来“纠正”动态结构中重定位项地址的重定位项目。
    
    
        printf("标志       类型         值\n");
        for(int i=0;i<dynamic_count;i++){
            printf("%08x ",pdynamic_links[i].d_tag);
            const char* types[100]={
                "DT_NULL","DT_NEEDED","DT_PLTRELSZ","DT_PLTGOT",
                "DT_HASH","DT_STRTAB","DT_SYMTAB","DT_RELA"};
                if(pdynamic_links[i].d_tag <100)
                {
                    printf("%10s\t",types[pdynamic_links[i].d_tag]);
                }else{
                    printf("%10s\t",types[99]);
                }
            
            // 显示值-动态链接库
            if(pdynamic_links[i].d_tag == DT_NEEDED){
                char *so_name = (char *)(pdynamic_links[i].d_un.d_val + dynamic_string_tables);
                printf("%10s\t",so_name);
            }else{
                printf("%08x\t",pdynamic_links[i].d_un.d_val);
            }
         printf("\n");
        }   
    }
    

10 ELF程序头表

  • 程序头表记录了程序运行时那些段需要加载到内存中

    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;
    
  • 类型书说明

类型说明
PT_NULL0此数组元素未用。结构中其他成员都是未定义的。
PT_LOAD1此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于 p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载 的段在程序头部表格中根据 p_vaddr 成员按升序排列
PT_DYNAMIC2数组元素给出动态链接信息。
PT_INTERP3数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被 当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能 在共享目标文件上发生) 。在一个文件中不能出现一次以上。如果存在 这种类型的段,它必须在所有可加载段项目的前面 ld-linux.so.6
PT_NOTE4此数组元素给出附加信息的位置和大小
PT_SHLIB5此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI 不符
PT_PHDR6此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置, 既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出 现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起 作用。如果存在此类型段,则必须在所有可加载段项目的前面
PT_LOPROC0x70000000此范围的类型保留给处理器专用语义
PT_HIPROC0x7fffffff此范围的类型保留给处理器专用语义
  • 程序解析

    void parse_elf_pragram(char *pbuff)
    {
        // 获取程序段
        Elf32_Phdr *phdr = get_elf_pragram(pbuff);
        // 程序段个数
        Elf32_Ehdr *ehdr = get_elf_head(pbuff);
        int pragram_count = ehdr->e_phnum;
        // 遍历所有程序段
     
        printf("类型\t\t 偏移     虚拟地址   物理地址   文件大小   内存大小   标志   对齐\n");
        for (int i = 0; i < pragram_count; i++)
        {
            const char *types[20] = {"PT_NULL", "PT_LOAD",
                                     "PT_DYNAMIC", "PT_INTERP",
                                     "PT_NOTE", "PT_SHLIB",
                                     "PT_PHDR"};
            if (phdr[i].p_type < 20)
            {
                printf("%-15s", types[phdr[i].p_type]);
            }
            else
            {
                printf("%08x       ", phdr[i].p_type);
            }
    
            // 偏移
            printf("%08x   ", phdr[i].p_offset);
            // 虚拟地址
            printf("%08x   ", phdr[i].p_vaddr);
            // 物理地址
            printf("%08x   ", phdr[i].p_paddr);
            //文件大小
            printf("%08x   ", phdr[i].p_filesz);
            // 内存大小
            printf("%08x   ", phdr[i].p_memsz);
            // 标志
            printf("%04d   ",phdr[i].p_flags);
            // 对齐
            printf("%04d   ",phdr[i].p_align);
    
            printf("\n");
        }
    }
    
    

11 GOT(.got.plt)全局偏移表(PE中的IAT表)

  • 位置独立的代码一般不能包含绝对的虚拟地址。全局偏移表在私有数据中包含绝对 地址,从而使得地址可用,并且不会影响位置独立性和程序代码的可共享性。程序使用 位置独立的寻址引用其全局偏移表,并取得绝对值,从而把位置独立的引用重定向到绝 对位置。

  • 全局偏移表中最初包含其重定位项中要求的信息。在系统为可加载目标创建内存段 以后,动态链接器要处理重定位项,其中有一些重定位项的类型是 R_386_GLOB_DAT, 是对全局偏移表的引用。动态链接器确定相关的符号取值,计算其绝对地址,并将相应 的内存表格项目设置为正确的数值。尽管在链接编辑器构造一个目标文件时还无法知道 绝对地址,动态链接器清楚所有内存段的地址,因而能够计算其中所包含的符号的绝对 地址。

  • 如果程序需要直接访问某个符号的绝对地址,那么该符号就会具有一个全局偏移表项。由于可执行文件和共享目标具有独立的全局偏移表,一个符号的地址可能出现在多 个表中。动态链接器在将控制交给进程映像中任何代码之前,要处理所有的全局偏移表重定位,因而确保了执行过程中绝对地址信息可用。

  • 表项 0 是保留的,用来存放动态结构的地址,可以用符号_DYNAMIC 引用之。这样, 类似动态链接器这种程序能够在尚未处理其重定位项的时候先找到自己的动态结构。对 于动态链接器而言这点很重要,因为它必须能够在不依赖其他程序来对其内存映像进行 重定位的前提下,初始化自己。在 32 位 Intel 体系结构下,全局偏移表中的表项 1 和 2 也是保留的。

  • 全局偏移表的格式和解释都是和处理器相关的。对于 32 位 Intel 体系结构而言, 符号GLOBAL_OFFSET_TABLE_ 可以用来访问该表 extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];

  • 符号 GLOBAL_OFFSET_TABLE_ 可能存在于 .got 节区的中间,允许使用负的/ 非负的下标来访问地址数组。

  • 使用的外部函数(so)都被保存再这个表中 可以hook

12 PLT 过程连接表

  • 全局偏移表(GOT,)用来将位置独立的地址计算重定向到绝对位置,与此相似, 过程链接表(PLT)能够把位置独立的函数调用重定向到绝对位置。链接编辑器物能解 析从一个可执行文件/共享目标到另一个可执行文件/共享目标控制转移(例如函数调 用) 。因此,链接编辑器让程序把控制转移给过程链接表中的表项。在 System V 中,过程链接表位于共享正文中,不过它们使用位于私有的全局偏移表中的地址。

  • 动态链接器能够确定目标处的绝对地址,并据此修改全局偏移表的内存映像。动态链接器因此能够对表项进行重定位,并且不会影响程序代码的位置独立性和可共享性。 可执行文件和共享目标文件拥有各自独立的过程链接表。

  • 通过一个例子来观察GOT和PLT表的作用

    .got.plt中保存的是数据,为每个动态调用保存一个条目,条目的内容应该是对动态库函数的调用所跳转到的目标地址。由于Linux采用了延迟绑定技术,可执行文件中got.plt中的地址并不是目标地址,而是动态链接器(ld-linux.so)中的地址。在程序执行的第一次调用时,ld-linux把.got.plt的地址填写正确,之后的调用,就可以使用.got.plt中的目标地址了。.plt段中的内容则是实现跳转操作的代码片段。

    偏移说明
    GOT[0]存放了指向可执行文件动态段的地址(Elf32_Dyn *),动态链接器利用该地址提取动态链接相关的信息。
    GOT[1]存放link_map结构的地址,动态链接器利用 该地址来对符号进行解析。
    GOT[2]存放了指向动态链接器_dl_runtime_resolve() 函数的地址,该函数用来解析共享库函数的实际符号地
    GOT[3]printf真实函数地址
    • 1 测试代码 main.c

      #include<stdio.h>
      int main()
      {
      	printf("hello");
      	printf("hello");
      	return 0;
      }
      // gcc main.c -g -m32 -o main 
      
    • 2 反汇编代码

      • objdump -d main
      0804840b <main>:
       804840b:	8d 4c 24 04          	lea    0x4(%esp),%ecx
       804840f:	83 e4 f0             	and    $0xfffffff0,%esp
       8048412:	ff 71 fc             	pushl  -0x4(%ecx)
       8048415:	55                   	push   %ebp
       8048416:	89 e5                	mov    %esp,%ebp
       8048418:	51                   	push   %ecx
       8048419:	83 ec 04             	sub    $0x4,%esp
       804841c:	83 ec 0c             	sub    $0xc,%esp
       804841f:	68 d0 84 04 08       	push   $0x80484d0
       8048424:	e8 b7 fe ff ff       	call   80482e0 <printf@plt>
       8048429:	83 c4 10             	add    $0x10,%esp
       804842c:	83 ec 0c             	sub    $0xc,%esp
       804842f:	68 d0 84 04 08       	push   $0x80484d0
       8048434:	e8 a7 fe ff ff       	call   80482e0 <printf@plt>
       8048439:	83 c4 10             	add    $0x10,%esp
       804843c:	b8 00 00 00 00       	mov    $0x0,%eax
       8048441:	8b 4d fc             	mov    -0x4(%ebp),%ecx
       8048444:	c9                   	leave  
       8048445:	8d 61 fc             	lea    -0x4(%ecx),%esp
       8048448:	c3                   	ret    
       8048449:	66 90                	xchg   %ax,%ax
       804844b:	66 90                	xchg   %ax,%ax
       804844d:	66 90                	xchg   %ax,%ax
       804844f:	90                   	nop
      
       
       ;plt 过程链接
       080482e0 <printf@plt>:				;*表示取内容,0x804a00c 是plt数组中的元素
       80482e0:	ff 25 0c a0 04 08    	jmp    *0x804a00c ;取得0x804a00c地址上的值; 第一次存放的是 0x80482e6
       80482e6:	68 00 00 00 00       	push   $0x0		  ;函数id =0  实际上GOT[3]
       80482eb:	e9 e0 ff ff ff       	jmp    80482d0 <_init+0x24>  ;解析符号填充到正确地址
       
      080482d0 <printf@plt-0x10>:
       80482d0:	ff 35 04 a0 04 08    	pushl  0x804a004  ;
       80482d6:	ff 25 08 a0 04 08    	jmp    *0x804a008 ;调用ld.so动态链接解析模块中的函数,填充到got.plt表中 (0x804a00c地址中),这样第二次调用printf,直接找到的printf函数
       80482dc:	00 00                	add    %al,(%eax)
      
      
       
      

      objdump -s main

      ;0x804a00c 内容是 0x8040286e 相当于跳转回原来地址下一个位置
      Contents of section .got:
       8049ffc 00000000                             ....            
      Contents of section .got.plt:
       804a000 149f0408 00000000 00000000 e6820408  ................
       804a010 f6820408                             ....            
      Contents of section .data:
       804a014 00000000 00000000                    ........     
      
      
    • objdump -R main

      main:     文件格式 elf32-i386
      DYNAMIC RELOCATION RECORDS
      OFFSET   TYPE              VALUE 
      08049ffc R_386_GLOB_DAT    __gmon_start__     ;got 
      0804a00c R_386_JUMP_SLOT   printf@GLIBC_2.0   ;got 
      0804a010 R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0 ;got
      
    • 执行gdb 跟踪调试

      • b main
      • display/i $pc
      • r
      • si 进入到printf@plt
      • x/20x 0x804a000 查看got.plt表
      (gdb) x/20x 0x804a000   ;0xf7fee000 in ?? () from /lib/ld-linux.so.2
      0x804a000:	0x08049f14	0xf7ffd918	0xf7fee000	0x080482e6; 是重点0x804a00c
      0x804a010:	0xf7e16550	0x00000000	0x00000000	0x00000000
      0x804a020:	0x00000000	0x00000000	0x00000000	0x00000000
      0x804a030:	0x00000000	0x00000000	0x00000000	0x00000000
      0x804a040:	0x00000000	0x00000000	0x00000000	0x00000000
      
      • finish
      • 执行完函数我们再观察0x804a00c(got.plt表)
      ;0x804a00c 中的值变成 0xf7e47680 ,这个值就是函数的真正地址
      0x804a000:	0x08049f14	0xf7ffd918	0xf7fee000	0xf7e47680 ; 是重点0x804a00c
      0x804a010:	0xf7e16550	0x00000000	0x00000000	0x00000000
      0x804a020:	0x00000000	0x00000000	0x00000000	0x00000000
      0x804a030:	0x00000000	0x00000000	0x00000000	0x00000000
      0x804a040:	0x00000000	0x00000000	0x00000000	0x00000000
      

13 fPIC与地址无关

  • 在编译程序时加入 -fPIC (支持动态链接库)

  • 测试程序

    #include <stdio.h>
    int add(int a,int b){
        return a+b;
    }
    int main(){
        int sum = add(100,200);
        printf("%d\n",sum);
        return 0;
    }
    
  • 普通编译-反汇编

    gcc -g -m32 main.c -o main
    
    .text:08048418 main            proc near               ; 
    .text:08048418
    .text:08048418 var_C           = dword ptr -0Ch
    .text:08048418 var_4           = dword ptr -4
    .text:08048418
    .text:08048418                 lea     ecx, [esp+4]
    .text:0804841C                 and     esp, 0FFFFFFF0h
    .text:0804841F                 push    dword ptr [ecx-4]
    .text:08048422                 push    ebp
    .text:08048423                 mov     ebp, esp
    .text:08048425                 push    ecx
    .text:08048426                 sub     esp, 14h
    .text:08048429                 push    0C8h
    .text:0804842E                 push    64h
    .text:08048430                 call    add
    .text:08048435                 add     esp, 8
    .text:08048438                 mov     [ebp+var_C], eax
    .text:0804843B                 sub     esp, 8
    .text:0804843E                 push    [ebp+var_C]
    .text:08048441                 push    offset format   ; "%d\n"  全局变量使用绝对地址
    .text:08048446                 call    _printf         ;printf("%d",add(100,200))
    .text:0804844B                 add     esp, 10h
    .text:0804844E                 mov     eax, 0
    .text:08048453                 mov     ecx, [ebp+var_4]
    .text:08048456                 leave
    .text:08048457                 lea     esp, [ecx-4]
    .text:0804845A                 retn
    .text:0804845A main            endp
    
  • 加入与地址无关选项

    gcc -g -m32 main.c -fPIC -o main
    
    .text:08048422                 public main
    .text:08048422 main            proc near               ; 
    .text:08048422
    .text:08048422 var_C           = dword ptr -0Ch
    .text:08048422
    .text:08048422                 lea     ecx, [esp+4]
    .text:08048426                 and     esp, 0FFFFFFF0h
    .text:08048429                 push    dword ptr [ecx-4]
    .text:0804842C                 push    ebp
    .text:0804842D                 mov     ebp, esp
    .text:0804842F                 push    ebx
    .text:08048430                 push    ecx
    .text:08048431                 sub     esp, 10h
    .text:08048434                 call    __x86_get_pc_thunk_bx  ;获取当前eip地址
    .text:08048439                 add     ebx, 1BC7h			 ;当前离全局“%d”数据偏移值
    .text:0804843F                 sub     esp, 8
    .text:08048442                 push    0C8h
    .text:08048447                 push    64h
    .text:08048449                 call    add
    .text:0804844E                 add     esp, 10h
    .text:08048451                 mov     [ebp+var_C], eax
    .text:08048454                 sub     esp, 8
    .text:08048457                 push    [ebp+var_C]
    .text:0804845A                 lea     eax, (unk_8048500 - 804A000h)[ebx];计算全局数据所在位置
    .text:08048460                 push    eax             ; format
    .text:08048461                 call    _printf
    .text:08048466                 add     esp, 10h
    .text:08048469                 mov     eax, 0
    .text:0804846E                 lea     esp, [ebp-8]
    .text:08048471                 pop     ecx
    .text:08048472                 pop     ebx
    .text:08048473                 pop     ebp
    .text:08048474                 lea     esp, [ecx-4]
    .text:08048477                 retn
    
    ;计算当前指令位置
    .text:08048340                 public __x86_get_pc_thunk_bx
    .text:08048340 __x86_get_pc_thunk_bx proc near         ; CODE XREF: _init_proc+4
    .text:08048340                 mov     ebx, [esp+0]
    .text:08048343                 retn
    .text:08048343 __x86_get_pc_thunk_bx endp
    

14 ELF程序加载流程(参考linux4.13.6源码)

14.1 运行一个程序的过程函数

函数调用过程说明文件位置
execve()运行可执行文件linux/fs/exec.c line:1910
do_execve()系统无关linux/fs/exec.c line:1828
do_execveat_common()启动进程相关数据linux/fs/exec.c line:1682
exec_binprm ()识别可执行文件格式linux/fs/exec.c line:1657
search_binary_handler ()查找可执行文件的处理函数linux/fs/exec.c line:1604
load_binary()加载可执行文件(linux 支持多种格式目前只看elf)linux/fs/exec.c line:1604
load_elf_binary()加载elf文件格式linux/fs/binfmt_elf.c line 679
start_thread()启动进程linux/fs/binfmt_elf.c line 1148

其中可执行文件存放与linux/fs/binfmts.h , 常见格式

格式格式定义加载函数
elfelf_formatload_elf_binary
script脚本script_formatload_scrip
a.outaout_formatload_aout_binary

14.2 加载ELF文件

  • ElF文件的加载从load_elf_binary函数开始

  • load_elf_binary加载流程概述

    • 01 读取目标程序并且检查ELF头部
    • 02 load_elf_phdrs加载目标程序的程序头表
    • 03 如果需要动态链接器(ld-linux.so), 则寻找和处理解释器段
    • 04 检查并读取解释器的程序表头
    • 05 装入目标程序的段(PT_LOAD) segment
    • **06 填写程序的入口地址 elf_entry **
    • 07 create_elf_tables填写目标文件的参数环境变量等必要信息
    • 08 start_thread 进入新的程序入口
  • 01 读取目标程序并且检查ELF头部 ( linux/fs/exec.c line:1604)

    struct pt_regs *regs = current_pt_regs();
    struct {
        struct elfhdr elf_ex;
        struct elfhdr interp_elf_ex;
    } *loc;
    struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
    
    loc = kmalloc(sizeof(*loc), GFP_KERNEL);
    if (!loc) {
        retval = -ENOMEM;
        goto out_ret;
    }
    
    /* Get the exec-header
        使用映像文件的前128个字节对bprm->buf进行了填充  */
    loc->elf_ex = *((struct elfhdr *)bprm->buf);
    
    retval = -ENOEXEC;
    /* First of all, some simple consistency checks
        比较文件头的前四个字节
        。*/
    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
        goto out;
    /*  还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库  */
    if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
        goto out;
    
  • 02 load_elf_phdrs加载目标程序的程序头表

       elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
        if (!elf_phdata)
                goto out;
    
    /**
     * load_elf_phdrs() - load ELF program headers
     * @elf_ex:   ELF header of the binary whose program headers should be loaded
     * @elf_file: the opened ELF binary file
     *
     * Loads ELF program headers from the binary file elf_file, which has the ELF
     * header pointed to by elf_ex, into a newly allocated array. The caller is
     * responsible for freeing the allocated data. Returns an ERR_PTR upon failure.
     */
    static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,
                                       struct file *elf_file)
    {
        struct elf_phdr *elf_phdata = NULL;
        int retval, size, err = -1;
    
        /*
         * If the size of this structure has changed, then punt, since
         * we will be doing the wrong thing.
         */
        if (elf_ex->e_phentsize != sizeof(struct elf_phdr))
                goto out;
    
        /* Sanity check the number of program headers... */
        if (elf_ex->e_phnum < 1 ||
                elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr))
                goto out;
    
        /* ...and their total size. */
        size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
        if (size > ELF_MIN_ALIGN)
                goto out;
    
        elf_phdata = kmalloc(size, GFP_KERNEL);
        if (!elf_phdata)
                goto out;
    
        /* Read in the program headers */
        retval = kernel_read(elf_file, elf_ex->e_phoff,
                             (char *)elf_phdata, size);
        if (retval != size) {
                err = (retval < 0) ? retval : -EIO;
                goto out;
        }
    
        /* Success! */
        err = 0;
    out:
        if (err) {
                kfree(elf_phdata);
                elf_phdata = NULL;
        }
        return elf_phdata;
    }
    
  • 03 如果需要动态链接, 则寻找和处理解释器段

    • 使用动态编译时才会存在解释器(PT_INTERP段) -dynamic-linker
    • 使用静态编译时不会出现解释器(PT_INTERP段) -static
    • PT_INTERP 通常是lib/ld-linux-xxxx.so.2,程序运行后主动权交给这个文件
    for (i = 0; i < loc->elf_ex.e_phnum; i++) {
            /*  3.1  检查是否有需要加载的解释器  如果存在PT_INIERP就需要保存解释器 */
            if (elf_ppnt->p_type == PT_INTERP) {
                /* This is the program interpreter used for
                 * shared libraries - for now assume that this
                 * is an a.out format binary
                 */
    
                /*  3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区  */
                retval = kernel_read(bprm->file, elf_ppnt->p_offset,
                             elf_interpreter,
                             elf_ppnt->p_filesz);
    
                if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
                    goto out_free_interp;
                /*  3.3 通过open_exec()打开解释器文件 */
                interpreter = open_exec(elf_interpreter);
    
                /* Get the exec headers 
                   3.4  通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/
                retval = kernel_read(interpreter, 0,
                             (void *)&loc->interp_elf_ex,
                             sizeof(loc->interp_elf_ex));
                break;
            }
            elf_ppnt++;
        }
    
  • 04 检查并读取解释器的程序表头

    /*   4.    检查并读取解释器的程序表头 */
        /* Some simple consistency checks for the interpreter 
           4.1  检查解释器头的信息  */
        if (elf_interpreter) {
            retval = -ELIBBAD;
            /* Not an ELF interpreter */
    
            /* Load the interpreter program headers
               4.2  读入解释器的程序头
             */
            interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
                               interpreter);
            if (!interp_elf_phdata)
                goto out_free_dentry;
    
  • 05 装入目标程序的段segment

    */
        for(i = 0, elf_ppnt = elf_phdata;
            i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
    
            /*  5.1   搜索PT_LOAD的段, 这个是需要装入的 */
            if (elf_ppnt->p_type != PT_LOAD)
                continue;
    
                /* 5.2  检查地址和页面的信息  */
                // ......
    
             /*  5.3  虚拟地址空间与目标映像文件的映射
             确定了装入地址后,
             就通过elf_map()建立用户空间虚拟地址空间
             与目标映像文件中某个连续区间之间的映射,
             其返回值就是实际映射的起始地址 */
            error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                    elf_prot, elf_flags, total_size);
            }
    
  • 06 填写程序的入口地址

      if (elf_interpreter) {
            unsigned long interp_map_addr = 0;
    
            elf_entry = load_elf_interp(&loc->interp_elf_ex,
                            interpreter,
                            &interp_map_addr,
                            load_bias, interp_elf_phdata);
                /*  入口地址是解释器映像的入口地址  */
        } else {
            /*  入口地址是目标程序的入口地址  */
            elf_entry = loc->elf_ex.e_entry;
            }
        }
    
  • 07 create_elf_tables填写目标文件的参数环境变量等必要信息

    install_exec_creds(bprm);
    retval = create_elf_tables(bprm, &loc->elf_ex,
                               load_addr, interp_load_addr);
    if (retval < 0)
        goto out;
    /* N.B. passed_fileno might not be initialized? */
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;
    
  • 08 start_thread 进入新的程序入口

    start_thread(regs, elf_entry, bprm->p);
    	retval = 0;
    out:
    	kfree(loc);
    out_ret:
    	return retval;
    
    	/* error cleanup */
    out_free_dentry:
    	kfree(interp_elf_phdata);
    	allow_write_access(interpreter);
    	if (interpreter)
    		fput(interpreter);
    out_free_interp:
    	kfree(elf_interpreter);
    out_free_ph:
    	kfree(elf_phdata);
    	goto out;
    }
    

    ELF文件格式

0

评论区