Linux下程序是如何被执行的

之前写过一篇文章 Linux下c语言中的main函数是如何被调用的,该篇文章侧重于从user space层面讲程序的运行,而文章中提到的有关kernel space层面的相关系统调用,比如fork、execve等,都被一笔带过。

今天我们主要来看下execve系统调用,直接看代码:

// fs/exec.c
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);
}

该方法调用了do_execve:

// fs/exec.c
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_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

该方法又调用了do_execveat_common:

// fs/exec.c
static int do_execveat_common(int fd, struct filename *filename,
                              struct user_arg_ptr argv,
                              struct user_arg_ptr envp,
                              int flags)
{
        return __do_execve_file(fd, filename, argv, envp, flags, NULL);
}

该方法又调用了__do_execve_file:

// fs/exec.c
static int __do_execve_file(int fd, struct filename *filename,
                            struct user_arg_ptr argv,
                            struct user_arg_ptr envp,
                            int flags, struct file *file)
{
        ...
        struct linux_binprm *bprm;
        ...
        bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
        ...
        if (!file)
                file = do_open_execat(fd, filename, flags);
        ...
        bprm->file = file;
        if (!filename) {
                ...
        } else if (fd == AT_FDCWD || filename->name[0] == '/') {
                bprm->filename = filename->name;
        } else {
                ...
        }
        ...
        retval = bprm_mm_init(bprm);
        ...
        retval = prepare_binprm(bprm);
        ...
        retval = copy_strings_kernel(1, &bprm->filename, bprm);
        ...
        retval = copy_strings(bprm->envc, envp, bprm);
        ...
        retval = copy_strings(bprm->argc, argv, bprm);
        ...
        retval = exec_binprm(bprm);
        ...
        return retval;
        ...
}

该方法的大致逻辑是:

1. 分配struct linux_binprm实例,并赋值给bprm。

2. 打开filename指向的程序,并赋值给file变量。

3. 将file变量赋值给bprm->file。

4. 将filename->name赋值给bprm->filename。

5. 调用bprm_mm_init方法,初始化进程内存的相关信息,并分配一个page作为进程的初始堆栈。

6. 调用prepare_binprm方法,从bprm->file中读取256字节到bprm->buf中。

7. 将程序的文件路径拷贝到堆栈中。

8. 将环境变量拷贝到堆栈中。

9. 将程序参数拷贝到堆栈中。

10. 调用exec_binprm方法继续执行该程序。

在看exec_binprm方法之前,我们先看下bprm_mm_init方法。

// fs/exec.c
static int bprm_mm_init(struct linux_binprm *bprm)
{
        int err;
        struct mm_struct *mm = NULL;

        bprm->mm = mm = mm_alloc();
        ...
        err = __bprm_mm_init(bprm);
        ...
        return 0;
        ...
}

该方法先分配了一个类型为struct mm_struct的实例,该实例就是用来存放有关进程内存的相关信息。

之后,又调用了__bprm_mm_init方法。

// fs/exec.c
static int __bprm_mm_init(struct linux_binprm *bprm)
{
        int err;
        struct vm_area_struct *vma = NULL;
        struct mm_struct *mm = bprm->mm;

        bprm->vma = vma = vm_area_alloc(mm);
        ...
        /*
         * Place the stack at the largest stack address the architecture
         * supports. Later, we'll move this to an appropriate place. We don't
         * use STACK_TOP because that can depend on attributes which aren't
         * configured yet.
         */
        vma->vm_end = STACK_TOP_MAX;
        vma->vm_start = vma->vm_end - PAGE_SIZE;
        ...
        err = insert_vm_struct(mm, vma);
        ...
        return 0;
        ...
}

该方法设置了堆栈的位置,并初始化堆栈大小为一个page。

堆栈的位置及大小后面的代码还会调整。

好,我们再回到__do_execve_file方法,该方法的最后又调用了exec_binprm方法。

// fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
        ...
        ret = search_binary_handler(bprm);
        ...
        return ret;
}

该方法又调用了search_binary_handler方法:

// fs/exec.c
int search_binary_handler(struct linux_binprm *bprm)
{
        ...
        struct linux_binfmt *fmt;
        ...
        list_for_each_entry(fmt, &formats, lh) {
                ...
                retval = fmt->load_binary(bprm);
                ...
        }
        ...
        return retval;
}
EXPORT_SYMBOL(search_binary_handler);

