学号:SA***355 姓名:**之
实验内容:编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体);分析fork和exec系统调用在内核中的执行过程;注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置;动态链接库在ELF文件格式中与进程地址空间中的表现形式
实验目的:通过实验理解进程的创建过程和可执行程序的加载过程
实验环境:Ubuntu 12.04 GCC 4.6.3
1 fork函数及exec函数族应用举例
fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程(子进程),在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和父进程做不同的事)。
1.1 fork函数
fork函数每成功调用一次创建一个新的进程,由fork创建的新进程称为子进程,原进程称为父进程。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本,并具有独立的地址空间。由于子进程在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。子进程返回0,父进程返回子进程ID,调用失败则返回-1(fork函数出错可能有两种原因:当前的进程数已经达到了系统规定的上限;系统内存不足)。下面看一段程序:
#include <stdio.h>
#include <unistd.h>
void main()
{
pid_t pid;
pid = fork();
if(pid<0)
{
printf("fork error\n");
}
else if(pid==0)
{
printf("child process is running.\n");
}
else
{
printf("parent process is running.\n");
}
}
根据上面的解说不难理解执行该程序将得到以下结果:
parent process is running.
child process is running.
1.2 exec函数簇
exec函数族提供了在一个进程中启动另一个进程的方法,它根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。调用exec函数后,原调用进程的内容除了进程号其他都被新的进程替换。
exec函数族原型:
int execl(const char *path,const char *arg, ...) |
int execlp(const char *file,const char *arg, ...) |
int execle(const char *path,const char *arg, ...,char * const envp[]) |
int execv(const char *path,char *const argv[]) |
int execvp(const char *file,char *const argv[]) |
int execve(const char *file,char *const argv[],char *const envp[]) |
其中,函数名后缀及其操作能力如下图:
下面看一段程序:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void main()
{
if(execl("/bin/ls","ls","-a",NULL)==-1)
{
perror("execl error");
exit(1);
}
}
它的运行效果和在终端中输入"ls -a"的效果是一样的。
2 fork和exec系统调用在内核中的执行过程
2.1 fork
fork()系统调用其实封装的是do_fork()函数,do_fork()函数实现如下:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Do some preliminary argument and permissions checking before we
* actually start allocating stuff
*/
if (clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) {
if (clone_flags & (CLONE_THREAD|CLONE_PARENT))
return -EINVAL;
}
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event(trace, nr);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}
由此可知,do_fork函数的实现过程主要是:复制父进程(copy_process)->唤醒子进程(wake_up_new_task)。
2.2 exec
对于exec函数来说,它主要执行的函数是do_execve_common(),其定义如下:
static int do_execve_common(const char *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
bool clear_in_exec;
int retval;
const struct cred *cred = current_cred();
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code. Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(&cred->user->processes) > rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
current->flags &= ~PF_NPROC_EXCEEDED;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
retval = check_unsafe_exec(bprm);
if (retval < 0)
goto out_free;
clear_in_exec = retval;
current->in_execve = 1;
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
retval = bprm_mm_init(bprm);
if (retval)
goto out_file;
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < 0)
goto out;
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < 0)
goto out;
retval = prepare_binprm(bprm);
if (retval < 0)
goto out;
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
goto out;
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
retval = search_binary_handler(bprm);
if (retval < 0)
goto out;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
acct_update_integrals(current);
free_bprm(bprm);
if (displaced)
put_files_struct(displaced);
return retval;
out:
if (bprm->mm) {
acct_arg_size(bprm, 0);
mmput(bprm->mm);
}
out_file:
if (bprm->file) {
allow_write_access(bprm->file);
fput(bprm->file);
}
out_unmark:
if (clear_in_exec)
current->fs->in_exec = 0;
current->in_execve = 0;
out_free:
free_bprm(bprm);
out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
return retval;
}
它的实现过程是:打开要执行的文件(open_exec)->初始化struct linux_binprm *bprm->
把elf文件读到内存中(search_binary_handler->load_binary)。
3 task_struct进程控制块,ELF文件格式与进程地址空间的联系
3.1 task_struct
在linux 中每一个进程都由task_struct 数据结构来定义, task_struct就是我们通常所说的PCB,它的定义如下图所示:
其中,state表示任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止),flags是进程号(在调用fork()时给出),counter表示运行时间片计数器(递减)等。各字段的具体含义见这里。
3.2 进程地址空间
进程地址空间如下图所示:
3.3 ELF文件格式
在终端中使用"readelf -S filename"可以查看ELF文件的格式信息,它通常包含.text、.rel.text、.data、 .bss、 .rodata、 .note.GNU-stack、.comment、 .shstrtab、.symtab、.strtab等字段。易知它的.data、.bss字段与进程地址空间的未初始化数据段及初始化数据段相互映射。
4 动态链接库在ELF文件格式中与进程地址空间中的表现形式
ELF文件要使用动态链接库时需要进行符号链接并加载动态链接库的代码段和数据段,只是它会映射到进程地址空间的共享库的存储映射区。