美文网首页我用 Linux程序员
Linux 如何装载和运行程序

Linux 如何装载和运行程序

作者: AMG架构师 | 来源:发表于2016-04-09 23:30 被阅读799次

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

1.理论基础

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

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

#include <stdio.h>
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!

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


2.ELF 文件格式

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

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

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

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

3.静态&动态链接

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

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

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

4.装载程序

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


do_execve:


do_execve_common:


exec_binprm:


我们发现 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 如何装载和运行程序

    《Linux 内核分析》 MOOC 课程实验 分析 Linux 操作系统如何装载链接并执行程序 1.理论基础 我们...

  • 如何在 Ubuntu/Debian Linux 上编写、编译和运

    如何在 Ubuntu/Debian Linux 上编写、编译和运行一个 C 程序[http://www.linux...

  • 程序的装载

    程序被装载的过程: 创建独立的虚拟内存空间,建立虚拟空间与物理内存的映射关系。在Linux中,程序被运行起来后,将...

  • 现代计算机(1)

    概述 现代计算机解决的是复杂环境下程序运行的问题。这就引出了以下问题: 程序如何存储在外存里? 程序如何装载在内存...

  • 操作系统 - 存储管理

    存储器工作原理 程序链接 链接库 静态链接 程序装载到内存和运行前链接 动态链接 内存一边装载程序一边链接,生成可...

  • Linux 部署netcore

    Linux 端口设置 Linux 运行 NETCORE 程序

  • 程序是怎么跑起来的——虚拟内存与动态链接

    Linux程序是怎么执行的——动态链接 0、前言 计算机的核心任务就是运行程序,而程序是如何运行的?这个问题一直困...

  • 【Linux学习笔记】Linux开机启动过程

    序言 计算机启动Linux系统的过程分为BIOS程序运行过程、引导加载程序运行过程、Linux系统运行过程。 BI...

  • 多线程的基本理解

    首先我们来了解线程和进程的关系 进程:可以理解成一个运行中的应用程序,是将程序装载到内存中,系统为它分配资源运行,...

  • Linux设备驱动程序学习----目录

    目录 设备驱动程序简介 1.设备驱动程序简介 构造和运行模块 2.内核模块和应用程序的对比 3.模块编译和装载 4...

网友评论

    本文标题:Linux 如何装载和运行程序

    本文链接:https://www.haomeiwen.com/subject/dbzvlttx.html