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

实验二:进程的创建与可执行程序的加载

学号:SA***424 姓名:**明

实验环境:VMware,ubuntu11.04

一.进程的创建

进程有内核态进程和用户态进程之分。所以进程的创建也就有两种方式:一是由操作系统创建 二是由父进程创建
在系统启动时,操作系统会创建一些进程,他们承担着管理和分配系统资源的任务,这些进程通常被称为系统进程
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构。整个Linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程(idle进程),他是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟贮存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号。。的若干终端注册进程getty。getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
上述过程可描述为:0号进程->1号内核进程->1号内核线程->1号用户进程(init进程)->getty进程->shell进程.
注意,上述过程描述中提到:1号内核进程调用执行init并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
1.init()函数在内核态运行,是内核代码
2.init进程是内核启动并运行的第一个用户进程,运行在用户态下
3.init()函数调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。

Linux操作系统中,用户创建一个新进程的一个方法是调用系统调用fork。在系统中调用fork返回时,子进程是父进程的一个拷贝,两个进程除了返回PID(Process ID不同外,具有完全一样的变量值,它们打开的文件都相同。fork创造的子进程复制了父进程的资源,包括内存的task_struct,新旧进程使用同一段代码,复制数据段和堆栈段,这里采用了copy_on_write技术,即一旦子进程开始运行,则新旧的进程地址空间已经分开,二者独立运行

二.ELF可执行文件加载到进程实体中

进程创建后,和父进程具有相同的地址空间,如果不让子进程具有自己的地址空间,则子进程的创建没有任何意义。子进程私有地址空间的建立是通过调用系统调用do_execve实现的。execve()是操作系统提供的非常重要的一个系统调用,其实在Linux中并没有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 *path, char *const argv[], char *const envp[]);

这几个都是都是libc中经过包装的的库函数,最后通过系统调用execve()实现。exec 函数的作用是在当前进程里执行可执行文件,也就是根据指定的文件名找到可执行文件,用它来取代当前进程的内容,并且这个取代是不可逆的,即被替换掉的内容不再保存,当可执行文件结束,整个进程也随之僵死。因为当前进程的代码段,数据段和堆栈等都已经被新的内容取代,所以exec函数族的函数执行成功后不会返回,失败是返回-1。可执行文件既可以是二进制文件,也可以是可执行的脚本文件,两者在加载时略有差别,这里主要分析二进制文件的运行。                                                  

sys_execve()是execve()系统调用的入口,系统调用的时候,把参数依次在:ebx,ecx,edx,esi,edi,ebp,eax寄存器第一个参数为可执行文件路径,第二个参数为参数的个数,第三个参数为可执行文件对应的参数,参考代码见附录(附录中分析)。

在用户态下调用execve(),引发系统中断后,在内核态执行的相应函数是do_sys_execve(),而do_sys_execve()会调用 do_execve()函数。do_execve()首先会读入可执行文件,如果可执行文件不存在,会报错。然后对可执行文件的权限进行检查。如果文件不是当前用户是可执行的,则execve()会返回-1,报permission denied的错误。否则继续读入运行可执行文件时所需的信息(见struct linux_binprm )。

