您的位置:首页 > 运维架构 > Linux

Linux操作系统分析-lab2-进程的创建与可执行程序的加载

2013-05-27 23:21 661 查看
学号:sa****340 姓名:**钰

一、进程的创建过程分析

1、创建进程

Linux提供了几个函数fork,vfork和clone系统调用创建新进程,其中,clone创建轻量级进程,必须指定要共享的资源,exec系统调用执行一个新程序,exit系统调用终止进程(进程也可以因收到信号而终止)。



新的进程通过复制旧的进程(即父进程)而建立。在内核态建立的总体流图如下:



2、进程创建之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)


fork()一般用于创建普通进程,clone()可用于创建线程,kernel_thread()通过sys_clone创建新的内核进程。Fork和clone都通过do_fork()函数执行进程创建的操作。

do_fork()的第一步是调用copy_process函数来复制一个进程,并对相应的标志位等进行设置,成功调用copy_process之后,系统会让新开辟的进行开始运行,这时子进程一般都会马上调用exec()函数来执行其他任务,可以避免写时复制的开销(为什么是子进程执行其他任务了:如果首先执行父进程,在父进程执行过程中,可能会向地址空间写入数据,这个时候,系统就会为子进程拷贝父进程原来的数据,而当子进程调用的时候,其紧接着执行exec()函数,那么此时系统又会为子进程拷贝新的数据,这样多了一份拷贝)。参考资料[2]

do_fork()主要完成的任务:



3、进程创建之copy_process()

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
wake_up_new_task(p,clone_flags)

do_fork()函数利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。具体作用参见资料[2]。

其copy_process()的实现:

p = dup_task_struct(current):为新进程创建一个内核栈,内核栈的空间指向内核地址空间。

thread_info和task_struct,这里完全copy了父进程的内容,到这里为止,父进程和子进程没有任何区别。

copy_process()主要完成的任务:



4、进程创建的整个过程的代码分析参考资料[2]。
二、可执行程序的加载
1、ELF可执行文件加载过程
do_fork()成功调用copy_process之后,系统会让新开辟的进程开始运行,这时子进程一般都会马上调用exec()函数来执行ELF任务,ELF可执行文件加载过程的总体流图如下:



Linux提供了execl、execlp、execle、execv、execvp和execve等六个用以执行一个可执行文件的函数。以上函数的本质都是调用在arch/i386/kernel/process.c文件中实现的系统调用sys_execve来执行一个可执行文件。

该系统调用所需要的参数pt_regs在include/asm-i386/ptrace.h文件中定义:

struct pt_regs
{
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};

该参数描述了在执行该系统调用时,用户态下的CPU寄存器在核心态的栈中的保存情况。通过这个参数,sys_execve可以获得保存在用户空间的以下信息:可执行文件路径的指针(regs.ebx中)、命令行参数的指针(regs.ecx中)和环境变量的指针(regs.edx中)。

真正执行程序的功能则是在fs/exec.c文件中的do_execve函数中实现的:

file =open_exec(filename);这个函数打开要执行的文件,并检查其有效性。

retval =prepare_binprm(bprm);这个函数检查文件是否可以被执行,填充linux_binprm结构中的e_uid和e_gid项,使用可执行文件的前128个字节来填充linux_binprm结构中的buf项。

retval =copy_strings_kernel(1, &bprm->filename,
bprm);

retval =copy_strings(bprm->envc, envp, bprm);

retval =copy_strings(bprm->argc, argv, bprm);

这三个函数的作用将文件名、环境变量和命令行参数拷贝到新分配的页面中。

retval =search_binary_handler(bprm,regs);这个函数的作用查询能够处理该可执行文件格式的处理函数,并调用相应的load_library方法进行处理。

该函数用到了一个类型为linux_binprm的结构体来保存要执行的文件相关的信息,该结构体在include/linux/binfmts.h文件中定义:

struct linux_binprm{
char buf[BINPRM_BUF_SIZE]; //保存可执行文件的头128字节
struct page *page[MAX_ARG_PAGES];
struct mm_struct *mm;
unsigned long p;    //当前内存页最高地址
int sh_bang;
struct file * file;     //要执行的文件
inte_uid, e_gid;    //要执行的进程的有效用户ID和有效组ID
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
void *security;
int argc, envc;     //命令行参数和环境变量数目
char * filename;    //要执行的文件的名称
char * interp;        //要执行的文件的真实名称,通常和filename相同
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};


在该函数的最后,又调用了fs/exec.c文件中定义的search_binary_handler函数来查询能够处理相应可执行文件格式的处理器,并调用相应的load_library方法以启动进程。这里,用到了一个在include/linux/binfmts.h文件中定义的linux_binfmt结构体来保存处理相应格式的可执行文件的函数指针如下:

struct linux_binfmt {
struct linux_binfmt * next;
struct module *module;
// 加载一个新的进程
int(*load_binary)(struct linux_binprm *, struct pt_regs * regs);
// 动态加载共享库
int(*load_shlib)(struct file *);
// 将当前进程的上下文保存在一个名为core的文件中
int(*core_dump)(long signr, struct pt_regs * regs, struct file * file);
unsigned long min_coredump;
};


在调用特定的load_binary函数加载一定格式的可执行文件后,程序将返回到sys_execve函数中继续执行。该函数在完成最后几步的清理工作后,将会结束处理并返回到用户态中,最后,系统将会将CPU分配给新加载的程序。

2、小结

