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

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

写在前面

本次实验着力分析Linux内核装载和启动一个可执行程序的过程,其中包括可执行文件格式的分析、可执行文件的装载和链接的过程,并通过GDB跟踪execve系统调用来梳理Linux系统加载可执行程序的过程。

可执行文件的格式分析

相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object File Format 通用对象文件格式)、ELF(Executable and Linking Format 可执行和链接格式。现在Linux中绝大部分的可执行文件格式都为ELF,这里只对ELF进行展开描述。

ELF文件格式结构体描述:

/* ELF文件头部 */
typedef struct
{
        unsigned char e_ident[EI_NIDENT];     /* 魔数和相关信息 */
        Elf32_Half    e_type;                 /* 目标文件类型 */
        Elf32_Half    e_machine;              /* 硬件体系 */
        Elf32_Word    e_version;              /* 目标文件版本 */
        Elf32_Addr    e_entry;                /* 程序进入点 */
        Elf32_Off     e_phoff;                /* 程序头部偏移量 */
        Elf32_Off     e_shoff;                /* 节头部偏移量 */
        Elf32_Word    e_flags;                /* 处理器特定标志 */
        Elf32_Half    e_ehsize;               /* ELF头部长度 */
        Elf32_Half    e_phentsize;            /* 程序头部中一个条目的长度 */
        Elf32_Half    e_phnum;                /* 程序头部条目个数  */
        Elf32_Half    e_shentsize;            /* 节头部中一个条目的长度 */
        Elf32_Half    e_shnum;                /* 节头部条目个数 */
        Elf32_Half    e_shstrndx;             /* 节头部字符表索引 */
} Elf32_Ehdr;

在Linux中可以使用readelf -h 来查看一个可执行文件的头部,如图所示:

下一个是ELF头部的程序头表,它是一个结构数组,包含了ELF头表中字段e_phnum定义的条目,结构描述一个段或其他系统准备执行该程序所需要的信息。

typedef struct {
      Elf32_Word  p_type;                 /* 段类型 */
      Elf32_Off   p_offset;       /* 段位置相对于文件开始处的偏移量 */
      Elf32_Addr  p_vaddr;              /* 段在内存中的地址 */
      Elf32_Addr  p_paddr;              /* 段的物理地址 */
      Elf32_Word  p_filesz;             /* 段在文件中的长度 */
      Elf32_Word  p_memsz;              /* 段在内存中的长度 */
      Elf32_Word  p_flags;              /* 段的标记 */
      Elf32_Word  p_align;              /* 段在内存中对齐标记 */
  } Elf32_Phdr;

用readelf查看的结果如下:

可执行文件的链接

可执行文件的链接一般分为静态链接和动态链接,

静态链接

静态库的核心思想是:将不同的可重定位模块打包成一个文件,在链接的时候会自动从这个文件中抽取出用到的模块。这样就无需手动列出需要用的模块,方便了链接的过程。优点是程序执行快,缺点是程序体大、不易维护。

动态链接

动态链接的核心思想是:代码共享和延迟绑定。 代码共享依靠虚拟存储器实现,延迟绑定的核心在于两张表:PLT(Procedure Linkage Table)和GOT(Global Offset Table)。基于虚拟存储器的代码共享使得在内存中只存在某个模块的唯一一份代码,通过虚拟存储器的内存映射机制将物理地址空间中的代码映射到不同进程的虚拟地址空间中。

动态链接节约了内存,并且易于维护。但不够安全,容易遭到劫持。

可执行文件的装载

在Linux中是通过execve系统调用来实现一个可执行文件的装载的,在内核中execve的定义如下:

SYSCALL_DEFINE3(execve,
        const char __user *, filename,
        const char __user *const __user *, argv,
        const char __user *const __user *, envp)
{
    return do_execve(getname(filename), argv, envp);
}
#ifdef CONFIG_COMPAT
COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
    const compat_uptr_t __user *, argv,
    const compat_uptr_t __user *, envp)
{
    return compat_do_execve(getname(filename), argv, envp);
}
#endif

可以看出来execve系统调用要通过调用do_execve或者compat_do_execve函数的,其中do_execve中的实现如下:

int do_execve(struct filename *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
{
    struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execve_common(filename, argv, envp);
}

首先将argv和envp赋值给用户空间的argv和envp结构体,然后调用do_execve_common函数。do_execve_common的主要实现如下:

static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
  struct linux_binprm *bprm;
  ...

    retval = prepare_bprm_creds(bprm);

    check_unsafe_exec(bprm);

    file = do_open_exec(filename);

    sched_exec();

    retval = bprm_mm_init(bprm);

    retval = prepare_binprm(bprm);

    retval = exec_binprm(bprm);

  ...
}

