前言说明

本篇为网易云课堂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_MMU
struct 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; // 生成BSS
retval = 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和esp
start_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

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

  1. linux内核分析 第七周 Linux内核如何装载和启动一个可执行程序

    一.编译链接的过程和ELF可执行文件格式 vi hello.c gcc -E -o hello.cpp hello.c -m32 //预处理.c文件,预处理包括把include的文件包含进来以及宏替换 ...

  2. Linux内核分析第七周学习笔记——Linux内核如何装载和启动一个可执行程序

    Linux内核分析第七周学习笔记--Linux内核如何装载和启动一个可执行程序 zl + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study. ...

  3. Linux内核分析-Linux内核如何装载和启动一个可执行程序

    ID:fuchen1994 实验要求: 理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节: 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态 ...

  4. Linux内核分析作业7:Linux内核如何装载和启动一个可执行程序

            1.可执行文件的格式 在 Linux 平台下主要有以下三种可执行文件格式: 1.a.out(assembler and link editor output 汇编器和链接编辑器的输出) ...

  5. linux内核分析第七周-Linux内核如何装载和启动一个可执行程序

    一.可执行文件的创建 可执行文件的创建就是三步:预处理.编译和链接. cd Code vi hello.c #写入最简单的helloworld的c程序 gcc -E -o hello.cpp hell ...

  6. 读书笔记之Linux系统编程与深入理解Linux内核

    前言 本人再看深入理解Linux内核的时候发现比较难懂,看了Linux系统编程一说后,觉得Linux系统编程还是简单易懂些,并且两本书都是讲Linux比较底层的东西,只不过侧重点不同,本文就以Linu ...

  7. 【读书笔记::深入理解linux内核】内存寻址【转】

    转自:http://www.cnblogs.com/likeyiyy/p/3837272.html 我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0 ...

  8. 【读书笔记::深入理解linux内核】内存寻址

    我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0xC0000000:这是内核地址空间的地址转换关系. 这句话瞬间让我惊呆了,根据我的CPU的知识,开 ...

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

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

随机推荐

  1. 《Python基础教程》 读书笔记 第六章 抽象 函数 参数

    6.1创建函数 函数是可以调用(可能包含参数,也就是放在圆括号中的值),它执行某种行为并且返回一个值.一般来说,内建的callable函数可以用来判断函数是否可调用: >>> x=1 ...

  2. 关于Ubuntu上的服务文件

    问题发现 今天在给ubuntu系统安装ftp服务时,一件奇怪的事引起了我的注意.当我服务安装完成后,想要测试一下是否能控制服务,便输入如下命令: service vsftpd restart 它返回的 ...

  3. 微信小程序---目录结构

    (1)目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page.一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下: (2)允许上传的文件 .

  4. iOS开发基础知识

    1:App跳转至系统Settings 跳转在IOS8以上跟以下是有区别的,如果是IOS8以上可以如下设置: NSURL *url = [NSURL URLWithString:UIApplicatio ...

  5. In line copy and paste to system clipboard

    On the Wiki Wiki Activity Random page Videos Photos Chat Community portal To do    Contribute  Watch ...

  6. 888. Fair Candy Swap@python

    Alice and Bob have candy bars of different sizes: A[i] is the size of the i-th bar of candy that Ali ...

  7. 数组合并--php

    常用的合并数组方法有以下几种: 1  array_merge 2  '+' 3  array_merge_recursive 下面是一段对比的代码 $array1 = array(2,4," ...

  8. java 图片合成文字或者awt包下的对话框引入自定义字体库

    成就卡图片字体问题:1.下载 xxx.ttf 文件2.mkdir /usr/local/jdk1.6.0_11/jre/lib/fonts/fallback.     在jre/lib/fonts 下 ...

  9. RPM Package Manager

    本文大部分内容来自鸟哥的Linux私房菜,并且由作者根据自己的学习情况做了一些更改,鸟哥原文链接 1. 程序的安装方式 源代码安装:利用厂商释出的Tarball 来进行软件的安装,每次安装程序都需要检 ...

  10. /etc/rc.d启动目录详解

    操作系统:CentOS6.6_32位 控制脚本目录/etc/rc.d,该目录下存在各个运行级别的脚本文件,执行ls /etc/rc.d,显示结果为:init.d  rc  rc0.d  rc1.d   ...