Linux可执行文件elf分析

Linux可执行文件elf分析

Posted by Albert on January 26, 2021

Linux可执行文件elf分析

​ 在嵌入式项目中,我们经常遇到应用程序的空指针异常、野指针、以及各种各样的段错误,在嵌入式系统中往往需要收集到coredump信息,然后再用交叉编译工具gdb等工具一步一步debug。 这种方式有时候是十分低效的,并不是每个工程师都能够十分熟练的使用gdb工具,排查问题往往需要数小时乃至数天。而且BUG并不一定可以很好的复现,我们当然想第一时间能够拿到出问题的调用栈。由于我们在打印调用栈的时候,需要打印链接库、以及可执行文件的符号表,故在此之前,我们需要先研究下Linux系统下的可执行文件elf。目的是利用内核栈回溯功能,在应用段错误发生之前,将应用的调用栈打印出来,从而提高整个部门工程师排查问题的效率。

​ 可行性: 1、Linux内核无论在处理应用程序还是内核程序的指针、内存访问异常的时候,都是MMU发起page fault异常,如果是应用程序,内核会向该程序发送段错误信息,然后该应用程序退出;如果是内核访问异常,那么触发OOPS. 基于此我们可以在内核page fault函数中处理这个异常。 2、由于所有的寄存器信息都是存在当前进程的pt_regs中,所以我们可以在访问异常的时候,找到进程的调用栈。 3、对于符号表,我们可以参考操作系统执行一个进程的过程来实现。 sys_execev(). 该系统调用详细的向大家展现了,系统如何读取elf文件,加载内核可执行文件等。

1. ELF文件简介

​ 在学习ELF文件之前,有必要先了解下进程的地址空间相关的内容,可以参考《深入理解Linux内核》这本书的相关章节。简单了解可以参考这篇博文:进程虚拟地址空间的分段

​ 每一种体系架构的ELF可能有所差异,泰晓科技的吴章金老师搜集了一些关于ELF相关的资料,放在Github上,另外他还做了视频教程放在了我最喜欢的公众号linux阅码场以及泰晓的官方平台,嫌自己整理资料以及学习麻烦的同学,可以去看他们的视频教程。另外耶律大学ELF文档北京大学ELF课件这两份资料都不错,大家可以去看看。

1.1 ELF文件分类

​ 首先,你需要知道的是所谓对象文件(Object files)有三个种类:

可重定位对象文件