初始化Linux可执行程序的结构体,为了能够执行它做了一系列的准备工作,最后调用exec_binprm函数执行这个可执行程序。

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret;

    /* Need to fetch pid before load_binary changes it */
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();

    ret = search_binary_handler(bprm);
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }

    return ret;
}

然后会通过search_binary_handler函数,内核要先注册各个不同类型可执行程序的处理模块,在解析一个新程序时查找该用哪个模块处理,最终完成可执行程序的加载。找到该可执行文件对应的处理函数,然后调用load_binary函数,实际上是load_elf_binary函数,即在search_binary_handler时,把load_binary函数指针指向了load_elf_binary,最后把整个可执行文件加载到内存中。

static int load_elf_binary(struct linux_binprm *bprm)
{
  ...
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)) {
            /*
             * load_elf_interp() returns relocation
             * adjustment
             */
            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会判断该可执行是否包含重定位段,即该可执行是否采用了动态链接,如果有的话会加载链接器,并把elf_entry的地址赋值为链接器的地址,否则地址就为ELF文件中的e_entry字段的地址。

总结

Linux中一个可执行文件的装载执行需要execve系统调用的支持,这个系统调用主要做的工作包括可执行文件的解析、查找相对应的处理程序比如ELF文件调用load_elf_binary、最后判断该文件是是否包含重定位段,进而为程序设置不同的入口地址,这就完成了整个可执行文件的加载运行,其中文件加载到内存中使用mmap函数实现的,它可以把文件映射到指定位置的内存中。

时间: 2024-10-08 11:42:45

Linux内核分析:实验七--Linux内核如何装载和启动一个可执行程序的相关文章

实验七:Linux内核如何装载和启动一个可执行程序

原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 题目自拟,内容围绕对Linux内核如何装载和启动一个可执行程序 可以结合实验截图.ELF可执行文件格式.用户态的相关代码等 博客内容中需要仔细分析新可执行程序的执行起点及对应的堆栈状态等 总结部分需要阐明自己对“Linux内核装载和启动一个可执行程序”的理解 实验报告: 理解编译链接的过程和ELF可执行文件格式: C代码——预处理——汇

Linux内核如何装载和启动一个可执行程序-----实验7

2015108 李泽源 Linux内核如何装载和启动一个可执行程序 理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节: 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式,详细内容参考本周第二节: 使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解,详细内容参考本周第三节:推荐在实验楼Linux虚拟机环境下完成实验.

Linux内核如何装载和启动一个可执行程序(转)

原文:http://www.cnblogs.com/petede/p/5351696.html 实验七:Linux内核如何装载和启动一个可执行程序 姓名:李冬辉 学号:20133201 注: 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 云课堂笔记: (1)可执行文件的创建 C代码(.c) - 经过编译器预处理,编译成汇编代码(.asm) - 汇编器,生成目标代码(.o) - 链接

【MOOC EXP】Linux内核分析实验七报告

程涵  原创博客 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 可执行程序的装载 知识点梳理 一.预处理.编译.链接和目标文件的格式 可执行程序是如何得来的 C源代码(.c)经过编译器预处理被编译成汇编代码(.asm) 汇编代码由汇编器被编译成目标代码(.o) 将目标代码链接成可执行文件(a.out) 可执行文件由操作系统加载到内存中执行 vi hello.c gcc -E -o hello.cpp hell

Linux内核分析实验七

------------------------------------------------------------------------- 刘旸 + 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ------------------------------------------------------------------------- 基础知识: 1. 可执行程序是怎么得

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

李俊锋 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.实验原理 1.elf可执行文件格式 ELF文件由4部分组成,分别是ELF头(ELF header).程序头表(Program header table).节(Section)和节头表(Section header table).实际上,一个文件中不一定包含全部内容,而且他们的位置也未必如同所示这样安排,只有ELF头的位置

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

概念总结 1.可执行程序是怎么得来的 C代码——预处理——汇编代码——目标代码——可执行文件 可执行文件的格式  可执行文件最初为A.out格式,后来演化为COFF格式,再后来变成PE(windows系统)和ELF(linux系统).ELF:executable and linkable format,即可执行可链接格式. 可执行程序的执行环境 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用. $ ls -l /usr/bin 列出/us

Linux内核装载和启动一个可执行程序

理解编译链接的过程和ELF可执行文件格式 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接 使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve 特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同? 参考文献: http://jzhihui.iteye.com/blog/1447570

20135327郭皓--Linux内核分析第七周 可执行程序的装载

第七周 可执行程序的装载 郭皓 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 一.预处理,编译,链接和目标文件格式 1.可执行程序是怎么得来的 c代码->预处理->汇编代码->汇编器->目标代码->链接成可执行文件->加载到内核执行 2.目标文件的格式ELF 符号修饰标准.变量内层布局.函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI