2021-2022-1 20212810 《Linux内核原理与分析》第八周作业

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

一.实验要求

1.理解编译链接的过程和 ELF 可执行文件格式
2.编程使用 exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式
3.使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve ,验证您对 Linux 系统加载可执行程序所需处理过程的理解
4.特别关注新的可执行程序是从哪里开始执行的?为什么 execve 系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?

二.实验步骤

1.编译链接命令:

gcc -E hello.c -o hello.i     //预处理
gcc -S hello.i -o hello.s    //编译
gcc -c hello.s -o hello.o  //汇编
gcc hello.o -o hello         //链接

请添加图片描述
预处理和编译完的文件均为文本文件,汇编和链接完的文件均为ELF文件。
"gcc -o hello.static hello.o -static"静态编译出来的hello.static把C库里需要的东西也放到可执行文件里了.请添加图片描述

2.ELF文件:

ELF(Excutable and Linking Format)是一个文件格式的标准。输入redelf -h hello可以查看hello的EIF头部,可以看到定义的入口地址与“new_ip”所指向的地址一致。
请添加图片描述
ELF的头保存的是元数据,也就是路线图,描述了文件的组织情况。比如程序头表(program header table)告诉系统如何来创建一个进程的内存映像。section头表(section header table)包含描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字,大小等等信息。ELF的剩余部分是sections,包括代码段,数据段。这些在程序变成进程映像时加载到内存的虚拟地址空间中,从ELF头开始加载。
ELF可执行文件中有三种主要的目标文件:

1.可重定位文件:这种文件一般是中间文件,还需要继续处理。文件中保存着代码和适当的数据,用来和其他的object文件一起创建一个可执行文件或者是一个共享文件。主要是.o文件。

2.可执行文件:文件中保存着一个用来执行的程序;该文件指出了exec如何创建程序进程映像。

3.共享目标文件:共享库,是指可以被可执行文件或者其他库文件使用的目标文件。

3、静态链接和动态链接

静态链接:

在编译链接时直接讲=将需要的执行代码复制到最终可执行文件中,优点是代码的装载速度快,执行速度也快,对外部环境依赖度低。编译时她会把所有需要的代码都链接进去,应用程序相对比较大。缺点是如果多个应用程序使用同一库函数,会被装载多次,浪费内存。

动态链接:

在装入或运行时进行链接。通常被链接的共享代码称为动态链接库或共享库。
动态链接分为可执行程序装载时动态链接和运行时动态链接。
dllibexample.h代码如下:

#ifndef _DL_LIB_EXAMPLE_H_
#define _DL_LIB_EXAMPLE_H_
 
#ifdef  __cplusplus
extern "C" {
    
    
#endif
 
int DynamicalLoadingLibApi();
 
#ifndef __cplusplus
}
#endif
#endif /* _DL_LIB_EXAMPLE_H_ */


dllibexample.c:

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


创建main函数,使其使用两种动态链接库。这里需要注意的是main函数中只include了shlibexample(共享库),没有include dllibexample(动态加载共享库),但是include了dlfcn。原因是前面加了共享库的接口文件,则可以直接调用共享库。但是如果要调用动态加载共享库,就要使用定义在dlfcn.h中的dlopen。

main代码如下:

#include <stdio.h>
#include "shlibexample.h" 
#include <dlfcn.h>
 
int main()
{
    
    
    printf("This is a Main program!\n");
    /* Use Shared Lib */
    printf("Calling SharedLibApi() function of libshlibexample.so!\n");
    SharedLibApi(); //直接调用共享库
    /* Use Dynamical Loading Lib */
    void * handle = dlopen("libdllibexample.so",RTLD_NOW);//打开动态库并将其加载到内存
    if(handle == NULL)
    {
    
    
        printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
        return   FAILURE;
    }
    int (*func)(void);
    char * error;
    func = dlsym(handle,"DynamicalLoadingLibApi");
    if((error = dlerror()) != NULL)
    {
    
    
        printf("DynamicalLoadingLibApi not found:%s\n",error);
        return   FAILURE;
    }    
    printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
    func();  
    dlclose(handle); //卸载库  
    return SUCCESS;
}


