Linux 如何装载和运行程序

《Linux 内核分析》 MOOC 课程实验 分析 Linux 操作系统如何装载链接并执行程序

1.理论基础

我们在本科刚开始学习 C 语言的时候,老师们都会讲 C 语言程序的执行必须经历预处理、编译、汇编、链接、执行程序等过程。由于那时候教学大多使用 VC6 集成开发环境,所以对于上述过程并没有很清晰的概念。今天,我们就通过 GDB 来跟踪分析一个 execve 系统调用内核处理函数 sys_execve,深入理解 Linux 操作系统装载链接和运行可执行程序的过程。

我们先以 hello_world.c 程序为例,搞清楚可执行程序是如何生成的:

#include 
int main(void)
{
    printf("hello, world!\n");
    return 0;
}

1.预处理,处理代码中的宏定义和 include 文件,并做语法检查

gcc -E hello_world.c -o hello_world.i

2.编译,生成汇编代码

gcc -S hello_world.i -o hello_world.s

3.汇编,生成 ELF 格式的目标代码

gcc -c hello_world.s -o hello_world.o

4.链接,生成可执行代码

gcc hello_world.o -o hello_world

5.执行程序

./hello_world
hello, world!

我们可以对这个过程做一个总结,如下图:


Linux 如何装载和运行程序_第1张图片

2.ELF 文件格式

ELF 格式:可执行和可链接格式 (Executable and Linkable Format) 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。

可重定位文件,如:.o 文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这一类。
可执行文件,如:/bin/bash 文件,包含可直接执行的程序,没有扩展名。
共享目标文件,如:.so 文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分。

ELF 文件由 ELF header 和文件数据组成,文件数据包括:

Program header table, 程序头:描述段信息
.text, 代码段:保存编译后得到的指令数据
.data, 数据段:保存已经初始化的全局静态变量和局部静态变量
Section header table, 节头表:链接与重定位需要的数据

Linux 如何装载和运行程序_第2张图片

3.静态&动态链接

链接,是收集和组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。一般分为两步:1.空间与地址分配,2.符号解析与重定位。一般有两种类型,一是静态链接,二是动态链接。

使用静态链接的好处是,依赖的动态链接库较少(这句话有点绕),对动态链接库的版本更新不会很敏感,具有较好的兼容性;不好地方主要是生成的程序比较大,占用资源多。使用动态链接的好处是生成的程序小,占用资源少。动态链接分为可执行程序装载时动态链接和运行时动态链接。

当用户启动一个应用程序时,它们就会调用一个可执行和链接格式映像。LinuxELF 支持两种类型的库:静态库包含在编译时静态绑定到一个程序的函数。动态库则是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的。

Linux 如何装载和运行程序_第3张图片

4.装载程序

我们在 sys_execve 处设置断点开始追踪分析,如下图:

Linux 如何装载和运行程序_第4张图片

Linux 如何装载和运行程序_第5张图片

do_execve:


Linux 如何装载和运行程序_第6张图片

do_execve_common:


exec_binprm:


Linux 如何装载和运行程序_第7张图片

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

search_binary_handler:

int search_binary_handler(struct linux_binprm *bprm)
{
    ...
    retry:
        read_lock(&binfmt_lock);
        //循环查找 linux_binfmt
        list_for_each_entry(fmt, &formats, lh) {
            if (!try_module_get(fmt->module))
                continue;
            read_unlock(&binfmt_lock);
            bprm->recursion_depth++;
            
            //对于 elf 文件,实际上执行的就是 load_elf_binary
            retval = fmt->load_binary(bprm);
            read_lock(&binfmt_lock);
            ...
}

load_elf_binary:

static int load_elf_binary(struct linux_binprm *bprm)
{
    ...

    //获取头
    loc->elf_ex = *((struct elfhdr *)bprm->buf);
    //读取头信息
    if (loc->elf_ex.e_phentsize != sizeof(struct elf_phdr))
        goto out;
    if (loc->elf_ex.e_phnum < 1 ||
        loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))
        goto out;
    size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
    retval = -ENOMEM;
    elf_phdata = kmalloc(size, GFP_KERNEL);
    if (!elf_phdata)
        goto out;
        ...
    //读取可执行文件的解析器
    for (i = 0; i < loc->elf_ex.e_phnum; i++) {
        if (elf_ppnt->p_type == PT_INTERP) {
            ...
    }
    ...
    //如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身的程序入口。
    if (elf_interpreter) {
        unsigned long interp_map_addr = 0;
        elf_entry = load_elf_interp(&loc->interp_elf_ex,
                        interpreter,
                        &interp_map_addr,
                        load_bias);
        if (!IS_ERR((void *)elf_entry)) {

            interp_load_addr = elf_entry;
            elf_entry += loc->interp_elf_ex.e_entry;
        }
        if (BAD_ADDR(elf_entry)) {
            retval = IS_ERR((void *)elf_entry) ?
                    (int)elf_entry : -EINVAL;
            goto out_free_dentry;
        }
        reloc_func_desc = interp_load_addr;
        allow_write_access(interpreter);
        fput(interpreter);
        kfree(elf_interpreter);
    } else {
        elf_entry = loc->elf_ex.e_entry;
        if (BAD_ADDR(elf_entry)) {
            retval = -EINVAL;
            goto out_free_dentry;
        }
    }
}

load_elf_binary 调用 start_thread 函数。修改 int 0x80 压入内核堆栈的 EIP,当 load_elf_binary 执行完毕,返回至 do_execve 再返回至 sys_execve 时,系统调用的返回地址,即 EIP 寄存器,已经被改写成了被装载的 ELF 程序的入口地址了。

5.总结

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

你可能感兴趣的:(Linux 如何装载和运行程序)