关于ELF文件的详细介绍,推荐阅读: ELF文件格式分析 —— 滕启明。
ELF文件由ELF头部、程序头部表、节区头部表以及节区4部分组成。

通过objdump工具和readelf工具,可以观察ELF文件详细信息。

ELF文件加载过程分析

从编译、链接和运行的角度,应用程序和库程序的链接有两种方式。一种是静态链接,库程序的二进制代码链接进应用程序的映像中;一种是动态链接,库函数的代码不放入应用程序映像,而是在启动时,将库程序的映像加载到应用程序进程空间。

在动态链接中,GNU将动态链接ELF文件的工作做了分工:ELF映像的载入与启动由Linux内核完成,而动态链接过程由用户空间glibc实现。并提供了一个“解释器”工具ld-linux.so.2。

Linux内核中,使用struct linux_binfmt结构定义一个ELF文件加载

/* binfmts.h */
struct linux_binfmt {struct list_head lh;struct module *module;int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);int (*load_shlib)(struct file *);int (*core_dump)(struct coredump_params *cprm);unsigned long min_coredump; /* minimal dump size */
};

load_binary函数指针指向的是一个可执行程序的处理函数。我们研究的ELF文件格式的定义如下:

/* binfmt_elf.c */
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,
};

Linux内核将这个数据结构注册到可执行程序队列,当运行一个可执行程序时,所有注册的处理程序(这里的load_elf_binary)逐一前来认领,若发现格式相符,则载入并启动该程序。

static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{struct file *interpreter = NULL; /* to shut gcc up */unsigned long load_addr = 0, load_bias = 0;int load_addr_set = 0;char * elf_interpreter = NULL;  //"解释器"/*......*/struct {struct elfhdr elf_ex;struct elfhdr interp_elf_ex;} *loc; //elf头结构loc = kmalloc(sizeof(*loc), GFP_KERNEL);/*......*//* Get the exec-header */loc->elf_ex = *((struct elfhdr *)bprm->buf);  //bprm->buf是内核读的的128字节映像头retval = -ENOEXEC;/* First of all, some simple consistency checks */if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)   //查看文件头4个字节,判断是否为"\177ELF"goto out;if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)    //是否为可执行文件或共享库?goto out;/*......*//* Now read in all of the header information *//*......*/retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, // kernel_read读取整个程序头表(char *)elf_phdata, size);/*......*/for (i = 0; i < loc->elf_ex.e_phnum; i++) {   //这个大for循环功能是加载"解释器"if (elf_ppnt->p_type == PT_INTERP) { //PT_INTERP指"解释器"段/* This is the program interpreter used for* shared libraries - for now assume that this* is an a.out format binary*//*......*/retval = kernel_read(bprm->file, elf_ppnt->p_offset,  //根据位置p_offset和大小p_filesz将"解释器"读入elf_interpreter,   //这里读入的其实是"解释器"名字"/lib/ld-linux.so.2"elf_ppnt->p_filesz);/*......*//* make sure path is NULL terminated */retval = -ENOEXEC;if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')goto out_free_interp;interpreter = open_exec(elf_interpreter);   //打开"解释器"retval = PTR_ERR(interpreter);if (IS_ERR(interpreter))goto out_free_interp;/** If the binary is not readable then enforce* mm->dumpable = 0 regardless of the interpreter's* permissions.*/would_dump(bprm, interpreter);retval = kernel_read(interpreter, 0, bprm->buf,  //读入128字节的"解释器"头部BINPRM_BUF_SIZE);/*......*//* Get the exec headers */loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);break;}elf_ppnt++;}/*......*//* Some simple consistency checks for the interpreter */if (elf_interpreter) { //对"解释器"段的校验/*......*/}/*......*/for(i = 0, elf_ppnt = elf_phdata;i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {int elf_prot = 0, elf_flags;unsigned long k, vaddr;if (elf_ppnt->p_type != PT_LOAD) //搜索类型为"PT_LOAD"的段(需载入的段)continue;if (unlikely (elf_brk > elf_bss)) {/*......*/}/*......*/}error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,elf_prot, elf_flags, 0); //建立用户虚拟地址空间与映射文件某连续区间的映射/*......*/}/*......*/if (elf_interpreter) { //如果要载入"解释器"(都是静态链接的情况)unsigned long uninitialized_var(interp_map_addr);elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias);     //载入"解释器"映像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; //用户空间入口地址设置为elf_entry}if (BAD_ADDR(elf_entry)) {force_sig(SIGSEGV, current);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);kfree(elf_interpreter);} else { //有动态链接存在elf_entry = loc->elf_ex.e_entry; //用户空间入口地址设置为映像本身地址if (BAD_ADDR(elf_entry)) {force_sig(SIGSEGV, current);retval = -EINVAL;goto out_free_dentry;}}kfree(elf_phdata);/*......*/start_thread(regs, elf_entry, bprm->p);  //修改eip与esp为新的地址,程序从内核返回应用态时的入口/*......*//* error cleanup *//*......*/
}

