Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1035800
  • 博文数量: 26
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 437
  • 用 户 组: 普通用户
  • 注册时间: 2019-09-08 12:19
个人简介

关于个人介绍,既然你诚心诚意的看了 我就大发慈悲的告诉你 为了防止世界被破坏 为了维护世界的和平 贯彻爱与真实的邪恶 可爱又迷人的反派角色 我们是穿梭在银河的火箭队 白洞,白色的明天在等着我们

文章分类

分类: LINUX

2019-10-10 17:33:46

    在<Linux应用程序 启动流程>中,我们引出了execve系统调用函数,我们依然以hello world为例子程序

点击(此处)折叠或打开

  1. #include <stdio.h>

  2. int main (int argc, char *argv[])
  3. {
  4.     printf ("Hello World\n");

  5.     return 0;
  6. }

保存为hello.c,我们可以通过gcc hello.c -o hello得到可执行程序hello.

    当我们在shell命令行终端调用./hello的时候, 实际上busybox执行execve函数来执行hello程序。用strace ./hello可以看到它的系统调用。

点击(此处)折叠或打开

  1. $strace ./hello
  2. execve("./hello", ["./hello"], [/* 33 vars */]) = 0
  3. brk(NULL) = 0xe91000
  4. access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
  5. access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
  6. open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
  7. fstat(3, {st_mode=S_IFREG|0644, st_size=118062, ...}) = 0
  8. mmap(NULL, 118062, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5181fce000
  9. close(3) = 0
  10. access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
  11. open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
  12. read(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) = 832
  13. fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
  14. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcd000
  15. mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f51819fc000
  16. mprotect(0x7f5181bbc000, 2097152, PROT_NONE) = 0
  17. mmap(0x7f5181dbc000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f5181dbc000
  18. mmap(0x7f5181dc2000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0)= 0x7f5181dc2000
  19. close(3) = 0
  20. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcc000
  21. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcb000
  22. arch_prctl(ARCH_SET_FS, 0x7f5181fcc700) = 0
  23. mprotect(0x7f5181dbc000, 16384, PROT_READ) = 0
  24. mprotect(0x600000, 4096, PROT_READ) = 0
  25. mprotect(0x7f5181feb000, 4096, PROT_READ) = 0
  26. munmap(0x7f5181fce000, 118062) = 0
  27. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 21), ...}) = 0
  28. brk(NULL) = 0xe91000
  29. brk(0xeb2000) = 0xeb2000
  30. write(1, "Hello World\n", 12Hello World
  31. ) = 12
  32. exit_group(0) = ?
  33. +++ exited with 0 +++
图 1
    由图1, 我们可以看出执行hello程序的第一步就是将hello的路径作为参数,传入execve函数。下面,我们需要一行一行的去看看execve函数的内核实现(系统调用陷入的详细过程将在以后的文章中说明)。注:由于篇幅限制,以下无法详细贴出所有代码,只是选择关键部分贴出来。
    在Linux当中,execve函数的内核实现为:

点击(此处)折叠或打开

  1. SYSCALL_DEFINE3(execve,
  2.         const char __user *, filename,
  3.         const char __user *const __user *, argv,
  4.         const char __user *const __user *, envp)
  5. {
  6.   return do_execve(getname(filename), argv, envp);
  7. }
    // 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函数,调用栈如下:

点击(此处)折叠或打开

  1. execve                  (user space)
  2. ---|----------------------------------------------------------------------------
  3.    v                    (kernel space)
  4. el0_sync     (arm64同步异常中断处理函数)
  5.     el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行
  6.         sys_execve
  7.             do_execve
  8.                 do_execveat_common
  9.                     __do_execve_file
    这里以hello可执行程序为例进行讲解,__do_execve_file函数实现如下:
