在Linux应用程序 启动流程>中,我们引出了execve系统调用函数,我们依然以hello world为例子程序。点击(此处)折叠或打开#include stdio.h>int main (int argc, char *argv[]){    printf ("Hello World\n");    return 0;}保存为hello.c,我们可以通过gcc hello.c -o hello得到可执行程序hello.    当我们在shell命令行终端调用./hello的时候, 实际上busybox执行execve函数来执行hello程序。用strace ./hello可以看到它的系统调用。点击(此处)折叠或打开$strace ./helloexecve("./hello", ["./hello"], [/* 33 vars */]) = 0brk(NULL) = 0xe91000access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=118062, ...}) = 0mmap(NULL, 118062, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5181fce000close(3) = 0access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcd000mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f51819fc000mprotect(0x7f5181bbc000, 2097152, PROT_NONE) = 0mmap(0x7f5181dbc000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f5181dbc000mmap(0x7f5181dc2000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0)= 0x7f5181dc2000close(3) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcc000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcb000arch_prctl(ARCH_SET_FS, 0x7f5181fcc700) = 0mprotect(0x7f5181dbc000, 16384, PROT_READ) = 0mprotect(0x600000, 4096, PROT_READ) = 0mprotect(0x7f5181feb000, 4096, PROT_READ) = 0munmap(0x7f5181fce000, 118062) = 0fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 21), ...}) = 0brk(NULL) = 0xe91000brk(0xeb2000) = 0xeb2000write(1, "Hello World\n", 12Hello World) = 12exit_group(0) = ?+++ exited with 0 +++图 1    由图1, 我们可以看出执行hello程序的第一步就是将hello的路径作为参数,传入execve函数。下面,我们需要一行一行的去看看execve函数的内核实现(系统调用陷入的详细过程将在以后的文章中说明)。注:由于篇幅限制,以下无法详细贴出所有代码,只是选择关键部分贴出来。    在Linux当中,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);}// Code in "fs/exec.c"    其中SYSCALL_DEFINE3是一个宏,表示这是一个含有3个参数的系统调用,这个宏会产生一个sys_execve函数。也就是当应用程序调用execve函数的时候,CPU产生系统调用中断,中断处理函数通过查表(sys_call_table),通过对应的系统调用号找到sys_execve函数并开始执行(R7/W8存放的系统调用号,参数以此类推),sys_execve函数到__do_execve_file并没有做什么操作,只是简单调用,因此现在直接到__do_execve_file函数,调用栈如下:点击(此处)折叠或打开execve                  (user space)---|----------------------------------------------------------------------------   v                    (kernel space)el0_sync     (arm64同步异常中断处理函数)    el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行)        sys_execve            do_execve                do_execveat_common                    __do_execve_file    这里以hello可执行程序为例进行讲解,__do_execve_file函数实现如下:点击(此处)折叠或打开// 这里fd值默认为AT_FDCWD,表示当前进程的工作空间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){    ...........    // 为hello文件的bprm申请内存空间, bprm是可执行程序文件,底层的一个结构描述    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);    if (!bprm)        goto out_files;    ............    // 打开hello文件,并返回文件描述结构体struct file    if (!file)        file = do_open_execat(fd, filename, flags);    retval = PTR_ERR(file);    if (IS_ERR(file))        goto out_unmark;    // SMP才使能,多核应用程序负载均衡调用。    sched_exec();    bprm->file = file;    // 获取hello文件的路径    if (!filename) {        bprm->filename = "none";    } else if (fd == AT_FDCWD || filename->name[0] == '/') {        bprm->filename = filename->name;    } else {        if (filename->name[0] == '\0')            pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);        else            pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",                     fd, filename->name);        if (!pathbuf) {            retval = -ENOMEM;            goto out_unmark;        }        if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))            bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;        bprm->filename = pathbuf;    }    bprm->interp = bprm->filename;        // 根据当前进程信息,为新程序hello的bprm结构初始化应用程序需要的内存,主要是mm_struct结构    retval = bprm_mm_init(bprm);    if (retval)        goto out_unmark;        // 将hello程序环境变量,参数和bprm结构做出预处理,主要是获取参数个数和环境变量个数    retval = prepare_arg_pages(bprm, argv, envp);    if (retval  0)        goto out;        // 读取hello文件的前128字节到bprm的buf,这里elf头部只有64字节    retval = prepare_binprm(bprm);    if (retval  0)        goto out;        // 将hello文件名字拷贝的新进程的内存空间    retval = copy_strings_kernel(1, &bprm->filename, bprm);    if (retval  0)        goto out;    bprm->exec = bprm->p;    // 将hello的环境变量拷贝到新进程的上下文内存空间    retval = copy_strings(bprm->envc, envp, bprm);    if (retval  0)        goto out;    // 将hello的参数拷贝到新进程上下文的内存空间    retval = copy_strings(bprm->argc, argv, bprm);    if (retval  0)        goto out;    would_dump(bprm, bprm->file);        // 运行bprm指向的应用程序, 这里是hello程序    retval = exec_binprm(bprm);    if (retval  0)        goto out;    return retval;}     exec_binprm函数就是用于为bprm结构找到合适的加载函数,定义如下:点击(此处)折叠或打开static int exec_binprm(struct linux_binprm *bprm){    ..........    // 通过search_binary_handler函数来查找bprm到底是什么文件格式,以便于调用对应的处理函数    ret = search_binary_handler(bprm);    ..........    return ret;}    search_binary_handler函数的实现如下:点击(此处)折叠或打开/* * cycle the list of binary formats handler, until one recognizes the image */int search_binary_handler(struct linux_binprm *bprm){    ........    // 遍历formats链表,去查找链表formats里面的所有linux支持的文件处理格式    list_for_each_entry(fmt, &formats, lh) {        if (!try_module_get(fmt->module))            continue;        // 调用load_binary函数去加载指定格式        retval = fmt->load_binary(bprm);        ........    }    read_unlock(&binfmt_lock);    .........    return retval;}    从上面,我们可以看到formats链表,这个链表是怎么来的呢?通过查找代码,我们可以发现内核实现了一个函数register_binfmt, 这个函数用于注册内核支持的处理程序(目前主要为a.out, elf, script等),如我们的elf的注册如下:点击(此处)折叠或打开static struct linux_binfmt elf_format = {    .module        = THIS_MODULE,    .load_binary    = load_elf_binary,    .load_shlib    = load_elf_library,    .core_dump    = elf_core_dump,    .min_coredump    = ELF_EXEC_PAGESIZE,};static int __init init_elf_binfmt(void){    // 注册一种可执行的文件格式    register_binfmt(&elf_format);    return 0;}static void __exit exit_elf_binfmt(void){    unregister_binfmt(&elf_format);}core_initcall(init_elf_binfmt);module_exit(exit_elf_binfmt);// 代码在  fs/binfmt_elf.c    其中register_binfmt的实现如下:点击(此处)折叠或打开void __register_binfmt(struct linux_binfmt * fmt, int insert){    write_lock(&binfmt_lock);    // 添加到链表    insert ? list_add(&fmt->lh, &formats) :         list_add_tail(&fmt->lh, &formats);    write_unlock(&binfmt_lock);}static inline void register_binfmt(struct linux_binfmt *fmt){    __register_binfmt(fmt, 0);}    可以发现register_binfmt其实就是把指定的结构插入到formats链表当中。我们继续看看elf的load_binary函数实现,至于其他格式文件,我们这里不考虑讲解。elf的load_binary实现如下:点击(此处)折叠或打开static int load_elf_binary(struct linux_binprm *bprm){    ........    // 从前面知道bprm->buf为elf文件的前128字节,因此,这里的操作是获取elf头部信息     loc->elf_ex = *((struct elfhdr *)bprm->buf);    retval = -ENOEXEC;    // 比较elf幻术magic,判断是否为elf,不是就退出    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)        goto out;    ............    // 获取elf的program header    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);    ........    // 这个for循环中间省略许多判断条件,主要是读取elf的动态解释器路径,并保存到elf_interpreter    for (i = 0; i elf_ex.e_phnum; i++) {        if (elf_ppnt->p_type == PT_INTERP) {            .......            retval = kernel_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, &pos);            .......        }    }    ........    // 加载动态解释器的program header    if (elf_interpreter) {        .......        interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,interpreter);        .......    }    ........    // 设置起始栈    current->mm->start_stack = bprm->p;    // 根据program header提供的信息对elf做必要的内存映射    for(i = 0, elf_ppnt = elf_phdata; i elf_ex.e_phnum; i++, elf_ppnt++) {        .......        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);        .......    }    ........    // 从动态解释器中获取动态解释器的入口地址,并赋值给elf_entry    if (elf_interpreter) {        ........        elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias, interp_elf_phdata);        ........    }    ...........    // 设置pc指针为动态解释器的入口地址    start_thread(regs, elf_entry, bprm->p);    ........}    最后,load_elf_binary函数调用start_thread去设置对应的regs结构,也就是内核空间返回到应用空间需要读取的结构(user_regs主要存放r0-r15相关的寄存器信息,用于恢复应用空间上下文),start_thread的实现如下:点击(此处)折叠或打开static inline void start_thread_common(struct pt_regs *regs, unsigned long pc){    memset(regs, 0, sizeof(*regs));    regs->syscallno = ~0UL;    regs->pc = pc;}static inline void start_thread(struct pt_regs *regs, unsigned long pc,                unsigned long sp){    start_thread_common(regs, pc);    regs->pstate = PSR_MODE_EL0t;    regs->sp = sp;}    这样,当cpu从系统调用返回到用户空间时,就从regs->pc确定的地址开始执行,达到了间接跳转的目的,而这个pc地址正好是动态解释器的入口地址(不再是execve的调用者地址),所以当execve函数返回的时候,就直接跳转到了动态解释器中运行,这个动态解释器通常是/lib64/ld-linux-x86-64.so.2。动态解释器怎么执行,怎么符号重定向,以后会有文章加以说明。调用栈如下:     
10-08 15:38
查看更多