​ 可重定位对象文件(relocatable object file):以某种形式包含二进制代码和数据(这是由汇编器汇编生成的 .o 文件),并且可以和其他的可重定位对象文件在编译时一起组合编译成另外一种对象文件-可执行对象文件。链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)(内核可加载模块 .ko 文件也是 Relocatable object file

可执行对象文件

​ 可执行对象文件(executable object file):以某种形式包含二进制代码和数据,特点是可以直接被复制到内存中并执行。我们比如常见的应用程序,可以执行的命令等都是可指向对象文件。

共享对象文件

​ 共享对象文件(shared object file):这是一种特殊形式的可重定位对象文件, 这些就是所谓的动态库文件,可以在加载时(load time)或运行时(run time)被动态加载进内存并链接。(例如共享库.so文件)

​ 编译器(CC)和汇编器(AS)生成可重定位对象文件(包括共享对象文件),链接器(LD)生成可执行对象文件。

2. ELF文件视图

​ 首先,如下图所示,ELF文件格式提供了两种视图,分别是链接视图执行视图

elf1

​ 链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接视图来看的,右侧的视角是执行视图来看的。总个文件可以分为四个部分:

  • ELF header: 描述整个文件的组织。
  • Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的,关于进程如何被操作系统加载请参考:进程执行
  • sections 或者 segmentssegments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segmentssections是包含的关系,一个segment包含若干个section
  • Section Header Table: 包含了文件各个section的属性信息,我们都将结合例子来解释。

elf2

​ 我们写如下代码来测试:

#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

int bss_var;
int data_var=100;

void text_var()
{
        printf("test function\r");
}

int main(int argc, char *argv[] )
{
        int stack_var;
        int fd;
        int *heap_var;
        void *mmap_var;

        fd=open("1.tst", O_CREAT|O_RDWR, 0777);
        mmap_var = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        munmap(mmap_var, 4096);
        heap_var = malloc(16);
        while(1){
            printf("stack_var=%p\n", &stack_var);
            printf("mmap_var=%p\n", mmap_var);
            printf("heap_var=%p\n",heap_var);
            printf("bss_var=%p\n",&bss_var);
            printf("data_var=%p\n", &data_var);
            printf("text_var=%p\n", &text_var);
            sleep(10);
        }
        return 0;
}

使用gcc -o addr_test addr_test.c, 我的机子上面的GCC默认是打开了PIE选项的,所以生成的是位置无关的代码。关于位置无关可以参考:这篇文章

链接视图

如下图,可以通过执行命令readelf -S addr_test来查看该可执行文件中有哪些section。在后面的章节中会对每一个section进行详细的说明。

image-20210125152925111

image-20210125153241367

执行视图

​ 通过执行命令readelf -s addr_test,可以查看该文件的执行视图。如下图所示,该可执行文件一共有9个segment, 每一个segment的偏移、大小等信息均可以通过下图看到。并且每个segment包好哪些section也可以在下图中的Section to Segment mapping中看到:例如:标号为02的为代码段:它包含了.interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame这些section. 。

image-20210125153519991

为何区分链接视图以及执行视图

segmentsection的一个集合,sections按照一定规则映射到segment。那么为什么需要区分两种不同视图呢。

​ 当ELF文件被加载到内存中后,系统会将多个具有相同权限section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。

3. ELF Header

​ 我们来观察一下ELF Header的结构体:

typedef struct elf32_hdr{
  unsigned char	e_ident[EI_NIDENT]; //作为开头主要用装一些标示信息,固定值来判断ELF文件的合法性,其开头4个字节固定为0x7f, ELF三个字符。
  Elf32_Half	e_type;   //是用来标志文件类型的比如有1.可重定位文件,2.可执行文件,3.共享目标文件。
  Elf32_Half	e_machine; //来用标志运行文件的机器类型
  Elf32_Word	e_version; //文件的版本
  Elf32_Addr	e_entry;   //Entry point 程序的入口的虚拟地址,作为执行文件可以作为执行向量地址*/
  Elf32_Off	e_phoff;       //表示程序表头相对于文件头的偏移量, e_phoff加上文件的头地址就可以定位到程序头表的位置
  Elf32_Off	e_shoff;  		//表示节头表相对于文件头的编移量, e_shoff加上文件的头地址就可以定位到节头表的位置
  Elf32_Word	e_flags;
  Elf32_Half	e_ehsize;	 // 表示ELF头部的大小(单位字节)
  Elf32_Half	e_phentsize; //表示每个程序头表条目大小(单位字节),每个segment头部的大小
  Elf32_Half	e_phnum;  	 // 表示有多少个程序头条目,多少个segment
  Elf32_Half	e_shentsize; //表示每个节头表(section header)的条目大小 
  Elf32_Half	e_shnum;     //表示有多少个节头表条目, 多少个section
  Elf32_Half	e_shstrndx;  //包含节名称的字符串,作为一个符号表
} Elf32_Ehdr;

​ 接着运行readelf -h addr_test命令,可以看到文件中ELF Header结构的内容。

image-20210125155237873

​ 关于每一个字段的含义,可以查看我在网络上面找的另外一张图片:这里我懒得画图了,就直接把网络上面的图贴出来。

这里写图片描述

​ 或者使用010Editor的ELF模板也可以看到ELF Header结构。强烈建议大家用010Editor来分析二进制,它会帮你把sectionsegment解析出来。如下图

image-20210125160633231

​ 至于该软件如何使用、安装,请自行百度安装使用。

​ 在ELF Header中我们需要重点关注以下几个字段:

e_entry

程序入口地址 .o文件的进入点是0x0(e_entry),这表明Relocatable objects file不会有程序进入点,所谓程序进入点是指当程序真正执行起来的时候,其第一条要运行的指令的运行时地址。而Relocatable objects file只是供再链接而已,所以它不存在进入点。

可执行文件动态库.so都存在所谓的进入点,且可执行文件的e_entry指向C库中的_start,而动态库.so中的进入点指向 call_gmon_start

关于如何查找程序的入口点请参考:查找linux可执行文件入口地址。https://reverseengineering.stackexchange.com/questions/18088/start-analysis-at-any-position-in-elf-is-entry-point

4. 链接视图中的section

4.1 Section Header Table

​ 一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section header table决定。在section header table中,针对每一个section,都设置有一个条目,用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。

​ 查看如下代码,描述了section header table的信息:(linux 2.6.11)


typedef struct {
  Elf32_Word	sh_name;  //sh_name表示节区名称,sh_name值实际上是.shstrtab中的索引,该string table中存储着所有section的名字
  Elf32_Word	sh_type;  //节区类型
  Elf32_Word	sh_flags; //与节区相关的标记
  Elf32_Addr	sh_addr;  //表示节区在第一个字节应处的位置
  Elf32_Off	sh_offset;  //表示节区第一个字节相对文件头的偏移, sh_offset加文件头地址可以定位到该节区的头地址
  Elf32_Word	sh_size;  //表示节区的大小(单位字节)
  Elf32_Word	sh_link;  //表示节区头部表索引链接
  Elf32_Word	sh_info;  //节区的附加信息
  Elf32_Word	sh_addralign; //sh_addralign用于地址对齐
  Elf32_Word	sh_entsize; //sh_entsize表示符号表相关
} Elf32_Shdr;

​ 用010edtor查看可执行文件, 我们可以看到Section Header Table中确实有29个条目。

elf_header显示有29个section

image-20210125164803657

​ 如下图果然有29个section

image-20210125165119422

​ 且索引为28确实为section header section string table

image-20210125165410715

从上图我们可以看到,每个entry的具体字段和Elf32_Shdr是对应的(这里我代码贴的是32位的,而可执行文件是64位的,稍微有点差异),.shstrtab的内容在这个在文件中的偏移为s_offset所指示,也就是0x196A。我们随便找另外一个节来找该节的名字:

这个节的sh_name.shstrtab中的偏移是0xea, 故在文件中的偏移为0x196A + 0xea = 0x1a54

image-20210125170104200

我们查看文件中0x1a54中的内容,果不其然,这里存放了.data这个字符串。(sh_name值实际上是.shstrtab中的索引,该string table中存储着所有section的名字)

image-20210125170538555

4.2 一些重要的Section

​ 接下来我们逐步分析一些可执行文件中一些重要的Section,包括符号表、重定位表、GOT表等。

4.2.1 符号表

符号表(.dynsym)

​ 符号表包含用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。关于编译器如何修饰符号,请Google相关资料,这里不再花时间阐述。

.dynsym 节保存在 text 段中。其保存了从共享库导入的动态符号表

​ 32位系统符号表项的格式如下:关于符号表的更加详解的解释,请参考:耶律大学ELF文档北京大学ELF课件这两份资料

typedef struct elf32_sym{
  Elf32_Word	st_name;  //符号表项名称(符串表索引(offset))。如果该值非0,则表示符号名字在.dynstr中的索引,否则符号表项没有名称。
  Elf32_Addr	st_value; //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
  Elf32_Word	st_size;  //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
  unsigned char	st_info;  //符号的类型和绑定属性。
  unsigned char	st_other; //这个字段未定义
  Elf32_Half	st_shndx; //每个符号表项都以和其他节区的关系的方式给出定义。此成员给出相关的节区头部表索引。
} Elf32_Sym;

readelf查看addr_test的符号表如下:

image-20210125173127474

符号表(.symtab)

.symtab节是一个 Elf32_Sym 的数组,保存了这个可执行文件或者是.so中的所有符号信息。.dynsym.symtab的一个子集,实际代码运行中其实只需要

.dynsym符号表就足够了。关于两个符号表的区别请参考:动态链接库中的.symtab和.dynsym。也请参考其他资料,以便于进一步区分。

readelf查看addr_test.symtab符号表如下:

image-20210125174845250

image-20210125174920806

4.2.2 字符串表

字符串表(.dynstr

​ 符号表的st_name是符号名的字符串表中的索引,那么字符串表中肯定存放着所有符号的名称字符串。.dynstr 保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。下面,我们先来看一看字符串表的section header表项:image-20210125180036782

​ 再看一下下图中字符串表的具体内容,字符串表.dynstr的内容在elf文件偏移0x3D8处,我们把这段内容显示出来:

image-20210125180341038

​ 看到了吧,里面的内容都是字符串,这个字符串表.dynstr是给符号表(.dynsym)使用的。而.strtab 节保存的符号字符串表,表中的内容会被 .symtabElfN_Sym 结构中的 st_name 引用。这点需要注意区分。

字符串表(.strtab

.strtab 节保存的符号字符串表,表中的内容会被 .symtabElfN_Sym 结构中的 st_name 引用, 他的内容格式实际上和.dynstr是一样的,这里不再具体说明,感兴趣的同学可以自行搜索两者之间的差异。

4.2.3 重定位表

​ 重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。 ​ 换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。 具体来说,就是把符号的value进行重新定位。

​ 可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。重定位表项的格式如下:

typedef struct {  
    Elf32_Addr r_offset;     //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
    Elf32_Word r_info;       //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
                             //其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
} Elf32_Rel; 

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)123

再看一下重定位表中的内容, 使用readelf -r addr_test来读出重定位表内容。

image-20210125181736690

​ 常见的重定位表类型

.rel.text

​ 重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,它由编译器编译产生,存在于obj文件内。链接器链接时,它用于最终可执行文件或者动态库的重定位。通过它修改源obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。

.rel.dyn

​ 重定位的地方在.got 段内。主要是针对外部数据变量符号。例如全局数据。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。

.rel.plt

​ 重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got.plt内,以后读取该.got.plt就直接得到最终函数地址。在重定位过程中,动态链接器根据r_offset找到.got.plt对应表项,来完成对.got.plt表项值的修改。

.plt段(过程链接表)

​ 所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是: ​ 调用对应桩函数—>桩函数取出.got.plt表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got.plt表对应处,同时跳转执行该地址指令.以后桩函数从.got.plt取得地址都是真实函数地址了。

.got(全局偏移表)

​ 包含.got(变量)以及.got.plt(函数)

.rel.dyn.rel.plt是动态定位辅助段。由链接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got.got.plt段,从而实现运行时重定位。关于延迟绑定以及重定位,请参考:地址无关代码延迟绑定理解重定位

其他section的内容,请参考耶律大学ELF文档北京大学ELF课件这两份资料。

5、执行视图中的Segment

5.1. Program Header Table

​ 程序头部(Program Header)描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息。可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的“段”包含一个或者多个“节区”,也就是“段内容(Segment Contents)”。程序头部仅对可执行文件共享目标文件有意义。程序头部的数据结构如下:

typedef struct {  
    Elf32_Word p_type;           //此数组元素描述的段的类型,或者如何解释此数组元素的信息。 
    Elf32_Off  p_offset;         //此成员给出从文件头到该段第一个字节的偏移
    Elf32_Addr p_vaddr;          //此成员给出段的第一个字节将被放到内存中的虚拟地址
    Elf32_Addr p_paddr;          //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
    Elf32_Word p_filesz;         //此成员给出段在文件映像中所占的字节数。可以为0。
    Elf32_Word p_memsz;          //此成员给出段在内存映像中占用的字节数。可以为0。
    Elf32_Word p_flags;          //此成员给出与段相关的标志。
    Elf32_Word p_align;          //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;

我们看到,以下两个工具确实是照此格式解析的。

readelf工具查看所有的段如下:

image-20210125153519991

010editor查看第一个可加载段:确实是按Elf32_phdr进行解析的。(32位和64位有少许差异)

image-20210125190333657


5.2 代码的位置无关性

​ 关于位置无关性具体原理,请参考博客深入理解 Linux 位置无关代码 PIC。 要实现位置无关性代码,有一点原则比较重要,那就是代码段和数据段之间的相对偏移是一个固定值才行。两个关键点

关键点#1 - 代码段和数据段的偏移

​ 代码段和数据段之间的偏移,在链接的时候由链接器给出,对于PIC来说非常重要。当链接器将各个目标文件的所有p组合到一起的时候,链接器完全知道每个p的大小和它们之间的相对位置。也就是说:要实现位置无关,代码段和数据段在代码加载到内存后的相对偏移一定是不变的

img

​ 代码段和数据段偏移示例

​ 如上图所示,示例中这里TEXT和DATA时紧紧挨着的,其实无论DATA和TEXT是否是相邻的,链接器都能知道这两个段的偏移。根据这个偏移,可以计算出在TEXT段内任意一条指令相对于DATA段起始地址的相对偏移量。如上图,无论TEXT段被放到了哪个虚拟地址上,假设一条mov指令在TEXT内部的0xe0偏移处,那么我们可以知道,DATA段的相对偏移位置就是:TEXT段的大小 - mov指令在TEXT内部的偏移 = 0xXXXXE000 - 0xXXXX00E0 = 0xDF20

关键点#2 - X86上指令相对偏移的计算

​ 如果使用相对位置进行处理,可以看到代码能够做到位置无关。但在X86平台上mov指令对于数据的引用需要一个绝对地址,那应该怎么办呢?

从“关键点1”里的描述来看,我们如果知道了当前指令的地址,那么就可以计算出数据段的地址。X86平台上没有获取当前指令指针寄存器IP的值的指令(X64上可以直接访问RIP),但可以通过一个小技巧来获取。来看一段伪代码:

img

​ X86平台获取指令地址汇编

​ 这段代码在实际运行时,会有以下的事情发生:

  • 当cpu执行 call STUB的时候,会将下一条指令的地址保存到stack上,然后跳到标签STUB处执行。
  • STUB处的指令是pop ebx,这样就将 “pop ebx”这条指令所在的地址从stack弹出放到了ebx寄存器中,这样就得到了IP寄存器的值。

5.3 可执行程序加载到内存后是如何保证代码和数据段的相对位置不变的

​ 我们来实际看下可执行文件中代码段和数据段的虚拟地址:从中可以看到带PIE的可执行程序的代码段的虚拟地址是:0x0000000000000000

而数据段的虚拟地址是0x0000000000200D90, 在可执行文件中虚拟地址之差是:

0x0000000000200D90-0x0000000000000000=0x200D90

image-20210125193159452

我们将程序运行起来,发现代码段段映射的地址是0x56170d519000 ~ 0x56170d51a000, 由于地址映射必须是按页对齐的,所以代码段的大小有2888个字节,但是这里映射了一个内存页(0x56170d51a000 - 0x56170d519000 = 0x1000 = 4k),而数据段的映射地址是:0x56170d719000 ~ 0x56170d71a000,也映射了4K大小。数据段和代码段之间的偏移是0x56170d719000 - 0x56170d519000 = 0x200000, 那么问题来了前面可执行程序中的数据段和代码段之间的偏

image-20210125193222880

移是0x200D90,显然对不上。如果偏移变了,那么位置无关性岂不是乱了套,代码段的代码无法通过相对偏移来访问数据段的内容。于是我又仔细看了下内核加载elf文件的相关代码,果然发现了奥妙。代码如下(linux_2.6.11):elf_map是把可执行文件中的各个段和地址空间进行映射的函数。

可执行文件的加载代码流程:do_execve-->load_elf_binary,不同内核版本可能会有差异,总的来说是以exec的系统调用为入口点。

/**
 * 为当前进程创建并初始化一个新的线性区。
 * 分配成功后,可以把这个新的线性区与进程已有的其他线性区进行合并。
 * file,offset-如果新的线性区将把一个文件映射到内存,则使用文件描述符指针file和文件偏移量offset.当不需要内存映射时,file和offset都会为空
 * addr-这个线性地址指定从休息开始查找一个空闲的区间。
 * len-线性地址区间的长度。
 * prot-这个参数指定这个线性区所包含页的访问权限。可能的标志有PROT_READ,PROT_WRITE,PROT_EXEC和PROT_NONE.前三个标志与VM_READ,VM_WRITE,WM_EXEC一样。PROT_NONE表示没有以上权限中的任意一个
 * flag-这个参数指定线性区的其他标志。MAP_GROWSDOWN,MAP_LOCKED,MAP_DENYWRITE,MAP_EXECURABLE
 */
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
	unsigned long len, unsigned long prot,
	unsigned long flag, unsigned long offset)
{
	unsigned long ret = -EINVAL;
	/**
	 * 首先检查是否溢出。offset 加上 商都,如果小于offset那么说明发生了溢出
	 */
	if ((offset + PAGE_ALIGN(len)) < offset)
		goto out;
	/**
	 * 检查是否对齐页。,也就是offset是否是4K对齐
	 */
	if (!(offset & ~PAGE_MASK))
		ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
	return ret;
}

static unsigned long elf_map(struct file *filep, unsigned long addr,
			struct elf_phdr *eppnt, int prot, int type)
{
	unsigned long map_addr;

	down_write(&current->mm->mmap_sem);

	//ELF_PAGESTART(addr)线性区开始地址,以页(4K)对齐
    //eppnt->p_filesz 这个段在文件中的长度
	//prot为访问权限
	//type-这个参数指定线性区的其他标志。MAP_GROWSDOWN,MAP_LOCKED,MAP_DENYWRITE,MAP_EXECURABLE,MAP_FIXD等
	//eppnt->p_offset为这个段从文件头的偏移
	//ELF_PAGEOFFSET, 是这个p_vaddr虚拟地址的最后12位在一个页里面的偏移
	map_addr = do_mmap(filep, ELF_PAGESTART(addr),
			   eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr), prot, type,
			   eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr));
	up_write(&current->mm->mmap_sem);
	return(map_addr);
}

