1:信息
linux 0.11
下面文件属于文件系统的高层操作和管理部分
open.c :文件访问操作系统调用
exex.c :程序加载和执行函数
stat.c, :取得一个文件的状态信息
fcntl.c :实现文件访问控制管理
ioctl.c :控制设备的访问操作
2 open.c
本文中实现了许多与文件操作相关的系统调用。主要是文件的创建,打开和关闭,文件宿主和属性的修改,文件访问权限的修改,文件操作时间的修改和系统文件root的变动
2.1 sys_open 打开或创建
参数:
- filename -文件名
- flag - 打开文件标志 O_RDINLY(只读), O_WEONLY(只写), O_RDWR(读写), O_CREAT(创建) O_EXCL(被创建文件必须存在), O_APPDEND(在文件尾部添加)
- mode - 指定文件许可属性。S_IRWXU (文件宿主具有读,写,执行),S_IRUSR(用户具有读文件权限), S_IRWXG (组成员具有读,写和执行权限)
调用操作成功返回文件句柄,否则返回出错码
int sys_open(const char * filename,int flag,int mode)
{
struct m_inode * inode;
struct file * f;
int i,fd;
//对参数进行处理,讲用户设置的模式和进程模式屏蔽码相与,产生需要的文件模式
mode &= 0777 & ~current->umask;
//为打开文件创建一个文件句柄
for(fd=0 ; fd<NR_OPEN ; fd++)//需要搜索进程接结构中文件结构指针数组
if (!current->filp[fd])//找到一个空闲项
break;
if (fd>=NR_OPEN)//当fd大于NR_OPEN说明已经没有空闲项,返回错误码
return -EINVAL;
//然后我们设置当前进程的执行时关闭文件句柄(close_on_exec ) 位图,复位对应的bit位。
//close_on_exec是一个进程所以文件的句柄的位图标志。每个bit位代表打开着的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄。
//当程序使用fork函数创建一个子线程时,通常会在该子进程调用execve函数执行另一个新程序
//此时,子进程中开始执行新程序,若一个文件句柄在close_on_exec中对应bit位被置位
//那么子执行ececve时该会议文件句柄讲被关闭,否则该文件句柄将始终处于打开的状态
//当打开一个文件时,默认情况下文件句柄在子进程中也处于打开状态。因此这里要复位对应的bit位
current->close_on_exec &= ~(1<<fd);
//为打开文件在文件表中寻找一个空闲结构项。我们零f指向文件表数组开始处。0+file_table等同于file_table和&file_table[0]
f=0+file_table;
//搜索空闲文件结构项(引用计数为0的项)。
for (i=0 ; i<NR_FILE ; i++,f++)
if (!f->f_count) break;
if (i>=NR_FILE)//若已经没有空闲文件表结构项。则返回错误码
return -EINVAL;
(current->filp[fd]=f)->f_count++;//令进程对应的文件句柄fd的文件结构体指针指向搜索到的文件结构,并文件引用计数递增1
if ((i=open_namei(filename,flag,mode,&inode))<0) {//然后调用open_namei执行打开操作,若返回值小于0,则说明出错
current->filp[fd]=NULL;//释放当申请的文件结构
f->f_count=0;
return i;//返回出错码i
}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
//根据打开文件的i节点的属性字段,我们可以知道文件的具体类型,对于不同的文件我们需要做一些特别的处理
if (S_ISCHR(inode->i_mode))//如果打开的是字符设备
if (MAJOR(inode->i_zone[0])==4) {//对于主设备号是4的字符设备(/dev/tty0)
if (current->leader && current->tty<0) {//if(当前进程是进程组的首领 && 当前的进程的tty字段小于0(没有终端))
current->tty = MINOR(inode->i_zone[0]);//则设置当前进程的tty号为该i节点的子设备号
tty_table[current->tty].pgrp = current->pgrp;//并设置当前进程tty对应的tty表项的父进程组号等于当前进程的进程组号。标识为还进程组(会话期)分配控制终端。
}
} else if (MAJOR(inode->i_zone[0])==5)//对于主设备号为5的文件字符(/dev/tty)
if (current->tty<0) {//若当前进程没有tty,说明出错
iput(inode);//放回i节点
current->filp[fd]=NULL;//释放申请的文件结构体
f->f_count=0;
return -EPERM;//返回出错码
}
/* Likewise with block-devices: check for floppy_change */
//同样对于块设备文件:需要检查盘片是否被更换
if (S_ISBLK(inode->i_mode))//如果打开的是块设备文件
check_disk_change(inode->i_zone[0]);//检查盘片是否被更换过。若更换过,则让高速缓冲区中该设备的所有缓冲块失效
//现在开始初始化打开文件的文件结构。
f->f_mode = inode->i_mode;//设置文件结构属性和标志
f->f_flags = flag;
f->f_count = 1;//置句柄引用计数为1
f->f_inode = inode;//设置i节点字段为打开文件的i节点
f->f_pos = 0;//初始化文件读写指针为0
return (fd);//返回文件句柄号
}
打开的过程:
- 传入要打开的文件名称
- 在系统当前进程的文件索引表current->file[fd]为空的
- 在系统的文件表中file_table[ ]找到一个空项
- 进行current->file[fd] 和 file_table[ ]的映射current->file[fd] = file_table[n]
- 调用open_namei函数创建一个对应文件名的inode节点
- 进行 file_table[n].inode = open_namei创建的inode节点
- 返回句柄
3 exec.c
- 程序运行前位于磁盘
- 在运行时,首先把程序头读出(高速缓冲区 -> 内存)
- 运行程序的函数线:程序头被载入高速缓冲区后 -> fork -> copy_process -> ldt(代码段,数据段) tss(程序运行的状态)
- 程序要运行要传入参数和环境变量
在程序运行时,要创建参数和环境变量的存储页面
设置新函数的sp
重新调整代码段
tss分为:
cs - 代码段
ds - 数据段
es - 扩展段
ss - 栈
fs - 用户数据段(用户数据空间)
ds | fs | |
---|---|---|
用户态工作 | 用户程序数据段 | 用户程序数据段 |
内核态工作 | 内核程序数据段 | 用户程序数据段 |
3.1 copy_string
copy_string函数用于从用户内存空间拷贝命令行参数和环境字符串到内核空闲页面中
赋值指定个数参数字符串到参数和环境空间中
参数:
- argc - 欲添加参数的个数
- argv - 参数指针数组
- page - 参数和环境空间页面指针
- p - 参数表空间中偏移指,始终指向已复制串头部。
- form_kmem - 字符串来源标志
在do_execve()函数中,p初始化为参数表(128KB)空间的最后一个长字处,参数字符串是以堆栈操作方式往其中复制存放的。
因此,p指针会随着复制信息的增加而逐渐减小,并始终指向参数字符串的头部。
字符串来源标志from_kmem应该是TYT为了给execve添加执行脚本文件的功能而新加的参数。
当没有运行脚本文件的功能时,所有的参数字符串都在用户数据空间中
返回:参数和环境空间当前头部指针。若出错返回0
/*
* 'copy_string()' copies argument/envelope strings from user
* memory to free pages in kernel mem. These are in a format ready
* to be put directly into the top of new user memory.
*
* Modified by TYT, 11/24/91 to add the from_kmem argument, which specifies
* whether the string and the string array are from user or kernel segments:
*
* from_kmem argv * argv **
* 0 user space user space
* 1 kernel space user space
* 2 kernel space kernel space
*
* We do this by playing games with the fs segment register. Since it
* it is expensive to load a segment register, we try to avoid calling
* set_fs() unless we absolutely have to.
*
* copy_string()函数从用户内存空间拷贝参数,环境字符串到内核空闲的页面中
* 这些已具有直接放到新用户内存的格式
* * from_kmem argv * argv **
* 0 用户空间 用户空间
* 1 内核空间 用户空间
* 2 内核空间 内核空间
* 我们通过巧妙处理fs段寄存器来操作的,由于加载一个段寄存器的代价太高
* 所以我们尽量避免使用set_fs。除非有必要
*/
//调用形式
// char* str[] = {"hello", "world"};
// copy_strings(2, str, page, p, 1)
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
unsigned long p, int from_kmem)
{
char *tmp, *pag;
int len, offset = 0;
unsigned long old_fs, new_fs;
if (!p)//验证指针偏移
return 0; /* bullet-proofing */
//取当前段寄存器ds(指内核数据段)和fs值。分别保存在new_fs 和 old_fs中
new_fs = get_ds();
old_fs = get_fs();
if (from_kmem==2)//若字符串和字符串数组(指针)来源于内核空间
set_fs(new_fs);//设置fs段指向内核数据段
//循环处理各个参数,从最后一个参数逆向开始复制,复制到指定偏移地址处。首先取需要复制的当前字符串指针
while (argc-- > 0) {
if (from_kmem == 1)//如果字符串在用户空间而字符串数组在内核空间
set_fs(new_fs);//则设置fs段寄存器指向内核数据段(ds)
if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))//在内核数据空间取了字符串指针tmp,最开始tmp指向最后一个参数,随着argc的递减,开始指向倒数第二个,倒数第三个....
panic("argc is wrong");
//取完tmp后
if (from_kmem == 1)//若串指针在内核空间
set_fs(old_fs);//恢复fs段寄存器的原值,fs在指向用户空间
//然后从用户空间取该字符串,并计算该参数字符串的长度len,此后tmp指向该字符串的末端
len=0; /* remember zero-padding */
do {
len++;
} while (get_fs_byte(tmp++));
//如果字符串的长度超过此时参数和环境空间中还剩余的长度,则空间不够了
if (p-len < 0) { /* this shouldn't happen - 128kB */
set_fs(old_fs);//恢复fs段寄存器的值
return 0;//返回0
}
//我们逆向逐个字符地把字符串复制到参数和环境空间的末端处、在循环复制字符串的字符过程中。
//我们首先判断参数和环境空间中相应的位置是否已经有内存页面。
//如果还没有就线为其申请1页内存页面。偏移量offset被作在一个页面中当前指针偏移量。
//因为刚执行函数时,offset被初始为0,所以offset - 1 < 0肯定成立而使得offset重新被设置为当前p指针在页面范围内的偏移值
while (len) {
--p; --tmp; --len;
if (--offset < 0) {
offset = p % PAGE_SIZE;
if (from_kmem==2)//如果字符串和字符串数组都在内和空间
set_fs(old_fs);//从内核数据空间复制字符串内容,下面会让fs指向内核数据段,这里则让恢复fs段寄存器原值
// 如果(当前偏移值p所在的串空间页面指针数组项page[p/PAGE_SIZE] == 0,则表示p指针所处的内存空间内存页面还不存在
// 则需要申请一个空闲页,并将该页填入到指针数组,同时也让页指针pag指向该新页面。若申请不到空闲页面则返回0 )
if (!(pag = (char *) page[p/PAGE_SIZE]) &&
!(pag = (char *) page[p/PAGE_SIZE] =
(unsigned long *) get_free_page()))
return 0;
if (from_kmem==2)//如果字符串在内核空间
set_fs(new_fs);//则设置fs段寄存器指向内核数据段(ds)
}
//从fs段中复制字符串的1字节到参数和环境变量空间的内存页面pag和offset处
*(pag + offset) = get_fs_byte(tmp);
}
}
//如果字符串和字符串数组在内核空间,则恢复fs段寄存器原值
if (from_kmem==2)
set_fs(old_fs);//恢复fs段寄存器原值
return p;//最后,返回参数和环境空间以复制参数的头部偏移值
}
在执行完copy_string后,通过do_execve()中p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
, p将被调整为从进程逻辑地址开始算起的参数和环境变量起始处指针,见下图的p’ 。 方法是把一个进程占用的最大逻辑空间长度64MB减去参数和环境变量占用的长度(128KB - p)。p’的左边部分还将使用create_table()函数来存放参数和环境变量的一个指针表。并且p’将再次向左调整为指向指针表的起始位置处。再把所得指针进行页面对齐。最终得到初始堆栈指针sp
3.2 create_tables
create_tables()函数用于根据给定的当前堆栈指针值p以及参数变量格式argc和环境变量的个数envc,在新的程序堆栈中创建环境和参数变量指针表。并返回此时的堆栈指针,再把该指针进行页面对齐处理,最终得到初始堆栈指针sp。创建完成后的堆栈指针表如下图所示
在新任务中创建参数和环境变量指针表
参数:
- p - 数据段中参数和环境变量的信息偏移指针
- argc - 参数个数
- envc - 环境变量个数
/*
* create_tables() parses the env- and arg-strings in new user
* memory and creates the pointer tables from them, and puts their
* addresses on the "stack", returning the new stack pointer value.
* create_table()函数再新任务内存中解析还环境变量和参数字符串,由此创建指针表
* 并把他们的地址放到栈上,然后返回新栈的地址
*/
static unsigned long * create_tables(char * p,int argc,int envc)
{
unsigned long *argv,*envp;
unsigned long * sp;
//真指针是4字节为边界寻址的,因此这里让sp为4的整数倍。
sp = (unsigned long *) (0xfffffffc & (unsigned long) p);
//此时sp位于参数环境表的末端
sp -= envc+1;//把sp向下移动,sp = sp - (envc + 1),让栈中空出环境变量指针占用的空间
envp = sp;//让环境变量指针envp指向目前sp的位置,由于移动了envc + 1个位置,多出的一个位置用于在最后存放NULL值
sp -= argc+1;//把sp继续向下移动,sp = sp - (argv + 1),让栈中空出参数指针占用的空间
argv = sp;//让参数指针argv指向目前sp的位置,由于移动了argv + 1个位置,多出的一个位置用于在最后存放NULL值
put_fs_long((unsigned long)envp,--sp);//把环境参数块指针ecvp压入栈中
put_fs_long((unsigned long)argv,--sp);//把参数指针argv压入栈中
put_fs_long((unsigned long)argc,--sp);//把命令参数个数argc压入栈中
//上面三个操作完成,sp又向下移动了3个位置
//通过上面得到参数指针argv的位置,通过循环依次放入参数地址(顺序是从下到上)
while (argc-->0) {
put_fs_long((unsigned long) p,argv++);//参数p的地址 放入argv对应的位置
while (get_fs_byte(p++)) /* nothing */ ;//p指针指向下一个参数串
}
//通过上面得到环境块指针envp的位置,通过循环依次放入环境变量地址(顺序是从下到上)
put_fs_long(0,argv);//最后一个放NULL值
while (envc-->0) {
put_fs_long((unsigned long) p,envp++);//环境变量p的地址 放入envp对应的位置
while (get_fs_byte(p++)) /* nothing */ ;;//p指针指向下一个参数串
}
put_fs_long(0,envp);//最后一个放NULL值
return sp;;//返回sp的位置
}
3.3 do_execve
//文件头结构体
struct exec {
unsigned long a_magic; /* Use macros N_MAGIC, etc for access 执行文件魔数,使用N_MAGIC访问*/
unsigned a_text; /* length of text, in bytes 代码段长度*/
unsigned a_data; /* length of data, in bytes 数据段长度*/
unsigned a_bss; /* length of uninitialized data area for file, in bytes BSS段长度*/
unsigned a_syms; /* length of symbol table data in file, in bytes 文件表号长度*/
unsigned a_entry; /* start address 程序入口点*/
unsigned a_trsize; /* length of relocation info for text, in bytes 文件重定义的代码长度*/
unsigned a_drsize; /* length of relocation info for data, in bytes 文件重定义的数据长度*/
};
#ifndef OMAGIC
/* Code indicating object file or impure executable. */
#define OMAGIC 0407 //指明为目标文件或者不纯的可执行文件的代号
/* Code indicating pure executable. */
#define NMAGIC 0410 //指明为纯可执行文件的代号。 0x108
/* Code indicating demand-paged executable. */
#define ZMAGIC 0413 //指明为需求分页处理的可执行文件 其头文件占开始处的1K空间,0x10b
#endif /* not OMAGIC */
/*
*
* 'do_execve()' executes a new program 执行一个新程序.
*/
//execve()系统调用函数,加载并执行子程序
//该函数时系统调用(int80)功能号为_NR_execve调用的函数。函数的参数是进入系统调用处理过程后调用本系统调用处理过程(system_call.s 第200行)
//和调用本函数之前(system_call.s 第203行)逐步压入栈中的值。这些值包括
//86-88行入栈的edx,ecx,ebx分别对应**envp,**argv, *filename
//94行调用sys_call_table中的sys_execve函数(指针)时压入栈的函数返回地址(tmp)
//202行调用本函数do_exevce前入栈的指向栈中调用系统中断代码指针eip
//参数:
//eip-调用系统中断的程序代码指针 参考:system_call.s程序开始部分的说明
//tmp-系统中断调用_sys_execve时的返回地址,无用
//filename - 被执行程序的文件名指针
//argv - 命令行参数指针数组的指针
//envp-环境变量指针数组的指针
//返回
//调用成功,则不反悔,调用失败返回出错号
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;//p指向参数和环境空间的最低部
//正式执行设置文件的运行环境之前,先做一些杂事。内核准备了128KB(32个page)空间来存放执行文件的命令参数和环境字符串。
//上一行把p初始化为位于128KB空间的最后一个字长处。在初始化参数和环境空间操作的过程中。p将用来表面在128KB空间的位置
//另外,eip[1]是调用本次系统调用的原用户程序代码段寄存器cs值。其中的段选择符,当然必须是当前任务的代码段选择符(0x000f)
//若不是该值,那么cs只能是内核代码段的选择符(0x0008)。但这是绝对不允许的,因为内核代码常驻内存是不能被替换掉的
if ((0xffff & eip[1]) != 0x000f)
panic("execve called from supervisor mode");
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table 初始化32个page的参数和环境空间*/
page[i]=0;
if (!(inode=namei(filename))) /* get executables inode 取出执行文件的i节点*/
return -ENOENT;
argc = count(argv);//计算参数个数
envc = count(envp);//计算环境变量的个数
restart_interp:
if (!S_ISREG(inode->i_mode)) { /* must be regular file *///不是普通文件则返回错误值
retval = -EACCES;
goto exec_error2;
}
//2.判断当前文件的执行权限
//根据执行文件i节点的属性,看看本进程是否有权执行它
i = inode->i_mode;
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
//如果执行文件属于运行进程的用户,则把文件数据值i右移6位。此时低3位是文件宿主的访问权限标志
if (current->euid == inode->i_uid)
i >>= 6;
//执行文件与当前运行进程的用户属于同组,则把文件数据值i右移3位。 此时低3位是文件组用户的访问权限标志
else if (current->egid == inode->i_gid)
i >>= 3;
//如果(相应用户没有执行权力(位0不是1) && 其他用户也没有权限或者当前进程不是超级用户)
//则表明当前进程没有权利执行这个文件
if (!(i & 1) &&
!((inode->i_mode & 0111) && suser())) {
retval = -ENOEXEC;//置出错码
goto exec_error2;//回到exec_error2执行
}
//否者,当前进程有执行文件的权限。那么就读出执行文件头部数据并且根据其中的信息来分析设置运行环境,或者运行一个shell程序来执行脚本程序
//3.读取当前执行文件或shell的文件头结构
if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {//读出文件第一块数据到高速缓冲区
retval = -EACCES;
goto exec_error2;
}
ex = *((struct exec *) bh->b_data); /* read exec-header *///复制缓冲区块的数据到ex中
//认定为shell脚本
//如果(执行文件开始两个字节时“#!”)说明是一个脚本执行文件
//想运行一个脚本文件,就需要执行脚本文件的解释程序(例如shell程序)
//通常脚本程序,第一行文本是#!/bin/bash。它指明了要运行的解释程序。
//运行方式是从脚本第一行(带字符‘#!’)中取出其中的解释程序名以及后面的参数(若存在)
//然后将这些参数和脚本文件名放进执行文件(此时为解释程序)的命令行参数空间中
//在这之前我们当然需要把函数指定的原有命令行参数和环境字符串放到128KB空间中,而这里建立的命令行参数则放到他们前面的位置(因为是逆向放)
//最后让内核执行脚本文件的解释程序
//下面就是在设置好解释程序的脚本文件名等参数后,取出解释程序的i节点并跳转restart_interp:去执行解释程序
//由于需要跳转执行,因此在下面确认并处理脚本文件之后需要设置一个禁止再次执行下面脚本处理的表示sh_bang。
//后面的代码标志中该标志也用来表示我们已经设置好执行文件的命令行参数,不需要重复设置
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
char buf[1023], *cp, *interp, *i_name, *i_arg;
unsigned long old_fs;
strncpy(buf, bh->b_data+2, 1022);//复制第一行字符#!之后的字符串到buf中,其中含有脚本解释程序名(/bin/sh)
brelse(bh);//释放缓冲块
iput(inode);//放回i节点
buf[1022] = '\0';//添加结尾符号
//删除开始的空格,制表符
if (cp = strchr(buf, '\n')) {//找到换行的位置
*cp = '\0';//将换行替换成\0
for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);//找到文件开头的空格或者制表符,让cp指向真正字符串开始的位置
}
if (!cp || *cp == '\0') {//该行没有其他内容,则出错
retval = -ENOEXEC; /* No interpreter name found */
goto exec_error1;
}
//此时我们得到脚本解释程序的第一行内容(字符串),cp指向起始位置
//首先取第一个字符串,它应该是解释程序名,此时i_name指向该名称
//若解释程序名后还有其他字符,则他们应该是解释程序的参数,用i_arg指向该串
//例如/bin/bash
interp = i_name = cp;
i_arg = 0;
//让i_name指向执行文件名
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) { //遍历字符串条件是cp不为空格并且不是制表符
if (*cp == '/')//如果遇到的"/"就更新i_name的指向
i_name = cp+1;//i_name最终指向bash
}
//当前*cp不是NULL,则为空格或者是\t,说明后面有参数
if (*cp) {
*cp++ = '\0';//让*cp(空格或者制表符) = NULL,然后cp++,这是cp指向了参数的首地址
i_arg = cp;//让i_arg指向参数
}
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
*/
//现在把解析出来的程序名i_name 和 i_arg和脚本文件名作为解释程序参数放到环境和参数中
//不过需要把函数提供的原来的参数和环境字符串先放进去,然后再放解析出来的。
//例如,对于命令行参数来说,如果原来的参数“-arg1 -arg2”,脚本名为example.sh。解释程序名为“bash” 其参数为“-iarg1,-arg2”。
//那么在放入这里参数之后,新的命令行类似于
//“bash -iarg1,-arg2 example.sh -arg1 -arg2”
//用sh_bang来作为是否第一次执行shell的状态,
if (sh_bang++ == 0) {//这里把sh_bang标志值上
//把函数参数提供的原有参数和环境字符串放到空间中
//环境字符串的个数是envc,参数格式是argc-1(有一个参数是执行文件名)
p = copy_strings(envc, envp, page, p, 0);//拷贝envc个环境变量
p = copy_strings(--argc, argv+1, page, p, 0);//拷贝argc-1个参数。在拷贝参数的时候不进行文件名的拷贝。所以跳过
}
/*
* Splice in (1) the interpreter's name for argv[0] //argv[0]中放的是解释程序的名字
* (2) (optional) argument to interpreter//可选的 解释程序的参数
* (3) filename of shell script//脚本程序的参数
*
* This is done in reverse order, because of how the
* user environment and arguments are stored.
* 这里是逆序进行处理的,是由于用户环境和参数的存放方式导致的
*/
//接着我们逆向复制文本名,解释程序参数和解释程序文件名到参数和环境空间中
p = copy_strings(1, &filename, page, p, 1);//拷贝一个文件名
argc++;
if (i_arg) {
p = copy_strings(1, &i_arg, page, p, 2);//拷贝解释程序参数(1个)
argc++;
}
p = copy_strings(1, &i_name, page, p, 2);//拷贝解释程序文件名(1个)
argc++;
if (!p) {//参数页满了,回到exec_error1
retval = -ENOMEM;//则置出错吗,
goto exec_error1;//跳转exec_error1
}
/*
* OK, now restart the process with the interpreter's inode.
* OK,现在使用解释程序的i节点重启进程
*/
//该函所使用的参数(文件名)是从用户数据空间获得的,即从段寄存器fs所指向的空间取得。
//因此在调用namei()函数之前我们需要先临时把fs指向内核数据空间。以让函数能从内核空间得到解释程序名
//并在namei()返回后恢复fs的默认设置。
old_fs = get_fs();//先临时保存fs段寄存器(指向用户数据段)的值
set_fs(get_ds());//并设置fs指向内核数据段
//取得解释进程程序i节点指针
if (!(inode=namei(interp))) { /* get executables inode */
set_fs(old_fs);
retval = -ENOENT;
goto exec_error1;
}
set_fs(old_fs);//恢复原fs的原值
goto restart_interp;//跳转执行新的执行文件-脚本文件的解释程序
}
//此时缓冲块中的执行文件结构已经复制到了ex中。于是先释放该缓冲块
brelse(bh);
//4.根据头文件信息进行当前程序的判断
//对于linux0.11内核来说,它仅支持ZMAGIC的文件格式,并且执行文件代码都是从逻辑地址0开始执行的。因此不支持含有代码或数据重定位的执行文件
//如果(执行文件不是ZMAGIC || 代码重定位长度不等于0 || 数据重定位长度不等于0 ||
//(代码段+数据段+堆 > 50 M)||
// 执行文件的长度 <(代码段+数据段+符号表长度+执行头部分))
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;
}
//执行文件中代码开始处没有位于1个页面(1024字节处)边界处,则也不能执行
//因为需求页(Demand paging)计数要求加载执行文件内容时以页面为单位,因此要求执行文件映像和数据都从页面边界处开始
if (N_TXTOFF(ex) != BLOCK_SIZE) {
printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
retval = -ENOEXEC;
goto exec_error2;
}
//5.拷贝执行文件的环境变量和参数
//如果sh_bang没有被置位,则复制指定个数的命令行参数和环境字符串到参数和环境空间中
//若已经设置,则表明运行了脚本解释程序,此时环境变量页面已经复制,不需要再复制
if (!sh_bang) {
p = copy_strings(envc,envp,page,p,0);
p = copy_strings(argc,argv,page,p,0);
if (!p) {//p = 0,表示环境变量与参数空间页面已满
retval = -ENOMEM;
goto exec_error2;
}
}
/* OK, This is the point of no return 下面开始就没有返回的地方了*/
//6.执行线程时,进程要做的切换和更新
//前面指针函数参数提供的信息对需要执行的文件的命令行参数和环境空间进行了设置,但是还没有为执行文件做过什么实质性的工作
//即还没有做过为执行文件初始化进程任务结构信息,建立页表等工作。
//由于执行文件直接使用当前进程的“躯壳”,即当前进程被改造成执行文件的进程
//因此我们需要首先释放当前进程占用的某些系统资源,包括关闭指定的已打开文件,占用的页表和内存页面等。
//然后根据执行文件头结构体信息休息修改当前当前进程使用的局部描述符表LDT中的描述符的内容
//重新设置代码段和数据段描述的限长,再利用前面处理得到的e_uid和g_uid等信息来设置进程任务中相关的字段。
//最后把执行本次系统调用程序的返回地址eip[]指向指向文件代码的起始位置处。
//这样当本系统调用退出返回后就会去运行新的执行文件的代码了
//注意:虽然此时新执行的文件代码和数据还没有从文件加载到内存中,但其参数和环境块已经再copy_strings()已经使用get_free_page()分配了物理内存页来保存数据,
//并再change_ldt()函数中使用put_page()放到了进程逻辑空间的末端处。
//另外,在create_tables()中也会由于在用户栈上存放参数和环境指针表而引发缺页异常,从而内存管理程序也会就此为用户栈内存空间映射物理内存页
//更新当前进程执行程序inode节点
if (current->executable)
iput(current->executable); //这里我们首先放回进程执行原执行程序的i节点
current->executable = inode;//让进程的executable指向新执行文件的i节点
//清空当前进程的信号处理函数,但是对于SIG_IGN句柄无需复位,因此这里应该在加上一个判断
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的值修改局部表中的描述符基址和段限长,并把128KB的参数和环境空间页面放置到数据段末端。
//执行下面语句后,p此时更改成以数据段起始处为原点的偏移值,但仍指向参数和环境空间数据的起始处,即转化为堆栈指针
p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
p = (unsigned long) create_tables((char *)p,argc,envc); //然后调用create_tables()在栈空间创建环境和参数变量指着表,供main()使用,并返回该栈指针
//修改进程字段值,为新执行文件的信息
//进程任务结构代码尾字段end_code等于执行文件的代码长度
//数据尾字段end_data等于执行文件的代码长度加上数据长度(a_text + a_data)
//进程堆结尾字段brk = a_text + a_data + a_bss,brk用于指明进程当前数据段(包括未初始化数据部分)末端位置
current->brk = ex.a_bss +
(current->end_data = ex.a_data +
(current->end_code = ex.a_text));
//设置进程栈开始字段为栈指针所在页面
current->start_stack = p & 0xfffff000;
//重置进程有效用户id和有效组id
current->euid = e_uid;
current->egid = e_gid;
//如果执行文件代码加数据长度末端不在页面边界上,则把最后不到1页长度的内存空间初始化为0
//实际上由于ZMAGIC格式化执行的文件,因此断码段和数据段长度均为页面的整数倍长度,因此put_fs_byte不会执行
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[3] = p; /* stack pointer */
return 0;
exec_error2:
iput(inode);//放回i节点
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++)
free_page(page[i]);//释放存放参数和环境串的内存页面
return(retval);//返回出错码
}
函数do_execve返回后(return 0)会把原系统调用中断程序在堆栈上的代码指针eip替换成指向新执行程序的入口点。并将栈指针替换成新执行文件的栈指针esp。此后这次系统调用的返回指令最终会弹出这些栈中的数据,并使得CPU去执行新执行文件。这个过程如下面所示:
图中左半部分是进程逻辑64MB的空间包含原执行程序的情况。右半部分是释放原执行程序代码和数据并且更新堆栈和代码指针时的情况。其中彩色部分是包含代码和数据的信息。进程任务结构中start_code是cpu线性空间中的地址,其余的几个变量均是进程逻辑空间的地址。