该方法遍历linux中可识别的可执行文件格式,找到对应的文件格式,并调用其load_binary方法。

linux下可执行文件的格式一般为elf,所以我们直接看其load_binary方法:

// fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
        struct file *interpreter = NULL; /* to shut gcc up */
        ...
        struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
        ...
        unsigned long elf_entry;
        ...
        struct {
                struct elfhdr elf_ex;
                struct elfhdr interp_elf_ex;
        } *loc;
        ...
        loc = kmalloc(sizeof(*loc), GFP_KERNEL);
        ...
        // 将之前从file中读出来的buf的内容,转成elf的header
        loc->elf_ex = *((struct elfhdr *)bprm->buf);
        ...
        // 从程序文件中读取elf的program header
        elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
        ...
        // 遍历program header,找到其中的interpreter
        elf_ppnt = elf_phdata;
        for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
                char *elf_interpreter;
                loff_t pos;

                if (elf_ppnt->p_type != PT_INTERP)
                        continue;
                ...
                elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
                ...
                pos = elf_ppnt->p_offset;
                // 从程序文件中读取interpreter的路径,一般为 /lib64/ld-linux-x86-64.so.2
                // 有关interpreter的信息,请看 http://man7.org/linux/man-pages/man8/ld.so.8.html
                retval = kernel_read(bprm->file, elf_interpreter,
                                     elf_ppnt->p_filesz, &pos);
                ...
                // 打开interpreter文件
                interpreter = open_exec(elf_interpreter);
                ...
                pos = 0;
                // 读取interpreter的elf header
                retval = kernel_read(interpreter, &loc->interp_elf_ex,
                                     sizeof(loc->interp_elf_ex), &pos);
                ...
                break;
                ...
        }
        ...
        // 关闭当前进程使用的资源,比如线程、内存、文件等
        retval = flush_old_exec(bprm);
        ...
        // 设置新程序的各种信息
        setup_new_exec(bprm);
        ...
        // 重新设置当前堆栈的位置及大小
        retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                                 executable_stack);
        ...
        // 初始化
        elf_bss = 0;
        elf_brk = 0;

        start_code = ~0UL;
        end_code = 0;
        start_data = 0;
        end_data = 0;

        // 遍历program header,将程序文件中的代码段、data段等映射到内存
        for(i = 0, elf_ppnt = elf_phdata;
            i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
                ...
                if (elf_ppnt->p_type != PT_LOAD)
                        continue;
                ...        
                vaddr = elf_ppnt->p_vaddr;
                ...
                // 映射程序代码等信息到内存的虚拟地址,类似于mmap系统调用
                //该操作会在进程的struct mm_struct实例中添加一个struct vm_area_struct实例
                error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                                elf_prot, elf_flags, total_size);
                ...
                // 设置程序的各个segment位置信息
                k = elf_ppnt->p_vaddr;
                if (k < start_code)
                        start_code = k;
                if (start_data < k)
                        start_data = k;
                ...
                k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

                if (k > elf_bss)
                        elf_bss = k;
                if ((elf_ppnt->p_flags & PF_X) && end_code < k)
                        end_code = k;
                if (end_data < k)
                        end_data = k;
                k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
                if (k > elf_brk) {
                        bss_prot = elf_prot;
                        elf_brk = k;
                }
        }
        // 调整程序各个segment的具体位置
        loc->elf_ex.e_entry += load_bias;
        elf_bss += load_bias;
        elf_brk += load_bias;
        start_code += load_bias;
        end_code += load_bias;
        start_data += load_bias;
        end_data += load_bias;
        ...
        // 设置堆的地址
        retval = set_brk(elf_bss, elf_brk, bss_prot);
        ...

        if (interpreter) {
                ...
                // 加载interpreter的入口地址
                elf_entry = load_elf_interp(&loc->interp_elf_ex,
                                            interpreter,
                                            &interp_map_addr,
                                            load_bias, interp_elf_phdata);
                if (!IS_ERR((void *)elf_entry)) {
                        ...
                        interp_load_addr = elf_entry;
                        elf_entry += loc->interp_elf_ex.e_entry;
                }
                ...
        } else {
                // 如果该程序没有interpreter,则使用程序自己的入口地址
                elf_entry = loc->elf_ex.e_entry;
                ...
        }
        ...
        // 进一步设置堆栈的各种信息,比如 auxiliary vector、环境变量、程序参数等
        retval = create_elf_tables(bprm, &loc->elf_ex,
                          load_addr, interp_load_addr);
        ...
        // 设置程序各个segment的地址
        current->mm->end_code = end_code;
        current->mm->start_code = start_code;
        current->mm->start_data = start_data;
        current->mm->end_data = end_data;
        current->mm->start_stack = bprm->p;

        if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
                ...
                current->mm->brk = current->mm->start_brk =
                        arch_randomize_brk(current->mm);
                ...
        }
        ...
        // 开始执行elf_entry指向的代码
        // 如果该程序有interpreter,则是执行interpreter中的入口地址
        // 如果没有,则是执行程序自己的入口地址
        // interpreter会检查该程序依赖的动态链接库,加载这些库,并解析相应的函数地址
        // 之后再调用源程序自己的入口函数,这样,也就对应到文章开始提到的
        // main函数是如何被调用的那篇文章了。
        start_thread(regs, elf_entry, bprm->p);
        ...
        return retval;
        ...
}

