父进程 fork的子进程的目的自然不是创建一个几乎与自己一模一样的进程。而是通过子进程调用 exec 函数簇去执行另外一个程序。exec() 系统调用必须定位该执行文件的二进制映像,加载并执行它。
exec() 的Linux实现支持不同的二进制格式,这是通过 linux_binfmt 结构来达到的,其中内嵌了两个指向函数的指针,一个用于加载可执行文件,另一个用于加载库函数,每种二进制格式都实现有这两个函数。
/* This structure defines the functions that are used to load the binary formats that
* linux accepts. */
struct linux_binfmt{
int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
int (*load_shlib)(int fd);
};
Linux的内核是单独实现 sys_execve() 调用的,它执行一个非常简单的任务:加载可执行文件的头部,并试着去执行它。如果头两个字节是 “#!”,那么就会解析该可执行文件的第一行并调用一个解释器来执行它,否则的话,就会顺序地调用各个注册过的二进制格式。
Linux 本身的格式是由 fs/exec.c 直接支持的,并且相关的函数是 load_aout_binary 和 load_aout_library。对于二进制,函数将加载一个“a.out” 可执行文件(Linux 环境下编程 gcc 生成的可执行文件默认就是 a.out)并以使用 mmap() 加载磁盘文件或调用 read_exec() 而结束。
Linux1.0(如无特殊说明,本系列均指Linux1.0 内核环境下)。其执行顺序为: execve() -> sys_execve() -> do_execve()
/*
* sys_execve() executes a new program.
*/
//系统调用的时候,把参数依次放在:ebx,ecx,edx,esi,edi,edp寄存器中
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
//在系统空间建立一个路径名的副本
error = getname((char *) regs.ebx, &filename);
if (error)
return error;
//执行一个新的程序
error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s);
//释放路径名副本
putname(filename);
return error;
}
/*
* sys_execve() executes a new program.
*/
static int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
{
//二进制程序结构声明
struct linux_binprm bprm;//用于存放加载二进制文件时用的参数
struct linux_binfmt * fmt;//定义了Linux接受的被用于加载二进制格式的函数,为函数结构体指针
unsigned long old_fs;
int i;
int retval;
int sh_bang = 0;
//如果regs所指的堆栈内容中的cs不是用户态的值,返回
if (regs->cs != USER_CS)
return -EINVAL;
//参数和环境字符串空间中的偏移指针,初始化为最后一个字节处
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-4;
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */
bprm.page[i] = 0;//清页面
//打开文件程序,bprm.inode 为对应文件路径名的i 节点指针
retval = open_namei(filename, 0, 0, &bprm.inode, NULL);
if (retval)
return retval;
bprm.filename = filename;//文件名
bprm.argc = count(argv);//参数个数
bprm.envc = count(envp);//环境变量个数
restart_interp:
//判断打开文件的有效性,必须是常规文件,可执行程序,权限允许等,
if (!S_ISREG(bprm.inode->i_mode)) { /* must be regular file */
retval = -EACCES;
goto exec_error2;
}
if (IS_NOEXEC(bprm.inode)) { /* FS mustn't be mounted noexec */
retval = -EPERM;
goto exec_error2;
}
if (!bprm.inode->i_sb) {
retval = -EACCES;
goto exec_error2;
}
i = bprm.inode->i_mode;
if (IS_NOSUID(bprm.inode) && (((i & S_ISUID) && bprm.inode->i_uid != current->
euid) || ((i & S_ISGID) && !in_group_p(bprm.inode->i_gid))) &&
!suser()) {
retval = -EPERM;
goto exec_error2;
}
/* make sure we don't let suid, sgid files be ptraced. */
//如果当前进程被置PF_PTRACED位,则设置结构中的u_uid和e_gid为当前进程中的值
if (current->flags & PF_PTRACED) {
bprm.e_uid = current->euid;
bprm.e_gid = current->egid;
} else {//否则取前面获得的文件类型和属性值
bprm.e_uid = (i & S_ISUID) ? bprm.inode->i_uid : current->euid;
bprm.e_gid = (i & S_ISGID) ? bprm.inode->i_gid : current->egid;
}
//检查将要执行文件的文件权限
if (current->euid == bprm.inode->i_uid)
i >>= 6;
else if (in_group_p(bprm.inode->i_gid))
i >>= 3;
//判断文件是否具有执行权限
if (!(i & 1) &&
!((bprm.inode->i_mode & 0111) && suser())) {
retval = -EACCES;
goto exec_error2;
}
//清空结构中的缓冲区
memset(bprm.buf,0,sizeof(bprm.buf));
//取得fs寄存器值
old_fs = get_fs();
//重置fs寄存器的内容为内核数据段
set_fs(get_ds());
//读取可执行文件,文件路径名,参数等信息放入buf中
retval = read_exec(bprm.inode,0,bprm.buf,128);
//设置fs为老的保留的值,恢复现场
set_fs(old_fs);
if (retval < 0)
goto exec_error2;//读取出错处理
//如果执行文件名开始的两个字节为#!,并且sh_bang为0,则处理脚本文件的执行
if ((bprm.buf[0] == '#') && (bprm.buf[1] == '!') && (!sh_bang)) {
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
char *cp, *interp, *i_name, *i_arg;
//释放该执行文件的i 节点
iput(bprm.inode);
bprm.buf[127] = '\0';//最后位设置0,换句话说读取前128字节,判断文件格式
//查找bprm.buf中是否含有换行符,如果没有则让cp指向最后一个字符处
if ((cp = strchr(bprm.buf, '\n')) == NULL)
cp = bprm.buf+127;
*cp = '\0';//如果有则设置为0,找到第一个换行符替换为0
//删除改行的空格以及制表符,替换为0
while (cp > bprm.buf) {
cp--;
if ((*cp == ' ') || (*cp == '\t'))
*cp = '\0';
else
break;
}
//检查文件名,前面两个为#!
for (cp = bprm.buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
//如果文件名为空,则转到exec_error1处
if (!cp || *cp == '\0') {
retval = -ENOEXEC; /* No interpreter name found */
goto exec_error1;
}
//让i_name指向程序名,如果有/指向最后一个,(/表示目录,如果有目录程序名则在最后)
//第一个字符串表示的脚本解释程序名
interp = i_name = cp;
i_arg = 0;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
if (*cp == '/')
i_name = cp+1;
}
//如果文件名后面还有字符的话,则应该是参数,让i_reg指向它
while ((*cp == ' ') || (*cp == '\t'))
*cp++ = '\0';
if (*cp)
i_arg = cp;
/*
* OK, 我们已经解析出程序的文件名以及(可选)参数
*/
//若sh_bang标志没有设置,则设置它,并复制指定个数的环境变量串和参数串到环境和参数空间中
//sh是程序为脚本程序的标志位
if (sh_bang++ == 0) {
bprm.p = copy_strings(bprm.envc, envp, bprm.page, bprm.p, 0);
bprm.p = copy_strings(--bprm.argc, argv+1, bprm.page, bprm.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.
*/
//复制脚本程序文件名到参数和环境空间中
bprm.p = copy_strings(1, &bprm.filename, bprm.page, bprm.p, 2);
bprm.argc++;
//复制解释程序的参数到参数和环境空间中
if (i_arg) {
bprm.p = copy_strings(1, &i_arg, bprm.page, bprm.p, 2);
bprm.argc++;
}
//复制解释程序文件名到参数和环境空间中
bprm.p = copy_strings(1, &i_name, bprm.page, bprm.p, 2);
bprm.argc++;
if (!bprm.p) {
retval = -E2BIG;
goto exec_error1;
}
/*
* OK, now restart the process with the interpreter's inode.
* Note that we use open_namei() as the name is now in kernel
* space, and we don't need to copy it.
*/
//打开文件
retval = open_namei(interp, 0, 0, &bprm.inode, NULL);
if (retval)
goto exec_error1;
goto restart_interp;
}
//如果sh_bang标志没有被设置,则复制指定个数的环境变量字符串到参数
if (!sh_bang) {
bprm.p = copy_strings(bprm.envc,envp,bprm.page,bprm.p,0);
bprm.p = copy_strings(bprm.argc,argv,bprm.page,bprm.p,0);
if (!bprm.p) {
retval = -E2BIG;
goto exec_error2;
}
}
//如果sh标志已经被设置了,则表明改程序是脚本程序,这个时候的环境变量页面已经被复制了
bprm.sh_bang = sh_bang;
fmt = formats;//让fmt执行对应的格式,即下面说明的三种格式之一
//让fn指向对应的加载二进制函数
//Linux1.0只支持3中二进制文件格式:a.out,elf,coff
do {
//函数指针赋值
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
break;
//加载程序,对应信息均位于bprm和regs中了
retval = fn(&bprm, regs);
//如果加载成功了,释放该i节点后返回
if (retval == 0) {
iput(bprm.inode);
current->did_exec = 1;
return 0;
}
fmt++;//遍历下一个格式,下一个就是加载共享库,一共只有两次加载机会
} while (retval == -ENOEXEC);
//出错处理
exec_error2:
iput(bprm.inode);
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++)
free_page(bprm.page[i]);
return(retval);
}
现在来讲讲上面 do_execve() 函数的执行情况:
1、do_execve() 开始执行后,马上处理参数和环境变量,目的就是要将参数和环境变量加载到内存的页面中,为了对参数和环境变量可能占用的内存页面进行管理。do_execve() 函数先对page管理结构清0(18-19行)。
2、系统想要对文件进行全方位检测,就先要对文件的 i 节点中提供的文件属性信息进行分析,文件自身的所有属性都记载在 i 节点中,其具体步骤是:调用 open_namei() 函数,从虚拟盘上读取文件的 i 节点,该函数是通过文件路径(如“/bin/sh”),最终找到该文件的 i 节点,并将其登记到 i 节点管理表中。(21行)
3、接下来对参数的个数和环境变量的个数进行统计(25-26行),这是因为,参数和环境变量是系统给用户预留的自由设置信息。默认设置是 init/main.c 中的(Linux1.0)
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", term, NULL };
4、接下来开始对文件的检测工作,对 i 节点进行分析。通过对 i 节点中“文件属性”的分析,可以得知这个文件是不是一个“常规文件”。只有常规文件(除块设备文件、字符设备文件、目录文件、管道文件等特殊文件外的文件)才有被载入的可能。以及根据 i 节点提供的:inode->i_mode、inode->i_uid、inode->i_gid,检测当前文件是否有执行文件的权限(30-68行)
5、对文件头进行检测,先将 inode对应信息读入缓冲区中(76行),然后对文件头进行分析,如果是脚本文件(82行),进入if 里面执行,解析该可执行文件的第一行(截断了换行符后面的内容)并调用一个解释器来执行它。(88-125行),否则直接执行后面的代码(168行开始)
6、将环境变量和参数拷贝到内存指定的页面中
7、加载二进制格式文件(181-195行)
上面do_execve() 函数中 fmt = formats(178行),这里指明加载方式(Linux1.0 支持三种格式:a.out、elf、coff)
/* Here are the actual binaries that will be accepted */
struct linux_binfmt formats[] = {
{load_aout_binary, load_aout_library},
#ifdef CONFIG_BINFMT_ELF
{load_elf_binary, load_elf_library},
#endif
#ifdef CONFIG_BINFMT_COFF
{load_coff_binary, load_coff_library},
#endif
{NULL, NULL}
};
经过183和187行,这样执行加载对应二进制格式的函数(exec.c中只有load_aout_binary 和 load_aout_library)
如果是之前子进程没有和父进程拉开区别,那么从加载二进制文件开始,子进程就真正开始它的旅途。首先得和父进程“撇清”关系,放弃从父进程继承而来的全部空间,不管是通过复制还是通过共享,统统放弃,建立自己独立的空间
/*
* These are the functions used to load a.out style executables and shared
* libraries. There is no binary dependent code anywhere else.
*/
/*该函数用于加载a.out类型的二进制文件格式和共享库。没有任何的二进制依赖代码*/
int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct exec ex;
struct file * file;
int fd, error;
unsigned long p = bprm->p;
//取得执行文件的头
ex = *((struct exec *) bprm->buf); /* exec-header */
/*继续对文件头部各种信息进行判断*/
//不是需要分页可执行文件(ZMAGIC)
if ((N_MAGIC(ex) != ZMAGIC && N_MAGIC(ex) != OMAGIC &&
N_MAGIC(ex) != QMAGIC) ||
ex.a_trsize || ex.a_drsize ||//代码、数据重定位部分长度不等于0
//可执行文件长度小于代码段+数据段+符号表长度+执行头部分长度的总和
bprm->inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
return -ENOEXEC;
}
//执行头部分文件长度限制
if (N_MAGIC(ex) == ZMAGIC &&
(N_TXTOFF(ex) < bprm->inode->i_sb->s_blocksize)) {
printk("N_TXTOFF < BLOCK_SIZE. Please convert binary.");
return -ENOEXEC;
}
//文件头部分长度不等于一个缓冲块大小,不能执行
if (N_TXTOFF(ex) != BLOCK_SIZE && N_MAGIC(ex) == ZMAGIC) {
printk("N_TXTOFF != BLOCK_SIZE. See a.out.h.");
return -ENOEXEC;
}
/* 释放从父进程继承来的用户空间,与父进程"决裂"*/
flush_old_exec(bprm);//冲刷所有当前执行程序的痕迹,以便新程序开始执行
//各种初始化
current->end_code = N_TXTADDR(ex) + ex.a_text;//设置当前进程代码段的大小
current->end_data = ex.a_data + current->end_code;//设置当前进程数据段的大小
current->start_brk = current->brk = current->end_data;//设置当前进程brk段的大小
current->start_code += N_TXTADDR(ex);//设置start_code
//各种值设置
current->rss = 0;
current->suid = current->euid = bprm->e_uid;
current->mmap = NULL;
current->executable = NULL; /* for OMAGIC files */
current->sgid = current->egid = bprm->e_gid;
//根据a.out具体的不同格式,以不同的方式装入代码段和数据段
//如果代码和数据段是紧跟在头部后面
if (N_MAGIC(ex) == OMAGIC) {
do_mmap(NULL, 0, ex.a_text+ex.a_data,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_FIXED|MAP_PRIVATE, 0);
read_exec(bprm->inode, 32, (char *) 0, ex.a_text+ex.a_data);
} else {//如果不是
if (ex.a_text & 0xfff || ex.a_data & 0xfff)//没有按页对齐
printk("%s: executable not page aligned\n", current->comm);
fd = open_inode(bprm->inode, O_RDONLY);//以只读方式打开节点
if (fd < 0)
return fd;
file = current->filp[fd];//让file指向文件描述符指针
//如果文件操作函数为空或者 mmap 为空,则关闭上面打开的文件描述符
if (!file->f_op || !file->f_op->mmap) {
sys_close(fd);
do_mmap(NULL, 0, ex.a_text+ex.a_data,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_FIXED|MAP_PRIVATE, 0);
read_exec(bprm->inode, N_TXTOFF(ex),
(char *) N_TXTADDR(ex), ex.a_text+ex.a_data);
goto beyond_if;
}
//重新映射该文件
error = do_mmap(file, N_TXTADDR(ex), ex.a_text,
PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_SHARED, N_TXTOFF(ex));
//如果返回值不等于文件代码段加载到内存后的地址,则关闭描述符,并发送SIGSEGV信号
if (error != N_TXTADDR(ex)) {
sys_close(fd);
send_sig(SIGSEGV, current, 0);
return 0;
};
//出错则重新映射
error = do_mmap(file, N_TXTADDR(ex) + ex.a_text, ex.a_data,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, N_TXTOFF(ex) + ex.a_text);
sys_close(fd);
//同样如果,映射地址出错
if (error != N_TXTADDR(ex) + ex.a_text) {
send_sig(SIGSEGV, current, 0);
return 0;
};
//executable为进程管理结构中的成员,表示该进程所对应的可执行文件的i节点指针
current->executable = bprm->inode;//指向inode,建立关系
bprm->inode->i_count++;
}
beyond_if:
sys_brk(current->brk+ex.a_bss);
//修改idt表中描述符基地址和段限长
p += change_ldt(ex.a_text,bprm->page);
p -= MAX_ARG_PAGES*PAGE_SIZE;
//在新的用户堆栈中创建环境和参数变量指针表,并返回该堆栈指针
p = (unsigned long) create_tables((char *)p,bprm->argc,bprm->envc,0);
current->start_stack = p;
//设置eip为可执行文件的入口点
regs->eip = ex.a_entry; /* eip, magic happens :-) */
regs->esp = p; /* stack pointer */
if (current->flags & PF_PTRACED)
send_sig(SIGTRAP, current, 0);
return 0;
}
ok,最后大致的总结一下:
1、sys_execve(),系统调用,让进程执行某个程序。首先在系统空间中建立一个路径名的副本,然后带着这个路径名信息去执行do_execve()
上面建立副本是通过 getname() 函数来实现的,该函数首先分配一个物理页面作为缓冲区,然后在系统空间中定义一个缓冲指针指向他,将路径名从用户空间复制到缓冲区,再用缓冲区指针指向这个物理页面。其实就是将用户空间的数据复制到系统空间的过程。
2、do_execve(),首先打开可执行文件(open_namei),然后初始化bprm这个用来保存可执行文件上下文的数据结构,从可执行文件中读取头128个字节到bprm缓冲区,这128个字节包含了关于可执行文件属性的必要信息,然后解析,将其中的文件路径名从系统空间复制到bprm,再将参数,环境变量从用户空间复制到bprm
3、load_aout_binary(),加载二进制格式的可执行文件。这里直接是加载a.out格式的可执行文件。首先进行各种检查,格式对不对,大小对不对,可否执行等等。然后冲刷掉继承而来的父进程的用户空间,开始与父进程真正区别,设置代码段,数据段,bss段等等初始化操作。然后根据a.out具体的不同格式,以不同的方式装入代码段和数据段。映射地址。然后与可执行文件建立关系。
在用户空间的堆栈区顶部建立一个虚拟内存空间,将参数与环境变量所占的物理页面与这些虚拟空间建立映射。
4、调整eip值,使之指向程序的第一条指令处,即程序的入口处。寄存器ip存放的就是下一条将要执行的指令地址。所以下一步系统将依据eip提供的指令地址继续执行,也就是新程序。
ok,fork子进程后,子进程加载二进制格式文件执行新程序的大致流程就是这样了。当然exec.c文件中还有一部分函数,请参考源文件。
参考资料《LInux 内核设计的艺术》