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

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

1.编译链接的过程和ELF可执行文件格式

从一个源代码文件到一个可执行程序文件大概要经历如下过程:
这里写图片描述
以C代码为例子,有如下代码的一个hello.c文件

//hello.c
#include <stdio.h>

int main()
{
    printf("hello world!");
    return 0;
}

在shell中,用gcc把其编译,链接成一个可执行程序,可以分解为以下步骤:(-m32代表将其编译成32位代码)
1. gcc -E -o hello.cpp hello.c -m32
参数-E是预处理,负责把include的文件包含进来,以及宏替换等工作。

  1. gcc -x cpp-output -S -o hello.s hello.cpp -m32
    参数-x cpp-output -S是编译过程,将.cpp文件编译成汇编代码。

  2. gcc -x assembler -c hello.s -o hello.o -m32
    参数-x assembler -c是汇编过程,将.s文件中的汇编代码汇编成二进制目标代码。

  3. gcc -o hello hello.o -m32
    将hello.o动态链接成可执行程序。

  4. gcc -o hello-static hello.o -m32 -static
    参数-static代表将hello.o静态链接成可执行程序。

最后得到的hello和hello-static文件就是目标文件。那么目标文件格式是怎么样的?

目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。

现在Linux下主要使用ELF格式(EXECUTABLE AND LINKABLE FORMAT)的目标文件,它的具体格式如下图所示:
这里写图片描述
ELF头描述了该文件的组织情况,(使用命令readelf -h hello 可以查看hello文件的elf头,其中一个很重要的项是Entry point address,它指定了该程序的代码起点地址。)其结构定义如下:
这里写图片描述
text节:被编译程序的机器代码。
rodata节:诸如printf语句中的格式串和switch语句的跳转表等只读数据。
data节:已初始化的全局变量。
bss节(.comm 节):未初始化的全局变量,在目标文件中不占实际的空间。

链接是一个收集、组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。链接过程分为两步:
-空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
-符号解析与重定位
使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。

可执行程序文件,要被装入内存才能执行,那么其在内存中的映像与文件中的映像是如何对应的呢?
这里写图片描述
这里写图片描述

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

动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
1.先从本周第二节课程下载源代码,放入一个目录中:
这里写图片描述

2.准备.so文件
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
编译成libshlibexample.so文件
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
这里写图片描述

dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
编译成libdllibexample.so文件
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
这里写图片描述

分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件

main.c (1.9 KB) - Main program
3.编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
$ 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!
这里写图片描述

使用的main.c代码如下:

#include <stdio.h>

#include "shlibexample.h" 

#include <dlfcn.h>

