进程的执行

Download Report

Transcript 进程的执行

程序的装入和执行
exec类函数
• Unix系统提供了一个函数家族,这些函数
能用可执行文件所描述的新上下文代替进
程的上下文,而调用前后进程ID并不改变。
这样的函数名以前缀exec开始,后跟一个
或两个字母,因此,家族中的一个普通函
数被当作exec类函数来引用。
• 用fork函数创建子进程后,子进程往往调用
一种exec函数执行另一个程序。此时,该
子进程完全由新程序代换,而新程序从其
main函数开始执行。
• 有六种不同的exec函数可供使用。
• #include <unistd.h>
• int execl(const char* pathname,const char*
arg0,…,(char *)0);
• int execv(const char* pathname,char *const
argv[]);
• int execle(const char* pathname,const char
*arg0,…, (char *)0,char * const envp[]);
• int execve(const char *pathname,char *const
argv[],char *const envp[]);
• int execlp(const char* pathname,const char
*arg0,…, (char *)0);
• int execvp(const char* pathname,const char
*arg[]);
•
•
•
•
•
•
•
•
•
•
•
•
•
//demofork.c—simply uses execve()
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main(int argc,char* argv[])
{
pid_t pid;
if((pid=fork())<0)
{
printf("fork error!\n");
exit(0);
}
•
•
•
•
•
•
•
•
•
•
•
•
• }
if (pid==0)//This is the child;
{
execlp("./a.out","myarg1",(char *)0);
exit(7);
}
//This is the father;
if(waitpid(pid,NULL,0)<0)
{
printf("wait error!\n");
exit(0);
}
exit(1);
C程序的组成
• 由于历史的原因,C程序由exec装入进程
地址空间后由下列几部分组成:
• 1. 正文段(text)。这是由CPU执行的机器指
令组成。通常是可共享的。
• 2. 初始化数据段(data)。它包含了程序中需
赋初值的变量。如,C程序中任何函数之外
的说明:int x=99;使此变量以初值存放在
初始化数据段中。
C程序的组成(续)
• 3. 非初始化数据段,通常称为bss段
(block started by symbol),在程序开始
执行之前,内核将此段初始化为0。例如,
函数外的说明:long sum[100];
• 4. 栈(stack)。自动变量以及每次函数调用
时所需保存的信息都存放在此段中。这就
允许C函数可以递归调用。
• 5. 堆(heap)。通常在堆中进行动态存储分
配。位于bss段顶和栈底之间。
一个典型的程序段组织
高虚拟地址
栈
堆
BSS段
数据段
低虚拟地址
代码段
命令行参数和
环境变量
由exec赋初值0
由 exec 从 程 序
文件中读到
Helloword的例子
•
•
•
•
•
•
•
•
•
•
//demotest.c Just for test
#include <stdio.h>
#include <string.h>
int main(int argc,char* argv[])
{
char buf[200];//For testing bss
strcpy(buf,"hello world!");
printf("%s",buf);
printf("\n");
}
Helloworld的例子(续)
• $ls
demofork.c demotest.c
• $gcc demotest.c
#编译产生a.out
• $gcc –o demofork demofork.c
• $ls
a.out demofork demofork.c demotest.c
$./demofork
hello world!
Helloworld的例子(续)
• $ size -A a.out
#用System V 格式输出
a.out :
section
size
addr
.text
396 134513324
.data
12 134517876
.bss
4 134518136
Helloworld的例子(续)
• $ objdump -f a.out
a.out: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482ac
Linux的执行格式
• Linux支持很多种不同格式的可执行文件,
如ELF、a.out、Unix BSD的COFF、MSDOS的EXE程序等。还有几种与平台相关的
可执行格式以及Linux下的各种脚本程序,
如Java、bash、perl、python等。
• Linux正式的可执行格式是ELF (Executable
and Linking Format),旧版的Linux支持
a.out的格式,我们将主要讨论a.out。
Linux的执行格式(续)
• Linux系统中对程序的执行格式的区分方法
就是识别可执行文件头部的签名(magic
number)。一般Linux可执行文件的前32位
在逻辑上分成两个部分:
• 高16位是一个代表目标CPU类型的代码,
如objdump命令输出的: architecture:
i386。
• 低16位即是magic number,它标记了可
执行文件的类型,如:flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
Linux的执行格式(续)
• Linux为每一个可执行的类型设计了一个
linux_binfmt结构,具体代码见
include/linux/binfmt.h
• Linux系统中使用format头指针将所有
linux_binfmt结构链接到一起,其定义在
linux/fs/exec.c中:
static struct linux_binfmt *format;
使用register_binfmt和unregister_binfmt
从链表中加入或删除一个可执行类型。
execve系统调用
• 在六个exec函数中,只有execve是系统调
用,其它的五个函数均调用该函数。
• sys_execve()服务例程接受下列参数:
1. 可执行文件名的地址(在用户地址空间中)
2. 以NULL结束的字符串数组的地址(数组
和字符串均在用户态地址空间)。每个字符
串表示一个命令行参数。
3. 以NULL结束的字符串数组。每个字符串
以NAME=value形式表示一个环境变量。
execve系统调用(续)
• sys_execve()把可执行文件路径名拷贝到
一个新分配的页框。然后调用do_execve()
函数,并将指向这个页框的指针、指针数
组的指针以及用户态寄存器的内容传递给
do_execve()函数。
• 具体代码位于arch/i386/kernel/process.c。
do_execve函数
• do_execve函数依次执行下列操作:
1. 静态地分配一个linux_binprm结构,并
用新的可执行文件的数据填充这个结构。
该结构是内核为了将运行一个可执行文件
时所需要的信息组织到一起而定义的,其
具体代码在include/linux/binfmts.h中。
do_execve代码在fs/exec.c中。
2.调用open_exec()获得与这个可执行文件相
关的文件对象指针。
do_execve函数(续)
3.设置linux_binprm结构中的各变量,调用
prepare_binprm()进行文件权能检查并将
可 执行文件的前128字节读入缓冲区。
4.把文件路径名、命令行参数及环境变量拷
贝到一个或多个新分配的页框中(最终这些
页框将被分配给用户态地址空间)。
5.调用search_binary_handler()函数对
formats链表进行扫描,并尽力应用每个元
素的load_binary方法,如果得到成功应答,
则终止扫描。
do_execve函数(续)
6.如果可执行文件格式不在formats链表中,
这时,如果内核支持动态安装模块,就根
据目标文件的第2和第3个字节生成binfmt
模块名,通过request_module装入模块。
再进行一次对链表的扫描。
7.如果所有元素的load_binary方法均返回ENOEXEC,则释放所有分配的页框返回。
8.否则,返回从这个文件可执行格式的
load_binary方法中获得的代码。
a.out格式文件的装载运行
• 由以上的分析可以看出,可执行文件装入
内存及运行的关键操作为各linux_binfmts
对象中定义的load_binary方法。回忆一下,
在调用load_binary方法前,binprm结构
中已经具有了从用户态空间中拷贝过来的
可执行文件名、命令行参数以及环境变量,
还拥有了从文件中读入的前128个字节。下
面,我们集中讨论a.out格式的装载和运行。
a.out格式文件的装载运行(续)
• 在讨论代码以前,先利用linux的工具简单认
识一下一个a.out格式的文件crt0.o。
• $ file crt0.o
crt0.o: Linux/i386 impure executable
(OMAGIC)
• $ objdump -h -f -r crt0.o
运行结果见objdump crt0
a.out格式文件的装载运行(续)
• a.out的数据结构为:
static struct linux_binfmt aout_format = {
NULL, THIS_MODULE,
load_aout_binary, load_aout_library,
aout_core_dump, PAGE_SIZE
};
可见,装载a.out格式的文件的函数为
load_aout_binary。
a.out格式文件的装载运行(续)
• 正如前面提及的一样,load_aout_binary
函数首先将对a.out格式的文件的开头进行
检查。所有的a.out格式的可执行文件(二
进制代码)的开头都是一个exec数据结构,
这是在include/asm-i386/a.out.h中定义的。
而针对其magic number的宏操作的定义在
include/linux/a.out.h。
a.out格式文件的装载运行(续)
详细代码见fs/binfmt_aout.c
1.检查文件前128字节中的magic number以
确定可执行格式,不匹配返回-ENOEXEC.
2.利用程序解释器的类型和当前可执行文件
首部提供的信息做一些一致性检查.
3.调用flush_old_exec 释放进程前一个计算
(例如调用fork继承自父进程的)的几乎所
有资源.
a.out格式文件的装载运行(续)
4.根据可执行文件的首部,设置当前进程的
内存描述符,如代码段、数据段、bss段等
的起止虚拟地址、大小等。
5.计算进程执行的权能。
6.调用do_brk为代码段和数据段一起分配物
理页面。
7.调用do_mmap分别为代码段和数据段的虚
拟地址和物理页面建立映射,同时分别从
文件中读入代码和数据段的内容。
a.out格式文件的装载运行(续)
8.调用do_brk为bss段分配物理页面并建立
其与虚拟地址的映射。
9.将以前分配的包含命令行参数和环境变量
的页面放到进程地址空间中并为其建立页
表。同时确定用户堆栈段空间。
10.在用户堆栈段空间中建立main函数的参数
表。
11.设置好寄存器的内容后返回。
a.out格式文件的装载运行(续)
• 当execve系统调用返回并调用进程重新恢
复它在用户态的执行时,执行上下文被彻
底地改变,调用系统调用的代码不再存在,
新程序已经被映射到进程的地址空间。
• 但是,新程序还不能执行,因为程序解释
器还要照顾到共享库的装载。
a.out格式文件的装载运行(续)
• 程序解释器的第一个工作就是从内核保存
在用户态堆栈的信息开始,为自己建立一
个基本的执行上下文。然后,它检查被执
行的程序,识别哪些共享库的哪些函数被
有效装入。接着,解释器发布几个mmap
调用创建虚拟地址区域对共享库代码进行
映射,并更新对共享库符号的所有引用。
最后,跳转到被执行程序的入口而终止解
释器的执行。