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

来源:互联网 发布:淘宝订单处理的流程 编辑:程序博客网 时间:2024/06/05 12:07

学号: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