前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux下程序是如何被执行的

Linux下程序是如何被执行的

作者头像
KINGYT
发布2019-06-28 10:35:53
3.1K0
发布2019-06-28 10:35:53
举报

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

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

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
// 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方法。

代码语言:javascript
复制
// 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方法。

代码语言:javascript
复制
// 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方法。

代码语言:javascript
复制
// fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
        ...
        ret = search_binary_handler(bprm);
        ...
        return ret;
}

该方法又调用了search_binary_handler方法:

代码语言:javascript
复制
// 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方法:

代码语言:javascript
复制
// 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/

完。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档