Linux0.11内核--加载可执行二进制文件之3.exec

最后剩下最核心的函数do_execve了,由于这里为了简单起见我不分析shell命令的情况,

/*
* ‘do_execve()‘函数执行一个新程序。
*/
//// execve()系统中断调用函数。加载并执行子进程(其它程序)。
// 该函数系统中断调用(int 0x80)功能号__NR_execve 调用的函数。
// 参数:eip - 指向堆栈中调用系统中断的程序代码指针eip 处,参见kernel/system_call.s 程序
// 开始部分的说明;tmp - 系统中断调用本函数时的返回地址,无用;
// filename - 被执行程序文件名;argv - 命令行参数指针数组;envp - 环境变量指针数组。
// 返回:如果调用成功,则不返回;否则设置出错号,并返回-1。
int
do_execve (unsigned long *eip, long tmp, char *filename,
	   char **argv, char **envp)
{
  struct m_inode *inode;	// 内存中I 节点指针结构变量。
  struct buffer_head *bh;	// 高速缓存块头指针。
  struct exec ex;		// 执行文件头部数据结构变量。
  unsigned long page[MAX_ARG_PAGES];	// 参数和环境字符串空间的页面指针数组。
  int i, argc, envc;
  int e_uid, e_gid;		// 有效用户id 和有效组id。
  int retval;			// 返回值。
  int sh_bang = 0;		// 控制是否需要执行脚本处理代码。
// 参数和环境字符串空间中的偏移指针,初始化为指向该空间的最后一个长字处。
  unsigned long p = PAGE_SIZE * MAX_ARG_PAGES - 4;

// eip[1]中是原代码段寄存器cs,其中的选择符不可以是内核段选择符,也即内核不能调用本函数。
  if ((0xffff & eip[1]) != 0x000f)
    panic ("execve called from supervisor mode");
// 初始化参数和环境串空间的页面指针数组(表)。
  for (i = 0; i < MAX_ARG_PAGES; i++)	/* clear page-table */
    page[i] = 0;
// 取可执行文件的对应i 节点号。
  if (!(inode = namei (filename)))	/* get executables inode */
    return -ENOENT;
// 计算参数个数和环境变量个数。
  argc = count (argv);
  envc = count (envp);

// 执行文件必须是常规文件。若不是常规文件则置出错返回码,跳转到exec_error2(第347 行)。
restart_interp:
  if (!S_ISREG (inode->i_mode))
    {				/* must be regular file */
      retval = -EACCES;
      goto exec_error2;
    }
// 检查被执行文件的执行权限。根据其属性(对应i 节点的uid 和gid),看本进程是否有权执行它。
  i = inode->i_mode;				// 取文件属性字段值。

  // 如果文件的设置用户ID 标志(set-user-id)置位的话,则后面执行进程的有效用户ID(euid)就
  // 设置为文件的用户ID,否则设置成当前进程的euid。这里将该值暂时保存在e_uid 变量中。
  // 如果文件的设置组ID 标志(set-group-id)置位的话,则执行进程的有效组ID(egid)就设置为
  // 文件的组ID。否则设置成当前进程的egid。
  e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
  e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
  // 如果文件属于运行进程的用户,则把文件属性字右移6 位,则最低3 位是文件宿主的访问权限标志。
  // 否则的话如果文件与运行进程的用户属于同组,则使属性字最低3 位是文件组用户的访问权限标志。
  // 否则属性字最低3 位是其他用户访问该文件的权限。
  if (current->euid == inode->i_uid)
    i >>= 6;
  else if (current->egid == inode->i_gid)
    i >>= 3;
  // 如果上面相应用户没有执行权并且其他用户也没有任何权限,并且不是超级用户,则表明该文件不
  // 能被执行。于是置不可执行出错码,跳转到exec_error2 处去处理。
  if (!(i & 1) && !((inode->i_mode & 0111) && suser ()))
    {
      retval = -ENOEXEC;
      goto exec_error2;
    }
// 读取执行文件的第一块数据到高速缓冲区,若出错则置出错码,跳转到exec_error2 处去处理。
  if (!(bh = bread (inode->i_dev, inode->i_zone[0])))
    {
      retval = -EACCES;
      goto exec_error2;
    }
// 下面对执行文件的头结构数据进行处理,首先让ex 指向执行头部分的数据结构。
  ex = *((struct exec *) bh->b_data);	/* read exec-header *//* 读取执行头部分 */
...
//shell
...
// 释放该缓冲区。
  brelse (bh);
// 下面对执行头信息进行处理。
// 对于下列情况,将不执行程序:如果执行文件不是需求页可执行文件(ZMAGIC)、或者代码重定位部分
// 长度a_trsize 不等于0、或者数据重定位信息长度不等于0、或者代码段+数据段+堆段长度超过50MB、
// 或者i 节点表明的该执行文件长度小于代码段+数据段+符号表长度+执行头部分长度的总和。
  if (N_MAGIC (ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
      ex.a_text + ex.a_data + ex.a_bss > 0x3000000 ||
      inode->i_size < ex.a_text + ex.a_data + ex.a_syms + N_TXTOFF (ex))
    {
      retval = -ENOEXEC;
      goto exec_error2;
    }
// 如果执行文件执行头部分长度不等于一个内存块大小(1024 字节),也不能执行。转exec_error2。
  if (N_TXTOFF (ex) != BLOCK_SIZE)
    {
      printk ("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
      retval = -ENOEXEC;
      goto exec_error2;
    }
// 如果sh_bang 标志没有设置,则复制指定个数的环境变量字符串和参数到参数和环境空间中。
// 若sh_bang 标志已经设置,则表明是将运行脚本程序,此时环境变量页面已经复制,无须再复制。
  if (!sh_bang)
    {
      p = copy_strings (envc, envp, page, p, 0);
      p = copy_strings (argc, argv, page, p, 0);
// 如果p=0,则表示环境变量与参数空间页面已经被占满,容纳不下了。转至出错处理处。
      if (!p)
	{
	  retval = -ENOMEM;
	  goto exec_error2;
	}
    }
/* OK, This is the point of no return */
/* OK,下面开始就没有返回的地方了 */
// 如果原程序也是一个执行程序,则释放其i 节点,并让进程executable 字段指向新程序i 节点。
  if (current->executable)
    iput (current->executable);
  current->executable = inode;
// 清复位所有信号处理句柄。但对于SIG_IGN 句柄不能复位,因此在322 与323 行之间需添加一条
// if 语句:if (current->sa[I].sa_handler != SIG_IGN)。这是源代码中的一个bug。
  for (i = 0; i < 32; i++)
    current->sigaction[i].sa_handler = NULL;
// 根据执行时关闭(close_on_exec)文件句柄位图标志,关闭指定的打开文件,并复位该标志。
  for (i = 0; i < NR_OPEN; i++)
    if ((current->close_on_exec >> i) & 1)
      sys_close (i);
  current->close_on_exec = 0;
// 根据指定的基地址和限长,释放原来程序代码段和数据段所对应的内存页表指定的内存块及页表本身。
  // 此时被执行程序没有占用主内存区任何页面。在执行时会引起内存管理程序执行缺页处理而为其申请
  // 内存页面,并把程序读入内存。
  free_page_tables (get_base (current->ldt[1]), get_limit (0x0f));
  free_page_tables (get_base (current->ldt[2]), get_limit (0x17));
// 如果“上次任务使用了协处理器”指向的是当前进程,则将其置空,并复位使用了协处理器的标志。
  if (last_task_used_math == current)
    last_task_used_math = NULL;
  current->used_math = 0;
// 根据a_text 修改局部表中描述符基址和段限长,并将参数和环境空间页面放置在数据段末端。
// 执行下面语句之后,p 此时是以数据段起始处为原点的偏移值,仍指向参数和环境空间数据开始处,
// 也即转换成为堆栈的指针。
  p += change_ldt (ex.a_text, page) - MAX_ARG_PAGES * PAGE_SIZE;
// create_tables()在新用户堆栈中创建环境和参数变量指针表,并返回该堆栈指针。
  p = (unsigned long) create_tables ((char *) p, argc, envc);
// 修改当前进程各字段为新执行程序的信息。令进程代码段尾值字段end_code = a_text;令进程数据
// 段尾字段end_data = a_data + a_text;令进程堆结尾字段brk = a_text + a_data + a_bss。
  current->brk = ex.a_bss +
    (current->end_data = ex.a_data + (current->end_code = ex.a_text));
// 设置进程堆栈开始字段为堆栈指针所在的页面,并重新设置进程的有效用户id 和有效组id。
  current->start_stack = p & 0xfffff000;
  current->euid = e_uid;
  current->egid = e_gid;
// 初始化一页bss 段数据,全为零。
  i = ex.a_text + ex.a_data;
  while (i & 0xfff)
    put_fs_byte (0, (char *) (i++));
// 将原调用系统中断的程序在堆栈上的代码指针替换为指向新执行程序的入口点,并将堆栈指针替换
// 为新执行程序的堆栈指针。返回指令将弹出这些堆栈数据并使得CPU 去执行新的执行程序,因此不会
// 返回到原调用系统中断的程序中去了。
  eip[0] = ex.a_entry;		/* eip, magic happens :-) *//* eip,魔法起作用了 */
  eip[3] = p;			/* stack pointer *//* esp,堆栈指针 */
  return 0;
exec_error2:
  iput (inode);
exec_error1:
  for (i = 0; i < MAX_ARG_PAGES; i++)
    free_page (page[i]);
  return (retval);
}

尽管删掉很大一部分,但代码还是很长。不过没有关系,核心代码还是一小部分,大部分是判断性的代码。判断性的代码就不做分析了,仔细看也是能看懂。

注意bh = bread (inode->i_dev, inode->i_zone[0]))首先读取可执行文件的第一块数据到高速缓冲区,紧接着ex = *((struct exec *) bh->b_data);把b_data数据复制到ex中,这说明文件的b_data型数据就是exec结构。

后面又是一堆对ex的判断。

接着调用copy_strings,这时p指向参数和环境空间的已使用的地址处。

后面又是一堆给当前进程current赋值的操作。

然后是两次free_page_tables释放LDT的代码段和数据段。

然后调用change_ldt设置当前进程的LDT,这时p指向的位置之前分析过了。

接下来分析create_tables:

/*
* create_tables()函数在新用户内存中解析环境变量和参数字符串,由此
* 创建指针表,并将它们的地址放到"堆栈"上,然后返回新栈的指针值。
*/
//// 在新用户堆栈中创建环境和参数变量指针表。
// 参数:p - 以数据段为起点的参数和环境信息偏移指针;argc - 参数个数;envc -环境变量数。
// 返回:堆栈指针。
static unsigned long *
create_tables (char *p, int argc, int envc)
{
  unsigned long *argv, *envp;
  unsigned long *sp;

// 堆栈指针是以4 字节(1 节)为边界寻址的,因此这里让sp 为4 的整数倍。
  sp = (unsigned long *) (0xfffffffc & (unsigned long) p);
// sp 向下移动,空出环境参数占用的空间个数,并让环境参数指针envp 指向该处。
  sp -= envc + 1;
  envp = sp;
// sp 向下移动,空出命令行参数指针占用的空间个数,并让argv 指针指向该处。
// 下面指针加1,sp 将递增指针宽度字节值。
  sp -= argc + 1;
  argv = sp;
// 将环境参数指针envp 和命令行参数指针以及命令行参数个数压入堆栈。
  put_fs_long ((unsigned long) envp, --sp);
  put_fs_long ((unsigned long) argv, --sp);
  put_fs_long ((unsigned long) argc, --sp);
// 将命令行各参数指针放入前面空出来的相应地方,最后放置一个NULL 指针。
  while (argc-- > 0)
    {
      put_fs_long ((unsigned long) p, argv++);
      while (get_fs_byte (p++)) /* nothing */ ;	// p 指针前移4 字节。
    }
  put_fs_long (0, argv);
// 将环境变量各指针放入前面空出来的相应地方,最后放置一个NULL 指针。
  while (envc-- > 0)
    {
      put_fs_long ((unsigned long) p, envp++);
      while (get_fs_byte (p++)) /* nothing */ ;
    }
  put_fs_long (0, envp);
  return sp;			// 返回构造的当前新堆栈指针。
}

create_tables()函数用于根据给定的当前堆栈指针值p 以及参数变量个数值argc 和环境变量个数
envc,在新的程序堆栈中创建环境和参数变量指针表,并返回此时的堆栈指针值sp。创建完毕后堆栈指
针表的形式见下图9-24 所示。

注意这里有三条连续的put_fs_long函数调用,这里的压入堆栈并不是真的压栈,而是以压栈的方式存数据。

然后是两个循环放置p对应的字节值,注意这里是p++,因为之前拷贝的时候是--p。

数据填充完成后返回sp。

接着后面给当前进程的start_stack赋值,赋值p所在的页面。

最后值得注意的是:

// 将原调用系统中断的程序在堆栈上的代码指针替换为指向新执行程序的入口点,并将堆栈指针替换
// 为新执行程序的堆栈指针。返回指令将弹出这些堆栈数据并使得CPU 去执行新的执行程序,因此不会
// 返回到原调用系统中断的程序中去了。
  eip[0] = ex.a_entry;		/* eip, magic happens :-) *//* eip,魔法起作用了 */
  eip[3] = p;			/* stack pointer *//* esp,堆栈指针 */

do_execve是在system_call.s中调用的:

#### 这是sys_execve()系统调用。取中断调用程序的代码指针作为参数调用C 函数do_execve()。
# do_execve()在(fs/exec.c,182)。
.align 2
_sys_execve:
lea EIP(%esp),%eax
pushl %eax
call _do_execve
addl $4,%esp 						# 丢弃调用时压入栈的EIP 值。
ret

可以观察到,这个EIP是调用_system_call之前压入的,所以do_execve的eip就是这个EIP:

/*
* 0(%esp) - %eax
* 4(%esp) - %ebx
* 8(%esp) - %ecx
* C(%esp) - %edx
* 10(%esp) - %fs
* 14(%esp) - %es
* 18(%esp) - %ds
* 1C(%esp) - %eip
* 20(%esp) - %cs
* 24(%esp) - %eflags
* 28(%esp) - %oldesp
* 2C(%esp) - %oldss
*/

所以eip[0]就是%eip,赋值了程序的入口地址,eip[3]就是%oldesp,赋值了p的值。

至此exec.c分析结束!

时间: 2024-10-31 05:27:06

Linux0.11内核--加载可执行二进制文件之3.exec的相关文章

Linux0.11内核--加载可执行二进制文件之2.change_ldt

前面分析完了copy_strings函数,这里来分析另一个注意的函数change_ldt. 先来看调用处: // 根据a_text 修改局部表中描述符基址和段限长,并将参数和环境空间页面放置在数据段末端. // 执行下面语句之后,p 此时是以数据段起始处为原点的偏移值,仍指向参数和环境空间数据开始处, // 也即转换成为堆栈的指针. p += change_ldt (ex.a_text, page) - MAX_ARG_PAGES * PAGE_SIZE; 解释的很清楚,也就是说p指向的是相当于

Linux0.11内核--加载二进制文件之1.copy_strings

从现在开始就是分析最后的核心模块exec.c了,分析完这个文件后,就会和之前的所有分析形成一个环路,从创建进程.加载进程程序到进程调度.内存管理. exec.c的核心do_execve函数很长,而且用到了很多其他的函数,copy_strings就是其中一个,我们这里就先来分析这个函数. 首先看调用处,在main.c中: static char *argv_rc[] = { "/bin/sh", NULL}; // 调用执行程序时参数的字符串数组. static char *envp_r

Linux-0.11内核源码分析系列:关于线性地址,逻辑地址,物理地址的关系与区别

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */     以下所有描述基于Linux0.11内核及其所编写的年

Linux0.11内核剖析–内核体系结构 &#169;Fanwu

Linux0.11内核剖析–内核体系结构 ©Fanwu <Linux内核完全注释>下载:http://files.cnblogs.com/files/HanBlogs/linux-kernel.pdf(进入pdf后要点击右下角保存喔^_^) 一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: 操作系统服务程序是指那些向用户所提供的服务被看作是操作系

一站式linux0.11内核head.s代码段图表详解

阅读本文章需要的基础: 计算机组成原理:针对8086,80386CPU架构的计算机硬件体系要有清楚的认知,我们都知道操作系统是用来管理硬件的,那我们就要对本版本的操作系统所依赖的硬件体系有系统的了解,有了系统的了解后才能全面的管理它,我们对8086,80386CPU架构的计算机硬件体系如果有非常深刻的认识,我们看源代码内核的时候,就可以更可能的以一种开发者的角度去思考代码的作用,先从全局的角度去思考问题,而不是采用一种众人摸象的思维从头看到末尾. 计算机编程C语言基础:linux内核基本都是用C

linux0.11内核fork实现分析(不看不知道,一看很简单)

pcDuino3下支持mmc启动,官方的Uboot是采用SPL框架实现的,因为内部的SRAM空间达到32K,我们完全可以在这32K空间内编写一个完整可用小巧的bootloader来完成引导Linux kernel的目的. 我们首先介绍下SPL框架,可以先看下<GNU ARM汇编--(十八)u-boot-采用nand_spl方式的启动方法>和<GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析>,NAND_SPL也算是SPL框架下的一种模式. 当使用Nand f

Linux-0.11内核内存管理get_free_page()函数分析

/* *Author : DavidLin*Date : 2014-11-11pm*Email : [email protected] or [email protected]*world : the city of SZ, in China*Ver : 000.000.001*history : editor time do 1)LinPeng 2014-11-11 created this file! 2)*/Linux-0.11内存管理模块是源代码中比较难以理解的部分,现在把笔者个人的理解

程序的加载和执行(六)——《x86汇编语言:从实模式到保护模式》读书笔记26

程序的加载和执行(六)--<x86汇编语言:从实模式到保护模式>读书笔记26 通过本文能学到什么? NASM的条件汇编 用NASM编译的时候,通过命令行选项定义宏 Makefile的条件语句 在make命令行中覆盖Makefile中的变量值 第13章习题解答 复习如何构造栈段描述符 我们接着上篇博文说. 在我修改后的文件中,用到了条件汇编. 比如: %ifdef DEBUG put_core_salt: ;打印内核的符号 ... ... put_usr_salt: ;打印用户的符号 ... .

JavaScript 的性能优化:加载和执行

随着 Web2.0 技术的不断推广,越来越多的应用使用 JavaScript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 JavaScript 的阻塞特性变的复杂,也就是说当浏览器在执行 JavaScript 代码时,不能同时做其他任何事情.本文详细介绍了如何正确的加载和执行 JavaScript 代码,从而提高其在浏览器中的性能. 概览 无论当前 JavaScript 代码是内嵌还是在外链文件中,页面的下载和渲染都必