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

    Linux内核如何装载和启动一个可执行程序

    本章知识点

    • 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执行。

    可执行程序的装载

    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先把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是返回到用户态第一条指令的地址。
    **

    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的值作为新程序的起点
  • 相关阅读:
    浅析Python模块的引入和调用
    一篇文章带你了解CSS定位知识
    盘点4大下载神器,教你分分钟搞定文件下载
    Mysql查询语句进阶知识集锦
    (原创)高DPI适配经验系列:(四)高DPI适配示例
    (原创)高DPI适配经验系列:(三)字体与字号、缩放锚点
    (原创)IconFont(矢量图标字体)在Winform中的应用
    (原创)高DPI适配经验系列:(二)按DPI范围适配
    (原创)高DPI适配经验系列:(一)缩放比例与DPI对应关系
    [C#] (原创)一步一步教你自定义控件——06,MaskLayer(遮罩层)
  • 原文地址:https://www.cnblogs.com/yuchao123/p/10028312.html
Copyright © 2011-2022 走看看