linux ELF 文件装载

March 23, 2021 Linux 访问: 35 次

虚拟地址空间

Linux操作系统上,每个程序运行起来后,都将会拥有自己的独立的虚拟地址空间(这和Windows上的进程是一样的),虚拟地址空间的大小是投CPU的位数决定的。一般来说,C语言指针大小的位数与虚拟空间的位数相同。

操作系统会提供一种机制,能够将不同进程的虚拟地址空间与不同内存的物理地址映射起来,这样所有的进程都不能直接访问物理地址,都只能访问自己的虚拟内存空间。

如图所示,无论是32位还是64位的elf,都为操作系统留了空间,
-w694

虚拟地址空间分布

Linux将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA)。当进程被创建时,就会在进行相对应的数据结构中设置一个.text段的VMA、.data段的VMA,如下图所示:

-w714

链接视图、装载视图

从链接视图来看elf,分为若干个节(section),比如说.text.bss等等,我们可以通过readelf -S [filename]来查看

❯ readelf -S  test
共有 31 个节头,从偏移量 0x19d8 开始:
 
节头:
  [号] 名称               类型              地址               偏移量      大小              全体大小           旗标     链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000   0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238   000000000000001c  0000000000000000   A       0     0     1
  ………………
  [30] .strtab           STRTAB           0000000000000000  000016b8   0000000000000210  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
 

从装载视图来看,elf则分为若干个段(segment),也就是程序头部分,我们可以通过readelf -l [filename]来查看,站在操作系统的角度来看,它不关心各个节的内容,而是只关心相同权限的节,把他们合并成一起成为段,然后进行映射。它描述了elf文件该如何被操作系统映射到进程的虚拟地址空间

❯ readelf -l test
 
Elf 文件类型为 EXEC (可执行文件)
入口点 0x400430
共有 9 个程序头,开始于偏移量 64
 