点击(此处)折叠或打开
  1. // 这里fd值默认为AT_FDCWD,表示当前进程的工作空间
  2. static int __do_execve_file(int fd, struct filename *filename,
  3.              struct user_arg_ptr argv,
  4.              struct user_arg_ptr envp,
  5.              int flags, struct file *file)
  6. {
  7.     ...........
  8.     // 为hello文件的bprm申请内存空间, bprm是可执行程序文件,底层的一个结构描述
  9.     bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
  10.     if (!bprm)
  11.         goto out_files;
  12.     ............

  13.     // 打开hello文件,并返回文件描述结构体struct file
  14.     if (!file)
  15.         file = do_open_execat(fd, filename, flags);
  16.     retval = PTR_ERR(file);
  17.     if (IS_ERR(file))
  18.         goto out_unmark;

  19.     // SMP才使能,多核应用程序负载均衡调用。
  20.     sched_exec();

  21.     bprm->file = file;
  22.     // 获取hello文件的路径
  23.     if (!filename) {
  24.         bprm->filename = "none";
  25.     } else if (fd == AT_FDCWD || filename->name[0] == '/') {
  26.         bprm->filename = filename->name;
  27.     } else {
  28.         if (filename->name[0] == '\0')
  29.             pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
  30.         else
  31.             pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
  32.                      fd, filename->name);
  33.         if (!pathbuf) {
  34.             retval = -ENOMEM;
  35.             goto out_unmark;
  36.         }
  37.         if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
  38.             bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
  39.         bprm->filename = pathbuf;
  40.     }
  41.     bprm->interp = bprm->filename;
  42.     
  43.     // 根据当前进程信息,为新程序hello的bprm结构初始化应用程序需要的内存,主要是mm_struct结构
  44.     retval = bprm_mm_init(bprm);
  45.     if (retval)
  46.         goto out_unmark;
  47.     
  48.     // 将hello程序环境变量,参数和bprm结构做出预处理,主要是获取参数个数和环境变量个数
  49.     retval = prepare_arg_pages(bprm, argv, envp);
  50.     if (retval < 0)
  51.         goto out;
  52.     
  53.     // 读取hello文件的前128字节到bprm的buf,这里elf头部只有64字节
  54.     retval = prepare_binprm(bprm);
  55.     if (retval < 0)
  56.         goto out;
  57.     
  58.     // 将hello文件名字拷贝的新进程的内存空间
  59.     retval = copy_strings_kernel(1, &bprm->filename, bprm);
  60.     if (retval < 0)
  61.         goto out;

  62.     bprm->exec = bprm->p;
  63.     // 将hello的环境变量拷贝到新进程的上下文内存空间
  64.     retval = copy_strings(bprm->envc, envp, bprm);
  65.     if (retval < 0)
  66.         goto out;

  67.     // 将hello的参数拷贝到新进程上下文的内存空间
  68.     retval = copy_strings(bprm->argc, argv, bprm);
  69.     if (retval < 0)
  70.         goto out;

  71.     would_dump(bprm, bprm->file);
  72.     
  73.     // 运行bprm指向的应用程序, 这里是hello程序
  74.     retval = exec_binprm(bprm);
  75.     if (retval < 0)
  76.         goto out;

  77.     return retval;
  78. }
     exec_binprm函数就是用于为bprm结构找到合适的加载函数,定义如下:

点击(此处)折叠或打开

  1. static int exec_binprm(struct linux_binprm *bprm)
  2. {
  3.     ..........
  4.     // 通过search_binary_handler函数来查找bprm到底是什么文件格式,以便于调用对应的处理函数
  5.     ret = search_binary_handler(bprm);
  6.     ..........
  7.     return ret;
  8. }
    search_binary_handler函数的实现如下:
点击(此处)折叠或打开
  1. /*
  2.  * cycle the list of binary formats handler, until one recognizes the image
  3.  */
  4. int search_binary_handler(struct linux_binprm *bprm)
  5. {
  6.     ........
  7.     // 遍历formats链表,去查找链表formats里面的所有linux支持的文件处理格式
  8.     list_for_each_entry(fmt, &formats, lh) {
  9.         if (!try_module_get(fmt->module))
  10.             continue;

  11.         // 调用load_binary函数去加载指定格式
  12.         retval = fmt->load_binary(bprm);
  13.         ........
  14.     }
  15.     read_unlock(&binfmt_lock);
  16.     .........
  17.     return retval;
  18. }
    从上面,我们可以看到formats链表,这个链表是怎么来的呢?通过查找代码,我们可以发现内核实现了一个函数register_binfmt, 这个函数用于注册内核支持的处理程序(目前主要为a.out, elf, script等),如我们的elf的注册如下:

点击(此处)折叠或打开

  1. static struct linux_binfmt elf_format = {
  2.     .module        = THIS_MODULE,
  3.     .load_binary    = load_elf_binary,
  4.     .load_shlib    = load_elf_library,
  5.     .core_dump    = elf_core_dump,
  6.     .min_coredump    = ELF_EXEC_PAGESIZE,
  7. };

  8. static int __init init_elf_binfmt(void)
  9. {
  10.     // 注册一种可执行的文件格式
  11.     register_binfmt(&elf_format);
  12.     return 0;
  13. }

  14. static void __exit exit_elf_binfmt(void)
  15. {
  16.     unregister_binfmt(&elf_format);
  17. }

  18. core_initcall(init_elf_binfmt);
  19. module_exit(exit_elf_binfmt);
  20. // 代码在  fs/binfmt_elf.c
    其中register_binfmt的实现如下:

