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分析结束!
Linux0.11内核--加载可执行二进制文件之3.exec的更多相关文章
- Linux0.11内核--加载可执行二进制文件之1.copy_strings
从现在开始就是分析最后的核心模块exec.c了,分析完这个文件后,就会和之前的所有分析形成一个环路,从创建进程.加载进程程序到进程调度.内存管理. exec.c的核心do_execve函数很长,而且用 ...
- Linux0.11内核--加载可执行二进制文件之2.change_ldt
前面分析完了copy_strings函数,这里来分析另一个注意的函数change_ldt. 先来看调用处: // 根据a_text 修改局部表中描述符基址和段限长,并将参数和环境空间页面放置在数据段末 ...
- ASM:《X86汇编语言-从实模式到保护模式》第13章:保护模式下内核的加载,程序的动态加载和执行
★PART1:32位保护模式下内核简易模型 1. 内核的结构,功能和加载 每个内核的主引导程序都会有所不同,因为内核都会有不同的结构.有时候主引导程序的一些段和内核段是可以共用的(事实上加载完内核以后 ...
- JavaScript 的性能优化:加载和执行
随着 Web2.0 技术的不断推广,越来越多的应用使用 javascript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 ...
- [转]JavaScript 的性能优化:加载和执行
原文链接:http://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/index.html?ca=drs- JavaScript 的性能优化: ...
- bootm命令中地址参数,内核加载地址以及内核入口地址
bootm命令只能用来引导经过mkimage构建了镜像头的内核镜像文件以及根文件镜像,对于没有用mkimage对内核进行处理的话,那直接把内核下载到连接脚本中指定的加载地址0x30008000再运行就 ...
- Linux-0.11内核源代码分析系列:内存管理get_free_page()函数分析
Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /* ...
- 内核加载与linux的grub
计算机系统的启动是一个复杂的过程,启动过程大致可以分为以下几个阶段: +------计算机系统启动流程----------------------------- ------------------- ...
- JavaScript的性能优化:加载和执行
随着 Web2.0 技术的不断推广,越来越多的应用使用 javascript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 ...
随机推荐
- ASP.NET MVC5 网站开发实践(二) Member区域–我的咨询列表及添加咨询
上次把咨询的架构搭好了,现在分两次来完成咨询:1.用户部分,2管理部分.这次实现用户部分,包含两个功能,查看我的咨询和进行咨询. 目录: ASP.NET MVC5 网站开发实践 - 概述 ASP.NE ...
- Power BI官方视频(1) Power BI Desktop 7月份更新功能概述
2016年7月,Power BI Desktop进行了一些功能更新,提高整体的用户体验.同时也有一些新的和令人兴奋的功能.看看大概介绍,更新功能要点: 本文原文地址:Power BI官方视频(1) P ...
- Create Volume 操作(Part I) - 每天5分钟玩转 OpenStack(50)
前面已经学习了 Cinder 的架构和相关组件,从本节我们开始详细分析 Cinder 的各种操作,首先讨论 Cinder 如何创建 volume. Create 操作流程如下: 客户(可以是 Open ...
- 完全抽离WebAPi之特殊需求返回HTML、Css、JS、Image
前言 今天我们来实现一个特殊的需求,这个需求说来也不过分,不过有点违背WebAPi的真实用途,WebAPi不过是作为传输数据而用,若非在项目开发中断不可想到还要实现一个页面来实时显示列表并进行后续其他 ...
- 前端编码风格规范之 JavaScript 规范
JavaScript 规范 全局命名空间污染与 IIFE 总是将代码包裹成一个 IIFE(Immediately-Invoked Function Expression),用以创建独立隔绝的定义域.这 ...
- Cesium原理篇:7最长的一帧之Entity(下)
上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...
- Equals和ReferenceEquals
稍微分析下一下两个方法的区别: public static bool Equals(object objA, object objB); public static bool ReferenceEqu ...
- Linux资源控制-CPU和内存
主要介绍Linux下, 如果对进程的CPU和内存资源的使用情况进行控制的方法. CPU资源控制 每个进程能够占用CPU多长时间, 什么时候能够占用CPU是和系统的调度密切相关的. Linux系统中有多 ...
- EF中的实体类型【Types of Entity in Entity】(EF基础系列篇8)
We created EDM for existing database in the previous section. As you have learned in the previous se ...
- 基于STM32Cube的DAC数模转化
1. STM32Cube配置 1.1 DAC配置 1.2 TIM6 配置 1.3 利用Cube产生工程程序,MDK打开软件 在主循环上添加语句: HAL_TIM_Base_S ...