我们这样一个Hello world程序,除非在编译时指定-static选项,否则都是动态链接的:

#include <stdio.h>
int main()
{printf("Hello world.\n");return 0;
}

Hello world程序被内存载入内存后,控制权先交给“解释器”,“解释器”完成动态库的装载后,再将控制权交给用户程序。

ELF文件符号的动态解析

“解释器”将所有动态库文件加载到内存后,形成一个链表,后面的符号解析过程主要是在这个链表中搜索符号的定义。

我们以上面Hello world程序为例,分析程序如何调用动态库中的printf函数:

000000000040052d <main>:40052d:   55                      push   %rbp40052e:   48 89 e5                mov    %rsp,%rbp400531:   bf d4 05 40 00          mov    $0x4005d4,%edi400536:   e8 d5 fe ff ff          callq  400410 <puts@plt>40053b:   b8 00 00 00 00          mov    $0x0,%eax400540:   5d                      pop    %rbp400541:   c3                      retq  400542:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)400549:   00 00 0040054c:   0f 1f 40 00             nopl   0x0(%rax)

从汇编代码看到,printf调用被换成了puts,其中callq指令就是调用的puts函数,它使用了puts@plt标号。要分析这段汇编代码,需要先了解2个基本概念:GOT(global offset table)和PLT(procedure linkage table)

GOT

当程序引用某个动态库中的符号时(如puts()函数),编译链接阶段并不知道这个符号在内存中的具体位置,只有在动态链接器将共享库加载到内存后,即在运行阶段,符号地址才会最终确定。因此要有一个结构来保存符号的绝对地址,这就是GOT。这样通过表中的某一项,就可以引用某符号的地址。

GOT表前3项是保留项,用于保存特殊的数据结构地址,其中GOT[1]保存共享库列表地址,上文提到“解释器”加载的所有共享库以列表形式组织。GOT[2]保存函数_dl_runtime_resolve的地址,这个函数的主要作用是找到某个符号的地址,并把它写到相应GOT项中,然后将控制转移到目标函数。

PLT

在编译链接时,链接器不能将控制从一个可执行文件或共享库文件转到另外一个,因为如前面所说的,这时函数地址还未确定。因此链接器将控制转移到PLT中的一项,PLT通过引用GOT的绝对地址,实现控制转移。

实际在通过objdump查看ELF文件,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。

21 .got          00000008  0000000000600ff8  0000000000600ff8  00000ff8  2**3CONTENTS, ALLOC, LOAD, DATA
22 .got.plt      00000030  0000000000601000  0000000000601000  00001000  2**3CONTENTS, ALLOC, LOAD, DATA

加到上面的汇编代码,我们看一下puts@plt是什么内容:

ezreal@ez:~/workdir$ objdump -d hello
...
Disassembly of section .plt:0000000000400400 <puts@plt-0x10>:400400:   ff 35 02 0c 20 00       pushq  0x200c02(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>400406:   ff 25 04 0c 20 00       jmpq   *0x200c04(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>40040c:   0f 1f 40 00             nopl   0x0(%rax)0000000000400410 <puts@plt>:400410:   ff 25 02 0c 20 00       jmpq   *0x200c02(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>400416:   68 00 00 00 00          pushq  $0x040041b:   e9 e0 ff ff ff          jmpq   400400 <_init+0x20>0000000000400420 <__libc_start_main@plt>:400420:   ff 25 fa 0b 20 00       jmpq   *0x200bfa(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>400426:   68 01 00 00 00          pushq  $0x140042b:   e9 d0 ff ff ff          jmpq   400400 <_init+0x20>0000000000400430 <__gmon_start__@plt>:400430:   ff 25 f2 0b 20 00       jmpq   *0x200bf2(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>400436:   68 02 00 00 00          pushq  $0x240043b:   e9 c0 ff ff ff          jmpq   400400 <_init+0x20>

我们看到puts@plt包含3条指令,程序中所有对puts的调用都会先来到这里。还可以看出除了PLT0(puts@plt-0x10标号)外,其余PLT项形式都是一样的,最后的jmpq指令都是跳转到400400即PLT0处。整个PLT表就像一个数组,除PLT0外所有指令第一条都是一个间接寻址。以puts@plt为例,从0x200c02(%rip)处的注释可以看到,这条指令跳转到了GOT中的一项,其内容为0x601018即地址0x400406处(0x601018-0x200c02),也即puts@plt的第二条指令。(RIP相对寻址模式)

转载于:https://www.cnblogs.com/gm-201705/p/9901555.html

ELF文件加载与动态链接(一)相关推荐

  1. ELF文件的加载和动态链接过程

    本文的目的:大家对于Hello World程序应该非常熟悉,随便使用哪一种语言,即使还不熟悉的语言,写出一个Hello World程序应该毫不费力,但是如果让大家详细的说明这个程序加载和链接的过程,以 ...

  2. GAMIT模型文件加载时错误链接解决方案

    以海洋潮文件为例 察看otl.grid属性,可知其链接的是同目录下的otl_FES2004.grid,其也是一个链接. 继续察看otl_FES2004.grid文件的属性 注意看起link targe ...

  3. 静态链接库(LIB)和动态链接库(DLL),DLL的静态加载和动态加载,两种LIB文件。

    静态链接库(LIB)和动态链接库(DLL),DLL的静态加载和动态加载,两种LIB文件. 一. 静态链接库(LIB,也简称"静态库")与动态链接库(DLL,也简称"动态库 ...

  4. vs2019加载调试动态库dll文件

    创建一个hello world 的空项目 根据dll文件时多少位的就配多少,然后点击运行 将dll相关的头文件全部拷贝到测试项目中 将dll文件和lib也同时拷在测试文件的cpp同目录下 将编写dll ...

  5. OpenSBI ELF rela.dyn和.dynsym动态链接过程

    在OpenSBI中重定位分成了两种,根据是否配置了FW_PIC宏来区分, 配置了FW_PIC,即本文描述的rela.dyn和.dynsym的动态链接. 未配置FW_PIC是加载地址和链接地址不相等情况 ...

  6. python反编译luac_Lua程序逆向之为Luac编写IDA Pro文件加载器

    距离上一次讲Lua程序逆向已经有一段时间了,这一次我们书接上回,继续开启Lua程序逆向系列之旅. 在软件逆向工程实践中,为第三方文件编写文件格式分析器与指令反汇编器是一种常见的场景.这一篇的主要目的是 ...

  7. 高性能javascript 文件加载阻塞

    高性能javascript javascript脚本执行过程中会中断页面加载,直到脚本执行完毕,此操作阻塞了页面加载,造成性能问题.   脚本位置和加载顺序: 如果将脚本放在head内,那么再脚本执行 ...

  8. java class文件 代码_java_基础——用代码编译.java文件+加载class文件

    java_基础--用代码编译.java文件+加载class文件 java_基础--用代码编译.java文件+加载class文件 [简单编译的流程] package com.zjm.www.test; ...

  9. 当心异步刷新后的脚本文件加载

    重现问题 我们现在编写一个示例来重现一个异步刷信的问题. 首先,我们建立一个名为"ScriptHandler.ashx"的Generic Handler,它的作用是模拟一个脚本文件 ...

  10. WPF 从文件加载字体

    原文:WPF 从文件加载字体 版权声明:博客已迁移到 http://lindexi.gitee.io 欢迎访问.如果当前博客图片看不到,请到 http://lindexi.gitee.io 访问博客. ...

最新文章

  1. 大学python用什么教材-清华大学出版社-图书详情-《Python大学教程》
  2. bzoj3299 [USACO2011 Open]Corn Maze玉米迷宫
  3. 增加标 和增加其内容
  4. docker启动sqlserver_Docker搭建SQLServer
  5. InstallShield SdShowMsg未关闭导致安装程序无法停止
  6. js当前时间格式化_JS时间格式化
  7. Sunscreen(POJ-3416)
  8. JavaScript 基础知识 - DOM篇(二)
  9. (一)SpringMVC学习笔记-概述
  10. python 3教程_Python 3 教程
  11. Prototype实例代码推荐
  12. t检验临界值表中的n是什么_t检验临界值分布表
  13. 移动硬盘变为raw格式时,如何进行数据恢复
  14. SpringCloud2.0 集成分布式事务管理 LCN
  15. 依赖计算机英语作文,过度依赖电脑的危害的英文作文
  16. JavaStream 常用操作(二)
  17. pygame 学习笔记(7)添加一个精灵:坦克的移动和旋转
  18. android中小数怎么定义,android 如何保留数据两位小数
  19. mac 双开应用的方法
  20. 完美世界手游不显示服务器,完美世界手游怎么玩的角色不见了

热门文章

  1. 【转载】关于大型asp.net应用系统的架构-架构的选择
  2. 【BMC_patrol常见问题汇总】Console License生成
  3. java 静态成员 实例成员变量_java对象实例化时的顺序(静态成员变量、静态代码块、成员变量、方法块加载、构造函数加载)...
  4. python3可以运行python2的代码吗_Python同时兼容python2和python3的8个技巧分享
  5. C#中常用字符串操作
  6. linux pandas教程_Python Anaconda教程–了解最受欢迎的数据科学平台
  7. 【渝粤教育】国家开放大学2018年秋季 0553-22T色彩 参考试题
  8. 【渝粤教育】电大中专成本会计作业 题库
  9. 【Python实例第20讲】手写数字识别问题的K-Means聚类
  10. 多物理场面向对象模拟环境MOOSE学习手册