Linux中可执行程序的装载和执行

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

1 可执行程序的由来

一个.c源文件是如何变为一个可执行文件的?
大致的步骤如下(以linux下的gcc为例):

  1. 预处理:gcc -E -o hello.cpp hello.c -m32,这一步主要是进行一些宏的替换,得到的仍然是一个文本文件
  2. 编译:gcc -x cpp-ouput -S -o hello.s hello.cpp -m32,这一步会将c代码翻译成汇编语言
  3. 汇编:gcc -x assembler -c hello.s -o hello.o -m32,将编译阶段生成的汇编代码(.S文件)转变为目标
    文件(.o),这一步得到的目标文件已经是ELF格式的二进制文件了,但还不能直接运行,需要和库文件链接在
    一起才可以运行,默认使用动态库进行链接
  4. 链接:gcc -o hello-static hello.o -m32 -static,将多个目标文件(.o文件)链接成ELF格式的可执行文件

可以总结成下图(省略了预编译的过程):

Linux中可执行程序的装载和执行_第1张图片

2 可执行文件的格式

为了更好的了解可执行程序的装载与执行,这里我们需要了解一下可执行文件的格式。

在不同的操作系统中,可执行文件的格式可能会有不同,比如在window下,可执行文件的格式为PE(装过系统的同学对这个肯定不会陌生,全称为Portable Executable),而在linux下,则为ELF(Executeable and linkable format) ,这里我们重点讨论ELF格式,上面提到过的.o目标文件和linux下的可执行文件都是属于这种格式的。

我们可以在linux下的命令行中使用 readelf -h excuteableFile 来查看一个ELF文件的头部信息:

Linux中可执行程序的装载和执行_第2张图片

这里我们使用的是静态链接的方式编译成的 hello 可执行文件,可以看到它的入口地址在 0x8048d0a。当运行一个可执行文件时,第一件事就是要将该文件加载到内存中去,从理论上来讲,系统会将该文件拷贝到一个虚拟的内存空间段里面去,在拷贝的过程中,可执行文件的格式和进程的地址空间存在如下的映射关系:

Linux中可执行程序的装载和执行_第3张图片

上图的右半部分即对应进程的虚拟地址空间,对于32为x86的机器而言,这个虚拟地址空间大小为4G,其中上面1G空间为内核使用,用户态不能访问,只有下面的3G空间可以进行代码和数据的存储。

ELF可执行文件默认被加载到内存0x8048000这个位置,即从这个位置开始加载。先加载ELF可执行文件的头部信息,再加载代码部分,但因不同文件头部大小不一样,第一行代码(即程序的实际入口地址)的位置也会有所不同,图中程序的入口地址为0x8048300。

3 shell程序启动可执行程序的过程

在linux下,我们一般都是在shell的环境下来运行一个可执行文件,那么它到底是如何实现的?下面有一个简化版的“shell”,不过对于理解shell的工作原理足够了:

#include 
#include 
#include 
int main(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   */
        execlp("/bin/ls","ls",NULL);
    } 
    else 
    {  
        /*     parent process  */
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!");
        exit(0);
    }
}

可以看到,在shell环境下运行一个可执行文件(比如 “ls”),实际上就是shell fork出来的子进程执行execve系统调用(ls 作为参数传递),执行完系统调用后,shell的子进程就变为了可执行程序 ls。

注意:一般的系统调用返回后,都是返回到原来的调用程序(调用程序 调用 系统调用),但是 execve和fork则是两个“奇葩”,execve通过修改 执行int 0x80 指令时压入的调用者的sp,ip信息等,来实现调用返回时,执行 ls 可执行程序,这与 庄周梦蝶的故事有点相似,庄周(调用execve的shell子进程)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的ls可执行程序);至于 fork系统调用,可以参考我的前面几篇文章。

那么Shell是如何将 ls 所需要的参数和环境变量传递给它的呢?
结论:先是通过函数调用参数进行传递,再通过系统调用进行参数传递,具体的调用流程如下:
Shell程序 -> execve -> sys_execve,然后在初始化新程序堆栈时将参数和环境变量拷贝进去

4 sys_execve内核处理过程

现在让我们深入内核,来看一看sys_execve的具体的实现过程。具体的调用顺序如下所示:

Linux中可执行程序的装载和执行_第4张图片

若要看具体的代码实现,可以参看下面这篇文章:
http://www.jianshu.com/p/84d96a6385b0

5 实验截图

最后放上实验的截图:

Linux中可执行程序的装载和执行_第5张图片

上图中的红色标注部分是menu程序中的 Makefile 文件需要修改的地方;另外,需要在 test.c 文件中新增 Exec 函数,并使用 MenuCongfig 进行命令的添加;还有一点要注意的是,要新建一个 hello.c 源文件。

实验的运行结果如下图:

Linux中可执行程序的装载和执行_第6张图片

调试过程图:

Linux中可执行程序的装载和执行_第7张图片

上图所示为,先在 sys_execve 处打断点,然后继续运行

Linux中可执行程序的装载和执行_第8张图片

上图为 在load_elf_binary 和 start_thread 出打断点并运行

Linux中可执行程序的装载和执行_第9张图片

上图为start_thread处的断点

Linux中可执行程序的装载和执行_第10张图片

上图为:对传递进 start_thread 的参数,new_ip ,也就是 上文提到过的 elf_entry 转换为16进制格式

Linux中可执行程序的装载和执行_第11张图片

从上图可以看到,待装载的可执行文件 hello 的入口地址 0x8048d0a 和 上面调试程序中 new_ip 的值是一样的,这也印证了我们的猜想。

参考文章:
http://www.jianshu.com/p/84d96a6385b0

你可能感兴趣的:(Linux内核分析)