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

学号:sa****340  姓名:**钰

参考:

  • fork和exec系统调用最终都是通过int 0x80软中断 + EAX寄存器(存储对应的系统调用号)进入内核,在内核中fork和exec对应找到sys_fork/do_fork和sys_exec/do_exec。do_fork主要的工作就是创建一个新进程,创建的方法是拷贝当前进程、分配新的进程pid、插入进程相关链表队列中等。do_exec的工作较为复杂,它的主要目标是将一个可执行程序加载到当前进程中来,返回到用户态时EIP指向可执行程序的入口位置(即0x08048000). 可执行程序的加载过程可以分为两种情况:一种是加载静态编译的ELF文件,只需要将代码段加载到0x08048000的位置,其他的数据也根据规则加载即可;另一种情况更常见需要动态链接,ELF文件中说明了它所依赖的其他动态链接库so文件(也是ELF文件格式),so文件还可以依赖其他的so文件,这就形成了一个以ELF可执行程序文件为根的依赖树,这时加载过程比较复杂,大致是根据深度优先遍历整个依赖树加载所有so文件,还需要重定位动态链接等工作,最终返回到用户态。ELF文件中的数据会加载到进程的地址空间中来,可以说ELF文件中的数据与进程的地址空间有种映射关系,不同的ELF文件格式是为了便于存储,而进程地址空间中的程序数据是为了便于执行。
  • 附录:
    • 1)编程使用fork和exec系统调用的实验情况
    • 2)系统调用的关键机制分析
    • 3)do_fork和task_struct进程控制块分析
    • 4)do_exec和ELF文件格式分析
    • 5)其他

一、进程的创建过程分析

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





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值