前言说明

本篇为网易云课堂Linux内核分析课程的第七周作业,本次作业我们将具体来分析exec*函数对应的系统调用处理过程,来分析Linux内核如何来执行一个可执行程序,由于有一个在网易云课堂共同学习的朋友,代码部分是我们二人共同完成代码分析注释。


关键词:exec, 系统调用进程,elf,可执行程序


*运行环境:**

  • Ubuntu 14.04 LTS x64
  • gcc 4.9.2
  • gdb 7.8
  • vim 7.4 with vundle

过程分析

分析说明

在进行详细的分析之前,首先我们来总结一下Linux内核装载执行ELF程序的大概过程:

  • 首先在用户层面,shell进行会调用fork()系统调用创建一个新进程
  • 新进程调用execve()系统调用执行制定的ELF文件
  • 原来的shell进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入

以上总结中,fork()系统调用过程在上一次作业中,我们都很清楚,这一次我们将来详细分析execve()系统调用,分析方法与上一次作业相同,即结合内核代码对整个流程进行抽象分析(对有中间的繁杂细节我们可以进行选择性的忽略,以能够让我们关注中间的重要流程),All reight,Let's rock and roll!

分析

execve()系统调用的原型如下:

int execve(const char *filename, char *const argv[],char *const envp[]);

它所对应的三个参数分别是程序文件名, 执行参数, 环境变量,通过对内核代码的分析,我们知道execve()系统调用的相应入口是sys_execve(),在sys_execve之后,内核会分别调用do_execve(),search_binary_handle(),load_elf_binary等等,其中do_execve()是最主要的函数,所以接下来我们主要对他来进行具体分析

do_execve

int do_execve(struct filename *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{return do_execve_common(filename, argv, envp);
}//do_execve_common
static int do_execve_common(struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp)
{// 检查进程的数量限制// 选择最小负载的CPU,以执行新程序sched_exec();// 填充 linux_binprm结构体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);// 调用里面的 search_binary_handler retval = exec_binprm(bprm);// exec执行成功}// exec_binprm
static int exec_binprm(struct linux_binprm *bprm)
{// 扫描formats链表,根据不同的文本格式,选择不同的load函数ret = search_binary_handler(bprm);// ...return ret;
}
  • 如果想要了解elf文件格式,可以在命令行下面man elf,Linux手册中有参考.
  • do_exec()中会调用do_execve_common(),这个函数的参数与do_exec()一模一样
  • do_execve_common()中的sched_exec(),会选择一个负载最小的CPU来执行新进程,这里我们可以得知Linux内核中是做了负载均衡的.
  • 在这段代码中间出现了变量bprm,这个是一个重要的结构体struct linux_binfmt,下面我贴出此结构体的具体定义:
/** This structure is used to hold the arguments that are used when loading binaries.*/
// 内核中注释表明了这个结构体是用于保存载入二进制文件的参数.
struct linux_binprm {char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMUstruct vm_area_struct *vma;unsigned long vma_pages;
#else//...unsigned interp_flags;unsigned interp_data;unsigned long loader, exec;
};
  • do_execve_common()中的search_binary_handler(),这个函数回去搜索和匹配合适的可执行文件装载处理过程,下面这个函数的精简代码:
int search_binary_handler(struct linux_binprm *bprm)
{// 遍历formats链表list_for_each_entry(fmt, &formats, lh) {if (!try_module_get(fmt->module))continue;read_unlock(&binfmt_lock);bprm->recursion_depth++;// 应用每种格式的load_binary方法retval = fmt->load_binary(bprm);read_lock(&binfmt_lock);put_binfmt(fmt);bprm->recursion_depth--;// ...}return retval;
}
  • 这里需要说明的是,这里的fmt变量的类型是struct linux_binfmt *, 但是这一个类型与之前在do_execve_common()中的bprm是不一样的,具体定义如下:
  • 这里的linux_binfmt对象包含了一个单链表,这个单链表中的第一个元素的地址存储在formats这个变量中
  • list_for_each_entry依次应用load_binary的方法,同时我们可以看到这里会有递归调用,bprm会记录递归调用的深度
  • 装载ELF可执行程序的load_binary的方法叫做load_elf_binary方法,下面会进行具体分析
/** This structure defines the functions that are used to load the binary formats that* linux accepts.*/
struct linux_binfmt {struct list_head lh; //单链表表头struct module *module;int (*load_binary)(struct linux_binprm *);int (*load_shlib)(struct file *);int (*core_dump)(struct coredump_params *cprm);unsigned long min_coredump; /* minimal dump size */
};

load_elf_binary()

static int load_elf_binary(struct linux_binprm *bprm)
{// ....struct pt_regs *regs = current_pt_regs();  // 获取当前进程的寄存器存储位置// 获取elf前128个字节,作为魔数loc->elf_ex = *((struct elfhdr *)bprm->buf);// 检查魔数是否匹配if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)goto out;// 如果既不是可执行文件也不是动态链接程序,就错误退出if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)// // 读取所有的头部信息// 读入程序的头部分retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,(char *)elf_phdata, size);// 遍历elf的程序头for (i = 0; i < loc->elf_ex.e_phnum; i++) {// 如果存在解释器头部if (elf_ppnt->p_type == PT_INTERP) {// // 读入解释器名retval = kernel_read(bprm->file, elf_ppnt->p_offset,elf_interpreter,elf_ppnt->p_filesz);// 打开解释器文件interpreter = open_exec(elf_interpreter);// 读入解释器文件的头部retval = kernel_read(interpreter, 0, bprm->buf,BINPRM_BUF_SIZE);// 获取解释器的头部loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);break;}elf_ppnt++;}// 释放空间、删除信号、关闭带有CLOSE_ON_EXEC标志的文件retval = flush_old_exec(bprm);setup_new_exec(bprm);// 为进程分配用户态堆栈,并塞入参数和环境变量retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),executable_stack);current->mm->start_stack = bprm->p;// 将elf文件映射进内存for(i = 0, elf_ppnt = elf_phdata;i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {if (unlikely (elf_brk > elf_bss)) {unsigned long nbyte;// 生成BSSretval = set_brk(elf_bss + load_bias,elf_brk + load_bias);// ...}// 可执行程序if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {elf_flags |= MAP_FIXED;} else if (loc->elf_ex.e_type == ET_DYN) { // 动态链接库// ...}// 创建一个新线性区对可执行文件的数据段进行映射error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,elf_prot, elf_flags, 0);}}// 加上偏移量loc->elf_ex.e_entry += load_bias;// ....// 创建一个新的匿名线性区,来映射程序的bss段retval = set_brk(elf_bss, elf_brk);// 如果是动态链接if (elf_interpreter) {unsigned long interp_map_addr = 0;// 调用一个装入动态链接程序的函数 此时elf_entry指向一个动态链接程序的入口elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias);// ...} else {// elf_entry是可执行程序的入口elf_entry = loc->elf_ex.e_entry;// ....}// 修改保存在内核堆栈,但属于用户态的eip和espstart_thread(regs, elf_entry, bprm->p);retval = 0;//
}
  • 这段代码相当之长,我们做了相当大的精简,虽然对主要部分做了注释,但是为了方便我还是把主要过程阐述一边:
  • 检查ELF的可执行文件的有效性,比如魔数,程序头表中段(segment)的数量
  • 寻找动态链接的.interp段,设置动态链接路径
  • 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码,数据,只读数据
  • 初始化ELF进程环境
  • 将系统调用的返回地址修改为ELF可执行程序的入口点,这个入口点取决于程序的连接方式,对于静态链接的程序其入口就是e_entry,而动态链接的程序其入口是动态链接器
  • 最后调用start_thread,修改保存在内核堆栈,但属于用户态的eipesp,该函数代码如下:

start_thread

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{set_user_gs(regs, 0); // 将用户态的寄存器清空regs->fs        = 0;regs->ds        = __USER_DS;regs->es        = __USER_DS;regs->ss        = __USER_DS;regs->cs        = __USER_CS;regs->ip        = new_ip; // 新进程的运行位置- 动态链接程序的入口处regs->sp        = new_sp; // 用户态的栈顶regs->flags     = X86_EFLAGS_IF;set_thread_flag(TIF_NOTIFY_RESUME);
}

总结

如你所见,执行程序的过程是一个十分复杂的过程,exec本质在于替换fork()后,根据制定的可执行文件对进程中的相应部分进行替换,最后根据连接方式的不同来设置好执行起始位置,然后开始执行进程.

实验截图



参考资料

  • Understanding The Linux Kernel, the 3rd edtion
  • Linux内核设计与实现,第三版,Robert Love, 机械工业出版社

署名信息

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

转载于:https://www.cnblogs.com/MarkWoo/p/4439267.html

通过分析exevc系统调用处理过程来理解Linux内核如何装载和启动一个可执行程序...相关推荐

  1. 深入理解Linux内核-第3版 译者序、前言、目录 内核2.6.11

    一.译者序 Linux是一个全新的世界,世界意味着博大精深,而新或许代表对旧的割舍和扬弃,加在一起,就是要我们在割舍和扬弃的同时还要积累知识到博大精深的地步,这容易做到吗?是的,这不容易做到.Gera ...

  2. 深入理解LINUX内核(影印版第3版)》的笔记

    书名: 深入理解LINUX内核(影印版第3版) 作者: Daniel P.Bovet/Marco Cesati 副标题: Understanding the Linux Kernel 页数: 923 ...

  3. 深入理解 Linux 内核

    Linux 内核系列文章 Linux 内核设计与实现 深入理解 Linux 内核 深入理解 Linux 内核(二) Linux 设备驱动程序 Linux设备驱动开发详解 文章目录 Linux 内核系列 ...

  4. 笔记:深入理解Linux内核(二)

    笔记:深入理解Linux内核(二) 二零二一年十月二十四日 文章目录 笔记:深入理解Linux内核(二) 第二章:内存寻址 内存地址 硬件中的分段 段选择符和段选择器 段描述符 快速访问段描述符 分段 ...

  5. 深入理解Linux内核之主调度器(下)

    4.进程上下文切换 接前文:深入理解Linux内核之主调度器(上) 前面选择了一个合适进程作为下一个进程,接下来做重要的上下文切换动作,来保存上一个进程的"上下文"恢复下一个进程的 ...

  6. 深入理解Linux内核 学习Linux内核的一些建议及书记推荐

    深入理解Linux内核 学习Linux内核的一些建议_华清远见教育集团 经典书籍 待到山花烂漫时,还是那些经典在微笑. 有关内核的书籍可以用汗牛充栋来形容,不过只有一些经典的神作经住了考验.首先是5本 ...

  7. 深入理解Linux内核(一)——Linux操作系统基础概念

    文章目录 前言 操作系统基本概念 多用户系统 用户和组 进程 内核体系结构 Unix文件系统概述 文件 硬链接和软链接 文件类型 文件描述符与索引节点 访问权限和文件模式 文件操作的系统调用 打开文件 ...

  8. 深入理解Linux内核之内存寻址

    说明: 本文基于第三版<深入理解 Linux 内核>,该部分以 80x86 处理器为基准进行介绍,并且略过了原文中详细介绍32位扩展分页部分. https://xcraft.tech/20 ...

  9. 《深入理解Linux内核》 读书笔记

    深入理解Linux内核 读书笔记 一.概论 操作系统基本概念 多用户系统 允许多个用户登录系统,不同用户之间的有私有的空间 用户和组 每个用于属于一个组,组的权限和其他人的权限,和拥有者的权限不一样. ...

最新文章

  1. Spring MVC 解决日期类型动态绑定问题
  2. 计算机音乐作曲排名2019,2019金曲排行榜_2019《全球华人歌曲排行榜》年度五强名单公布...
  3. OpenStack Liberty 连接vCenter
  4. 分布式--ActiveMQ 消息中间件(一) https://www.jianshu.com/p/8b9bfe865e38
  5. java 验证登陆_java登陆界面验证
  6. 【渝粤教育】广东开放大学 行政管理 形成性考核 (35)
  7. wps流程图怎么不能添加文字_windows不能访问共享文件夹,不能添加共享打印机时,怎么解决呢...
  8. 关于把字符串整数转换成整数的程序
  9. Godaddy域名解锁、获取转移码(Authorization Code)及转出注意事项
  10. C语言程序设计-谭浩强第五版习题【答案解析】2022.5.10
  11. 电路基础知识总结(精华版)
  12. cmd 环境下载文件的几种方法
  13. 【我的世界】自定义局域网服务器-LanServerPropertie-1.17.x-自定义端口+关正版验证
  14. mysql 锁设置_MySQL锁之二:锁相关的配置参数
  15. 我是如何在B站自学Java的?
  16. 雷达感应模组技术,存在感应雷达传感器,智能电视开关机应用
  17. 新星计划、原力计划新动态,大波的奖牌来袭速来领取
  18. 从51开始的单片机之旅(一)----流水灯、矩形键盘、电子时钟
  19. 分享自己在uniapp开发中用的css样式
  20. RGB 到HSV转换 摘自wiki百科

热门文章

  1. 【机器学习】24个终极项目提升您的机器学习知识和技能
  2. 深刻剖析与实战BCELoss详解(主)和BCEWithLogitsLoss(次)以及与普通CrossEntropyLoss的区别(次)
  3. 让机器“自愈化”引领新科技变革
  4. 教AI区分因果关系和相关性,将改变下一代 AI 的研发
  5. Science: 四万张大脑图像首次揭示人脑白质的基因基础
  6. AI领域五年引用量最高的10大论文:Adam登顶,AlphaGo、Transfromer上榜
  7. 5G时代下,边缘计算产品的未来展望
  8. 3 : 1,从21世纪科技趋势分析美国为什么一定要遏制华为
  9. 潘建伟团队再登Nature:全球首次实现器件无关量子随机数,量子保密通信安全再升级...
  10. 裁掉杰森伯恩,招揽人工智能,AI间谍厉害在哪?