进程的创建和ELF可执行程序加载的过程到这里结束了。下面的附录主要是从代码的角度介绍fork()和exec()函数,这里我选取了execl()函数;以及ELF可执行文件格式的分析。

三、附录

1、fork()和execl()函数
Linux提供了六个exec()函数,这些函数的第一个参数都是要被执行的程序的路径,第二个参数则向程序传递了命令行参数,第三个参数则向程序传递环境变量。
execl()函数说明:

函数定义:int
execl(const char * path,const char * arg,....,(char*)0);

函数说明:execl()用来执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

返回值:如果执行成功则函数不会返回,执行失败则直接返回-1。

强调:函数定义中的最后一个参数必须为(char*)0

Linux系统下execl函数特点:

当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

execl.c

#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid = fork();
if(pid > 0)/*parent process*/
{
printf("In parent process\n");
printf("child_pid=%d\n",pid);
exit(EXIT_SUCCESS);
}
else if(pid == 0)/*child process*/
{
printf("In child process\n");
if(execl("./args","args","songzeyu","wahaha",(char *)0) < 0)
perror("error");
printf("the sentence is not coming!\n");/*nerver calls printf*/
}
else
{
puts("fork failure!");
exit(EXIT_FAILURE);
}
}
args.c

#include<stdlib.h>
#include<string.h>
#include<stdio.h>
int main(int argc , char *argv[])
{
if(argc != 3)
{
printf("argc = %d\n",argc);
printf("error!\n");
exit(0);
}
char a[10],b[10];
strcpy(a,argv[1]);
strcpy(b,argv[2]);
printf("a = %s b = %s\n",a,b);
return 0;
}
编译并运行结果截图:



执行说明:执行可执行程序args必须使argc=3,函数功能为打印除可执行文件名外的其它命令参数。



执行说明:函数创建了一个新的进程,新的进程运行后去执行可执行文件args。
2、args.c文件的汇编文件







仅分析args.c的汇编文件,从汇编文件的开头几行知道:该文件的ELF将右.string ,./text, ./globl, ./type,.size, .ident, .section节。(这里就不分析execl.c的汇编文件了)
3、task_struct进程控制块,ELF文件格式分析
stack_struct进程控制块图:



stack_struct进程控制块中的一些基本符号所代表的意思:



4、ELF可执行文件的格式
目标文件既要参与程序链接又要参与程序执行,目标文件有两种并行视图,(参考资料[1])如下图:



ELF文件的简要说明:ELF文件包括三部分,ELF header,Program header table,Section
header table.
ELF header:在文件的开始,保存了路线图,描述该文件的组织情况。
Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件具有程序头部表,可重定位文件没有这个表。
Section header table:包含了描述文件节区的信息,每个节区在表中都有一个项,给出节区的名称、节区大小这类心里。用于链接的目标文件(可重定向文件)必须包含节区头部表,而可执行文件可以没有。
结合execl.c 和args.c生成的execl.o ,execl,args.o,args文件来分析ELF文件格式,需要用到的hexdump,objdump,readelf等工具来查看ELF文件格式。
具体分析如下:
用readelf命令的 -h选项查看它们的ELF header,格式如下:



从上面红色框框里面的内容可以看出,program headers的长度为0,因为这是可重定向文件,没有program header头部,所以长度为0。
下面是execl可执行文件的ELF header,这个输出很重要,如下图:



section:在一个ELF文件中有一个section header table,通过它可以定位到所有的section,而在ELF
header中的e_shoff变量中保存section header table入口对文件头的偏移量。而每个section都会对应一个section header,所以只要在section header table中找到每个section header,就可以通过section header找到你想要的section。
以可执行文件execl为例,以保存字符串表的section为例来讲解读取某个section的过程。选择保存字符串表的section因为我们从ELF
header中就可以得知它在section header table的索引值为27。
用命令 readelf -S execl查看execl中所有的section header,如下图:



可以从中得到索引值为27的section header是.shstrtab。也就是要查看的字符串表section。这里用readelf命令查看.shstrtab这个section中的内容。
命令为:readelf -x 27 execl,结果如下图:



再用hexdump命令去查看.shstrtab这个section中的内容。在ELF
header中从e_shoff变量中得到section header table相对文件头的偏移量是4432字节。每个section header的大小是40字节,索引值是27,所以可以得到.shstrtab这个section header的偏移量:4432+40*27=5512。



对照上面的十六进制值和section header结构体Elf32_Shdr,我们需要得到sh_offset这个变量的值,即section的第一个字节与文件头之间的偏移。这个变量是section
header的17-20字节,所以我们得到52 10 00 00。那么这个section的首地址是0x1052=4178。我们还可以得到这个section的大小,在sh_offset后四个字节中,保存在变量sh_size中为fc 00 00 00:0xfc=252。所以我们可以得到:



结论:通过hexdump和readelf得到的.shatrtab和section结果相同。
program:可执行文件或者共享目标文件的程序头部都是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其它信息。这里的段是指segment,有些segment中保存着机器指令,有些保存着已初始化的变量,有些则作为进程镜像的一部分被操作系统读入内存。
我们从ELF中可以获得关于program的信息就是Program Header Table的偏移量e_phoff: 52 (bytes
into file),Program Header大小e_phentsize:32 (bytes),Program Header总数e_phnum:7。



四.参考资料
[1]ELF 文件格式学习.http://www.verydemo.com/demo_c92_i190978.html
[2]fork系统调用分析do_fork()http://edsionte.com/techblog/archives/2131
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: