20169217 《Linux内核原理与分析》 第九周作业

 实验内容

    理解编译链接的过程和ELF可执行文件格式。

    编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式。   

    使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解。    

    特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?

   1.我们以hello.c为例

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("hello, world!
");
    return 0;
}

   我们写的c代码经过编译器的预处理,预处理之后编译成汇编代码,由汇编器编译成目标代码,然后把它链接成一个可执行文件,这时这个可执行文件由操作系统加载到内存里面来执行。下图一个简单完整的过程,

 

    gcc -E -o hello.cpp hello.c -m32 生成预处理文件hello.cpp,预处理负责把include的文件包含进来及宏替换等工作
    gcc -x cpp-output -S -o hello.s hello.cpp -m32 编译成汇编代码hello.s
    gcc -x assembler -c hello.s -o hello.o -m32 编译成目标代码,得到二进制文件hello.o
    gcc -o hello hello.o -m32 链接成可执行文件hello
    ./hello 运行hello文件
    gcc -o hello.static hello.o -m32 -static 使用静态链接编译
    ls -l 查看静态链接的文件大小

   

   

2.理解ELF可执行文件格式

  我们简单的了解了一下elf格式文件,它是一个格式标准,比较繁琐、复杂。我们简单的来简化理解一下,以elf可执行文件为例,我们了解了它有一个头部,头部里面有一些关键信息,比如程序入口,入口是0x8048300,elf格式文件的头部,后面它会有一些代码数据等等,我们对于进程来讲,有一个进程的地址空间,对于32位x86,4G的进程地址空间,1G内核用,剩下的用户态可访问。当elf文件加载到内存的时候,把代码的数据加载到一块内存中来,很多段代码。加载进来之后默认从0x8048000开始加载,前面是elf头部的一些信息,一般头部的大小会有不同,加载的入口点的位置可能是0x8048300(x00)程序的实际入口。当启动一个刚加载过可执行文件的进程的时候,开始执行的入口点。文件是一个elf的静态连接文件,链接的时候已经链接好了。从这(0x8048300)开始执行,压栈出栈,从main函数到结束,所有的链接在静态链接时候已经设定好了。

3.两种动态链接

    装载时动态链接(Load-time Dynamic Linking):这种方法的前提是在编译之前已经明确知道要调用的动态库的哪些函数,编译时在目标文件中只保留必要的链接信息,而不含动态库函数代码;当程序执行时,调用函数的时候利用链接信息加载动态库函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)    

    运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些动态库函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存);并标识内存地址,其他程序也可以使用该程序,并获得动态库函数的入口地址。(动态库在内存中只存在一份,处在运行阶段)

4.gdb跟踪分析

do_execve:

do_execve_common:

exec_binprm:

    我们发现 execve 是比较特殊的系统调用,exec_binprm 在保存了 bprm 后调用该函数来进一步操作,execve 加载的可执行文件会把当前的进程覆盖掉,返回之后就不是原来的程序而是新的可执行程序起点。这个函数除了保存 pid 以外,还执行了 search_binary_handler 来查询能够处理相应可执行文件格式的处理器,并调用相应的load_binary 方法以启动新进程。

   Linux 系统通过用户态 execve 函数调用内核态 sys_execve 系统调用,负责将新的程序代码和数据替换到新的进程中,打开可执行文件,载入依赖的库文件,申请新的内存空间,最后执行 start_thread 函数设置 new_ipnew_sp,完成新进程的代码和数据替换,然后返回并执行新的进程代码。

原文地址:https://www.cnblogs.com/dkyliuhongyi/p/6069550.html