欢迎访问 生活随笔!

生活随笔

当前位置: 首页 >

深入分析ELF文件结构及其载入过程

发布时间:2023/12/14 74 豆豆
生活随笔 收集整理的这篇文章主要介绍了 深入分析ELF文件结构及其载入过程 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

文章目录

    • 前言
    • ELF目标文件类型
    • 以下面例子深入分析ELF
      • 详解file命令结果的各个部分
      • ELF的文件结构
      • ELF知识扩展
    • Linux系统装载ELF的过程
      • 用户层面
      • 系统层面

前言

一般程序符号和数据,包括:全局变量,静态全局变量,全局函数,静态全局函数,外部符号(函数/变量),局部变量,局部静态变量,字面量(常量)等。程序从源码(如:C语言)到ELF二进制可执行文件,一般需要通过编译器和链接器来处理并生产。

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

ELF目标文件类型

(1)可重定位的对象文件(Relocatable file)

Linux中.o文件。这类文件包含了代码和数据,可以用来链接生成可执行或共享目标文件,静态链接库也可以归为这一类。

(3)可执行的对象文件(Executable file)

ELF可执行文件。

(3)可共享库文件(Shared object file)

Linux中.so文件。这类文件可以跟其他的重定位文件和.so文件链接,产生新的.so文件。第二种是动态链接器可以将几个这种.so文件与可执行文件结合,作为进程映像的一部分来运行。

(4) Linux下的核心转存文件(Core Dump File)

当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其它信息转存到此Dump File。

以下面例子深入分析ELF

以下面的C程序为例:

#include <pthread.h> #include <stdio.h>const char *FLAG = "[INFO]";char *infoprefixstr = "ThdID:";const int const_num = 111; int gbl_num = 222;static void * static_func(){printf("static_func be called.\n");return NULL; }void *thread_start(void *args) {printf("%s%s%ld. const_num:%d. gbl_num:%d\n",FLAG, infoprefixstr, *((pthread_t *) args),const_num, gbl_num);return static_func(); }int main(int argc, char **argv) {pthread_t thds[argc - 1];for (int i = 0; i < argc; i++) {pthread_create(&thds[i], NULL, &thread_start, &thds[i]);}for (int i = 0; i < argc; i++) {pthread_join(thds[i], NULL);}static int static_scope_var = 333;printf("main exitting......static_scope_var:%d\n",static_scope_var);return 0; }

CMakeList.txt配置如下:

cmake_minimum_required(VERSION 3.15) project(test1 C)set(CMAKE_C_STANDARD 99)add_executable(test1 main.c) target_link_libraries(test1 PUBLIC -lpthread)

编译构建产生test1二进制程序。

详解file命令结果的各个部分

使用file命令查看test1的文件详情,得到如下结果:

$ file test1/cmake-build-debug/test1 test1/cmake-build-debug/test1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=45d6523007a7906dfb699d5f6fc66f3f4b7ec720, with debug_info, not stripped

ELF 64-bit表示文件是64位ELF格式的。

LSB shared object表示ELF文件是一个共享对象。

注:“LSB executable”(ET_EXEC)和"LSB shared object"(ET_DYN)的区别是什么?

  • 在Linux内核/动态加载程序中ET_EXEC与ET_DYN的主要作用是通知可执行文件是否可以通过ASLR放置在随机存储器中。GCC在编译时,默认会增加-pie选项,使得生成的ELF是ET_DYN的。PIE可执行文件是DYN的,它们可以被地址随机化,就像共享库so一样。