程序头:
  Type           Offset             VirtAddr           PhysAddr           FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c  R      1  [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000006fc 0x00000000000006fc  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000005d4 0x00000000004005d4 0x00000000004005d4 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0  R      1
 
 Section to Segment mapping:
  段节...
   00
   01     .interp
   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
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .jcr .dynamic .got

其中在程序头部分,只有typeLOAD类型的段需要被映射。

如图所示,elf文件被划分成了三个部分,一部分是可读可执行的段被映射到了VMA 0,另一部分是可读可写的段映射到了VMA 1
-w593

问题

0x01
-w954
-w1308

LOAD类型的segment只有两个,但是在内存中的会出现三个segment,为什么?

经过一番查阅后,我发现程序头的最后一项的地址也是0x0000000000600e10,大小是0x00000000000001f0,权限是R,刚好是0x0000000000600e10+0x00000000000001f0=0x601000
GNU_RELRO是有关于RELRO保护的
RELRO:-z norelro / -z lazy / -z now 关闭 / 部分开启 / 完全开启

0x02
-w1301

-w892

第二个LOAD是从文件偏移0xe10处开始映射,然后映射到内存的0x600e10的位置,那么0x600000到0x600e10中间为什么有数据呢?

在进行内存映射的时候,映射是以页为单位进行映射的,考虑到对齐的原因,要把文件偏移0xe10所在的一页拷贝到0x600e10所在的那一页

静态链接、动态链接

静态链接是指在代码编译的时候,代码中用到so库中的函数,把库函数的在so中的代码抽取出来直接放入到我们编译后的文件中,在运行的时候就不用对so文件调用啦

动态链接是指不用再编译链接的时候把库函数的代码装到程序里,当我们运行程序的时候,需要调用库里的某一个函数的时候,通过“延迟绑定”的方法来找到函数在so库里的真是地址,然后程序调到so库里进行执行。

分析elf执行过程

我们在命令行通过strace执行程序,可以看到linux是通过调用execve来执行程序的
-w951

graph TD
   execve --> do_execve  --> do_execveat_common-->__do_execve_file-->exec_binprm-->search_binary_handler-->load_elf_binary

Linux可执行文件类型的注册机制

linux支持不同格式的可执行程序,比如elf、ms-dos等等。原因是linux内核对所支持的每种可执行程序类型都有一个struct linux_binfmt的数据结构

/*~/MyFile/mac_file/linux-5.7/include/linux/binfmts.h:102*/
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;
函数描述
load_binary通过读取存放在可执行文件中的信息为当前进程创建一个新的执行环境
load_shlib用于动态的把一个共享库文件捆绑到一个已经在运行的进程上
core_dump在名为core的文件中,存放当前进程的执行上下文

当我们执行一个可执行程序的时候,内核会对所有的linux_binfmt进行遍历,调用load_binary尝试进行加载,知道加载成功为止

elf对应的linux_binfmt结构体如下所示

/*~/MyFile/mac_file/linux-5.7/fs/binfmt_elf.c:93*/
static struct linux_binfmt elf_format = {
    .module        = THIS_MODULE,
    .load_binary    = load_elf_binary,
    .load_shlib    = load_elf_library,
    .core_dump    = elf_core_dump,
    .min_coredump    = ELF_EXEC_PAGESIZE,
};

在ELF文件格式中,处理函数是load_elf_binary函数

检查文件头部

    /* First of all, some simple consistency checks   头部检查 */
    if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
        goto out;

    if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
        goto out;
    if (!elf_check_arch(elf_ex))
        goto out;
    if (elf_check_fdpic(elf_ex))
        goto out;
    if (!bprm->file->f_op->mmap)
        goto out;

加载文件中的program headers

elf_phdata = load_elf_phdrs(elf_ex, bprm->file);

load_elf_phdrs函数中调用了elf_read来对文件进行读取
-w708

寻找和处理解释器(段INTERP)

紧接着是一个for循环,遍历程序头,找到type为PT_INTERPsegment

-w529

找到之后对p_filesz字段做check
-w671

其次是申请空间,把这个字符串读入到内存里,
-w741
然后调用open_exec来打开这个文件,调用elf_read来读取header信息
-w646

设置栈的执行权限

遍历所有的段,找到STACK,查看flags并设置executable_stack
-w533

PT_LOPROC ... PT_HIPROC 范围的类型保留给处理器专用语义,调用架构相关函数处理(x86和ARM中均为空函数)

对解释器进行检查

检查文件格式、结构等
-w628

内存映射前的初始化

对处理器相关的进行检查操作,并且清除掉了父进程的所有相关代码,调用setup_new_exec来创建一个新的线程,来进行后续的操作,然后对一些变量进行初始化

-w705

进行内存映射

首先遍历所有segment,找出所有类型为LOADsegment
-w444

然后对地址和页面的信息进行检查
-w538
-w575

进而通过调用elf_map来建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射
-w629

elf_map 函数内部

-w909

可以看到内部是调用了vm_mmap来进行内存映射,第六个参数指明了文件的那个部分被映射

使用set_brk调整bss段的大小

必须在映射加载器之前为bss段申请空间,防止加载器占用bss段的空间
-w736

映射加载器

如果需要把加载器映射到内存中的话,就调用了load_elf_interp来进行映射,最后把执行入口地址赋值成解释器的地址

如果不需要加载器的映射,那么会直接把执行入口地址赋值成elf的入口地址

加载器需不需要映射取决于该elf是不是静态链接文件

-w540
load_elf_interp函数内部映射方式和映射elf文件到内存中是一样的,首先做一些一致性的检查,然后遍历解释器的程序头,把LOAD类型的segmentelf_map来进行映射到内存中

做执行前的准备

调用create_elf_tables函数填写目标文件的参数环境变量等必要信息,然后填充新进程内存描述符(mm_struct)中的堆栈
-w721

调用start_thread开始执行elf文件

该函数会把riprsp改成新的地址,然后CPU就会进入到新的程序入口地址进行执行。
-w428

Referer

elf格式分析
[程序员的自我修养]()
ELF文件加载过程

添加新评论