存储器层次结构——代码如何变成程序

简介:

  本章基于linux主要讲解l编辑好的hello.c文件如何从一个存储介质上的文件编译为可执行程序,以及加载到内存执行的过程。

  第一节讲述文本方式的代码及在介质上的存储方式(ELF文件),以及关于文本如何编译成可执行文件的简单介绍。

  第二节讲述可执行文件如何加载到内存中,涉及虚拟内存和文件如何加载到内存中并执行的过程。

一:文件方式存储的代码

  1.1 代码编写

    本文以如下代码从文本方式存储在存储介质:hello.c

#include<stdio.h>
int main()
{
	printf("Hello World!
");
	return 0;
}

  1.2 代码编译

    GCC可以很方便的帮我们编译文件,因为不涉及文件方式的改变,这一部分简略写过,想详细了解的可以参考编译相关书籍,毕竟这又是另一个很长的故事了。

    • 源文件预编译展开头文件,gcc命令很简单就是把C文件包含的头文件都展开到了生成的文件中,输出太长可以自行实验。命令: gcc -i hello.c > hello.i
    • 利用预编译后的文件生成汇编文件,可以生成一个叫做hello.s的汇编文件如下。命令: gcc -s hello.i
              .file   "hello.c"
              .text
              .section        .rodata
      .LC0:
              .string "Hello World!"
              .text
              .globl  main
              .type   main, @function
      main:
      .LFB0:
              .cfi_startproc
              pushq   %rbp
              .cfi_def_cfa_offset 16
              .cfi_offset 6, -16
              movq    %rsp, %rbp
              .cfi_def_cfa_register 6
              leaq    .LC0(%rip), %rdi
              call    puts@PLT
              movl    $0, %eax
              popq    %rbp
              .cfi_def_cfa 7, 8
              ret
              .cfi_endproc
      .LFE0:
              .size   main, .-main
              .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
              .section        .note.GNU-stack,"",@progbits  
    • 生成可重定位目标文件hello.o, 命令:as -o hello.o hello.s 或者gcc -c hello.c
    • 生成可执行目标文件hello,命令: gcc -o hello hello.c(本来想用ld命令,不过很多依赖的东西需要找,这目前不在讨论范围所以暂时不提供ld编译的命令,如果想看ld做了什么可以用这个命令看一下: gcc hello.c -o hello -Wl,-v  
      • 总算找出ld的命令了,真是复杂,机器不同需要搜索自己机器上的库文件对应的位置否则会不匹配,不过.o文件的名字是一样的,命令如下:ld -static /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello
      • 实验结果截图:

   1.3    代码转变成目标文件

    代码转变成目标文件的过程就是上面生成可重定位目标文件的过程,将hello.s汇编文件和库文件相关的汇编代码的机器码抽取,并且添加上目标文件的头部信息,共同组成了ELF格式(linux为例)的可重定位目标文件。

    ELF的目标文件分为三类:

      • 可重定位目标文件(.o)
        • 其代码和数据可和其他可重定位文件合并为可执行文件
        • 每个 .o 文件由对应的 .c 文件生成
        • 每个 .o 文件的代码和数据地址都是从0开始的偏移
      • 可执行目标文件(默认为a.out)
        • 包含的代码和数据可以被直接复制到内存并执行
        • 代码和数据的地址是虚拟地址空间中的地址
      • 共享的目标文件(.so 共享库)
        • 特殊的可重定位目标文件,能在装载到内存或运行时自动被链接,称为共享库文件

    使用objdump -D反汇编最后生成的可重定位目标文件hello.o,得到汇编对应的机器码,使用二进制方式打开hello.o文件进行对比;

    可以观察反汇编文件中的main函数开始的机器码开始位置为55 48 80 e5:

    

    二进制方式打开hello。o文件,可以找到对应的机器码。上方的64字节可以根据零星的文本判断是ELF文件头信息。

    

  1.4 链接--可重定位目标文件到可执行目标文件

    程序连接主要包括两部分,使连接后的程序可以被加载器加载到内存特定的位置,本文之叙述了基本概念来串联从代码到程序执行的过程,详细的内容后续有机会再重开一篇文章补充(如果感兴趣可以参考《链接器和加载器》或者《深入理解计算机系统》第三版第七章 或《程序员的自我修养——链接、装载和库》)。

    重定位:编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必须被加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。在很多系统中,重定位不止进行一次。对于链接器的一种普遍情景是由多个子程序来构建一个程序,并生成一个链接好的起始地址为0的输出程序,各个子程序通过重定位在大程序中确定位置。当这个程序被加载时,系统会选择一个加载地址,而链接好的程序会作为整体被重定位到加载地址。

    符号解析:当通过多个子程序来构建一个程序时,子程序间的相互引用是通过符号进行的;主程序可能会调用一个名为sqrt的计算平方根例程,并且数学库中定义了sqrt例程。链接器通过标明分配给sqrt的地址在库中来解析这个符号,并通过修改目标代码使得call指令引用该地址。

    1.3章节中可以看到未进行链接的文件的各个段的位置都是0,实际上程序一般是不允许从0地址开始运行的,更何况所有代码段的地址都是0的话加载会前后覆盖,所以肯定是不行的,这就涉及到了代码的重定位,使代码可以加载到能运行的位置。

    同样的我们在main.c中调用了printf这个函数,汇编对应的是 e8 00 00 00 00 callq 10 <main+0x10>, 可是在hello.c中并没有printf这个函数,它其实是libc中进行的实现我们使用#include<stdio.h>将他的符号扩展过来了,所以我们在hello.o中并不知道这个符号具体含义是什么也不知道它在哪儿,这个就是链接器需要做的符号解析的工作,将printf找出来使程序可以正常调用。加黑的部分是汇编的机器码:e8表示mov指令,后面的是函数地址,可以看到这里的地址是一个无效值00 00 00 00.

    1.4.1 ELF文件格式

      ELF分为两种视图:

      • 链接视图:可重定位文件(Relocatable object files)
      • 执行视图:可执行目标文件(Executable object files)  

      

     可以看出比较明显的区别是链接视图有节区(section)执行视图替换成了段区(segment),这是因为section太过零散所以链接的时候把相同性质的section(比如可读写属性的所有section/所有可读可执行的section)组合到一个段中。这么做的好处是在加载进内存的时候可以有效的减少内存碎片的产生(因为内存加载一般需要按页对齐,且每个段单独加载,这样如果section大小不足一页4096Byte也需要占一页的空间就造成了很多内存碎片)。

    另外,执行视图的文件一般是由多个可重定位文件组成的,这就涉及到了这些文件的组合规则,组合的规则一般是使用链接脚本进行控制,所以下一节讲链接脚本。

      1.4.1.1 ELF文件头

        我们可以使用readelf -h查看文件的ELF头,如下图所示:

        

      可以看出ELF头中定义了ELF文件魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度、及段的数量等。

      ELF格式定义位置在/usr/include/elf.h的Elf32_Ehdr,结构如下

#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;                          //程序入口的虚拟地址,若没有,可为0
    Elf32_Off e_phoff;                            //程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0
    Elf32_Off e_shoff;                            //节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0
    Elf32_Word e_flags;                        //保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。
    Elf32_Half e_ehsize;                        //ELF 头部的大小(以字节计算)。
    Elf32_Half e_phentsize;                   //程序头部表格的表项大小(按字节计算)。
    Elf32_Half e_phnum;                      //程序头部表格的表项数目。可以为 0。
    Elf32_Half e_shentsize;                  //节区头部表格的表项大小(按字节计算)。
    Elf32_Half e_shnum;      //节区头部表格的表项数目。可以为 0。
    Elf32_Half e_shstrndx;  //节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。
}Elf32_Ehdr;

      1.4.1.2 段表

        可重定位目标文件有很多各种各样的段,段表(section header table)就是保存这些段基本属性的结构。如:每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。段表在文件中的位置是由ELF文件头中的“e_shoff”决定。

        可以有两种方式查看段表信息:1. objdump -h hello.o查看ELF中的一些关键段。 2. readelf -S hello.o 查看ELF中详细的段信息

        

         段表是由“Elf32_Shedr"(段描述符)的结构体为元素的数组,段描述符结构如下所示,文件位置/usr/include/elf.h

typedef struct
{
  Elf32_Word    sh_name;                /* Section name (string tbl index) */
  Elf32_Word    sh_type;                /* Section type, */
  Elf32_Word    sh_flags;               /* Section flags,该段在进程虚拟空间中的属性,如是否可写是否可执行等 */
  Elf32_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf32_Off     sh_offset;              /* Section file offset */
  Elf32_Word    sh_size;                /* Section size in bytes */
  Elf32_Word    sh_link;                /* Link to another section */
  Elf32_Word    sh_info;                /* Additional section information */
  Elf32_Word    sh_addralign;           /* Section alignment */
  Elf32_Word    sh_entsize;             /* Entry size if section holds table */
} Elf32_Shdr;

       1.4.1.4 程序头表

        可执行目标文件同样有很多的段Segment信息,这些信息是将可执行文件中属性相同的段Section组合到了一起以方便加载时进行映射方便节省空间。从段表一节可以看到我们的程序有13个Section,使用readelf -l hello可以查看程序头表——即Section合并后的Segment信息。

      

        可以看到合并后13个Section只剩下了6个。这里我们主要关注两个LOAD段,这两个段是会加载到内存中去的,其余的都是一些辅助段不涉及加载,我们暂不分析。Section和Segment的转换关系可以参照下图,VM0和VM1分别表示两个LOAD,只是他们有不同的属性(如可读可执行和可读可写)。程序头表就描述了这写Segment在虚拟内存和物理存储中的位置以方便加载进行。

    

        同样,在内核代码中程序头表是以结构体方式定义的,它仍然在/usr/include/elf.h中可以找到

typedef struct
{
  Elf32_Word    p_type;                 /* Segment type,暂只关注LOAD类型,其他还有动态链接DYNAMIC等 */
  Elf32_Off     p_offset;               /* Segment file offset, Segment在文件中的偏移 */
  Elf32_Addr    p_vaddr;                /* Segment virtual address, Segment在虚拟地址中第一个字节的位置 */
  Elf32_Addr    p_paddr;                /* Segment physical address, Segment的物理装载地址 */
  Elf32_Word    p_filesz;               /* Segment size in file, Segment在文件中所占空间的大小 */
  Elf32_Word    p_memsz;                /* Segment size in memory, Segment在虚拟空间中所占用的长度 */
  Elf32_Word    p_flags;                /* Segment flags ,权限属性,如R W X*/
  Elf32_Word    p_align;                /* Segment alignment , 对齐属性,如两字节对齐*/
} Elf32_Phdr;

      1.4.1.3 重定位表

        在readelf -S获取的段表中可以看到一个名为.rela.text的段,它的类型为RELA,也就是说这是一个重定位表(Relocation Table)。链接器在处理目标文件时,需要对目标文件某些部位进行重定位即代码段和数据段中那些绝对地址的引用位置。这些重定位信息都保存在重定位表里,每一个需要重定位的段都会有一个对应的重定位表。

        重定位表定义如下:

typedef struct {
    Elf32_Addr r_offset;    //给出了重定位动作所适用的位置
    Elf32_Word r_info;    //给出要进行重定位的符号表索引,以及将实施的重定位类型.
} Elf32_Rel;
typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
    Elf32_Word r_addend;  //给出一个常量补齐,用来计算将被填充到可重定位字段的数值。
} Elf32_Rela;

      重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。

      1.4.1.4 字符串表

        ELF中使用了很多字符串,比如段名、变量名等。因为字符串长度往往时不固定的,所以用固定的结构表示它们比较困难。一种常见的作法是把字符串集中起来放到一个表,然后使用字符串在表中的偏移来引用字符串。

        

        通过这种方法,ELF文件中引用字符串只需要提供一个下标就可以,不用考虑字符串长度的问题。一般字符串表在ELF文件中存放在段”.strtab“或者”.shstrtab“中。分别表示字符串表(String Table)和段字符串表(Section Header String Table)。

        值得一提的是,ELF文件头最后一个变量”e_shstrndx“-Section Header string table index,存的就是短字符串表的位置,查看readelf -h的最后一个段数值是12,readelf -S的下标12的就是段shstrtbl。解析ELF头可以得到段表和段表名称的表位置,从而可以利用段和段名解析整个ELF文件。

    1.4.2 链接过程控制

      由于连接过程由很多内容要确定,使用那些目标文件?使用那些库文件?是否在最终可执行文件中保留调试信息、输出文件格式(可执行文件还是动态库)、还要考虑是否到处某些符号以供调试器或程序本身或其他程序使用。

       1.4.2.1  链接控制脚本

        链接器一般提供多种控制整个链接过程的方法,用来产生用户所需要的文件,一般由如下三种方法:

        • 使用命令行给链接器指定参数,如ld -o指定输出文件, -e main 指定链接后函数入口, -T *.lds指定链接脚本
        • 将链接指令存放在目标文件里面,编译器经常通过这种方法向链接器传送指令。如visual c++编译器会把链接参数放在放在PE文件的.derectve段用来传递参数
        • 使用链接控制脚本,属于最为灵活也最为强大的方法

        本节基于linux的链接脚本进行讲解,1.2节中的ld链接命令并没有指定链接脚本,在不指定的情况下将会使用linux默认的链接脚本,可以使用ld -verbose进行查看。

        默认的链接脚本放在/usr/lib/ldscripts目录下,不同的机器平台和输出文件都有不同的链接脚本。 普通的可执行文件链接脚本后缀为 *.x, 共享库的链接脚本后缀为 *.xs等。

      1.4.2.2 链接脚本语法

        ld链接器的链接脚本的链接语法继承AT&T链接器命令语言的语法,它由一系列语句组成,语句分两种,一种是命令语句,一种是赋值语句。语法与C语言相似处如下:

        • 语句间使用“;”作为分隔符, 原则上语句都需要以;作为结束符,不过对于命令语句来说可以以换行作为结束符,赋值语句则必须以“;”作为结束符
        • 表达式与运算符 脚本语言允许使用C语言类似的运算符,如: +、-、*、/、+=、-=、*=等,甚至包括&、 |、 >>、<<
        • 注释和字符引用 使用/**/作为注释,脚本文件中使用到的文件名、格式名或者段名凡是包含“;”或者其他分割符的都要使用双引号将该名字全称包含起来,

         一个简单的链接脚本示例:

ENTRY(main)

SESSIONS
{
    .=0X08048000 + SIZEOF_HEADERS;
    text : { *(.text) *(.data) *(.rodata) }
    /DISCARD/ : { *(.comment }      
}  

     ENTRY(main): 指定程序执行的入接口为main,一般程序入接口为_start。还可以使用ld的-e main,指定函数入口为main,命令行优先级比链接脚本高。 
      .=0X08048000 + SIZEOF_HEADERS : 表示当前的虚拟地址设置为0x08048000 + SIZEOF_HEADERS,
SIZEOF_HEADERS为输出文件文件头的大小。
      text : { *(.text) *(.data) *(.rodata) } : 段转换规则作用参考上方红字,含义是所有输入段中的text、data、rodata依次合并到输出文件的text段中。
     /DISCARD/ : { *(.comment } :特殊关键字/DISCARD/, 将输入中的comment段都丢弃,不放入输出文件中。

     其他一些命令语句:

      • INCLUDE filename: 包含名为 filename 的链接脚本。相当于 c 程序里的 #include 宏指令,用以包含另一个链接脚本。

      • INPUT(files): 将括号内的文件作为链接过程的输入文件。

      • OUTPUT(FILENAME): 定义输出文件的名字。

      • GROUP(files): 指定需要重复搜索符号定义的多个输入文件, file 必须是库文件,且 file 文件作为一组被 ld 重复扫描,直到不再有新的未定义的引用出现。

      • SEARCH_DIR(PATH): 定义搜索路径。

      • STARTUP(filename): 指定 filename 为第一个输入文件。

      • PROVIDE 关键字: 该关键字用于定义这类符号:在目标文件内被引用,但没有在任何目标文件内被定义的符号。

二、 加载--可执行文件放入内存
  通过前一章可以知道一个程序是如何从我们编写的代码变成一个可以执行的文件的。但是此时它仍是放在磁盘上的一个文件,并不是我们通常理解的程序--在内存上运行的一段代码。  
  程序运行在内存上,所以首先我们需要了解虚拟内存的一些基本知识,然后我们以linux上在shell会话中执行./hello这一命令来跟踪可执行文件是如何在内存中运行起来的。

  2.1 执行shell时发生了什么
    我们可以使用strace命令跟踪程序执行的过程,在shell执行脚本时其实是根据文件开头的脚本类型声明调用对应的脚本解释器,如果shell发现执行的是可执行程序则会调用execv系统调用。
    

     可以看出shell调用了一个叫做execve的系统调用来执行hello这个程序,系统调用如何执行到的下一篇再分析,先在这里留个坑。最终会执行内核的d__do_execve_file这个函数,我们接下来分析它时如何执行ELF格式的文件的。

    2.2  execve

    函数位置: fs/exec.c,基本调用关系如下图,前边流程是准备过程,最后一步红线标注的为调用可执行文件的过程,函数的简化流程见下方
    


static int __do_execve_file(int fd, struct filename *filename, struct user_arg_ptr argv,
struct user_arg_ptr envp,
                int flags, struct file *file)
{
    char *pathbuf = NULL;
    struct linux_binprm *bprm;
    struct files_struct *displaced;
    int retval;
。。。
retval = unshare_files(&displaced); //不使用shell程序打开文件 bprm
= kzalloc(sizeof(*bprm), GFP_KERNEL); retval = prepare_bprm_creds(bprm);  //对文件进行安全策略检测 if (!file) file = do_open_execat(fd, filename, flags); //因为传入的file为null,在此处就根据filename打开可执行文件 sched_exec(); //在这个函数中使用调度类对当前的进程进行调度 bprm->file = file; bprm->filename = filename->name; bprm->interp = bprm->filename; retval = bprm_mm_init(bprm); //初始化bprm的mm结构体,即内存相关分配,主要是初始化了mm_struct retval = prepare_arg_pages(bprm, argv, envp); //计算出入参和环境变量的数量 retval = prepare_binprm(bprm); //填充gid和uid用于权限管理,并且使用elf的前128字节填充buf数组 retval = copy_strings_kernel(1, &bprm->filename, bprm); //拷贝文件名到新分配的页面中 bprm->exec = bprm->p; retval = copy_strings(bprm->envc, envp, bprm); //拷贝环境变量,因为栈向下增长所以先拷贝环境变量会使它处在栈中入参的后方 retval = copy_strings(bprm->argc, argv, bprm); //拷贝入参 would_dump(bprm, bprm->file); retval = exec_binprm(bprm); //这里开始执行可执行文件 /* execve succeeded */
。。。
  //下方是执行错误或成功后的处理流程,暂不分析
}

  程序中对文件的操作都使用到了这个结构体linux_binprm,在调用实际执行文件的函数时入参也是这个,结构体定义在binfmts.h中

struct linux_binprm{
    char buf[BINPRM_BUF_SIZE];  //保存课执行文件的头128个字节
#ifdef CONFIG_MMU
    struct vm_area_struct *vma;
    unsigned long vma_pages; //当前内存页的最高地址
#else
# define MAX_ARG_PAGES    32
    struct page *page[MAX_ARG_PAGES];
#endif
    struct mm_struct *mm;
    unsigned long p; /* current top of mem */
    unsigned int
        cred_prepared:1,/* true if creds already prepared (multiple
                 * preps happen for interpreters) */
        cap_effective:1;/* true if has elevated effective capabilities,
                 * false if not; except for init which inherits
                 * its parent's caps anyway */
#ifdef __alpha__
    unsigned int taso:1;
#endif
    unsigned int recursion_depth;
    struct file * file;   //要执行的文件
    struct cred *cred;    /* new credentials */
    int unsafe;        /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
    unsigned int per_clear;    /* bits to clear in current->personality */
    int argc, envc;   //命令行参数和环境变量参数
    char * filename;    /* Name of binary as seen by procps */  //要被执行的文件的名的二进制
    char * interp;        /* Name of the binary really executed. Most
                   of the time same as filename, but could be
                   different for binfmt_{misc,script} */  //要被执行的文件的真实名,通常和filename相同
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};

   2.3 execve-->load_elf_binary

    上一节找到了"__do_execve_file" -> "exec_binbprm" -> "search_binary_handler" -> "fmt->load_binary"这一条路径,那怎么到了load_elf_binary这个函数呢?我们返回search_binary_handler函数查看一下

int search_binary_handler(struct linux_binprm *bprm)
{
    bool need_retry = IS_ENABLED(CONFIG_MODULES);
    struct linux_binfmt *fmt;  //我们之前说的用于文件操作的结构体
    int retval;
//省略部分无关代码
 retry:
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {    //从formats中遍历找到符合条件的文件格式:fmt
        if (!try_module_get(fmt->module))        //如果一个模块处于活动状态
            continue;
        read_unlock(&binfmt_lock);

        bprm->recursion_depth++;
        retval = fmt->load_binary(bprm);    //调用对应格式注册的load_binary函数,bprm格式和遍历到的不一致内部会返回错误并继续搜寻剩余的,我们简化为这里找到了对应的文件,即ELF格式
        bprm->recursion_depth--;

        read_lock(&binfmt_lock);
    }
    read_unlock(&binfmt_lock);
    //省略无关代码

    return retval;
}

    可以看到,这里根据fomats为头的链表逐个遍历,找到和bprm的fmt一致的已经注册到内核的结构进行load操作,那么formats从哪儿来的呢?

    在上节的结构体linux_binprm的文件中可以找到来历,其实在search_binary_handler中就可以看到,判断文件类型是否相符的入参就是linux_binprm类型,所以formats的来历从它找准没错,我们到binfmts.h中可以找到如下代码:

/*
 * This structure defines the functions that are used to load the binary formats that
 * linux accepts.
 */
struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump;    /* minimal dump size */
} __randomize_layout;

extern void __register_binfmt(struct linux_binfmt *fmt, int insert);

/* Registration of default binfmt handlers */
static inline void register_binfmt(struct linux_binfmt *fmt)
{
    __register_binfmt(fmt, 0);
}
/* Same as above, but adds a new binfmt at the top of the list */
static inline void insert_binfmt(struct linux_binfmt *fmt)
{
    __register_binfmt(fmt, 1);
}

  __register_binfmt函数在exec.c中,所以从这里基本就可以看出来,例如处理ELF格式的模块在模块初始化时就把模块名和加载方法通过注册方式添加到formts的链表中,所以在执行文件的时候就可以根据遍历formats来寻找系统可用的格式。

static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);

