zoukankan      html  css  js  c++  java
  • 《Linux内核分析》第七周学习笔记

    《Linux内核分析》第七周学习笔记 可执行程序的装载

    郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

    【学习视频时间:1小时35分钟 实验时间:1小时 撰写博客时间:2小时40分钟】

    【学习内容:共享库和动态链接、exec系统调用的执行过程、可执行程序的装载】

    一、预处理、编译、链接和目标文件的格式

    1.1 可执行程序如何得来?

    过程:

    1. C源代码(.c)经过编译器预处理被编译成汇编代码(.asm)
    2. 汇编代码由汇编器被编译成目标代码(.o)
    3. 将目标代码链接成可执行文件(a.out)
    4. 可执行文件由操作系统加载到内存中执行

    vi hello.c
    gcc -E -o hello.cpp hello.c -m32 //预处理.c文件,预处理包括把include的文件包含进来以及宏替换等工作
    
    vi hello.cpp
    gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码.s
    
    vi hello.s
    gcc -x assembler -c hello.s -o hello.o -m32 //将汇编代码.s编译成二进制目标文件.o(不可读,含有部分机器指令但不可执行)
    
    vi hello.o
    gcc -o hello hello.o -m32 //将目标文件链接成可执行二进制文件hello
    
    vi hello
    gcc -o hello.static hello.o -m32 -static 

    注:

    • hello和hello.o都是ELF格式的文件
    • .static文件将所有用到C库文件都放到某一可执行程序中,所以占用空间较多

    1.2 目标文件的格式ELF

    1. .out是最古老的可执行文件,目前Windows系统上多是PE,Linux系统上多是ELF。ELF文件已经是适应到某一种CPU体系结构的二进制兼容文件了

    2. 目标文件的三种形式:

    • 可重定位文件.o,用来和其他object文件一起创建可执行文件和共享文件
    • 可执行文件,指出应该从哪里开始执行
    • 共享文件,主要是.so文件,用来被链接编辑器和动态链接器链接

    3. ELF格式

    • ELF头描述了该文件的组织情况,程序投标告诉系统如何创建一个进程的内存映像,section头表包含了描述文件sections的信息。当系统要执行一个文件的时候,理论上它会把程序段拷贝到虚拟内存中某个段
    • ELF文件的头部规定了许多与二进制兼容性相关的信息。所以在加载ELF文件的时候,必须先加载头部,分析ELF的具体信息
    • entry代表刚加载过新的可执行文件之后的程序的入口地址,头部后是代码和数据,进程的地址空间是4G,上面的1G是内核用,下面的3G是程序使用。默认的ELF头加载地址是0x8048000

    1.3 静态链接的ELF可执行文件和进程的地址空间

    1. 可执行文件加载到内存时:

    • 加载效果:将代码段数据加载到内存中,再把数据加载到内存,默认从0x8048000地址开始加载
    • 启动一个刚加载过可执行文件的进程时,可执行文件加载到内存之后执行的第一条代码地址
    • 一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段

    2. 流程

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

    二、可执行程序、共享库和动态链接

    2.1 装载可执行程序之前的工作

    1. 可执行程序的执行环境

    • 一般执行一个程序的Shell环境,实验中直接使用execve系统调用
    • Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身,如:

    int main(int argc, char *argv[])
    
    int main(int argc, char argv[], char envp[])//envp是shell的执行环境
    • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

    int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

    2. 命令行参数和环境串都放在用户态堆栈中

    • fork子进程的时候完全复制了父进程
    • 调用exec的时候,要加载的可执行程序把原来的进程环境覆盖掉,用户态堆栈也被清空
    • 命令行参数和环境变量进入新程序的堆栈时,把环境变量和命令行参数压栈,相当于main函数启动
    • shell程序——>execve——>sys_execve,然后在初始化新程序堆栈的时候拷贝进去
    • 先传递函数调用参数,再传递系统调用参数

    2.2 装载时动态链接和运行时动态链接应用举例

    1. 动态链接分为可执行程序装载时动态链接和运行时动态链接,大部分使用可执行程序装载时动态链接。

    2. 共享库的动态链接

    • 准备.so文件(在Linux下动态链接文件格式,在Windows中是.dll)

    #ifndef _SH_LIB_EXAMPLE_H_
    #define _SH_LIB_EXAMPLE_H_
    
    #define SUCCESS 0
    #define FAILURE (-1)
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
    * Shared Lib API Example
    * input : none
    * output    : none
    * return    : SUCCESS(0)/FAILURE(-1)
    *
    */
    int SharedLibApi();//内容只有一个函数头定义
    
    
    #ifdef __cplusplus
    }
    #endif
    #endif /* _SH_LIB_EXAMPLE_H_ */
    /*------------------------------------------------------*/
    
    #include <stdio.h>
    #include "shlibexample.h"
    
    int SharedLibApi()
    {
        printf("This is a shared libary!
    ");
        return SUCCESS;
    }/* _SH_LIB_EXAMPLE_C_ */
    • 编译成.so文件

    gcc -shared shlibexample.c -o libshlibexample.so -m32

    3. 动态加载库

    #ifndef _DL_LIB_EXAMPLE_H_
    #define _DL_LIB_EXAMPLE_H_
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Dynamical Loading Lib API Example
     * input    : none
     * output   : none
     * return   : SUCCESS(0)/FAILURE(-1)
     *
     */
    int DynamicalLoadingLibApi();
    
    #ifdef __cplusplus
    }
    #endif
    #endif /* _DL_LIB_EXAMPLE_H_ */
    /*------------------------------------------------------*/
    
    #include <stdio.h>
    #include "dllibexample.h"
    
    #define SUCCESS 0
    #define FAILURE (-1)
    
    /*
     * Dynamical Loading Lib API Example
     * input    : none
     * output   : none
     * return   : SUCCESS(0)/FAILURE(-1)
     *
     */
    int DynamicalLoadingLibApi()
    {
        printf("This is a Dynamical Loading libary!
    ");
        return SUCCESS;
    }

    4. main.c

    #include <stdio.h>
    #include "shlibexample.h" //只include了共享库
    #include <dlfcn.h>
    /*
     * Main program
     * input    : none
     * output   : none
     * return   : SUCCESS(0)/FAILURE(-1)
     *
     */
    int main()
    {
        printf("This is a Main program!
    ");
        /* Use Shared Lib */
        printf("Calling SharedLibApi() function of libshlibexample.so!
    ");
        SharedLibApi();//可以直接调用,因为include了这个库的接口
        /* Use Dynamical Loading Lib */
        void * handle = dlopen("libdllibexample.so",RTLD_NOW);//先打开动态加载库
        if(handle == NULL)
        {
            printf("Open Lib libdllibexample.so Error:%s
    ",dlerror());
            return   FAILURE;
        }
        int (*func)(void);
        char * error;
        func = dlsym(handle,"DynamicalLoadingLibApi");
        if((error = dlerror()) != NULL)
        {
            printf("DynamicalLoadingLibApi not found:%s
    ",error);
            return   FAILURE;
        }    
        printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!
    ");
        func();  
        dlclose(handle);//与dlopen函数配合,用于卸载链接库       
        return SUCCESS;
    }

    dlsym函数与上面的dlopen函数配合使用,通过dlopen函数返回的动态库句柄(由dlopen打开动态链接库后返回的指针handle)以及对应的符号返回符号对应的指针。

    5. 编译main.c

     $ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
        $ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
        $ ./main
        This is a Main program!
        Calling SharedLibApi() function of libshlibexample.so!
        This is a shared libary!
        Calling DynamicalLoadingLibApi() function of libdllibexample.so!
        This is a Dynamical Loading libary!

    注:这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl。

    三、可执行程序的装载

    3.1 可执行程序的装载相关关键问题分析

    1. execve与fork是比较特殊的系统调用

    • execve用它加载的可执行文件把当前的进程覆盖掉,返回之后就不是原来的程序而是新的可执行程序起点;
    • fork函数的返回点ret_ from_fork是用户态起点

    2. sys_ execve内核处理过程

    • do_ execve -> do_ execve_ common -> exec_ binprm -> search_ binary_handler,最后根据文件头部信息寻找对应的文件格式处理模块

    3.2 sys_execve的内部处理过程

    1. exec一般和fork调用,常规用法是fork出一个子进程,然后在子进程中执行exec,替换为新的代码。

    2. 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。

    3. sys_execve的内部处理过程

    • 打开file文件,找到文件头部,把命令行参数和环境变量copy到结构体中
    • 寻找打开的可执行文件处理函数
    • 寻找能够解析当前可执行文件的模块,load_ binary加载这个模块,它实际调用的是binfmt_ elf.c
    • 需要动态链接的可执行文件先加载连接器ld;否则直接把ELF文件entry地址赋值给entry
    • start_ thread(regs, elf_ entry, bprm->p)将CPU控制权交给ld来加载依赖库并完成动态链接。对于静态链接的文件elf_entry是新程序执行的起点

    3.3 使用gdb跟踪sys_execve内核函数的处理过程(实验)

    1. 开始先更新内核,再用test_exec.c将test.c覆盖掉

    2. test.c文件中增加了exec系统调用,Makefile文件中增加了gcc -o hello hello.c -m32 -static

    3. 启动内核并验证execv函数

    4. 启动gdb调试

    5. 先停在sys_execve处,再设置其它断点

    6. 进入函数单步执行

    7. new_ip是返回到用户态的第一条指令

    8. 退出调试状态后输入redelf -h hello可以查看hello的EIF头部

    3.4 可执行程序的装载与庄周梦蝶的故事

      庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)

    3.5 浅析动态链接的可执行程序的装载

    1. 动态链接的过程中,内核做了什么?

    ldd test
    
    ldd libfuse.so //可执行程序需要依赖动态链接库,而这个动态链接库可能会依赖其他的库,实际上动态链接库的依赖关系会形成一个图

    2. 是由内核负责加载可执行程序依赖的动态链接库吗?

    • 动态链接器负责加载这些库并进行解析当前的可执行文件,装载所有需要的动态链接库,动态链接库的装载过程是一个图的遍历(广度)
    • 装载和链接后ld将CPU的控制权交给可执行程序
    • 动态链接的过程主要由动态链接器完成,并不是内核

    总结

      通过对本周视频的学习,我了解到exec系统调用的执行过程与fork有些不同。fork一个新进程时,子进程的堆栈和父进程完全相同,寄存器信息也完全相同,仅仅把系统调用的返回值eax清零。而这里将寄存器清零,堆栈是全新分配的,对于eip,如果是静态链接的可执行文件,那么eip指向该elf文件的文件头e_entry所指的入口地址;如果是动态链接,eip指向动态链接器。

  • 相关阅读:
    Importing multi-valued field into Solr from mySQL using Solr Data Import Handler
    VMware Workstation 虚拟机使用无线wifi上网配置
    Linux开发黑客
    GitHub 使用说明
    虹软人脸检测和识别C#
    C#将结构体和指针互转的方法
    笔记本电脑连接wifi,同时提供热点wifi给手机使用
    基于STM32L4的开源NBIOT开发资料
    ESP8266擦除工具完整安装
    开发快平台(M302I小e开发板系列教程)
  • 原文地址:https://www.cnblogs.com/20135228guoyao/p/5360264.html
Copyright © 2011-2022 走看看