由于该方法比较长,关于方法的描述已用注释的形式在方法内部标注出来,请参考方法中的中文注释。

在阅读该方法之前,要先了解下elf的具体格式:

http://man7.org/linux/man-pages/man5/elf.5.html

参照该格式以及之前的一篇文章 Linux进程的内存分布,对照着看代码,会更好理解一些。

好了,到这里,整个程序的内核部分的执行流程就讲完了,结合本文开始提到的那篇文章 Linux下c语言中的main函数是如何被调用的,有关linux下程序的执行就全部讲清楚了。

希望对这方面感兴趣的朋友有所帮助。

在结束本文之前,推荐两篇相关文章,也是写的非常好的,如果你对本文还有不太明白地方,没准能在这里找到答案。

https://lwn.net/Articles/630727/

https://lwn.net/Articles/631631/

完。

本文分享自微信公众号 - Linux内核及JVM底层相关技术研究(ytcode),作者:wangyuntao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Linux内核是如何巧妙的初始化各个模块的

    相信很多在研究linux内核源码的同学,经常会发现一些模块的初始化函数找不到调用者,比如下面的网络模块的初始化函数:

    wangyuntao
  • linux内核启动流程分析 - efi_stub_entry

    接上一篇文章 linux内核启动流程分析 - efi_pe_entry,我们继续看efi_stub_entry函数。

    wangyuntao
  • Linux内核的Makefile中cmd-check是如何检查前后两次执行的命令是一致的?

    Linux内核的构建工具用的是GNU Make,在其相关的Makefile中,有一个变量叫做cmd-check,其定义如下:

    wangyuntao
  • Linux 5.2.1 发布 最新的稳定版内核

    在 Linux 5.2 发布一周后,第一个修订版本 5.2.1 也已经发布了,用来处理各种错误/回归。需要注意的是5.2并非长期支持(LTS)分支,推荐注重稳定...

    Debian社区
  • golang中实现通用http参数与结构体的转换

    最近基于golang 实现一个通用的http的协议代理,把来自http的请求转换成内部的通信协议。内部协议是基于pb的,所以关键就是实现pb和http请求中的参...

    衡阵
  • 售价高达60万元的3D打印汽车,你有兴趣吗?

    镁客网
  • Nginx+ownCloud+PHP+MySQL搭建私有云

    ownCloud是一个免费开源的软件,用于为分享文件,日历,联系人,书签和个人音频/视频,它拥有全客户端,方便使用,同时也非常容易安装和管理。

    zhangheng
  • python网络-TFTP客户端开发(25)

    TFTP(Trivial File Transfer Protocol,简单文件传输协议)

    Se7eN_HOU
  • HanLP-朴素贝叶斯分类预测缺陷

    文章整理自 baiziyu 的知乎专栏,感兴趣的朋友可以去关注下这位大神的专栏,很多关于自然语言处理的文章写的很不错。昨天看到他的分享的两篇关于朴素贝叶斯分类预...

    IT小白龙
  • 我的编码习惯 - 配置规范

    工作中少不了要制定各种各样的配置文件,这里和大家分享一下工作中我是如何制定配置文件的,这是个人习惯,结合强大的spring,效果很不错。

    哲洛不闹

扫码关注云+社区

领取腾讯云代金券