void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
    BUG_ON(!fmt);
    if (WARN_ON(!fmt->load_binary))
        return;
    write_lock(&binfmt_lock);
    insert ? list_add(&fmt->lh, &formats) :
         list_add_tail(&fmt->lh, &formats);
    write_unlock(&binfmt_lock);
}

  那怎么注册进来的呢? 在fs/binfmt_elf.c可以找到答案。这部分需要linux模块加载的基本知识,不了解的可以去搜一下。简单理解就是一个模块加载进linux系统的时候会先执行一个module_init的程序初始化自己,elf注册到formats的过程就在elf模块的初始化函数处。

static int __init init_elf_binfmt(void)
{
    register_binfmt(&elf_format);
    return 0;
}

static void __exit exit_elf_binfmt(void)
{
    /* Remove the COFF and ELF loaders. */
    unregister_binfmt(&elf_format);
}

core_initcall(init_elf_binfmt);
module_exit(exit_elf_binfmt);

   好了,讲完来历下面我们可以来看一下这个函数是如何加载和执行我们的输入文件了。

2.4 load_elf_binary
  实在太长了,先加个注释吧,有时间了再试着画个图梳理下。几个相对复杂的调用下方做一些分析,可以先看下面涉及的函数辅助注释进行理解。
static int load_elf_binary(struct linux_binprm *bprm){struct file *interpreter = NULL; /* to shut gcc up */
     unsigned long load_addr = 0, load_bias = 0;
    int load_addr_set = 0;
    unsigned long error;
    struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
    unsigned long elf_bss, elf_brk;
    int bss_prot = 0;
    int retval, i;
    unsigned long elf_entry;
    unsigned long interp_load_addr = 0;
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long reloc_func_desc __maybe_unused = 0;
    int executable_stack = EXSTACK_DEFAULT;
    struct {
        struct elfhdr elf_ex;
        struct elfhdr interp_elf_ex;
    } *loc;
    struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
    struct pt_regs *regs;

    loc = kmalloc(sizeof(*loc), GFP_KERNEL);
    if (!loc) {
        retval = -ENOMEM;
        goto out_ret;
    }
    
    /* 填充ELF头信息
       在load_elf_binary之前内核已经使用映像文件的前128个字节对bprm->buf进行了填充, 
       这里使用这此信息填充映像的文件头,参考上一节内容
     */
    loc->elf_ex = *((struct elfhdr *)bprm->buf);

    retval = -ENOEXEC;
    /* First of all, some simple consistency checks 
    比较文件头的前四个字节,查看是否是ELF文件类型定义的"177ELF"*/
    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
        goto out;
    /*除前4个字符以外,还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库
    */
    if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
        goto out;
    /*  检查特定的目标机器标识   */
    if (!elf_check_arch(&loc->elf_ex))
        goto out;
    if (elf_check_fdpic(&loc->elf_ex))
        goto out;
    if (!bprm->file->f_op->mmap)
        goto out;

    /* 
        load_elf_phdrs 加载程序头表
        load_elf_phdrs函数就是通过kernel_read读入整个program header table
        从函数代码中可以看到,一个可执行程序必须至少有一个段(segment),
        而所有段的大小之和不能超过64K。
    */
    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
    if (!elf_phdata)
        goto out;

    /*
        3.     寻找和处理解释器段
         这个for循环的目的在于寻找和处理目标映像的"解释器"段。
         "解释器"段的类型为PT_INTERP,
         找到后就根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区。
         "解释器"段实际上只是一个字符串,
         即解释器的文件名,如"/lib/ld-linux.so.2"。
         有了解释器的文件名以后,就通过open_exec()打开这个文件,
        再通过kernel_read()读入其开关128个字节,即解释器映像的头部。*/
    elf_ppnt = elf_phdata;
    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {    //根据段数目逐条遍历各个段
        char *elf_interpreter;
        loff_t pos;
        /* 3.1 检查段类型是否为PT_INTERP即解释器段,不是则遍历下一个 */
        if (elf_ppnt->p_type != PT_INTERP)
            continue;

        /*
         * This is the program interpreter used for shared libraries -
         * for now assume that this is an a.out format binary.
         */
        retval = -ENOEXEC;
        if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
            goto out_free_ph;

        retval = -ENOMEM;
         /* 为动态连接器分配空间并读取加载 */
        elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
        if (!elf_interpreter)
            goto out_free_ph;

        /*  3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区  */
        pos = elf_ppnt->p_offset;
        retval = kernel_read(bprm->file, elf_interpreter,
                     elf_ppnt->p_filesz, &pos);
        if (retval != elf_ppnt->p_filesz) {
            if (retval >= 0)
                retval = -EIO;
            goto out_free_interp;
        }
        /* make sure path is NULL terminated */
        retval = -ENOEXEC;
        if (elf_interpreter[elf_ppnt->p_filesz - 1] != '')
            goto out_free_interp;

        /*    3.3 通过open_exec()打开解释器文件
            内核把新进程的堆栈中设置一些标记对,
            以指示动态链接器的相关操作,详见open_exec实现 */
        interpreter = open_exec(elf_interpreter);
        kfree(elf_interpreter);
        retval = PTR_ERR(interpreter);
        if (IS_ERR(interpreter))
            goto out_free_ph;

        /*
         * If the binary is not readable then enforce mm->dumpable = 0
         * regardless of the interpreter's permissions.
         */
        would_dump(bprm, interpreter);

        /* Get the exec headers */
        pos = 0;
        /* 3.4    通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/
        retval = kernel_read(interpreter, &loc->interp_elf_ex,
                     sizeof(loc->interp_elf_ex), &pos);
        if (retval != sizeof(loc->interp_elf_ex)) {
            if (retval >= 0)
                retval = -EIO;
            goto out_free_dentry;
        }

        break;

out_free_interp:
        kfree(elf_interpreter);
        goto out_free_ph;
    }

    elf_ppnt = elf_phdata;
    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
        switch (elf_ppnt->p_type) {
        /* 检查堆栈可执行性,保存在executable_stack */
        case PT_GNU_STACK:
            if (elf_ppnt->p_flags & PF_X)
                executable_stack = EXSTACK_ENABLE_X;
            else
                executable_stack = EXSTACK_DISABLE_X;
            break;
        /* PT_LOPROC和PT_HIPROC类型的Segment用来提供给特定的计算机体系进行检查 */
        case PT_LOPROC ... PT_HIPROC:
            retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,
                          bprm->file, false,
                          &arch_state);
            if (retval)
                goto out_free_dentry;
            break;
        }

    /*     4.    检查并读取解释器的程序表头 */
    /* Some simple consistency checks for the interpreter */
    /* 4.1    检查解释器头的信息  
     检查是否由动态连接器,无论是否有动态连接器都会执行elf文件 */
    if (interpreter) {
        retval = -ELIBBAD;
        /* Not an ELF interpreter */
        if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
            goto out_free_dentry;
        /* Verify the interpreter has a valid arch */
        if (!elf_check_arch(&loc->interp_elf_ex) ||
            elf_check_fdpic(&loc->interp_elf_ex))
            goto out_free_dentry;

        /* 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;

        /* Pass PT_LOPROC..PT_HIPROC headers to arch code */
        elf_ppnt = interp_elf_phdata;
        for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
            switch (elf_ppnt->p_type) {
            case PT_LOPROC ... PT_HIPROC:
                retval = arch_elf_pt_proc(&loc->interp_elf_ex,
                              elf_ppnt, interpreter,
                              true, &arch_state);
                if (retval)
                    goto out_free_dentry;
                break;
            }
    }

    /*
     * Allow arch code to reject the ELF at this point, whilst it's
     * still possible to return an error to the code that invoked
     * the exec syscall.
     */
    retval = arch_check_elf(&loc->elf_ex,
                !!interpreter, &loc->interp_elf_ex,
                &arch_state);
    if (retval)
        goto out_free_dentry;

    /*  Flush all traces of the currently running executable
        在此清除掉了父进程的所有相关代码 */
    retval = flush_old_exec(bprm);
    if (retval)
        goto out_free_dentry;

    /* Do this immediately, since STACK_TOP as used in setup_arg_pages
       may depend on the personality.  */
    /* 设置elf可执行文件的特性 */
    SET_PERSONALITY2(loc->elf_ex, &arch_state);
    if (elf_read_implies_exec(loc->elf_ex, executable_stack))
        current->personality |= READ_IMPLIES_EXEC;

    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
        current->flags |= PF_RANDOMIZE;

    setup_new_exec(bprm);
    install_exec_creds(bprm);

    /* Do this so that we can load the interpreter, if need be.  We will
       change some of these later
    为下面的动态连接器执行获取内核空间page */
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);
    if (retval < 0)
        goto out_free_dentry;
    /*  bss段,brk段先初始化为0  */
    elf_bss = 0;
    elf_brk = 0;
    
    /*    code代码段 */
    start_code = ~0UL;
    end_code = 0;
    
    /*  data数据段 */
    start_data = 0;
    end_data = 0;

    /* Now we do a little grungy work by mmapping the ELF image into
       the correct location in memory.
       5  装入目标程序的段segment
       这段代码从目标映像的程序头中搜索类型为PT_LOAD的段(Segment)。在二进制映像中,只有类型为PT_LOAD的段才是需要装入的。
       当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的p_vaddr域的值(上面省略这部分内容)。
       确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。
    */
  /*如果要加载的文件数据类型为ET_EXEC,则在固定地址上分配虚拟内存,因此要加上MAP_FIXED标志,
  而如果要加载的数据类型为ET_DYN,则需要从ELF_ET_DYN_BASE地址处开始映射时,在设置了PF_RANDOMIZE标志位时,需要加上arch_mmap_rnd()随机因子,将偏移记录到load_bias中。total_size为计算的需要映射的内存大小。
  再往下就通过elf_map函数将文件映射到虚拟内存中。
  如果是第一次映射,则需要记录虚拟的elf文件装载地址load_addr,如果是ET_DYN类型的数据,需要加上偏移load_bias。
  每次映射后,都要修改bss段、代码段、数据段、堆的起始位置,
  对同一个elf文件而言,start_code向上增长,start_data向下增长,elf_bss向上增长,end_code 向上增长,end_data 向上增长,elf_brk向上增长,
  因此从虚拟内存中看,从低地址到高地址依次为代码段,数据段,bss段和堆的起始地址。当装载完毕退出循环后需要将这些变量加上偏移load_bias。
  最后通过set_brk在elf_bss到elf_brk之间分配内存空间。*/
/* 按照先前获取的程序头表,循环将所有的可执行文件加载到内存中 */ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE; unsigned long k, vaddr; unsigned long total_size = 0; /* 5.1 搜索PT_LOAD的段, 这个是需要装入的 */ if (elf_ppnt->p_type != PT_LOAD) continue; if (unlikely (elf_brk > elf_bss)) { unsigned long nbyte; /* 5.2 检查地址和页面的信息 */ /* There was a PT_LOAD segment with p_memsz > p_filesz before this one. Map anonymous pages, if needed, and clear the area. */ retval = set_brk(elf_bss + load_bias, elf_brk + load_bias, bss_prot); if (retval) goto out_free_dentry; nbyte = ELF_PAGEOFFSET(elf_bss); if (nbyte) { nbyte = ELF_MIN_ALIGN - nbyte; if (nbyte > elf_brk - elf_bss) nbyte = elf_brk - elf_bss; if (clear_user((void __user *)elf_bss + load_bias, nbyte)) { /* * This bss-zeroing can fail if the ELF * file specifies odd protections. So * we don't check the return value */ } } /* * Some binaries have overlapping elf segments and then * we have to forcefully map over an existing mapping * e.g. over this newly established brk mapping. */ elf_fixed = MAP_FIXED; } elf_prot = make_prot(elf_ppnt->p_flags); elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE; vaddr = elf_ppnt->p_vaddr; /* * If we are loading ET_EXEC or we have already performed * the ET_DYN load_addr calculations, proceed normally. */ if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { elf_flags |= elf_fixed; } else if (loc->elf_ex.e_type == ET_DYN) { /* * This logic is run once for the first LOAD Program * Header for ET_DYN binaries to calculate the * randomization (load_bias) for all the LOAD * Program Headers, and to calculate the entire * size of the ELF mapping (total_size). (Note that * load_addr_set is set to true later once the * initial mapping is performed.) * * There are effectively two types of ET_DYN * binaries: programs (i.e. PIE: ET_DYN with INTERP) * and loaders (ET_DYN without INTERP, since they * _are_ the ELF interpreter). The loaders must * be loaded away from programs since the program * may otherwise collide with the loader (especially * for ET_EXEC which does not have a randomized * position). For example to handle invocations of * "./ld.so someprog" to test out a new version of * the loader, the subsequent program that the * loader loads must avoid the loader itself, so * they cannot share the same load range. Sufficient * room for the brk must be allocated with the * loader as well, since brk must be available with * the loader. * * Therefore, programs are loaded offset from * ELF_ET_DYN_BASE and loaders are loaded into the * independently randomized mmap region (0 load_bias * without MAP_FIXED). */ if (interpreter) { load_bias = ELF_ET_DYN_BASE; if (current->flags & PF_RANDOMIZE) load_bias += arch_mmap_rnd(); elf_flags |= elf_fixed; } else load_bias = 0; /* * Since load_bias is used for all subsequent loading * calculations, we must lower it by the first vaddr * so that the remaining calculations based on the * ELF vaddrs will be correctly offset. The result * is then page aligned. */ load_bias = ELF_PAGESTART(load_bias - vaddr); total_size = total_mapping_size(elf_phdata, loc->elf_ex.e_phnum); if (!total_size) { retval = -EINVAL; goto out_free_dentry; } } /* 5.3 虚拟地址空间与目标映像文件的映射 确定了装入地址后, 就通过elf_map()建立用户空间虚拟地址空间 与目标映像文件中某个连续区间之间的映射, 其返回值就是实际映射的起始地址 */ error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); if (BAD_ADDR(error)) { retval = IS_ERR((void *)error) ? PTR_ERR((void*)error) : -EINVAL; goto out_free_dentry; } if (!load_addr_set) { load_addr_set = 1; load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); if (loc->elf_ex.e_type == ET_DYN) { load_bias += error - ELF_PAGESTART(load_bias + vaddr); load_addr += load_bias; reloc_func_desc = load_bias; } } k = elf_ppnt->p_vaddr; if (k < start_code) start_code = k; if (start_data < k) start_data = k; /* * Check to see if the section's size will overflow the * allowed task size. Note that p_filesz must always be * <= p_memsz so it is only necessary to check p_memsz. */ if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz || elf_ppnt->p_memsz > TASK_SIZE || TASK_SIZE - elf_ppnt->p_memsz < k) { /* set_brk can never work. Avoid overflows. */ retval = -EINVAL; goto out_free_dentry; } k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; if (k > elf_bss) elf_bss = k; if ((elf_ppnt->p_flags & PF_X) && end_code < k) end_code = k; if (end_data < k) end_data = k; k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; if (k > elf_brk) { bss_prot = elf_prot; elf_brk = k; } } /* 更新读入内存中相关信息的记录 */ loc->elf_ex.e_entry += load_bias; elf_bss += load_bias; elf_brk += load_bias; start_code += load_bias; end_code += load_bias; start_data += load_bias; end_data += load_bias; /* Calling set_brk effectively mmaps the pages that we need * for the bss and break sections. We must do this before * mapping in the interpreter, to make sure it doesn't wind * up getting placed where the bss needs to go. */ /* 使用set_brk调整bss段的大小 */ retval = set_brk(elf_bss, elf_brk, bss_prot); if (retval) goto out_free_dentry; if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) { retval = -EFAULT; /* Nobody gets to see this, but.. */ goto out_free_dentry; } /* 6 填写程序的入口地址 这段程序的逻辑非常简单: 如果需要装入解释器,就通过load_elf_interp装入其映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值, 即解释器映像的入口地址。 而若不装入解释器,那么这个入口地址就是目标映像本身的入口地址。 */ if (interpreter) { /* 存在动态链接器 内核把控制权传递给动态链接器。 动态链接器检查程序对共享库的依赖性, 并在需要时对其进行加载,由load_elf_interp完成 */ unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); if (!IS_ERR((void *)elf_entry)) { /* * load_elf_interp() returns relocation * adjustment */ interp_load_addr = elf_entry; elf_entry += loc->interp_elf_ex.e_entry; } if (BAD_ADDR(elf_entry)) { retval = IS_ERR((void *)elf_entry) ? (int)elf_entry : -EINVAL; goto out_free_dentry; } reloc_func_desc = interp_load_addr; allow_write_access(interpreter); fput(interpreter); } else { elf_entry = loc->elf_ex.e_entry; if (BAD_ADDR(elf_entry)) { retval = -EINVAL; goto out_free_dentry; } } kfree(interp_elf_phdata); kfree(elf_phdata); set_binfmt(&elf_format); #ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES retval = arch_setup_additional_pages(bprm, !!interpreter); if (retval < 0) goto out; #endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */ /* 7 create_elf_tables填写目标文件的参数环境变量等必要信息 在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息, 这些信息包括常规的argc、envc等等,还有一些"辅助向量(Auxiliary Vector)"。 这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。 这里的create_elf_tables()就起着这个作用。 */ retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr); if (retval < 0) goto out; 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; if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) { /* * For architectures with ELF randomization, when executing * a loader directly (i.e. no interpreter listed in ELF * headers), move the brk area out of the mmap region * (since it grows up, and may collide early with the stack * growing down), and into the unused ELF_ET_DYN_BASE region. */ if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) && loc->elf_ex.e_type == ET_DYN && !interpreter) current->mm->brk = current->mm->start_brk = ELF_ET_DYN_BASE; current->mm->brk = current->mm->start_brk = arch_randomize_brk(current->mm); #ifdef compat_brk_randomized current->brk_randomized = 1; #endif } if (current->personality & MMAP_PAGE_ZERO) { /* Why this, you ask??? Well SVr4 maps page 0 as read-only, and some applications "depend" upon this behavior. Since we do not have the power to recompile these, we emulate the SVr4 behavior. Sigh. */ error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, 0); } /* 读取寄存器数据 */ regs = current_pt_regs(); #ifdef ELF_PLAT_INIT /* * The ABI may specify that certain registers be set up in special * ways (on i386 %edx is the address of a DT_FINI function, for * example. In addition, it may also specify (eg, PowerPC64 ELF) * that the e_entry field is the address of the function descriptor * for the startup routine, rather than the address of the startup * routine itself. This macro performs whatever initialization to * the regs structure is required as well as any relocations to the * function descriptor entries when executing dynamically links apps. */ ELF_PLAT_INIT(regs, reloc_func_desc); #endif finalize_exec(bprm); /* 8 最后,start_thread()这个宏操作会将eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口。 如果存在解释器映像,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。 那么什么情况下有解释器映像存在,什么情况下没有呢? 如果目标映像与各种库的链接是静态链接,因而无需依靠共享库、即动态链接库,那就不需要解释器映像; 否则就一定要有解释器映像存在。 对于一个目标程序, gcc在编译时,除非显示的使用static标签,否则所有程序的链接都是动态链接的,也就是说需要解释器。 由此可见,我们的程序在被内核加载到内存,内核跳到用户空间后并不是执行我们程序的, 而是先把控制权交到用户空间的解释器,由解释器加载运行用户程序所需要的动态库(比如libc等等), 然后控制权才会转移到用户程序。 */ /* 开始执行程序,这时已经是子进程了 */ 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_ph: kfree(elf_phdata); goto out; }
 ① load_elf_binary->flush_old_exec: 主要用来进行新进程地址空间的替换,并删除同线程组中的其他线程
