zoukankan      html  css  js  c++  java
  • 201920201 20199320《Linux内核原理与分析》第八周作业

    第七章 可执行程序的工作原理

    一、本章知识点

    1.1 ELF目标文件格式

    • ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。
      所谓对象文件(Object files)有三个种类:
    1. 可重定位的对象文件(Relocatable file)

    这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。

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

    这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。

    1. 可被共享的对象文件(Shared object file)

    这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程中,必须经过两个步骤:

    a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。

    b) 在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。

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

    • ELF文件:ELF(Excutable and Linking Format)是一个文件格式的标准。通过readelf-h hello查看可执行文件hello的头部(-a查看全部信息,-h只查看头部信息),头部里面注明了目标文件类型ELF32。Entry point address是程序入口,地址为0x400440,如下图所示:

    具体结构定义如下:

     1    #define EI_NIDENT       16
     2 
     3   typedef struct {
     4       unsigned char       e_ident[EI_NIDENT];
     5       Elf32_Half          e_type;
     6       Elf32_Half          e_machine;
     7       Elf32_Word          e_version;
     8       Elf32_Addr          e_entry;
     9       Elf32_Off           e_phoff;
    10       Elf32_Off           e_shoff;
    11       Elf32_Word          e_flags;
    12       Elf32_Half          e_ehsize;
    13       Elf32_Half          e_phentsize;
    14       Elf32_Half          e_phnum;
    15       Elf32_Half          e_shentsize;
    16       Elf32_Half          e_shnum;
    17       Elf32_Half          e_shstrndx;
    18   } Elf32_Ehdr;
    

    e_type 它标识的是该文件的类型。
    e_machine 表明运行该程序需要的体系结构。
    e_version 表示文件的版本。
    e_entry 程序的入口地址。
    e_phoff 表示Program header table 在文件中的偏移量(以字节计数)。
    e_shoff 表示Section header table 在文件中的偏移量(以字节计数)。
    e_flags 对IA32而言,此项为0。
    e_ehsize 表示ELF header大小(以字节计数)。
    e_phentsize 表示Program header table中每一个条目的大小。
    e_phnum 表示Program header table中有多少个条目。
    e_shentsize 表示Section header table中的每一个条目的大小。
    e_shnum 表示Section header table中有多少个条目。
    e_shstrndx 包含节名称的字符串是第几个节(从零开始计数)。

    而exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件,如果不是可以执行的文件,那么就解释成为一个shell文件,shell执行。

    1.2 可执行程序的装载

    gcc –e –o hello.cpp hello.c   //预处理
    gcc -x cpp-output -S -o hello.s hello.cpp //编译   
    gcc -x assembler -c hello.s -o hello.o-m32  //汇编
    gcc -o hello hello.o   //链接成可执行文件,使用共享库
    

    输入到终端截图如下:

    可以看到hello只有8K,hello.static有大概800K。

    1.3 静态链接可执行文件的调试

    1先把menu删掉,在克隆一个,用test_exec.c覆盖掉test.c。
    2打开test.c。发现增加了一句MenuConfig。
    3打开Makefile,首先静态编译了hello.c,生成根文件系统时把init和hello都放入rootfs image里面,这样执行exec的时候就自动的帮我们加载hello这个文件。
    4执行结果hello world! 是新加载的一个可执行程序输出的。
    

    输入到终端截图如下:

    -S -s单步调试,窗口被冻结;设置三个断点:sys_execve,load_elf_binary,start_thread。list列出来跟踪;输入s可以进入do_execve的内部。按c继续执行,跑到load_elf_binary;list查看代码,输入n一句一句跟踪,nnnc,追踪到start_thread。

    观察hello这个可执行程序的入口,发现也是0x8048d0a,和new_ip的位置一样。new_ip是返回到用户态第一条指令的地址。

    1.4 exec系统调用的执行过程

    do_exec函数:

    int do_execve(struct filename *filename,
        const char __user *const __user *__argv,
        const char __user *const __user *__envp)
    {
        return do_execve_common(filename, argv, envp);
    }
    
    
    static int do_execve_common(struct filename *filename,
                    struct user_arg_ptr argv,
                    struct user_arg_ptr envp)
    {
        // 检查进程的数量限制
    
        // 选择最小负载的CPU,以执行新程序
        sched_exec();
    
        // 填充 linux_binprm结构体
        retval = prepare_binprm(bprm);
    
        // 拷贝文件名、命令行参数、环境变量
        retval = copy_strings_kernel(1, &bprm->filename, bprm);
        retval = copy_strings(bprm->envc, envp, bprm);
        retval = copy_strings(bprm->argc, argv, bprm);
    
        // 调用里面的 search_binary_handler 
        retval = exec_binprm(bprm);
    
        // exec执行成功
    
    }
    
    static int exec_binprm(struct linux_binprm *bprm)
    {
        // 扫描formats链表,根据不同的文本格式,选择不同的load函数
        ret = search_binary_handler(bprm);
        // ...
        return ret;
    }
    

    从上面的代码中可以看到,do_execve调用了do_execve_common,而do_execve_common又主要依靠了exec_binprm,在exec_binprm中又有一个至关重要的函数,叫做search_binary_handler。

    search_binary_handler函数:

    int search_binary_handler(struct linux_binprm *bprm) { // 遍历formats链表 list_for_each_entry(fmt, &formats, lh) { // 应用每种格式的load_binary方法 retval = fmt->load_binary(bprm); // ... } return retval; }
    

    它的运行逻辑是依次遍历formats中得每种格式,然后根据不同的格式调用响应的load函数。例如,对于elf文件执行load_elf_bianry,对于a.out文件执行load_aout_binary函数

    load_elf_bianry函数:

    static int load_elf_binary(struct linux_binprm *bprm)
    {
        // ....
        struct pt_regs *regs = current_pt_regs();  // 获取当前进程的寄存器存储位置
    
        // 获取elf前128个字节
        loc->elf_ex = *((struct elfhdr *)bprm->buf);
    
        // 检查魔数是否匹配
        if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
            goto out;
    
        // 如果既不是可执行文件也不是动态链接程序,就错误退出
        if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
            // 
        // 读取所有的头部信息
        // 读入程序的头部分
        retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
                     (char *)elf_phdata, size);
    
        // 遍历elf的程序头
        for (i = 0; i < loc->elf_ex.e_phnum; i++) {
    
            // 如果存在解释器头部
            if (elf_ppnt->p_type == PT_INTERP) {
                // 
                // 读入解释器名
                retval = kernel_read(bprm->file, elf_ppnt->p_offset,
                             elf_interpreter,
                             elf_ppnt->p_filesz);
        
                // 打开解释器文件
                interpreter = open_exec(elf_interpreter);
    
                // 读入解释器文件的头部
                retval = kernel_read(interpreter, 0, bprm->buf,
                             BINPRM_BUF_SIZE);
    
                // 获取解释器的头部
                loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);
                break;
            }
            elf_ppnt++;
        }
    
        // 释放空间、删除信号、关闭带有CLOSE_ON_EXEC标志的文件
        retval = flush_old_exec(bprm);
    
    
        setup_new_exec(bprm);
    
        // 为进程分配用户态堆栈,并塞入参数和环境变量
        retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                     executable_stack);
        current->mm->start_stack = bprm->p;
    
        // 将elf文件映射进内存
        for(i = 0, elf_ppnt = elf_phdata;
            i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
    
            if (unlikely (elf_brk > elf_bss)) {
                unsigned long nbyte;
                    
                // 生成BSS
                retval = set_brk(elf_bss + load_bias,
                         elf_brk + load_bias);
                // ...
            }
    
            // 可执行程序
            if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
                elf_flags |= MAP_FIXED;
            } else if (loc->elf_ex.e_type == ET_DYN) { // 动态链接库
                // ...
            }
    
            // 创建一个新线性区对可执行文件的数据段进行映射
            error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                    elf_prot, elf_flags, 0);
    
            }
    
        }
    
        // 加上偏移量
        loc->elf_ex.e_entry += load_bias;
        // ....
    
    
        // 创建一个新的匿名线性区,来映射程序的bss段
        retval = set_brk(elf_bss, elf_brk);
    
        // 如果是动态链接
        if (elf_interpreter) {
            unsigned long interp_map_addr = 0;
    
            // 调用一个装入动态链接程序的函数 此时elf_entry指向一个动态链接程序的入口
            elf_entry = load_elf_interp(&loc->interp_elf_ex,
                            interpreter,
                            &interp_map_addr,
                            load_bias);
            // ...
        } else {
            // elf_entry是可执行程序的入口
            elf_entry = loc->elf_ex.e_entry;
            // ....
        }
    
        // 修改保存在内核堆栈,但属于用户态的eip和esp
        start_thread(regs, elf_entry, bprm->p);
        retval = 0;
        // 
    }
    

    上面程序的大致流程就是:

    1. 分析头部
    2. 查看是否需要动态链接。如果是静态链接的elf文件,那么直接加载文件即可。如果是动态链接的可执行文件,那么需要加载的是动态链接器。
    3. 装载文件,为其准备进程映像。
    4. 为新的代码段设定寄存器以及堆栈信息
    

    start_thread函数:

    void
    start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
    {
        set_user_gs(regs, 0); // 将用户态的寄存器清空
        regs->fs        = 0;
        regs->ds        = __USER_DS;
        regs->es        = __USER_DS;
        regs->ss        = __USER_DS;
        regs->cs        = __USER_CS;
        regs->ip        = new_ip; // 新进程的运行位置- 动态链接程序的入口处
        regs->sp        = new_sp; // 用户态的栈顶
        regs->flags     = X86_EFLAGS_IF;
        
        set_thread_flag(TIF_NOTIFY_RESUME);
    

    我们可以看到上面的程序主要是: 寄存器清空,设定寄存器的值,尤其是eip和esp的值。

    二、本章总结

    1. 新的可执行程序是从哪里开始执行的?
    当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。
    如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。
    如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。

    2. 对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
    execve系统调用会调用sys_execve,然后sys_execve调用do_execve,然后do_execve调用do_execve_common,然后do_execve_common调用exec_binprm,在exec_binprm中:
    对于ELF文件格式,fmt函数指针实际会执行load_elf_binary,load_elf_binary会调用start_thread,在start_thread中通过修改内核堆栈中EIP的值,使其指向elf_entry,跳转到elf_entry执行。
    对于静态链接的可执行程序,elf_entry是新程序的执行起点。对于动态链接的可执行程序,需要先加载链接器ld,
    elf_entry = load_elf_interp(…)
    将CPU控制权交给ld来加载依赖库,再由ld在完成加载工作后将CPU控制权还给新进程。

    3.总结
    可执行文件是一个普通的文件,它描述了如何初始化一个新的执行上下文,也就是如何开始一个新的计算。可执行文件类别有很多,在内核中有一个链表,在init的时候会将支持的可执行程序解析程序注册添加到链表中,那么在对可执行文件进行解析时,就从链表头开始找,找到匹配的处理函数就可以对其进行解析。
    在shell中启动一个可执行程序时,会创建一个新进程,它通过覆盖父进程(也就是shell进程)的进程环境,并将用户态堆栈清空,获得需要的执行上下文环境。
    命令行参数和环境变量会通过shell传递给execve,excve通过系统调用参数传递,传递给sys_execve,最后sys_execve在初始化新进程堆栈的时候拷贝进去。
    load_elf_binary->start_thread(…)通过修改内核堆栈中EIP的值作为新程序的起点

  • 相关阅读:
    k3 cloud中获取自己开发的单据
    k3 cloud列表中出现很多空白
    k3 cloud支付申请单下推付款单时候提示未将对象引用设置到对象的实例
    k3 cloud出现应收单下推收款单,把收款单是结算方式修改成银行承兑汇票之后保存提示:收款单明细中结算方式为票据业务的实收金额之和不等于票据的当前占用金额之和,请检查数据!
    k3 cloud中出现合计和汇总以后没有显示出来,合价要新增一行以后才出现值
    共享打印机
    k3 cloud总账凭证点击保存的时候提示未将对象引用到对应的实例
    k3 cloud成本调整单引入单据后,再做出库成本核算。成本调整单列表已审核的单据消失,非已审核的单据还在,这是出库成本核算设置参数的问题吗?
    K3 cloud选单时候必须把必录的数据录完以后才可以选单
    k3 cloud成本调整单提示期末余额不存在调整单分录的维度,请先出库核算确认是否存在核算维度的数据
  • 原文地址:https://www.cnblogs.com/liangxu111/p/11828153.html
Copyright © 2011-2022 走看看