接着系统调用search_binary_handler(),根据可执行文件的类型(如shell,a.out,ELF等),查找到相应的处理函数(系统为每种文件类型创建了一个struct linux_binfmt,并把其串在一个链表上,执行时遍历这个链表,找到相应类型的结构。如果要自己定义一种可执行文件格式,也需要实现这么一个 handler()。然后执行相应的load_binary()函数开始加载可执行文件。

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

返回到用户态后EIP寄存器直接跳转到ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。

三.实验结果

附录1 :shell.c源码

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
int main(){
  pid_t pid;
  pid=fork();
  if(pid==0){ printf("this is Children process!\n");
              sleep(1);
              execl("/bin/ls","ls",NULL);
             } 
  else if(pid>0){
    wait(NULL);
    printf("this is Parent process!\n");
    execl("/bin/ps","ps",NULL);
    }
 else printf("fork faliure");
      exit(0);
}

 

 shell.c程序使用了fork(),可以看到fork出来了一个子进程,执行了pid=0的情况,父子进程都调用了execl()函数,分别执行在当前目录下使用“ls”,“ps”命令来查看当前目录下的所有目录以及当前运行的进程情况。用wait(NULL)使得父进程在子进程结束后才执行。

附录2:fork系统调用在内核中的执行过程

Starting program: /home/zhujianming/Linux_exp/exp2/fork 8

Breakpoint 1, main () at fork.c:8
8	  pid=fork();
(gdb) disassemble fork
Dump of assembler code for function fork:
   0x001c8020 <+0>:	push   %ebp
   0x001c8021 <+1>:	mov    %esp,%ebp
   0x001c8023 <+3>:	push   %edi
   0x001c8024 <+4>:	push   %esi
   0x001c8025 <+5>:	push   %ebx
   0x001c8026 <+6>:	call   0x145c6f
   0x001c802b <+11>:	add    $0xc3fc9,%ebx
   0x001c8031 <+17>:	sub    $0x20,%esp
   0x001c8034 <+20>:	lea    0x0(%esi,%eiz,1),%esi
   0x001c8038 <+24>:	mov    0x3744(%ebx),%esi
   0x001c803e <+30>:	test   %esi,%esi
   0x001c8040 <+32>:	je     0x1c8278 <fork+600>
   0x001c8046 <+38>:	mov    0x14(%esi),%eax
   0x001c8049 <+41>:	test   %eax,%eax
   0x001c804b <+43>:	je     0x1c8038 <fork+24>
   0x001c804d <+45>:	mov    0x3744(%ebx),%edx
   0x001c8053 <+51>:	lea    0x1(%eax),%ecx
   0x001c8056 <+54>:	lock cmpxchg %ecx,0x14(%edx)
   0x001c805b <+59>:	jne    0x1c8038 <fork+24>
   0x001c805d <+61>:	xor    %edi,%edi
   0x001c805f <+63>:	jmp    0x1c806e <fork+78>
   0x001c8061 <+65>:	lea    0x0(%esi,%eiz,1),%esi

附录3:可执行文件加载进程实体中代码导读(按照流程图分析源码)

asmlinkage int sys_execve(struct pt_regs regs)
//该系统调用所需要的参数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;}
{
    int error;
    char * filename;
    //将用户空间的第一个参数(也就是可执行文件的路径)复制到内核
    filename = getname((char __user *) regs.ebx);
    error = PTR_ERR(filename);
    if (IS_ERR(filename))
        goto out;
//执行可执行文件
    error = do_execve(filename,
            (char __user * __user *) regs.ecx,
            (char __user * __user *) regs.edx,
            &regs);
    if (error == 0) {
        task_lock(current);
        current->ptrace &= ~PT_DTRACE;
        task_unlock(current);
        //确定没有返回正在使用的系统调用
        set_thread_flag(TIF_IRET);
    }
    //释放内存
    putname(filename);
out:
    return error;
}


 

int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
{
//linux_binprm:保存可执行文件的一些参数
	struct linux_binprm bprm;
struct file *file;
	int retval;
	int i;
	file = open_exec(filename);//在内核中打开这个可执行文件
	retval = PTR_ERR(file);
	if (IS_ERR(file))//如果打开失败
		return retval;		
	bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
//保留堆栈最顶部的一个字,bprm.p是bprm.page上的相对堆栈指针,同时反映了参	数页可用内存的大小
	memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0]));
 //清除参数页面指针表
	bprm.file = file;
	bprm.filename = filename;
	bprm.sh_bang = 0;
	bprm.loader = 0;
	bprm.exec = 0;
	if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) {
//扫描用户参数数组,计算参数个数
		allow_write_access(file);
		fput(file);
		return bprm.argc;
	}
	if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) {
//扫描用户环境数组,计算环境变量的个数
		allow_write_access(file);
		fput(file);
		return bprm.envc;
	}
//检查文件能否被执行,并使用可执行文件的前128个字节来填充Linux_binprm结构中的buf项
	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,regs);
//查询能够处理该可执行文件的处理函数,并调用相应的load_library方法
	if (retval >= 0)
		//执行成功
		return retval;
out:
	//发生错误,返回inode,并释放资源
	allow_write_access(bprm.file);
	if (bprm.file)
		fput(bprm.file);
	for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
		struct page * page = bprm.page[i];
		if (page)
			__free_page(page);
	}
	return retval;
}
1.struct linux_binprm
struct linux_binprm
{
		char buf[BINPRM_BUF_SIZE]; // 保存可执行文件的头128字节
		struct page *page[MAX_ARG_PAGES];
		struct m_struct *mm;
		unsigned long p; // 当前内存页最高地址
		int sh_bang;
		struct file * file; // 要执行的文件
		int e_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;
	};
