
Linux内核-exec系统调用
这里我们以0.1内核为例,查看execve的系统调用实现
copy_strings
该函数的作用是拷贝execve传递进入的agrv参数表到内核空间中
几个段寄存器
在正式开始分析代码之前,我们需要先有几个概念:
- cs:代码段
- ds:数据段
- es:额外段
- ss:栈
- fs:用户数据段
而ds和fs有两种情况:
| ds | fs | |
|---|---|---|
| 用户态工作 | 用户程序数据段 | 用户程序数据段 |
| 内核态工作 | 内核程序数据段 | 用户程序数据段 |
函数声明
/*
* '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.
*/
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
unsigned long p, int from_kmem)
其中argc,argv都是传递进入的参数,而page是在当前进程空间找到的一片内存页,一共32页对应128KB;p指向page的最上方

这里还有一个需要注意的就是from_kmem参数,其实从注释也能大致知道是什么:
* from_kmem argv * argv **
* 0 user space user space
* 1 kernel space user space
* 2 kernel space kernel space
根据值的不同argv *和agrv **指向内核或者用户空间
获取ds,fs指针
char *tmp, *pag;
int len, offset = 0;
unsigned long old_fs, new_fs;
if (!p)
return 0; /* bullet-proofing */
new_fs = get_ds();
old_fs = get_fs();
首先是获取得到ds和fs指针
if (from_kmem==2)
//设置fs为ds
set_fs(new_fs);
这里如果from_kmem为2就说明argv *和agrv **指向内核空间,直接让fs的值指向ds
循环拷贝参数表
while (argc-- > 0) {
if (from_kmem == 1)
//设置fs为ds
set_fs(new_fs);
//tmp指向最后一个参数结尾地址
if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))
panic("argc is wrong");
if (from_kmem == 1)
//设置fs为fs
set_fs(old_fs);
len=0; /* remember zero-padding */
do {
len++;
} while (get_fs_byte(tmp++)); //计算每一条参数的长度并且存在len里面
if (p-len < 0) { /* this shouldn't happen - 128kB */
set_fs(old_fs);
return 0;
}
while (len) {
--p; --tmp; --len;
if (--offset < 0) {
offset = p % PAGE_SIZE;
if (from_kmem==2)
set_fs(old_fs);
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);
}
*(pag + offset) = get_fs_byte(tmp);
}
}
1.首先让tmp指向最后一个参数结尾地址
//tmp指向最后一个参数结尾地址
if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))
2.然后计算每一条参数的长度并且存在len里面
len=0; /* remember zero-padding */
do {
len++;
} while (get_fs_byte(tmp++)); //计算每一条参数的长度并且存在len里面
3.通过一个while循环将该参数每一个字节拷贝到page数组中
while (len) {
--p; --tmp; --len;
if (--offset < 0) {
offset = p % PAGE_SIZE;
if (from_kmem==2)
set_fs(old_fs);
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);
}
*(pag + offset) = get_fs_byte(tmp);
}
注意这里拷贝的顺序,参数表最后一个拷贝到page数组第一个,以此类推,可以建上图
返回内存中的page表
return p;
需要注意此时p指针已经指向了page最底部,也就是argv[0]所在的底部
原因是上面的--p;
do_execve
do_execve()函数时系统中断调用(int 0x80)调用的C处理函数,是exec()函数族的内核实现函数 。当一个程序使用fork函数创建了一个子进程后,通常会在该子进程中调用exec()簇函数之一以加载执行另一个新程序,此时子进程的代码,数据段,将完全被新程序替换掉,并在子进程中开始执行新程序
/*
* 'do_execve()' executes a new program.
*
* 1 检查文件类型和权限等
* 2 读取文件的第一块数据到缓冲区
* 3 脚本文件与可执行文件的判断
* 4 校验可执行文件是否能执行
* 5 进程管理结构的调整
* 6 释放进程占有的页面
* 7 调整线性地址空间、参数列表、堆栈地址等
* 8 设置 eip 和 esp,完成摇身一变
*
*/
int do_execve(unsigned long * eip,long tmp,char * filename,
char ** argv, char ** envp)
{
struct m_inode * inode;
struct buffer_head * bh;
struct exec ex;
unsigned long page[MAX_ARG_PAGES];
int i,argc,envc;
int e_uid, e_gid;
int retval;
int sh_bang = 0;
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;
查看调用程序传入的eip[1]值
//查看权限
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;
参数eip[1]是调用本次系统调用的原用户程序代码段寄存器CS值,其中的段选择符必须是当前任务的代码段选择符(0x000f)。若不是那么CS只能是内核代码段的选择符(0x0008),这是不允许的,因为内核代码是常住内存不能被替换掉。
这里panic打印的意思就是是内核态调用的这个系统调用
获取得到文件inode
//打开指定文件的inode
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 = inode->i_mode;
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
//文件所有者与当前进程uid相同
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;
}
在这里有一个restart_interp标签,用于goto的,后续会用到
获取文件头内容
//读取当前执行程序或shell脚本的文件头结构
if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { // 根据 inode 读取文件第一块数据(1024KB)
retval = -EACCES;
goto exec_error2;
}
ex = *((struct exec *) bh->b_data); /* read exec-header */
这里获取得到inode的i_zone[0]也就对应文件头内容,然后使用一个struct exec指针指向它,其结构定义如下(这时的是.out格式):
// 执行文件结构。
// =============================
// unsigned long a_magic // 执行文件魔数。使用 N_MAGIC 等宏访问。
// unsigned a_text // 代码长度,字节数。
// unsigned a_data // 数据长度,字节数。
// unsigned a_bss // 文件中的未初始化数据区长度,字节数。
// unsigned a_syms // 文件中的符号表长度,字节数。
// unsigned a_entry // 执行开始地址。
// unsigned a_trsize // 代码重定位信息长度,字节数。 文件被其他文件连接或者引用时需要修改的值
// unsigned a_drsize // 数据重定位信息长度,字节数。
// -----------------------------
struct exec {
unsigned long a_magic; /* Use macros N_MAGIC, etc for access */
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 */
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 */
};
判断是否是shell脚本
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
一个shell脚本的第一行是以#!开头的,这里先判断是否是一个shell脚本
获取解析文件名和执行文件名
char buf[1023], *cp, *interp, *i_name, *i_arg;
unsigned long old_fs;
strncpy(buf, bh->b_data+2, 1022);
brelse(bh);
iput(inode);
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;
}
//解析出shell脚本的解析程序名 ./run
interp = i_name = cp;
i_arg = 0;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
if (*cp == '/')
//runw文件名
i_name = cp+1;
}
一般一个shell脚本里面都会有真正要去执行的文件名,这里就是去找到执行程序文件名i_name
参数和环境变量放入page
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
*/
if (sh_bang++ == 0) {
p = copy_strings(envc, envp, page, p, 0);
p = copy_strings(--argc, argv+1, page, p, 0);
}
现在最上层放入环境变量,然后紧接着放入命令参数,这里--argc是因为传入的agrv中最后一个是NULL ,要减去
shell文件名和执行程序文件名放入page
/*
* 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;
}
这里可以看到copy_strings最后一位from_kmem参数,在放入filename是1,而放入i_name是2,因为一个是用户空间的一个是内和空间
获取可执行文件的inode
/*
* OK, now restart the process with the interpreter's inode.
*/
old_fs = get_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);
goto restart_interp;
最后我们就取得执行文件的inode,并且跳转到restart_interp执行可执行程序
这里需要注意的是获取inode我们调用了namei函数,但是参数是从用户空间得到的,也就是fs段获得的,因此在调用函数前需要先临时让fs指向内核数据空间,以让内核空间得到执行文件名,并在调用结束之后恢复fs
文件信息判定
此时我们已经来到了执行用户程序阶段,首先会去根据文件头信息进行一些文件判定
brelse(bh);
//根据文件头信息对文件进行一些判定
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;
}
//文件头不为1K则出错
if (N_TXTOFF(ex) != BLOCK_SIZE) {
printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
retval = -ENOEXEC;
goto exec_error2;
}
//拷贝执行文件的环境变量和参数
if (!sh_bang) {
p = copy_strings(envc,envp,page,p,0);
p = copy_strings(argc,argv,page,p,0);
if (!p) {
retval = -ENOMEM;
goto exec_error2;
}
}
这里的if (!sh_bang)是根据前面shell脚本分支中的sh_bang进行判断,如果前面执行了shell分支,这里就不用拷贝了,前面拷贝过了
执行新的进程前的切换和更新工作
//执行线程,进程时要做的切换和更新
/* OK, This is the point of no return */
//更新当前进程的执行程序的inode节点
if (current->executable)
iput(current->executable);
current->executable = inode;
//清空当前进程的信号处理函数
for (i=0 ; i<32 ; i++)
current->sigaction[i].sa_handler = NULL;
//关闭原进程指定执行新进城后需要关闭的文件并清空其位图
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;
//设置整个程序的内存映射,并且更新进程的各段信息
p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
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;
current->euid = e_uid;
current->egid = e_gid;
i = ex.a_text+ex.a_data;
//清空bss段
while (i&0xfff)
put_fs_byte(0,(char *) (i++));
eip[0] = ex.a_entry; /* eip, magic happens :-) */
eip[3] = p; /* stack pointer */
return 0;
exec_error2:
iput(inode);
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++)
free_page(page[i]);
return(retval);
释放原来进程内存页表
//释放原进程的代码段和数据段
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;
根据当前进程(还未执行新进程)指定的基地址和限长,释放原来的程序的代码段和数据段所对应的内存页表
放入新程序的代码段和数据段
//设置整个程序的内存映射,并且更新进程的各段信息
p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
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;
current->euid = e_uid;
current->egid = e_gid;
i = ex.a_text+ex.a_data;
这里首先根据新执行文件头结构中的代码长度字段a_text的值修改局部描述表中描述符基地址和段限长,并以128KB的参数和环境空间页面放置在数据段末尾
然后调用create_tables在栈空间创建环境和参数变量指针表 ,供程序main函数使用,并返回栈指针p
接着在修改进程task_struct的各个字段值为新执行文件的信息:
- 代码尾字段
end_code等于执行文件的代码段长度a_text - 数据尾字段
end_data等于执行文件的代码段长度加数据段长度a_data - 进程堆结尾字段
brk = a_text + a_data + a_bss
然后设置进程的有效用户id和组id
最终内存布局
由于数据段限长为 64MB,进程的线性地址空间被划分为以下区域(从低地址到高地址):
| 地址范围 | 区域 | 状态 |
|---|---|---|
| 0x00000000 起始 | 代码段(Text) 长度 = ex.a_text(对齐至页边界) | 页表项为空。未分配物理页,也未读入磁盘数据。执行时触发缺页中断,由内核根据 inode 从磁盘按需读入。 |
| 代码段结束 | 数据段(Data) 长度 = ex.a_data | 页表项为空。按需加载。 |
| 数据段结束 | BSS 段 长度 = ex.a_bss | 页表项为空(但请注意,do_execve 末尾有一段 while (i&0xfff) put_fs_byte(0, ...) 循环,它会在 BSS 段首部对应的线性地址上执行写零操作。由于页表为空,这会立即触发缺页中断,并在缺页处理中分配一个清零的物理页,因此 BSS 段的第一页(或未对齐部分)在返回用户态前已被实际分配并清零,其余部分仍是按需分配零页)。 |
| BSS 结束 ~ 64MB – 128KB | 空闲/未映射区域 | 未分配页表项。 |
| 64MB – 128KB ~ 64MB (即 0x03FE0000 ~ 0x04000000) | 参数(argv)和环境变量(envp)页面 大小固定为 MAX_ARG_PAGES * 4KB = 128KB | 已映射到物理内存。change_ldt 中的 for 循环从数据段末端(data_base + 64MB)反向递减,调用 put_page 将内核中存放参数字符串的物理页面挂载到这些线性地址上。这是当前唯一实际存在于物理内存中的程序数据。 |