int flush_old_exec(struct linux_binprm * bprm)
{
    de_thread(current);
    set_mm_exe_file(bprm->mm, bprm->file);
    exec_mmap(bprm->mm);

    bprm->mm = NULL;
    set_fs(USER_DS);
    current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD |
                    PF_NOFREEZE | PF_NO_SETAFFINITY);
    flush_thread();
    current->personality &= ~bprm->per_clear;
    return 0;
} 

   因为即将要替换新进程的地址空间,所以首先通过de_thread函数用来删除同线程组中的其他线程。

  set_mm_exe_file函数设置新进程的路径,即mm_struct中的exe_file成员变量。

  通过exec_mmap函数将新进程的地址空间设置为bprm中创建并设置好的地址空间。

  flush_thread函数主要用来初始化thread_struct中的TLS元数据信息。

  最后设置进程的标志位flags和personality,personality用来兼容linux的旧版本或者BSD等其他版本。

 ② load_elf_binary->setup_new_exec: 对新进程的新进程的mm_struct结构进行设置
void setup_new_exec(struct linux_binprm * bprm)
{
    arch_pick_mmap_layout(current->mm);
    current->sas_ss_sp = current->sas_ss_size = 0;

    if (uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid()))
        set_dumpable(current->mm, SUID_DUMP_USER);
    else
        set_dumpable(current->mm, suid_dumpable);

    perf_event_exec();
    __set_task_comm(current, kbasename(bprm->filename), true);

    current->mm->task_size = TASK_SIZE;
    if (!uid_eq(bprm->cred->uid, current_euid()) ||
        !gid_eq(bprm->cred->gid, current_egid())) {
        current->pdeath_signal = 0;
    } else {
        would_dump(bprm, bprm->file);
        if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP)
            set_dumpable(current->mm, suid_dumpable);
    }

    current->self_exec_id++;
    flush_signal_handlers(current, 0);
    do_close_on_exec(current->files);
}

  arch_pick_mmap_layout函数对设置了mmap的起始地址和分配函数。
  然后更新mm的标志位,通过kbasename函数根据文件路径bprm->filename获得最后的文件名,再调用__set_task_comm函数设置进程的文件路径,最终设置到task_struct的comm变量中。
  flush_signal_handlers用于清空信号的处理函数。最后调用do_close_on_exec关闭对应的文件。

  ③ load_elf_binary->setup_arg_pages;

