Linux操作系统分析-lab2-进程的创建与可执行程序的加载
2013-05-27 23:21
661 查看
学号:sa****340 姓名:**钰
一、进程的创建过程分析
1、创建进程
Linux提供了几个函数fork,vfork和clone系统调用创建新进程,其中,clone创建轻量级进程,必须指定要共享的资源,exec系统调用执行一个新程序,exit系统调用终止进程(进程也可以因收到信号而终止)。
新的进程通过复制旧的进程(即父进程)而建立。在内核态建立的总体流图如下:
2、进程创建之do_fork()
fork()一般用于创建普通进程,clone()可用于创建线程,kernel_thread()通过sys_clone创建新的内核进程。Fork和clone都通过do_fork()函数执行进程创建的操作。
do_fork()的第一步是调用copy_process函数来复制一个进程,并对相应的标志位等进行设置,成功调用copy_process之后,系统会让新开辟的进行开始运行,这时子进程一般都会马上调用exec()函数来执行其他任务,可以避免写时复制的开销(为什么是子进程执行其他任务了:如果首先执行父进程,在父进程执行过程中,可能会向地址空间写入数据,这个时候,系统就会为子进程拷贝父进程原来的数据,而当子进程调用的时候,其紧接着执行exec()函数,那么此时系统又会为子进程拷贝新的数据,这样多了一份拷贝)。参考资料[2]
do_fork()主要完成的任务:
3、进程创建之copy_process()
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文件中定义:
该参数描述了在执行该系统调用时,用户态下的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文件中定义:
在该函数的最后,又调用了fs/exec.c文件中定义的search_binary_handler函数来查询能够处理相应可执行文件格式的处理器,并调用相应的load_library方法以启动进程。这里,用到了一个在include/linux/binfmts.h文件中定义的linux_binfmt结构体来保存处理相应格式的可执行文件的函数指针如下:
在调用特定的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
执行说明:执行可执行程序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
一、进程的创建过程分析
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
相关文章推荐
- Linux操作系统分析-lab2-进程的创建与可执行程序的加载
- 【Linux操作系统分析】进程的创建与可执行程序的加载
- Linux操作系统分析(2)- 进程的创建与可执行程序的加载
- Linux操作系统分析(2)- 进程的创建与可执行程序的加载
- Linux操作系统分析之进程的创建与可执行程序的加载
- Linux操作系统分析-(2)进程的创建与可执行程序的加载
- 进程信号Linux操作系统分析(2)- 进程的创建与可执行程序的加载
- 【Linux操作系统分析】进程的创建与可执行程序的加载
- Linux操作系统学习_用户进程之由新进程创建到可执行程序的加载
- Linux进程的创建与可执行程序的加载
- Linux操作系统分析(2)- 进程的创建与可执行程序的加载
- Linux操作系统分析(二)进程的创建与可执行程序的加载
- Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度(十一)
- Linux操作系统实验二:进程的创建与可执行程序的加载
- 深入理解Linux之进程的创建和可执行程序的加载
- Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度(十一)
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载