覆盖范围

程序启动流程

参考 《程序员的自我修养 —— 6.5 Linux 内核装载 ELF 过程简介》

step1 execve()

首先在用户层面,bash 进程会调用 fork() 系统调用创建一个新的进程,然后新的进程调用 execve() 系统调用执行指定的 ELF 文件,原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

在内核中,execve() 系统调用相应的入口是 sys_execve(),它被定义在 arch/i386/kernel/Process.c 中。sys_execve()进行一些参数的检查复制之后,调用do_execve()。

step2 读取可执行文件

do_execve() 会首先查找被执行的文件,如果找到文件,则读取文件的前 128 字节(目的是判断文件类型:ELF、a.out、shell 脚本、Java 程序)。然后调用相应的装载处理函数,比如 ELF 可执行文件的装载处理函数为 load_elf_binary(),a.out 可执行文件的装载处理函数为 load_aout_binary(),装载可执行脚本的函数为 load_script()。

step3 ELF 可执行文件的装载

load_elf_binary() 被定义在 fs/Binfmt_elf.c,主要步骤和功能:

  1. 检查 ELF 可执行文件格式的有效性,比如魔数、程序头表中段(segment)的数量;
  2. 寻找动态链接的 “.interp” 段,设置动态链接器路径;
  3. 根据 ELF 可执行文件的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据。
  4. 初始化 ELF 进程环境,比如进程启动时 EDX 寄存器的地址应该是 DT_FINT 的地址;
  5. 将系统调用的返回地址修改成 ELF 可执行文件的入口点。这个入口点取决于程序的链接方式,对于静态链接的 ELF 可执行文件,这个程序入口就是 ELF 文件的文件头中 e_entry 所指的地址;对于动态链接的 ELF 可执行文件,程序入口点是动态链接器。

load_elf_binary() 执行完毕,返回至 do_execve(),再返回至 sys_execve() 时,上面第 5 步中已经把系统调用的返回地址改成了被装载的 ELF 程序的入口地址了。所以当 sys_execve() 系统调用从内核态返回到用户态时,EIP 寄存器直接跳转到了 ELF 程序的入口地址,于是新的程序开始执行,ELF 可执行文件装载完成。

step3.1 动态链接器

再上面第 5 步中,如果程序是动态链接的,操作系统不能在装载完可执行文件之后就把控制权交给可执行文件,因为我们知道可执行文件依赖很多共享对象。这时候,可执行文件里对于很多外部符号的引用还处于无效地址的状态,即还没有跟相应的共享对象中的实际位置链接起来。所以在映射完可执行文件之后,操作系统会先启动一个动态链接器(Dynamic Linker)。

在 Linux 下,动态链接器 ld.so 实际上是一个共享对象,操作系统通过同样映射的方法将它加载到进程的地址空间中。操作系统在加载完动态链接器之后,就将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址)。当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。

“.interp” 段

那么系统中哪个才是动态链接器呢?它的位置由谁决定?时不是所有的类 UNIX 系统的动态链接器都位于 /lib/ld.so 呢?实际上,动态链接器的位置既不是由系统配置指定,也不是由环境参数决定,而是由 ELF 可执行文件决定的。在动态链接的 ELF 可执行文件中,有一个专门的段叫做“.interp”段(“interp”是“interpreter”(解释器)的缩写)。如果我们使用 objdump 工具来查看,可以看到 “.interp” 的内容:

$ objdump -s main.out 

main.out:     文件格式 elf64-x86-64

Contents of section .interp:
 0318 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 0328 7838362d 36342e73 6f2e3200           x86-64.so.2. 

“.interp”的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径,在Linux下,可执行文件所需要的动态链接器的路径几乎都是“/lib/ld-linux.so.2”。它通常是一个软链接,比如在我的机器上,它指向 /lib/ld-2.6.1.so,这个才是真正的动态链接器。

在 Linux中,操作系统在对可执行文件的进行加载的时候,它会去寻找装载该可执行文件所需要相应的动态链接器,即“.interp”段指定的路径的共享对象。

动态链接器在 Linux 下是 Glibc 的一部分,也就是属于系统库级别的。

还可以使用下面的命令查看:

$ readelf -l main.out | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]