do_mmap的含义就是从ELF_PAGESTART(addr)开始找一段虚拟地址来映射filep指向的文件的偏移为 eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr)的内容,而映射长度为eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr)。前面我们说到映射的虚拟地址必须是页对齐的:

数据段和代码段偏移

上面代码的含义是可执行文件数据段的开始内容,并没有映射到0x56170d51a000,而是把数据段向前偏移ELF_PAGEOFFSET(eppnt->p_vaddr)的内容映射到

0x56170d51a000,那么这样实际上数据段在内存中的开始地址就是0x56170d51aD90, 那么数据段和代码段的偏移实际上就是0x56170d51aD90 - 0x56170d519000 = 0x200D90,就和可执行文件中的偏移对应上了,从而就可以实现位置无关性代码了。

​ 关于可执行文件的装载,请参考如下文章:可执行文件装载Linux进程替换(exec)动态链接.

6. 参考链接

1、耶律大学的文档

2、为什么会Permission denied

3、Linux进程替换(exec)

4、https://www.mdeditor.tw/pl/gbsQ

5、https://github.com/tinyclub/open-c-book/blob/master/zh/chapters/02-chapter4.markdown

6、理解重定位

7、延迟绑定

8、动态链接

9、通过一道pwn题详细分析retdlresolve技术

10、使用反汇编理解动态库函数调用方式GOT/PLT

11、通过 GDB 调试理解 GOT/PLT

12、https://www.h3399.cn/201810/628457.html

13、https://markrepo.github.io/kernel/2018/08/19/dynamic-link/

14、https://markrepo.github.io/kernel/2018/08/17/load-and-process/