简介:
本章基于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] != '