/*
* 该文件主要是实现do_execve函数,主要是实现对于二进制文件的加载执行
* 和shell脚本文件的加载执行
*/
/*
* linux/fs/exec.c
*
* (C) 1991 Linus Torvalds
*/
/*
* #!-checking implemented by tytso.
*/
/*
* Demand-loading implemented 01.12.91 - no need to read anything but
* the header into memory. The inode of the executable is put into
* "current->executable", and page faults do the actual loading. Clean.
*
* Once more I can proudly say that linux stood up to being changed: it
* was less than 2 hours work to get demand-loading completely implemented.
*/
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <a.out.h> // 定义了a.out 执行文件格式和一些宏
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <asm/segment.h>
extern int sys_exit(int exit_code);
extern int sys_close(int fd);
/*
* MAX_ARG_PAGES defines the number of pages allocated for arguments
* and envelope for the new program. 32 should suffice, this gives
* a maximum env+arg of 128kB !
*/
/*
* MAX_ARG_PAGES 定义了新程序分配给参数和环境变量使用的内存最大页数。
* 32 页内存应该足够了,这使得环境和参数(env+arg)空间的总合达到128kB!
*/
#define MAX_ARG_PAGES 32
/*
* 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.
*/
/*
* stack
* |--------| <- p
* | |
* |--------|
* | | <- sp (return)
* 参见图片<新程序堆栈中指针表示意图create_tables.JPG>
*/
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 指针指向该处
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);
// argc array
while (argc-->0) {
put_fs_long((unsigned long) p,argv++);
while (get_fs_byte(p++)) /* nothing */ ;
}
// NULL
put_fs_long(0,argv);
// envc array
while (envc-->0) {
put_fs_long((unsigned long) p,envp++);
while (get_fs_byte(p++)) /* nothing */ ;
}
// NULL
put_fs_long(0,envp);
return sp;
}
/*
* count() counts the number of arguments/envelopes
*/
/* 计算参数个数,即是数组argv中元素个数 */
static int count(char ** argv)
{
int i=0;
char ** tmp;
if (tmp = argv)
while (get_fs_long((unsigned long *) (tmp++)))
i++;
return i;
}
/*
* '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.
*/
/*
* from_kmem argv * argv **
* 0 用户空间 用户空间
* 1 内核空间 用户空间
* 2 内核空间 内核空间
*/
/* 复制指定个数的参数字符串到参数和环境空间 */
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
unsigned long p, int from_kmem)
{
// 参数:argc - 欲添加的参数个数;argv - 参数指针数组;page - 参数和环境空间页面指针数组。
// p -在参数表空间中的偏移指针,始终指向已复制串的头部;from_kmem - 字符串来源标志。
// 在do_execve()函数中,p 初始化为指向参数表(128kB)空间的最后一个长字处,参数字符串
// 是以堆栈操作方式逆向往其中复制存放的,因此p 指针会始终指向参数字符串的头部。
// 返回:参数和环境空间当前头部指针
char *tmp, *pag;
int len, offset = 0;
unsigned long old_fs, new_fs;
if (!p)
return 0; /* bullet-proofing */ /* 偏移指针验证 */
// 取ds 寄存器值到new_fs,并保存原fs 寄存器值到old_fs
new_fs = get_ds();
old_fs = get_fs();
// 如果字符串和字符串数组来自内核空间,则设置fs 段寄存器指向内核数据段(ds)
if (from_kmem==2)
set_fs(new_fs);
// 循环处理各个参数,从最后一个参数逆向开始复制,复制到指定偏移地址处
while (argc-- > 0)
{
// 如果字符串在用户空间而字符串数组在内核空间,则设置fs 段寄存器指向内核数据段(ds)
if (from_kmem == 1)
set_fs(new_fs);
// 从最后一个参数开始逆向操作,取fs 段中最后一参数指针到tmp,如果为空,则出错死机
// tmp指向的是一个字符串,char ** argv
if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))
panic("argc is wrong");
// 如果字符串在用户空间而字符串数组在内核空间,则恢复fs 段寄存器原值
if (from_kmem == 1)
set_fs(old_fs);
// 得到tmp指向的字符串的长度
len=0; /* remember zero-padding */
do {
len++;
} while (get_fs_byte(tmp++));
// 如果该字符串长度超过此时参数和环境空间中还剩余的空闲长度
if (p-len < 0) { /* this shouldn't happen - 128kB */
/* 不会发生-因为有128kB 的空间 */
set_fs(old_fs);
return 0;
}
// 复制fs 段中当前指定的参数字符串,是从该字符串尾逆向开始复制
while (len)
{
--p; --tmp; --len;
// 函数刚开始执行时,偏移变量offset 被初始化为0,因此若offset-1<0,说明是首次复制字符串,
// 则令其等于p 指针在页面内的偏移值,并申请空闲页面
if (--offset < 0)
{
offset = p % PAGE_SIZE;
// 如果字符串和字符串数组在内核空间,则恢复fs 段寄存器原值
if (from_kmem==2)
set_fs(old_fs);
// 如果当前偏移值p 所在的串空间页面指针数组项page[p/PAGE_SIZE]==0,表示相应页面还不存在,
// 则需申请新的内存空闲页面,将该页面指针填入指针数组,并且也使pag 指向该新页面,若申请不
// 到空闲页面则返回
if (!(pag = (char *) page[p/PAGE_SIZE]) &&
!(pag = (char *) page[p/PAGE_SIZE] =
(unsigned long *) get_free_page()))
return 0;
// 如果字符串和字符串数组来自内核空间,则设置fs 段寄存器指向内核数据段(ds)
if (from_kmem==2)
set_fs(new_fs);
}
// 从fs 段中复制参数字符串中一字节到pag+offset 处
*(pag + offset) = get_fs_byte(tmp);
}
}
// 如果字符串和字符串数组在内核空间,则恢复fs 段寄存器原值
if (from_kmem==2)
set_fs(old_fs);
// 最后,返回参数和环境空间中已复制参数信息的头部偏移值
return p;
}
/* 修改局部描述符表中的描述符基址和段限长,并将参数和环境空间页面放置在数据段末端 */
/* 参数text_size是执行文件头部a_text字段给出的代码段的长度,page参数和环境空间页面 */
/* 指针数组,函数返回的是数据段限长 */
static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{
unsigned long code_limit,data_limit,code_base,data_base;
int i;
// 根据执行文件头部a_text 值,计算以页面长度为边界的代码段限长
code_limit = text_size+PAGE_SIZE -1;
code_limit &= 0xFFFFF000;
// 设置数据段长度为64MB
data_limit = 0x4000000;
// 取当前进程中局部描述符表代码段描述符中代码段基址,代码段基址与数据段基址相同
code_base = get_base(current->ldt[1]);
data_base = code_base;
// 重新设置局部表中代码段和数据段描述符的基址和段限长
set_base(current->ldt[1],code_base);
set_limit(current->ldt[1],code_limit);
set_base(current->ldt[2],data_base);
set_limit(current->ldt[2],data_limit);
/* make sure fs points to the NEW data segment */
// fs 段寄存器中放入局部表数据段描述符的选择符(0x17)
__asm__("pushl $0x17\n\tpop %%fs"::);
// 将参数和环境空间已存放数据的页面(共可有MAX_ARG_PAGES 页,128kB)放到数据段线性地址的
// 末端。是调用函数put_page()进行操作的
data_base += data_limit;
for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--)
{
data_base -= PAGE_SIZE;
if (page[i]) // 如果该页面存在
put_page(page[i],data_base); // 就放置该页面
}
return data_limit; // 最后返回数据段限长(64MB)
}
/*
* 'do_execve()' executes a new program.
*/
/* execve()系统中断调用函数。加载并执行子进程(其它程序),其中参数eip */
/* 系统中断的程序代码指针eip 处,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]; // 32,足够
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;
// 内核不能调用本函数
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;
if (!(inode=namei(filename))) /* get executables inode */
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;
}
// 根据其属性(对应i 节点的uid 和gid),看本进程是否有权执行它
i = inode->i_mode;
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
if (current->euid == inode->i_uid)
i >>= 6;
else if (current->egid == inode->i_gid)
i >>= 3;
if (!(i & 1) &&
!((inode->i_mode & 0111) && suser())) { // 没有权限?
retval = -ENOEXEC;
goto exec_error2;
}
// 读取执行文件的第一块数据到高速缓冲区
if (!(bh = bread(inode->i_dev,inode->i_zone[0])))
{
retval = -EACCES;
goto exec_error2;
}
/* read exec-header */
ex = *((struct exec *) bh->b_data);
// 如果可执行文件的头部是'#!',并且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中
brelse(bh);
iput(inode); // 释放该执行文件i 节点
// 取第一行内容,并删除开始的空格、制表符
buf[1022] = '\0';
if (cp = strchr(buf, '\n')) {
*cp = '\0';
for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
}
// 若该行没有其它内容,则出错
if (!cp || *cp == '\0') {
retval = -ENOEXEC; /* No interpreter name found */
goto exec_error1;
}
interp = i_name = cp;
i_arg = 0;
// 首先取第一个字符串,其应该是脚本解释程序名,iname 指向该名称
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
if (*cp == '/')
i_name = cp+1;
}
// 若文件名后还有字符,则应该是参数串,令i_arg 指向该串
if (*cp) {
*cp++ = '\0';
i_arg = cp;
}
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
*/
// // 若sh_bang 标志没有设置,则设置它
if (sh_bang++ == 0)
{
// 复制指定个数的环境变量串和参数串到参数和环境空间中(page中)
p = copy_strings(envc, envp, page, p, 0);
p = copy_strings(--argc, argv+1, page, p, 0);
}
/*
* Splice in (1) the interpreter's name for 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);
argc++;
}
// 复制解释程序文件名到参数和环境空间中
p = copy_strings(1, &i_name, page, p, 2);
argc++;
if (!p) {
retval = -ENOMEM;
goto exec_error1;
}
/*
* OK, now restart the process with the interpreter's inode.
*/
old_fs = get_fs(); // 保留原fs 段寄存器
set_fs(get_ds()); // 现置其指向内核数据段
if (!(inode=namei(interp))) { /* get executables inode */
set_fs(old_fs);
retval = -ENOENT;
goto exec_error1;
}
set_fs(old_fs);
// 跳转到restart_interp 处重新处理
goto restart_interp;
}
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;
}
// 如果执行文件执行头部分长度不等于一个内存块大小
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 */
if (current->executable) // 释放当前进程所占有的i节点
iput(current->executable);
current->executable = inode; // 设置成指向inode(需要执行程序的i节点)
// 清复位所有信号处理句柄
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));
// 重新设置全局变量(是否使用协处理器)last_task_used_math
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);
// 修改当前进程各字段为新执行程序的信息
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;
// 初始化一页bss 段数据,全为零
i = ex.a_text+ex.a_data;
while (i&0xfff)
put_fs_byte(0,(char *) (i++));
// 将原调用系统中断的程序在堆栈上的代码指针替换为指向新执行程序的入口点
eip[0] = ex.a_entry; /* eip, magic happens :-) */
// 并将堆栈指针替换为新执行程序的堆栈指针
eip[3] = p; /* stack pointer */
// 返回指令将弹出这些堆栈数据并使得CPU 去执行新的执行程序
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,该函数首先是检查当前进程是否
* 能够执行该可执行文件,然后读取该执行文件,复制进程的参数和进程
* 的环境参数,改变当前进程的相关信息,使得CPU开始执行被加载的程序
* 如果失败,函数返回的是出错号。
*/
参考《linux内核完全注释》和网上相关资料