int setup_arg_pages(struct linux_binprm *bprm,
            unsigned long stack_top,
            int executable_stack)
{
    unsigned long ret;
    unsigned long stack_shift;
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma = bprm->vma;
    struct vm_area_struct *prev = NULL;
    unsigned long vm_flags;
    unsigned long stack_base;
    unsigned long stack_size;
    unsigned long stack_expand;
    unsigned long rlim_stack;

    stack_top = arch_align_stack(stack_top);
    stack_top = PAGE_ALIGN(stack_top);

    stack_shift = vma->vm_end - stack_top;

    bprm->p -= stack_shift;
    mm->arg_start = bprm->p;
    bprm->exec -= stack_shift;

    ...

    if (stack_shift) {
        shift_arg_pages(vma, stack_shift);
    }

    stack_expand = 131072UL;
    stack_size = vma->vm_end - vma->vm_start;

    rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK;
    if (stack_size + stack_expand > rlim_stack)
        stack_base = vma->vm_end - rlim_stack;
    else
        stack_base = vma->vm_start - stack_expand;
    current->mm->start_stack = bprm->p;
    expand_stack(vma, stack_base);
}

  传入的参数stack_top添加了随机因子,首先对该stack_top进行页对齐,然后计算位移stack_shift,再将该位移添加到栈的指针bprm->p也即当前参数的存放地址mm->arg_start。省略的部分是对标志位的修改,再往下既然修改了栈的指针,就要通过shift_arg_pages函数修改堆栈对应的虚拟内存了。最后需要通过expand_stack函数拓展栈的大小,默认为stack_expand即4个页面。

  ④ load_elf_binary->elf_map: 

static unsigned long elf_map(struct file *filep, unsigned long addr,
        struct elf_phdr *eppnt, int prot, int type,
        unsigned long total_size)
{
    unsigned long map_addr;
    unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr);
    unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr);
    addr = ELF_PAGESTART(addr);
    size = ELF_PAGEALIGN(size);

    if (!size)
        return addr;

    if (total_size) {
        total_size = ELF_PAGEALIGN(total_size);
        map_addr = vm_mmap(filep, addr, total_size, prot, type, off);
        if (!BAD_ADDR(map_addr))
            vm_munmap(map_addr+size, total_size-size);
    } else
        map_addr = vm_mmap(filep, addr, size, prot, type, off);

    return(map_addr);
}

  传入的参数filep是文件指针,addr是即将映射的内存中的虚拟地址,size是文件映像的大小,off是映像在文件中的偏移。elf_map函数主要通过vm_mmap为文件申请虚拟空间并进行相应的映射,然后返回虚拟空间的起始地址map_addr。

  ⑤ load_elf_binary->start_thread

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    start_thread_common(regs, new_ip, new_sp,
                __USER_CS, __USER_DS, 0);
}

static void
start_thread_common(struct pt_regs *regs, unsigned long new_ip,
            unsigned long new_sp,
            unsigned int _cs, unsigned int _ss, unsigned int _ds)
{
    loadsegment(fs, 0);
    loadsegment(es, _ds);
    loadsegment(ds, _ds);
    load_gs_index(0);
    regs->ip        = new_ip;
    regs->sp        = new_sp;
    regs->cs        = _cs;
    regs->ss        = _ss;
    regs->flags     = X86_EFLAGS_IF;
    force_iret();
}

  传入的参数regs为保存的寄存器,new_ip为解释器或者应用程序的起始代码地址,new_sp为用户空间的堆栈指针。设置完这些变量后,最后通过force_iret强制返回,跳到new_ip指向的地址处开始执行。对于glibc而言,最终就会跳转到_start函数中



原文地址:https://www.cnblogs.com/edver/p/13449843.html