/*
 * Main program
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
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;
}

装入时动态链接:

就是在开头include了shlibexample.h,那么在装入时,就会将libshlibexample.so中的SharedLibApi()装入进程映像中。

运行时动态链接:

先使用dlopen(),打开一个动态链接库,并返回动态链接库的句柄。他相当于Win32 API函数LoadLibrary()。

void * handle = dlopen("libdllibexample.so",RTLD_NOW);

然后定义了一个函数指针,其指针数据类型要与调用的so引出函数相吻合:

 int (*func)(void);

然后使用了dlsym(),根据 动态链接库 操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。相当于Win32 API函数GetProcAddress()。

func = dlsym(handle,"DynamicalLoadingLibApi");

这样就可以函数指针func就来调用so函数。

func(); 

最后用dlclose()来卸载打开的库。

dlclose(handle);  

3. Linux系统加载可执行程序所需处理过程的理解

3.1 新的可执行程序是从哪里开始执行的?

当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。

如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。

如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。

3.2 为什么execve系统调用返回后新的可执行程序能顺利执行?

首先我们看,新的可执行程序执行,需要哪些东西:
1. 它所需要的库函数。
2. 属于它的进程空间:代码段,数据段,内核栈,用户栈等。
3. 它所需要的运行参数。
4. 它所需要的系统资源。
如果满足以上4个条件,那么新的可执行程序就会处于可运行态,只要被调度到,就可以正常执行。我们一个一个看这几个条件能不能满足。
条件1:如果新进程是静态链接的,那么库函数已经在可执行程序文件中,条件满足。如果是动态链接的,新进程的入口地址是动态链接器ld的起始地址,可以完成对所需库函数的加载,也能满足条件。
条件2:execve系统调用通过大幅度修改执行上下文,将用户态堆栈清空,将老进程的进程空间替换为新进程的进程空间,新进程从老进程那里继承了所需要的进程空间,条件满足。
条件3:我们一般在shell中,输入可执行程序所需要的参数,shell程序把这些参数用函数参数传递的方式传给给execve系统调用,然后execve系统调用以系统调用参数传递的方式传给sys_execve,最后sys_execve在初始化新程序的用户态堆栈时,将这些参数放在main函数取参数的位置上。条件满足。
条件4:如果当前系统中没有所需要的资源,那么新进程会被挂起,直到资源有了,唤醒新进程,变为可运行态,条件可以满足。
综上所述,新的可执行程序可以顺利执行。

3.3 对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?

execve系统调用会调用sys_execve,然后sys_execve调用do_execve,然后do_execve调用do_execve_common,然后do_execve_common调用exec_binprm,在exec_binprm中:

ret = search_binary_handler(bprm);//寻找符合文件格式的对应解析模块(如ELF)
...
//
一个循环:
    retval = fmt->load_binary(bprm);
...

对于ELF文件格式,fmt函数指针实际会执行load_elf_binary,load_elf_binary会调用start_thread,在start_thread中通过修改内核堆栈中EIP的值,使其指向elf_entry,跳转到elf_entry执行。
对于静态链接的可执行程序,elf_entry是新程序的执行起点。对于动态链接的可执行程序,需要先加载链接器ld,
elf_entry = load_elf_interp(…)
将CPU控制权交给ld来加载依赖库,再由ld在完成加载工作后将CPU控制权还给新进程。

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

首先从github下载最新的代码,然后进入menu目录:
这里写图片描述
然后用test_exec.c 替换test.c,再重新编译生成根文件系统:
这里写图片描述
然后启动调试内核:
这里写图片描述
再开一个shell,打开gdb,并加载符号表,连接到端口1234:
这里写图片描述
然后在gdb,中设置断点:这里我设置了sys_execve,load_elf_binary,
start_thread三处断点。
这里写图片描述
这里写图片描述
然后在menu界面中输入命令:exec ,触发了断点。
这里写图片描述
发现程序第一个断点停在了sys_execve处:
这里写图片描述
按s,单步执行,发现接下来会运行do_execve:
这里写图片描述
按c继续执行,发现接下来停在了load_elf_binary处:
这里写图片描述
按c继续执行,发现接下来停在了start_thread处,这时使用命令:
po new_ip 查看new_ip的值,它等于0x8048d0a,再另外打开一个shell,使用命令readelf -h hello查看hello的elf头,可以看到elf头中的程序入口点地址正是0x8048d0a
这里写图片描述
按s,单步执行,可以看到在start_thread中对进程栈的修改。
这里写图片描述

5. 自己对“Linux内核装载和启动一个可执行程序”的理解

可执行文件是一个普通的文件,它描述了如何初始化一个新的执行上下文,也就是如何开始一个新的计算。可执行文件类别有很多,在内核中有一个链表,在init的时候会将支持的可执行程序解析程序注册添加到链表中,那么在对可执行文件进行解析时,就从链表头开始找,找到匹配的处理函数就可以对其进行解析。
在shell中启动一个可执行程序时,会创建一个新进程,它通过覆盖父进程(也就是shell进程)的进程环境,并将用户态堆栈清空,获得说需要的执行上下文环境。
命令行参数和环境变量会通过shell传递给execve,excve通过系统调用参数传递,传递给sys_execve,最后sys_execve在初始化新进程堆栈的时候拷贝进去。
load_elf_binary->start_thread(…)通过修改内核堆栈中EIP的值作为新程序的起点。
如果新程序的动态链接的,那么就需要加载所需要的库函数,动态连接器ld会负责加载过程,动态链接库的装载过程类似于一个图的广度优先遍历过程,装载完成后,ld将CPU控制权交给可执行程序,继续执行可执行程序。

参考资料

  1. http://wenku.baidu.com/link?url=kgryIi9UETCgiAQaHEEvDvUH6v9kSEBkjisias0dPrmF6B6sX4geXI4AVYMYX6gJrCgXkJSfvBf2y5Cw8ahOW7bxYVOhzXih5-daJUZVp2m
  2. http://blog.sina.com.cn/s/blog_a401a1ea0101iurl.html
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值