2.struct linux_binfmt
在search_binary_handler函数内,根据读入数据结构linux_binprm内的二进制文件128字节头中的关键字,决定调用哪种加载函数,该加载函数定义在数据结构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;
};


 附录4:task_struct进程控制块,ELF文件格式与进程地址空间的联系,Exec系统调用返回到用户态时EIP指向的位置

在linux 中每一个进程都由task_struct 数据结构来定义。task_struct就是我们通常所说的PCB.她是对进程控制的唯一手段也是最有效的手段. 当我们调用fork() 时, 系统会为我们产生一个task_struct结构。然后从父进程,那里继承一些数据, 并把新的进程插入到进程树中, 以待进行进程管理。

struct task_struct {
long state; /*任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)*/
long counter;/*运行时间片计数器(递减)*/
long priority;/*优先级*/
long signal;/*信号*/
struct sigaction sigaction[32];/*信号执行属性结构,对应信号将要执行的操作和标志信息*/
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;/*任务执行停止的退出码*/
unsigned long start_code,end_code,end_data,brk,start_stack;/*代码段地址 代码长度(字节数)
代码长度 + 数据长度(字节数)总长度 堆栈段地址*/
long pid,father,pgrp,session,leader;/*进程标识号(进程号) 父进程号 父进程组号 会话号 会话首领*/
unsigned short uid,euid,suid;/*用户标识号(用户id) 有效用户id 保存的用户id*/ 
unsigned short gid,egid,sgid; /*组标识号(组id) 有效组id 保存的组id*/
long alarm;/*报警定时值*/
long utime,stime,cutime,cstime,start_time;/*用户态运行时间 内核态运行时间 子进程用户态运行时间
子进程内核态运行时间 进程开始运行时刻*/
unsigned short used_math;/*标志:是否使用协处理器*/
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;/*文件创建属性屏蔽位*/
struct m_inode * pwd;/*当前工作目录i 节点结构*/
struct m_inode * root;/*根目录i节点结构*/
struct m_inode * executable;/*执行文件i节点结构*/
unsigned long close_on_exec;/*执行时关闭文件句柄位图标志*/
struct file * filp[NR_OPEN];/*进程使用的文件表结构*/
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];/*本任务的局部描述符表。0-空,1-代码段cs,2-数据和堆栈段ds&ss*/
/* tss for this task */
struct tss_struct tss;/*本进程的任务状态段信息结构*/
};

ELF文件里面,每一个 sections 内都装载了性质属性都一样的内容,比方:

1) .text section 里装载了可执行代码;

2) .data section 里面装载了被初始化的数据;

3) .bss section 里面装载了未被初始化的数据;

4) 以 .rec 打头的 sections 里面装载了重定位条目;

5) .symtab 或者 .dynsym section 里面装载了符号信息;

6) .strtab 或者 .dynstr section 里面装载了字符串信息;

7) 其他还有为满足不同目的所设置的section,比方满足调试的目的、满足动态链接与加载的目的等等。



 操作系统装载ELF可执行文件时,最主要关心的是段的权限(可读、可写、可执行)。

为了减小页内的内部碎片,一个简单的方案就是:对于相同权限的段,把它们合并到一起当作一个段进行映射。基于这种方法,ELF可执行文件引入了一个概念叫做“Segment”,一个“Segment”包含一个或多个属性类似的“Section”(段)。 如把“.text”段和“.init”段合并在一起看作一个“Segment”,那么装载时就可以将它们看作一个整体一起映射,也就是说映射以后进程虚拟空间只有一个对应的VMA(虚拟地址空间),从而减少内存碎片,节省内存空间。

“segment”概念实际上是从装载的角度重新划分了ELF的各个段。在将目标文件链接成可执行文件的时候,链接器会尽量把相同权限属性的段分配在同一空间。比如可读可执行的段都放在一起,这种段的典型是代码段;可读可写的段都放在一起,这种段的典型是数据段。在ELF中把这些属性相似的、又连接在一起的段叫做一个“Segment”,而系统正式按照“Segment”而不是“Section”来映射可执行文件。

附录5:动态链接库在ELF文件格式中与进程地址空间中的表现形式

链接可以再编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件。它有三种不同的形式:可重定位的、可执行的、可加载和共享的。

链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。

加载器将可执行文件的内容映射到存储器,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件有对定义在共享库中的程序和数据的未解析的引用。在加载时,加载器将部分链接的可执行映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。



 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值