zoukankan      html  css  js  c++  java
  • 2017-2018-1 20179202《Linux内核原理与分析》第八周作业

    一 、可执行程序的装载

    1. 预处理、编译、链接

    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   //链接成可执行文件,使用共享库
    

    gcc -o hello.static hello.o -static静态编译出来的hello.static把C库里需要的东西也放到可执行文件里了。用命令ls –l,可以看到hello只有7K,hello.static有大概700K。

    2. ELF文件

    ELF(Excutable and Linking Format)是一个文件格式的标准。通过readelf-h hello查看可执行文件hello的头部(-a查看全部信息,-h只查看头部信息),头部里面注明了目标文件类型ELF32。Entry point address是程序入口,地址为0x8048310,
    即可执行文件加载到内存中开始执行的第一行代码地址。头部后还有一些代码数据等等。可执行文件的格式和进程的地址空间有一个映射的关系,当程序要加载到内存中运行时,将ELF文件的代码段和数据段加载到进程的地址空间。

    ELF文件里面三种目标文件:可重定位(relocatable)文件保存着代码和适当的数据,用来和其它的object文件一起来创建一个可执行文件或者是一个共享文件(主要是.o文件);可执行(executable)文件保存着一个用来执行的程序,该文件指出了exec(BA_OS)如何来创建程序进程映象(操作系统怎么样把可执行文件加载起来并且从哪里开始执行);共享object文件保存着代码和合适的数据,用来被两个链接器链接。第一个是链接编辑器(静态链接),可以和其它的可重定位和共享object文件来创建其它的object。第二个是动态链接器,联合一个可执行文件和其它的共享object文件来创建一个进程映象。

    3. 动态链接

    动态链接有可执行装载时的动态链接(大多数)和运行时的动态链两种方式。

    (1)共享库

    shlibexample.h中定义了SharedLibApi()函数,shlibexample.c是对此函数的实现。用```gcc -shared shlibexample.c -o libshlibexample.so -m32``(在64位环境下执行时加上-32)生成.so文件。这样就生成了共享库文件。

    #include <stdio.h>
    #include "shlibexample.h"
    
    int SharedLibApi()
    {
        printf("This is a shared libary!
    ");
        return SUCCESS;
    }
    

    (2)动态加载共享库

    dllibexample.h定义了DynamicalLoadingLibApi()函数,dllibexample.c是对此函数的实现。同样使用gcc -shared dllibexample.c -o libdllibexample.so 得到动态加载共享库。

    #include <stdio.h>
    #include "dllibexample.h"
    
    #define SUCCESS 0
    #define FAILURE (-1)
    
    int DynamicalLoadingLibApi()
    {
        printf("This is a Dynamical Loading libary!
    ");
        return SUCCESS;
    }
    

    (3)main函数使用两种动态链接库。

    #include <stdio.h>
    #include "shlibexample.h" 
    #include <dlfcn.h>
    
    int main()
    {
        printf("This is a Main program!
    ");
        /* Use Shared Lib */
        printf("Calling SharedLibApi() function of libshlibexample.so!
    ");
        SharedLibApi(); //直接调用共享库
        /* 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); //卸载库  
        return SUCCESS;
    }
    

    可以看到main函数中只include了shlibexample(共享库),没有include dllibexample(动态加载共享库),但是include了dlfcn。因为前面加了共享库的接口文件,所以可以直接调用共享库。但是如果要调用动态加载共享库,就要使用定义在dlfcn.h中的dlopen。

    gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 生成可执行文件。注意,这里只提供shlibexample的-L,并没有提供dllibexample的相关信息,只是指明了-ldl。-dl动态加载,编译main.c的时候,没有指明任何相关信息,只是在程序内部指明了。实验截图如下:

    3. 代码分析

    当前的可执行程序在执行,执行到execve的时候陷入到内核态,用execve的加载的可执行文件把当前进程的可执行程序给覆盖掉,当execve的系统调用返回的时候,已经返回的不是原来的那个可执行程序了,是新的可执行程序的起点(main函数)。shell环境会执行execve,把命令行参数和环境变量都加载进来,当系统调用陷入到内核里面的时候,system call调用sys_execve。sys_execve中调用了do_execve。

    //sys_execve
    SYSCALL_DEFINE3(execve,
                    const char __user *, filename,                //可执行程序的名称
                    const char __user *const __user *, argv,      //程序的参数
                    const char __user *const __user *, envp)      //环境变量
    {
        return do_execve(getname(filename), argv, envp);
    }
    
    //do_execve
    int do_execve(struct filename *filename,
        const char __user *const __user *__argv,
        const char __user *const __user *__envp)
    {
        struct user_arg_ptr argv = { .ptr.native = __argv };
        struct user_arg_ptr envp = { .ptr.native = __envp };
        return do_execve_common(filename, argv, envp);
    }
    

    很明显,继续分析其中调用的do_execve_common:

    static int do_execve_common(struct filename *filename,
    				struct user_arg_ptr argv,
    				struct user_arg_ptr envp)
    {
    	struct linux_binprm *bprm; 
    	struct file *file;             
    	struct files_struct *displaced;
    	int retval;
    	...
    	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);//在堆上分配一个linux_binprm结构体
        ...
    	file = do_open_exec(filename);//打开需要加载的可执行文件,file中就包含了打开的可执行文件的信息
        ...
    	bprm->file = file;                             //赋值file指针
    	bprm->filename = bprm->interp = filename->name;//赋值文件名
    
    	retval = bprm_mm_init(bprm);                   //创建进程的内存地址空间
    	...
    	bprm->argc = count(argv, MAX_ARG_STRINGS);//赋值参数个数
    	...
    	bprm->envc = count(envp, MAX_ARG_STRINGS);//赋值环境变量个数
    	...
    	retval = copy_strings_kernel(1, &bprm->filename, bprm); //从内核空间获取文件路径;
    	...
    	bprm->exec = bprm->p;                         //p为当前内存页最高地址
    	retval = copy_strings(bprm->envc, envp, bprm);//把环境变量拷贝到bprm中
        ...
    	retval = copy_strings(bprm->argc, argv, bprm);//把命令行参数拷贝到bprm中
    	...
    	retval = exec_binprm(bprm);//处理可执行文件
    	...
        return retval;
    }
    
    

    linux_binprm结构体用来保存要执行文件的相关信息, 如文件的头128字节、文件名、命令行参数、环境变量、文件路径、内存描述符信息等。exec_binprm函数保存当前的pid,其中ret = search_binary_handler(bprm); 调用 search_binary_handler 寻找可执行文件的相应处理函数。

    int search_binary_handler(struct linux_binprm *bprm)
     {
        bool need_retry = IS_ENABLED(CONFIG_MODULES);
    	struct linux_binfmt *fmt;
    	int retval;
        ...
        read_lock(&binfmt_lock);
        list_for_each_entry(fmt, &formats, lh) {            //遍历文件解析链表
               if (!try_module_get(fmt->module))
                       continue;
               read_unlock(&binfmt_lock);
               bprm->recursion_depth++;
                    //解析elf格式执行的位置
               retval = fmt->load_binary(bprm);// 加载可执行文件的处理函数
               read_lock(&binfmt_lock);
               ...
            }
        return retval;
    
    

    linux_binfmt结构体定义了一些函数指针,不同的Linux可接受的目标文件格式(如load_binary,load_shlib,core_dump)采用不同的函数来进行目标文件的装载。每一个linux_binfmt结构体对应一种二进制程序处理方法。这些结构体实例会通过init_elf_binfmt以注册的方式加入到内核对应的format链表中去,通过register_binfmt()unregister_binfmt()在链表中插入和删除对象。

    struct linux_binfmt {
        struct list_head lh;
        struct module *module;
        int (*load_binary)(struct linux_binprm *);//用于加载一个新的进程(通过读取可执行文件中的信息)
        int (*load_shlib)(struct file *);         //用于动态加载共享库
        int (*core_dump)(struct coredump_params *cprm);//在core文件中保存当前进程的上下文
        unsigned long min_coredump;     
     };
    

    目标文件的格式是ELF,所以retval = fmt->load_binary(bprm);中load_binary实际上调用load_elf_binary完成ELF二进制映像的认领、装入和启动。load_elf_binary这个函数指针被包含在一个名为elf_format的结构体中:

    static structlinux_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,  
    }; 
    

    全局变量elf_format赋给了一个指针,在init_elf_binfmt里把变量注册注册到文件解析链表中,就可以在链表里找到相应的文件格式。继续分析load_elf_binary:

     static int load_elf_binary(struct linux_binprm *bprm)
    {
        ...
        if (elf_interpreter) {                        // 动态链接的处理  
             ... 
             } else {                                 // 静态链接的处理  
                      elf_entry =loc->elf_ex.e_entry; 
                      ...
                      }  
             }  
        ...
        //将ELF文件映射到进程空间中,execve系统调用返回用户态后进程就拥有了新的代码段、数据段。
        current->mm->end_code = end_code;  
        current->mm->start_code =start_code;  
        current->mm->start_data =start_data;  
        current->mm->end_data = end_data;  
        current->mm->start_stack =bprm->p;  
        ...
        start_thread(regs, elf_entry, bprm->p);
    }
    

    ELF文件中的Entry point address字段指明了程序入口地址,这个地址一般是0x8048000(0x8048000以上的是内核段内存)。该入口地址被解析后存放在elf_ex.e_entry中,所以静态链接程序的起始位置就是elf_entry。这个函数中还有一个关键点start_thread:

    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);
    }
    

    regs中为系统调用时SAVE_ALL宏压入内核栈的部分。new_ip的值等于参数elf_entry的值,即把ELF文件中定义的main函数起始地址赋值给eip寄存器,进程返回到用户态时的执行位置从原来的int 0x80的下一条指令变成了new_ip的位置。

    总结一下,调用顺序是sys_execve -> do_execve -> do_execve_common -> exec_binprm,当系统调用从内核态返回到用户态时,eip直接跳转到ELF程序的入口地址,CPU也得到新的用户态堆栈(包含新程序的命令行参数和shell上下文环境)。这样,新程序就开始执行了。

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

    用test_exe.c覆盖test.c,增加了一句MenuConfig()执行一个程序。

    int Exec(int argc, char *argv[])
    {
            int pid;
            /* fork another process */
            pid = fork();
            if (pid < 0)
            {
                    /* error occurred */
                    fprintf(stderr,"Fork Failed!");
                    exit(-1);
            }
            else if (pid == 0)
            {
                    /*       child process  */
            printf("This is Child Process!
    ");
                    execlp("/hello","hello",NULL);
            }
            else
            {
                    /*      parent process   */
            printf("This is Parent Process!
    ");
                    /* parent will wait for the child to complete*/
                    wait(NULL);
                    printf("Child Complete!
    ");
            }
    }
    

    makefile做了一些修改,编译了hello.c,在生成根文件系统的时候,把init和hello都放到rootfs.img内。这样在执行execve时就自动的加载hello可执行文件:

    在前面分析的关键点设置断点,一边一句一句向下跟踪,一边对照执行过程。追踪到start_thread,用po new_ip,得到的是0x804887f。

    readelf –h hello可以看到这个可执行程序它的入口点地址也是0x804887f。

    5.遇到的问题及解决

    (1)看了视频后对动态链接的第二种方式依然理解模糊,通过搜索资料解决。

      如果要调用动态加载共享库,就要使用定义在dlfcn.h中的dlopen。给出文件名libdllibexample.so和标志RTLD_NOW打开动态链接库,返回handle句柄。dlsym函数与上面的dlopen函数配合使用,根据操作句柄(由dlopen打开动态链接后返回的指针)handle与符号(要求获取的函数或全局变量的名称)DynamicLoadingLibApi,返回符号对应的地址。使用此地址可以获得库中特定函数的地址,并且调用库中的相应函数。这样就可以使用动态加载共享库里面所定义的函数了。

    (2)不理解调试中的po

    po是print_object的缩写,不仅仅可以输出显示定义的对象,也可以输出表达式的结果。我尝试了p、po、p/d、px,对比它们的执行结果:

    可以发现p、p/d(10进制)、px(16进制)输出值前都会有一个类似"$1="的前缀,它们是变量,在后面的表达式中可以使用,而po并不能把它的返回值存储到变量里。至于po还能在哪些地方看的不太清,以后遇到了再具体分析。

    (3)在第六周实验中,Rename函数实现把"hello.c"重命名为"newhello.c",在当前文件夹中放一个hello.c文件即可实现。但在MenuOS上,把hello.c文件尝试放在menu文件夹下,执行rename命令显示不成功:

    所以我一直不知道该把hello.c文件放在哪里才可以重命名成功。这周孟老师修改Makefile文件提醒了我,我修改了Makefile,把hello.c打包到镜像文件中:

    虽然显示执行成功,不幸的是hello.c并没有重命名为newhello.c:

    我想,修改的应该是rootfs.img中的hello.c,所以这里的hello.c才没被修改(不知道思考的对不对,想打开rootfs.img,试了几种方法都没有解决)。

    二 、课本笔记

    虚拟文件系统

    1.虚拟文件系统(VFS)是linux内核和存储设备之间的抽象层。VFS中有四个主要的对象类型,分别是超级块对象、索引节点对象、目录项对象、文件对象。

    2.超级块主要存储特定文件系统相关的信息,存储在磁盘上,在使用时创建在内存中的。对于磁盘文件系统来说,这个对象通常对应磁盘上的一个文件系统控制块(磁盘super block)。

    3.索引节点包含内核在操作文件或目录时需要的全部信息。一个索引节点代表文件系统中的一个文件(这里的文件不仅是指我们平时所认为的普通的文件,还包括目录,特殊设备文件等等)。索引节点存储在磁盘上,当被应用程序访问到时才会在内存中创建。

    4.通过索引节点已经可以定位到指定的文件,但索引节点对象的属性非常多,在查找,比较文件时,直接用索引节点效率不高,所以引入了目录项(dentry)的概念。目录项并不实际存在于磁盘上,在使用的时候在内存中创建目录项对象。

    5.在一个文件路径中,路径中的每一部分都被称为目录项。每个目录项对象都有被使用,未使用和负状态3种状态。一个被使用的目录项对应一个有效的索引节点,并且该对象由一个或多个使用者;一个未被使用的目录项对应一个有效的索引节点,但是VFS当前并没有使用这个目录项;一个负状态的目录项没有对应的有效索引节点。

    6.在Linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待即“一切皆文件”。文件对象表示进程已打开的文件,从用户角度来看,我们在代码中操作的就是一个文件对象。虽然一个文件对应的文件对象不是唯一的,但其对应的索引节点和目录项对象却是唯一的。

    7.VFS中还有2个专门针对文件系统的2个对象,struct file_system_type用来描述各种特定文件系统类型(比如ext3,ext4或UDF),struct vfsmount 用来描述一个安装文件系统的实例。被Linux支持的文件系统,都有且仅有一 个file_system_type结构而不管它有零个或多个实例被安装到系统中。当文件系统被实际安装时,会在安装点创建一个vfsmount结构体。

    8.以下3个结构体和进程紧密联系在一起:

    • struct files_struct:由进程描述符中的 files 目录项指向,所有与单个进程相关的信息(比如打开的文件和文件描述符)都包含在其中。
    • struct fs_struct:由进程描述符中的 fs 域指向,包含文件系统和进程相关的信息。
    • struct mmt_namespace:由进程描述符中的 mmt_namespace 域指向。

    块I/O层

    1.I/O设备主要有字符设备和块设备,相比字符设备的只能顺序读写设备中的内容,块设备能够随机读写设备中的内容。字符设备只能顺序访问,块设备随机访问。

    2.块设备最小的可寻址单元是扇区。扇区的大小一般是2的整数倍,最常见的大小是512个字节。扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作。虽然物理磁盘寻址是按照扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的。为了便于文件系统管理,块的大小一般是扇区的整数倍,并且小于等于页的大小。

    3.当一个块被调入内存时,它要存储在一个缓冲区中。每一个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。每个缓冲区都有一个对应的描述符,用buffer_head结构体表示,称作缓冲区头,包含了内和操作缓冲区所需要的全部信息。

    4.bio结构体表示了一次I/O操作所涉及到的所有内存页。通过用片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证I/O操作的执行。

    5.bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。

    6.块设备将它们挂起的块I/O请求保存在请求队列中,该队列由request_queue结构体表示。请求队列表中的每一项都是一个单独的请求,由reques结构体表示。因为一个请求可能要操作多个连续的磁盘块,所有每个请求可有由多个bio结构体组成。

    7.虽然磁盘上的块必须连续,但是在内存中的这些块并不一定要连续。

    8.I/O调度程序的工作是管理块设备的请求队列。通过合并与排序减少磁盘寻址时间。

    9.为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间,I/O调度程序称作电梯调度。

    10.2.6内核中内置了4种I/O调度: 预测(as)、完全公正排队(cfq)、最终期限(deadline)、空操作(noop)。通过命令行选项 elevator=xxx 来启用其中的任何一种。

  • 相关阅读:
    Oracle基础知识整理
    linux下yum安装redis以及使用
    mybatis 学习四 源码分析 mybatis如何执行的一条sql
    mybatis 学习三 mapper xml 配置信息
    mybatis 学习二 conf xml 配置信息
    mybatis 学习一 总体概述
    oracle sql 语句 示例
    jdbc 新认识
    eclipse tomcat 无法加载导入的web项目,There are no resources that can be added or removed from the server. .
    一些常用算法(持续更新)
  • 原文地址:https://www.cnblogs.com/Jspo/p/7858153.html
Copyright © 2011-2022 走看看