注:-pie、-fpie、-fPIE、-fpie、fPIC的区别是什么?

  • -fPIE与-fpie是等价的。

  • -pie,往往和-fpie或-fPIE配合使用,用于在目标机器上生成与位置无关的可执行文件。-pie选项在链接时指定,-fpie或-fPIE选项在编译时指定。PIE(Position-Independent-Executable)是Binutils,glibc和gcc的一个功能,能用来创建能像共享库一样可重分配地址的程序,这种程序须连接到Scrt1.o。标准的可执行程序需要固定的地址,并且只有被装载到这个地址时,程序才能正确执行。PIE能使程序像共享库一样在主存任何位置装载,这需要将程序编译成位置无关,并链接为ELF共享对象。

  • -fpic,使用于在目标机支持编译共享库时使用。编译出的代码将通过全局偏移表(Global Offset Table)中的常数地址访存,动态装载器将在程序开始执行时解析GOT表项(注意,动态装载器操作系统的一部分,连接器是GCC的一部分)。而gcc中的-fPIC选项则是针对某些特殊机型做了特殊处理,比如适合动态链接并能避免超出GOT大小限制之类的错误。

  • -fPIC与-fpic都是在编译时加入的选项,用于生成位置无关的代码(Position-Independent-Code)。这两个选项都是可以使代码在加载到内存时使用相对地址,所有对固定地址的访问都通过全局偏移表(GOT)来实现。-fPIC和-fpic最大的区别在于是否对GOT的大小有限制。-fPIC对GOT表大小无限制,所以如果在不确定的情况下,使用-fPIC是更好的选择。

x86-64表示目标机CPU指令集架构。

version 1 (SYSV)表示操作系统和ABI标识符,ELF规范中包含如下几类:

Table 5. Operating System and ABI Identifiers, e_ident[EI_OSABI] Name Value Meaning ELFOSABI_SYSV 0 System V ABI ELFOSABI_HPUX 1 HP-UX operating system ELFOSABI_STANDALONE 255 Standalone (embedded) application

dynamically linked表示ELF是动态链接的。

interpreter /lib64/ld-linux-x86-64.so.2表示程序的加载器。

for GNU/Linux 3.2.0表示操作系版本号。

BuildID[sha1]=45d6523007a7906dfb699d5f6fc66f3f4b7ec720表示文件的构建码。个人理解是m

with debug_info表示ELF文件带有debug信息。

not stripped表示保留ELF的所有符号表信息,未删除一些符号表信息。如果输出的是stripped表示已经删除了ELF中一些符号表信息。

注:一般编译出来的ELF中都有符号表(symbol table),该表中包括所有的符号(程序的入口点还有变量的地址等等)。这些符号表可以用 strip工具去除,这样的话这个文件就无法让debug程序跟踪了,但是会生成比较小的可执行文件。ELF可执行文件中的符号表可以部分去除,由于部分符号在加载运行时起着重要的作用,所以用strip永远不可能完全去除elf格式文件中的符号表。对未连接的目标文件来说如果用strip去掉符号表的话,会导致连接器无法连接。

ELF文件中除了包含指令、数据,还包括符号表、调试信息、字符串等,如果是可重定位对象文件还包含链接时所须的一些信息。一般目标文件将这些信息按不同的属性以Section(节)的形式存储,有时候也叫Segment(段),在一般情况下,它们都表示一个一定长度的区域,基本上不加以区别。后面将统一称为“段”。

ELF的文件结构

基本结构如下所示:

+====================+ + ELF header + // 包含了整个文件的基本属性,如:文件版本,目标机器型号,入口地址。 +====================+ +Program header table+ // 程序标头表是一组程序标头,它们定义了运行时程序的内存布局。对于.obj文件可选的 +====================+ + .interp + // 可执行文件所需要的动态链接器的位置。 +--------------------+ + .note.ABI-tag + // 用于声明ELF的预期运行时ABI。包括操作系统名称及其运行时版本。 +--------------------+ + .note.gnu.build-id + // 表示唯一的构建ID位串。 +--------------------+ + .gnu.hash + // 符号hash表。若段名是.hash,则使用的是SYSV hash,其比gnu hash性能差。 +--------------------+ + .dynsym + // 动态符号表用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。 +--------------------+ + .dynstr + // 动态符号字符串表,用于保存符号名的字符串表。静态链接时为.strtab。 +--------------------+ + .gnu.version + // 表中条目与.dynsym动态符号表相同。每个条目指定了相应动态符号定义或版本要求。 +--------------------+ + .gnu.version_r + // 版本定义。 +--------------------+ + .rela.dyn + // 包含共享库(PLT除外)所有部分的RELA类型重定位信息。 +--------------------+ + .rela.plt + // 包含共享库或动态链接的应用程序的PLT节的RELA类型重定位信息。 +--------------------+ + .init + // 程序初始化段。 +--------------------+ + .plt + // 过程链接表(Procedure Linkage Table),用来实现延迟绑定。 +--------------------+ + .plt.got + // 暂无。。。。。 +--------------------+ + .text + // 代码段 +--------------------+ + .fini + // 程序结束段 +--------------------+ + .rodata + // 只读变量(const修饰的)和字符串变量。 +--------------------+ + .rodata1 + // 据我所知,.rodata和.rodata1是相同的。一些编译器会.rodata分为2个部分。 +--------------------+ + .eh_frame_hdr + // 包含指针和二分查找表,(一般在C++)运行时可以有效地从eh_frame中检索信息。 +--------------------+ + .eh_frame + // 它包含异常解除和源语言信息。此部分中每个条目都由单个CFI(呼叫帧信息)表示。 +--------------------+ + .init_array + // 包含指针指向了一些初始化代码。初始化代码一般是在main函数之前执行的。 +--------------------+ + .fini_array + // 包含指针指向了一些结束代码。结束代码一般是在main函数之后执行的。 +--------------------+ + .dynamic + // 保存动态链接器所需的基本信息。 +--------------------+ + .got + // 全局偏移表,存放所有对于外部变量引用的地址。 +--------------------+ + .got.plt + // 保存所有对于外部函数引用的地址。延迟绑定主要使用.got.plt表。 +--------------------+ + .data + // 全局变量和静态局部变量。 +--------------------+ + .data1 + // 据我所知,.data和.data1是相同的。一些编译器会.data分为2个部分。 +--------------------+ + .bss + // 未初始化的全局变量和局部局部变量。 +--------------------+ + .comment + // 存放编译器版本信息 +--------------------+ + .debug_aranges + // 内存地址和编译之间的映射 +--------------------+ + .debug_info + // 包含DWARF调试信息项(DIE)的核心DWARF数据 +--------------------+ + .debug_abbrev + // .debug_info部分中使用的缩写 +--------------------+ + .debug_line + // 程序行号 +--------------------+ + .debug_str + // .debug_info使用的字符串表 +--------------------+ + .symtab + // 静态链接时的符号表,保存了所有关于该目标文件的符号的定义和引用。 +--------------------+ + .strtab + // 默认字符串表。 +--------------------+ + .shstrtab + // 字符串表。 +====================+ +Section header table+ // 用于引用Sections的位置和大小,并且主要用于链接和调试目的。对于Exec文件可选 +====================+

ELF知识扩展

关于ELF格式说明的更多信息,点击查看ELF Specification、Object File Format。

关于Program Header Table的更多信息,点击查看Program Header、Program Header Table。

关于Section header table的更多信息,点击查看Section header table。

关于.debug_xxx段的更多信息,点击查看[DWARF调试格式介绍](http://www.dwarfstd.org/doc/Debugging using DWARF-2012.pdf)。

Linux系统装载ELF的过程

用户层面

bash进程会调用fork()系统调用创建一个新的进程,然后在新的进程调用execve()系统调用执行指定的ELF文件。进入execve()系统调用之后,Linux内核就开始进行真正的装载工作。

系统层面

注:以下分析将使用linux-3.18.6的内核,其他版本大同小异。

在内核中execve()系统调用相应的入口是sys_execve(),它被定义在linux-3.18.6/include/linux/syscalls.h。sys_execve()函数将调用linux-3.18.6/fs/exec.c文件中第1430行的do_execve_common函数进行处理

1427 /* 1428 * sys_execve() executes a new program. 1429 */ 1430 static int do_execve_common(struct filename *filename, 1431 struct user_arg_ptr argv, 1432 struct user_arg_ptr envp) 1433 { ... 1474 file = do_open_exec(filename); // 打开可执行文件 1475 retval = PTR_ERR(file); 1476 if (IS_ERR(file)) 1477 goto out_unmark; 1478 1479 sched_exec(); // 是一个宝贵的平衡机会,因为此时任务具有最小的有效内存和高速缓存占用空间。 1480 1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name; 1483 1484 retval = bprm_mm_init(bprm); // 创建一个新的mm_struct(将赋值给bprm->mm字段),并使用临时堆栈vm_area_struct填充它。 此时我们没有足够的上下文来设置堆栈标志,权限和偏移量,因此我们使用临时值。稍后将在setup_arg_pages()中对其进行更新。 1485 if (retval) 1486 goto out_unmark; 1487 1488 bprm->argc = count(argv, MAX_ARG_STRINGS); // 参数个数 1489 if ((retval = bprm->argc) < 0) 1490 goto out; 1491 1492 bprm->envc = count(envp, MAX_ARG_STRINGS); // 环境变量 1493 if ((retval = bprm->envc) < 0) 1494 goto out; 1495 1496 retval = prepare_binprm(bprm); // 检查文件权限,并读取文件前128个byte确定文件格式和类型 1497 if (retval < 0) 1498 goto out; ... 1513 retval = exec_binprm(bprm); // 执行 1514 if (retval < 0) 1515 goto out; ... 1547 }

do_execve_common中1496行,将调用linux-3.18.6/fs/exec.c文件中prepare_binprm函数,读取文件首128个字节来判断文件格式。(注:每种可执行文件格式的开头几个字节都是很特殊的,特别是开头的魔数Magic Number,通过对魔数的判断可以确定文件的格式和类型。)如下:

1253 /* 1254 * Fill the binprm structure from the inode. 1255 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes 1256 * 1257 * This may be called multiple times for binary chains (scripts for example). 1258 */ 1259 int prepare_binprm(struct linux_binprm *bprm) 1260 { 1261 struct inode *inode = file_inode(bprm->file); 1262 umode_t mode = inode->i_mode; 1263 int retval; 1264 1265 1266 /* clear any previous set[ug]id data from a previous binary */ 1267 bprm->cred->euid = current_euid(); // 清除之前的信任证 1268 bprm->cred->egid = current_egid(); // 清除之前的信任证 ... 1292 /* fill in binprm security blob */ 1293 retval = security_bprm_set_creds(bprm); // 设置安全信任证 1294 if (retval) 1295 return retval; 1296 bprm->cred_prepared = 1; 1297 1298 memset(bprm->buf, 0, BINPRM_BUF_SIZE); 1299 return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE); // BINPRM_BUF_SIZE定义为128 1300 }

do_execve_common中1513行,调用exec_binprm函数执行文件。exec_binprm函数中1416行调用search_binary_handler函数,来搜索和匹配合适的可执行文件装载处理程序。

1405 static int exec_binprm(struct linux_binprm *bprm) 1406 { 1407 pid_t old_pid, old_vpid; 1408 int ret; 1409 1410 /* 需要在load_binary更改之前获取pid */ 1411 old_pid = current->pid; 1412 rcu_read_lock(); 1413 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); 1414 rcu_read_unlock(); 1415 1416 ret = search_binary_handler(bprm); // 搜索和匹配合适的可执行文件装载处理过程。 1417 if (ret >= 0) { 1418 audit_bprm(bprm); 1419 trace_sched_process_exec(current, old_pid, bprm); 1420 ptrace_event(PTRACE_EVENT_EXEC, old_vpid); 1421 proc_exec_connector(current); 1422 } 1423 1424 return ret; 1425 }

看一下search_binary_handler函数是如何搜索匹配,并执行加载的。search_binary_handler函数。

注意:

  • search_binary_handler函数第1369行中的formats是一个静态全局变量,formats是struct list_head类型。实际上formats的作用是一个列表的头,列表中每个struct linux_binfmt元素是经过register_binfmt/insert_binfmt函数注册/插入进列表的。linux-3.18.6内核版本中注册的文件加载器有:

  • register_binfmt(&elf_fdpic_format); // 将fdpic二进制文件加载到内存。load an fdpic binary into various bits of memory
  • register_binfmt(&aout_format); // 这些是用于加载a.out样式的可执行文件和共享库的函数。 在其他任何地方都没有二进制相关代码。These are the functions used to load a.out style executables and shared libraries. There is no binary dependent code anywhere else.
  • register_binfmt(&elf_format); // 加载elf二进制文件。load elf binary
  • register_binfmt(&em86_format); //
  • register_binfmt(&som_format); // 这些是用于加载SOM可执行文件和共享库的功能。 在其他任何地方都没有二进制相关代码。These are the functions used to load SOM executables and shared libraries. There is no binary dependent code anywhere else.
  • **register_binfmt(&script_format); ** // 加载脚本文件。load script file
  • register_binfmt(&flat_format); // 这些是用于加载flat样式可执行文件和共享库的函数。 在其他任何地方都没有二进制相关代码。These are the functions used to load flat style executables and shared libraries. There is no binary dependent code anywhere else.

代码如下:

1349 /* 1350 * cycle the list of binary formats handler, until one recognizes the image 1351 */ 1352 int search_binary_handler(struct linux_binprm *bprm) 1353 { 1354 bool need_retry = IS_ENABLED(CONFIG_MODULES); 1355 struct linux_binfmt *fmt; ... 1367 retry: 1368 read_lock(&binfmt_lock); 1369 list_for_each_entry(fmt, &formats, lh) { // 循环便利formats列表,fmt是每个元素的指针 1370 if (!try_module_get(fmt->module)) 1371 continue; 1372 read_unlock(&binfmt_lock); 1373 bprm->recursion_depth++; 1374 retval = fmt->load_binary(bprm); // load_binary是struct linux_binfmt结构体中的一个成员,指定加载函数的指针。 1375 read_lock(&binfmt_lock); ... 1388 } 1389 read_unlock(&binfmt_lock); ... 1400 1401 return retval; 1402 } 1403 EXPORT_SYMBOL(search_binary_handler);

最终search_binary_handler函数将在1374行调用./linux-3.18.6/fs/binfmt_elf.c文件中571行的load_elf_binary函数,load_elf_binary函数将对ELF文件进行装载。

load_elf_binary函数主要做的事情包括:

  • 检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
  • 寻找动态链接的.interp段,设置动态链接器路径(与动态链接器有关)。
  • 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
  • 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(参照动态链接)。
  • 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。
  • 571 static int load_elf_binary(struct linux_binprm *bprm)572 { ...599 /* Get the exec-header */600 loc->elf_ex = *((struct elfhdr *)bprm->buf); // 获取ELF程序头部信息601 602 retval = -ENOEXEC;603 /* 一些简单的一致性检查 */604 if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)605 goto out;606 607 if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) // 类型检查608 goto out;609 if (!elf_check_arch(&loc->elf_ex)) // 指令集架构检查610 goto out;611 if (!bprm->file->f_op->mmap) // 与文件关联的mmap操作有效性检查612 goto out;613 614 /* 读取所有头部信息 */615 if (loc->elf_ex.e_phentsize != sizeof(struct elf_phdr))616 goto out;617 if (loc->elf_ex.e_phnum < 1 ||618 loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))619 goto out;620 size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);621 retval = -ENOMEM;622 elf_phdata = kmalloc(size, GFP_KERNEL); // elf程序头表(program header table)数据623 if (!elf_phdata)624 goto out;625 626 retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,627 (char *)elf_phdata, size); // 读取elf程序头表数据628 if (retval != size) {629 if (retval >= 0)630 retval = -EIO;631 goto out_free_ph;632 }633 634 elf_ppnt = elf_phdata;635 elf_bss = 0;636 elf_brk = 0;637 638 start_code = ~0UL;639 end_code = 0;640 start_data = 0;641 end_data = 0;642 643 for (i = 0; i < loc->elf_ex.e_phnum; i++) {644 if (elf_ppnt->p_type == PT_INTERP) { // 寻找动态链接的.interp段645 /* This is the program interpreter used for646 * shared libraries - for now assume that this647 * is an a.out format binary648 */649 retval = -ENOEXEC;650 if (elf_ppnt->p_filesz > PATH_MAX ||651 elf_ppnt->p_filesz < 2)652 goto out_free_ph;653 654 retval = -ENOMEM;655 elf_interpreter = kmalloc(elf_ppnt->p_filesz,656 GFP_KERNEL);657 if (!elf_interpreter)658 goto out_free_ph;659 660 retval = kernel_read(bprm->file, elf_ppnt->p_offset,661 elf_interpreter,662 elf_ppnt->p_filesz); // 读取并设置动态链接器路径。663 if (retval != elf_ppnt->p_filesz) {664 if (retval >= 0)665 retval = -EIO;666 goto out_free_interp;667 }668 /* make sure path is NULL terminated */669 retval = -ENOEXEC;670 if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') // 检查动态链接器路径671 goto out_free_interp;672 673 interpreter = open_exec(elf_interpreter); // 打开执行动态链接器674 retval = PTR_ERR(interpreter);675 if (IS_ERR(interpreter))676 goto out_free_interp;677 678 /*679 * If the binary is not readable then enforce680 * mm->dumpable = 0 regardless of the interpreter's681 * permissions.682 */683 would_dump(bprm, interpreter); // 如果二进制文件不可读,则不管解释器的权限如何,都强制执行mm->dumpable = 0。684 685 retval = kernel_read(interpreter, 0, bprm->buf,686 BINPRM_BUF_SIZE);687 if (retval != BINPRM_BUF_SIZE) {688 if (retval >= 0)689 retval = -EIO;690 goto out_free_dentry;691 }692 693 /* 获取ELF Header */694 loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);695 break;696 }697 elf_ppnt++;698 }699 700 elf_ppnt = elf_phdata;701 for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)702 if (elf_ppnt->p_type == PT_GNU_STACK) { // 根据ELF文件中的GNU_STACK段的存在性,以及对应的标识位来确定是否要使栈可执行。703 if (elf_ppnt->p_flags & PF_X)704 executable_stack = EXSTACK_ENABLE_X;705 else706 executable_stack = EXSTACK_DISABLE_X;707 break;708 }709 710 /* 解释器的一些简单一致性检查 */711 if (elf_interpreter) {712 retval = -ELIBBAD;713 /* 检查不是ELFinterpreter */714 if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)715 goto out_free_dentry;716 /* 验证解释器是否具有有效的ch */717 if (!elf_check_arch(&loc->interp_elf_ex))718 goto out_free_dentry;719 } ...746 /* 现在,我们通过一些繁琐的工作来将ELF文件映射到内存中的正确位置。747 */748 for(i = 0, elf_ppnt = elf_phdata;749 i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {750 int elf_prot = 0, elf_flags;751 unsigned long k, vaddr;752 753 if (elf_ppnt->p_type != PT_LOAD) // 筛选出程序头中指定的可加载的段754 continue; ...791 vaddr = elf_ppnt->p_vaddr;792 if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {793 elf_flags |= MAP_FIXED;794 } else if (loc->elf_ex.e_type == ET_DYN) {795 /* Try and get dynamic programs out of the way of the796 * default mmap base, as well as whatever program they797 * might try to exec. This is because the brk will798 * follow the loader, and is not movable. */799 #ifdef CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE // 使内存地址映射随机化800 /* Memory randomization might have been switched off801 * in runtime via sysctl or explicit setting of802 * personality flags.803 * If that is the case, retain the original non-zero804 * load_bias value in order to establish proper805 * non-randomized mappings.806 */807 if (current->flags & PF_RANDOMIZE)808 load_bias = 0; // 装入的起点就是映像自己提供的地址vaddr。809 else810 load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);811 #else812 load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);813 #endif814 }815 816 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,817 elf_prot, elf_flags, 0); // 建立用户空间虚存区间与目标映像文件中某个连续区间之间的映射。818 if (BAD_ADDR(error)) {819 retval = IS_ERR((void *)error) ?820 PTR_ERR((void*)error) : -EINVAL;821 goto out_free_dentry;822 } ...864 }865 866 loc->elf_ex.e_entry += load_bias;867 elf_bss += load_bias;868 elf_brk += load_bias;869 start_code += load_bias;870 end_code += load_bias;871 start_data += load_bias;872 end_data += load_bias;873 874 /* Calling set_brk effectively mmaps the pages that we need875 * for the bss and break sections. We must do this before876 * mapping in the interpreter, to make sure it doesn't wind877 * up getting placed where the bss needs to go.878 */879 retval = set_brk(elf_bss, elf_brk); // 调用set_brk有效地映射了我们用于bss和break部分的页面。 我们必须在解释器中进行映射之前执行此操作,以确保不会将其放置在需要放bss的位置。880 if (retval)881 goto out_free_dentry;882 if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {883 retval = -EFAULT; /* Nobody gets to see this, but.. */884 goto out_free_dentry;885 }886 887 if (elf_interpreter) {888 unsigned long interp_map_addr = 0;889 890 elf_entry = load_elf_interp(&loc->interp_elf_ex,891 interpreter,892 &interp_map_addr,893 load_bias); //仅读取具有ELF标头的ld.so库。894 if (!IS_ERR((void *)elf_entry)) {895 /*896 * load_elf_interp() returns relocation897 * adjustment898 */899 interp_load_addr = elf_entry;900 elf_entry += loc->interp_elf_ex.e_entry; // 调整入口地址901 }902 if (BAD_ADDR(elf_entry)) {903 retval = IS_ERR((void *)elf_entry) ?904 (int)elf_entry : -EINVAL;905 goto out_free_dentry;906 }907 reloc_func_desc = interp_load_addr;908 909 allow_write_access(interpreter);910 fput(interpreter);911 kfree(elf_interpreter);912 } else {913 elf_entry = loc->elf_ex.e_entry;914 if (BAD_ADDR(elf_entry)) {915 retval = -EINVAL;916 goto out_free_dentry;917 }918 }919 920 kfree(elf_phdata);921 922 set_binfmt(&elf_format);923 924 #ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES925 retval = arch_setup_additional_pages(bprm, !!elf_interpreter);926 if (retval < 0)927 goto out;928 #endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */929 930 install_exec_creds(bprm); // 安装新的信任证931 retval = create_elf_tables(bprm, &loc->elf_ex,932 load_addr, interp_load_addr);933 if (retval < 0)934 goto out;935 /* N.B. passed_fileno might not be initialized? */936 current->mm->end_code = end_code;937 current->mm->start_code = start_code;938 current->mm->start_data = start_data;939 current->mm->end_data = end_data;940 current->mm->start_stack = bprm->p;941 942 #ifdef arch_randomize_brk943 if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {944 current->mm->brk = current->mm->start_brk =945 arch_randomize_brk(current->mm);946 #ifdef CONFIG_COMPAT_BRK947 current->brk_randomized = 1;948 #endif949 }950 #endif951 952 if (current->personality & MMAP_PAGE_ZERO) {953 /* Why this, you ask??? Well SVr4 maps page 0 as read-only,954 and some applications "depend" upon this behavior.955 Since we do not have the power to recompile these, we956 emulate the SVr4 behavior. Sigh. */957 error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,958 MAP_FIXED | MAP_PRIVATE, 0);959 }960 961 #ifdef ELF_PLAT_INIT // 初始化进程启动环境,进程启动时EDX寄存器的地址应该是DT_FINI的地址962 /*963 * The ABI may specify that certain registers be set up in special964 * ways (on i386 %edx is the address of a DT_FINI function, for965 * example. In addition, it may also specify (eg, PowerPC64 ELF)966 * that the e_entry field is the address of the function descriptor967 * for the startup routine, rather than the address of the startup968 * routine itself. This macro performs whatever initialization to969 * the regs structure is required as well as any relocations to the970 * function descriptor entries when executing dynamically links apps.971 */972 ELF_PLAT_INIT(regs, reloc_func_desc);973 #endif974 975 start_thread(regs, elf_entry, bprm->p); // 调用start_thread()函数修改保存在内核态堆栈但属于用户态寄存器的EIP和ESP的值,以使它们分别指向DL的入口(如果没有获得DL则指向ELF的入口)和新的用户态栈的栈顶;976 retval = 0;977 out:978 kfree(loc);979 out_ret:980 return retval;981 982 /* error cleanup */983 out_free_dentry:984 allow_write_access(interpreter);985 if (interpreter)986 fput(interpreter);987 out_free_interp:988 kfree(elf_interpreter);989 out_free_ph:990 kfree(elf_phdata);991 goto out;992 }

    当load_elf_binary()执行完毕,返回到do_execve_common函数,再返回到sys_execve()函数时,load_elf_binary()中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。

    所以当sys_execve()系统调用从内核态返回到用户态时,RIP寄存器直接跳到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件加载完成。

    总结

    以上是生活随笔为你收集整理的深入分析ELF文件结构及其载入过程的全部内容,希望文章能够帮你解决所遇到的问题。

    如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。