点击(此处)折叠或打开

  1. void __register_binfmt(struct linux_binfmt * fmt, int insert)
  2. {
  3.     write_lock(&binfmt_lock);
  4.     // 添加到链表
  5.     insert ? list_add(&fmt->lh, &formats) :
  6.          list_add_tail(&fmt->lh, &formats);
  7.     write_unlock(&binfmt_lock);
  8. }

  9. static inline void register_binfmt(struct linux_binfmt *fmt)
  10. {
  11.     __register_binfmt(fmt, 0);
  12. }
    
可以发现register_binfmt其实就是把指定的结构插入到formats链表当中。我们继续看看elf的load_binary函数实现,至于其他格式文件,我们这里不考虑讲解。elf的load_binary实现如下:
点击(此处)折叠或打开
  1. static int load_elf_binary(struct linux_binprm *bprm)
  2. {
  3.     ........
  4.     // 从前面知道bprm->buf为elf文件的前128字节,因此,这里的操作是获取elf头部信息
  5.     loc->elf_ex = *((struct elfhdr *)bprm->buf);
  6.     retval = -ENOEXEC;
  7.     // 比较elf幻术magic,判断是否为elf,不是就退出
  8.     if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
  9.         goto out;
  10.     ............
  11.     // 获取elf的program header
  12.     elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
  13.     ........
  14.     // 这个for循环中间省略许多判断条件,主要是读取elf的动态解释器路径,并保存到elf_interpreter
  15.     for (i = 0; i < loc->elf_ex.e_phnum; i++) {
  16.         if (elf_ppnt->p_type == PT_INTERP) {
  17.             .......
  18.             retval = kernel_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, &pos);
  19.             .......
  20.         }
  21.     }
  22.     ........
  23.     // 加载动态解释器的program header
  24.     if (elf_interpreter) {
  25.         .......
  26.         interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,interpreter);
  27.         .......
  28.     }
  29.     ........
  30.     // 设置起始栈
  31.     current->mm->start_stack = bprm->p;
  32.     // 根据program header提供的信息对elf做必要的内存映射
  33.     for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
  34.         .......
  35.         error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
  36.         .......
  37.     }
  38.     ........
  39.     // 从动态解释器中获取动态解释器的入口地址,并赋值给elf_entry
  40.     if (elf_interpreter) {
  41.         ........
  42.         elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias, interp_elf_phdata);
  43.         ........
  44.     }
  45.     ...........
  46.     // 设置pc指针为动态解释器的入口地址
  47.     start_thread(regs, elf_entry, bprm->p);
  48.     ........
  49. }
    最后,load_elf_binary函数调用start_thread去设置对应的regs结构,也就是内核空间返回到应用空间需要读取的结构(user_regs主要存放r0-r15相关的寄存器信息,用于恢复应用空间上下文),start_thread的实现如下:

点击(此处)折叠或打开

  1. static inline void start_thread_common(struct pt_regs *regs, unsigned long pc)
  2. {
  3.     memset(regs, 0, sizeof(*regs));
  4.     regs->syscallno = ~0UL;
  5.     regs->pc = pc;
  6. }

  7. static inline void start_thread(struct pt_regs *regs, unsigned long pc,
  8.                 unsigned long sp)
  9. {
  10.     start_thread_common(regs, pc);
  11.     regs->pstate = PSR_MODE_EL0t;
  12.     regs->sp = sp;
  13. }
    这样,当cpu从系统调用返回到用户空间时,就从regs->pc确定的地址开始执行,达到了间接跳转的目的,而这个pc地址正好是动态解释器的入口地址(不再是execve的调用者地址),所以当execve函数返回的时候,就直接跳转到了动态解释器中运行,这个动态解释器通常是/lib64/ld-linux-x86-64.so.2。动态解释器怎么执行,怎么符号重定向,以后会有文章加以说明。
调用栈如下:
     

阅读(9283) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

BugMan2019-10-12 15:30:15

因此,execve出来的进程是延用老的进程的task_struct,只是mm_struct发生了变动。bprm_mm_init函数主要是设置新的task_struct的栈的。

BugMan2019-10-12 15:28:21

在load_elf_binary函数里面,有注意函数flush_old_exec,这个函数的作用是停止current进程里面的所有线程,释放老的mm_struct,并用新的bprm->mm替代新的。调用栈如下:
flush_old_exec
    de_thread       杀死线程
    exec_mmap
        mm_release  释放old_mm
    flush_thread
setup_new_exec
    arch_pick_mmap_layout  设置新的mmap layout
    __set_task_comm     修改进程名字