运行过程如下:

gcc main.c -o main -L./ -l shlibexample -ldl -m32
export LD_LIBRARY_PATH=$PWD
./main

4.exec函数

所有的exec函数都是C库定义的封装例程,并利用execve()系统调用,也是Linux所提供的处理程序执行的唯一系统调用。ELF文件会被默认映射到0x8048000这个地址。

sys_execve()服务例程接受下列参数:

  • 可执行文件路径名的地址
  • 以NULL结束的字符串指针数组的地址。每个字符串表示一个命令行参数。
  • 以NULL结束的字符串指针数组的地址。每个字符串表示一个命令行参数。每个字符串以NAME=value形式表示一个环境变量。

sys_execve()依次执行以下操作:

  1. 动态地分配一个linux_binprm数据结构,并用新的可执行文件的数据填充这个结构。
  2. 调用path_lookup(),dentry_open()和path_release(),以获得与可执行文件相关的目录项对象,文件对象和索引节点对象。
  3. 检查是否可以由当前的进程执行该文件,再检查索引节点的i_writecount字段,以确定可执行文件没有被写入。
  4. 在多处理器系统中,调用sched_exec()函数来确定最小负载CPU以执行程序。
  5. 调用init_new_context()检查当前进程是否使用自定义局部描述符表。
  6. 调用prepare_binprm()函数填充linux_binprm()数据结构。
  7. 把文件路径名、命令行参数及环境串拷贝到一个或多个新分配叶框中。
  8. 调用search_binary_handler()函数对formats链表进行扫描,并尽力应用每个元素的load_binary方法,把Linux_binprm数据结构传递给这个函数。只要load_binary方法成功应答了文件的可执行格式,对formats的扫描就终止。
  9. 如果在formats链表中,就释放所分配的所有页框并返回错误代码。
  10. 否则,函数释放linux_binprm数据结构,返回从这个文件可执行格式的load_binary方法中所获得的代码。

如果可执行文件是静态链接,可执行文件对应的load_binary()需要将程序的正文段,数据段,bss段和堆栈段映射到进程线性区,然后把用户态eip寄存器的内容设置为新程序的入口。
1.设置断点sys_execeve,并继续
请添加图片描述
代码执行到了SyS_execve。在QEMU中执行exec,可以看到只能出现两句,没有完全执行完毕。请添加图片描述
设置断点load_elf_binary和start_thread,并执行,可以看到代码停在了两个断点处。请添加图片描述
在代码停到SyS_execve处时候,list进入代码:

            old = ACCESS_ONCE(mm->flags);
           new = (old & ~MMF_DUMPABLE_MASK) | value;
       } while (cmpxchg(&mm->flags, old, new) != old);
   }
   
    SYSCALL_DEFINE3(execve,
            const char __user *, filename,
          const char __user *const __user *, argv,
           const char __user *const __user *, envp)
    {

这段代码中DEFINE3中的filename对应文件名,argv对应环境变量,envp对应命令行 请添加图片描述
继续执行,进入do_execve的内部
请添加图片描述
继续执行直到start_thread,显示的po new_ip中的内存地址是0x8048d0a。同时新开终端在menu文件中执行readelf -h hello命令,可以发现入口点地址同样是0x8048d0a。请添加图片描述
继续执行直到代码执行完毕 :请添加图片描述
请添加图片描述

三 .总结

程序是以可执行文件的形式放到磁盘上的,可执行文件既包括执行函数的目标代码,也包括这些函数所使用的数据。当装入一个程序的时候,用户可以提供影响程序执行方式的环境变量和命令行参数两种信息。
当execve()终止时,系统调用的代码不复存在,要执行的新成效已经被映射到进程的地址空间。这样程序就可以成功运行了。

猜你喜欢

转载自blog.csdn.net/weixin_62167029/article/details/121316107