ELF文件加载与动态链接(一)

ELF格式文件简单介绍

关于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   %rbp
  40052e:   48 89 e5                mov    %rsp,%rbp
  400531:   bf d4 05 40 00          mov    $0x4005d4,%edi
  400536:   e8 d5 fe ff ff          callq  400410 <[email protected]>
  40053b:   b8 00 00 00 00          mov    $0x0,%eax
  400540:   5d                      pop    %rbp
  400541:   c3                      retq
  400542:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400549:   00 00 00
  40054c:   0f 1f 40 00             nopl   0x0(%rax)

从汇编代码看到,printf调用被换成了puts,其中callq指令就是调用的puts函数,它使用了[email protected]标号。要分析这段汇编代码,需要先了解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**3
                 CONTENTS, ALLOC, LOAD, DATA
22 .got.plt      00000030  0000000000601000  0000000000601000  00001000  2**3
                 CONTENTS, ALLOC, LOAD, DATA

加到上面的汇编代码,我们看一下[email protected]是什么内容:

[email protected]:~/workdir$ objdump -d hello
...
Disassembly of section .plt:

0000000000400400 <[email protected]>:
  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 <[email protected]>:
  400410:   ff 25 02 0c 20 00       jmpq   *0x200c02(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400416:   68 00 00 00 00          pushq  $0x0
  40041b:   e9 e0 ff ff ff          jmpq   400400 <_init+0x20>

0000000000400420 <[email protected]>:
  400420:   ff 25 fa 0b 20 00       jmpq   *0x200bfa(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400426:   68 01 00 00 00          pushq  $0x1
  40042b:   e9 d0 ff ff ff          jmpq   400400 <_init+0x20>

0000000000400430 <[email protected]>:
  400430:   ff 25 f2 0b 20 00       jmpq   *0x200bf2(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  400436:   68 02 00 00 00          pushq  $0x2
  40043b:   e9 c0 ff ff ff          jmpq   400400 <_init+0x20>

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

时间: 2024-10-01 04:25:24

ELF文件加载与动态链接(一)的相关文章

ELF文件加载与动态链接(二)

GOT应该保存的是puts函数的绝对虚地址,这里为什么保存的却是[email protected]的第二条指令呢? 原来“解释器”将动态库载入内存后,并没有直接将函数地址更新到GOT表中,而是在函数第一次被调用时,才会进行函数地址的重定位,这样做的好处是可以加快程序加载速度,尤其对大型程序来说.有关这方面的更详细的信息,可以搜索“动态链接库的延迟绑定技术”. 继续看第二条指令,pushq $0x0代表什么? 查看Hello world程序的重定位节: [email protected]:~/wo

cmake中设置ELF文件加载动态库的位置

1. 三个文件 1. world.c #include<stdio.h> void world(void) { printf("world.\n"); } 2. hello.c #include <stdio.h> void world(void); void hello(void) { printf("hello\n"); world(); } 3. main.c void main(void) { hello(); } 2. 编译动态库

使用js加载器动态加载外部Javascript文件

今天在网上找到了一个可以动态加载js文件的js加载器,具体代码如下: JsLoader.js 1 var MiniSite=new Object(); 2 /** 3 * 判断浏览器 4 */ 5 MiniSite.Browser={ 6 ie:/msie/.test(window.navigator.userAgent.toLowerCase()), 7 moz:/gecko/.test(window.navigator.userAgent.toLowerCase()), 8 opera:/o

XIB文件链接controller文件&amp;&amp;加载rootviewcontroller

1.定义好的xib文件rootView.xib,选中files·owners  在class中选择对应的viewController 2.如何在window中指定rootViewController ------在appdelegate.h中声明属性 @property (strong,nonatomic)rootViewController* rvc; ------在appdelegate.m中的didFinishLauchingWithOptions:方法中加载rvc self.rvc =

Android WebView加载Chromium动态库的过程分析

Chromium动态库的体积比较大,有27M左右,其中程序段和数据段分别占据25.65M和1.35M.如果按照通常方式加载Chromium动态库,那么当有N个正在运行的App使用WebView时,系统需要为Chromium动态库分配的内存为(25.65 + N x 1.35)M.这是非常可观的.为此,Android使用了特殊的方式加载Chromium动态库.本文接下来就详细分析这种特殊的加载方式. 老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注! 为什么当有

JS文件加载:比较async和DOM Script

async与script动态加载都能使文件异步加载,本文叙述它们对页面渲染和load加载的影响方面. 目前我用demo.js作为执行文件操作.代码: var now = function() { return +(new Date()); } var t_s = now(); while(now() - t_s < 2000) { } 用sleep.php作为请求文件操作.代码: <?php sleep(3); echo 'var bb'; ?> 1. 一般script标签加载 <

getClass()和getClassLoader()区别 以及ClassLoader详解及用途(文件加载,类加载)

获得ClassLoader的几种方法可以通过如下3种方法得到ClassLoader this.getClass().getClassLoader(); // 使用当前类的ClassLoader Thread.currentThread().getContextClassLoader(); // 使用当前线程的ClassLoader ClassLoader.getSystemClassLoader(); // 使用系统ClassLoader,即系统的入口点所使用的ClassLoader.(注意,s

在IIS上新发布的网站,样式与js资源文件加载不到(资源文件和网页同一个域名下)

在IIS上新发布的网站,网站能打开,但样式与js资源文件加载不到(资源文件和网页是同一个域名下,例如:网页www.xxx.com/index.aspx,图片www.xxx.com/pic.png). 然后单独打开资源文件(例如打开图片的链接)是,报错: 这个问题应该是web.config配置文件的设置问题. 在配置文件的<httpHandlers>下的节点,对应的资源文件的type值设置可能是“System.Web.DefaultHttpHandler”值(默认),例如: <httpHa

Javascript文件加载:LABjs和RequireJS

传统上,加载Javascript文件都是使用<script>标签. 就像下面这样: <script type="text/javascript" src="example.js"></script> <script>标签很方便,只要加入网页,浏览器就会读取并运行.但是,它存在一些严重的缺陷. (1)严格的读取顺序.由于浏览器按照<script>在网页中出现的顺序,读取Javascript文件,然后立即运行,