学号:SA*****259 姓名:吕良
关键字:fork() exec() task_struct 进程地址空间 ELF文件格式 动态链接库
实验总结(应该说是回答实验问题,因为300~500字真的不够,还是看附录吧)
一、fork和exec系统调用在内核中的执行过程
1>libc库对系统调用进行了封装,在执行int $0x80进入内核之前,封装例程就已经把系统调用号装入eax寄存器了。
2>当用户态进程发出int $0x80指令时,CPU切换到内核态并开始从地址system_call处开始执行指令。( 在trap_init()中:set_system_gate(0x80,&system_call); )
3>system_call()函数:
- 首先把系统调用号和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中。
- 如果系统调用号无效,该函数就把-ENOSYS值存放在栈中曾保存eax寄存器的单元中。当进程恢复它在用户态的执行时,会在eax中发现一个负的返回码。
- 如果系统调用号有效,则调用与eax中所包含的系统调用号对应的特定服务例程:call *sys_call_table(0,%eax,4);
二、exec系统调用返回到用户态时EIP指向的位置
EIP指向ELF可执行文件的入口点,这个入口点取决于程序的链接方式:
- 对于静态链接的ELF可执行文件,这个程序入口点就是ELF文件的文件头中e_entry所指的地址;
- 对于动态链接的ELF可执行文件,这个程序入口点就是动态链接器。
三、task_struct进程控制块,ELF文件格式与进程地址空间的联系(详见附录)
四、动态链接库在ELF文件格式中与进程地址空间中的表现形式(详见附录)
附录导读:
附录
一、进程的创建--fork()
do_fork()函数负责处理clone()、fork()和vfork()系统调用
1、为子进程分配新的PID
2、调用copy_process()复制进程描述符task_struct
a.执行alloc_task_struct()宏,为新进程获取进程描述符(task_struct结构)
b.执行alloc_thread_info宏以获取一块空闲内存区,用来存放新进程的thread_info结构和内核栈
3、设置子进程的状态,并将其插入到对应的进程队列
4、结束并返回子进程的PID
二、进程描述符task_struct-----进程存在的唯一标识
进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。
打开/include/linux/sched.h可以找到task_struct 的定义
三、mm_struct-----task_struct与进程地址空间的纽带
这里有必要先介绍一下进程的地址空间。进程的地址空间是一个线性地址空间。在一个带虚拟存储器的系统中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间成为虚拟地址空间。说到底,进程地址空间是虚拟地址空间,这使得每个进程都有一个独立的进程地址空间。要注意区别进程地址空间与物理内存地址,这是两个完全不同的概念,它俩是通过页表联系起来的。
创建进程的地址空间
当创建一个新的进程时内核调用copy_mm()函数。这个函数通过建立新进程的所有页表和内存描述符来创建进程的地址空间。
mm_struct
在task_struct中有一个名为mm的mm_struct数据结构,这个数据结构叫做内存描述符,包含了与进程地址空间有关的全部信息。
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct vm_area_struct *area);
unsigned long mmap_base; /* base of mmap area */
unsigned long free_area_cache; /* first hole */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;
unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long saved_auxv[42]; /* for /proc/PID/auxv */
unsigned dumpable:1;
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
/* Token based thrashing protection. */
unsigned long swap_token_time;
char recent_pagein;
/* coredumping support */
int core_waiters;
struct completion *core_startup_done, core_done;
/* aio bits */
rwlock_t ioctx_list_lock;
struct kioctx *ioctx_list;
struct kioctx default_kioctx;
unsigned long hiwater_rss; /* High-water RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
};
进程的地址空间都是通过task_struct中的mm_struct结构来管理的
mm_struct中的两个字段:pgd和mmap
pgd指向第一级页表(页全局目录)的基址,当内核运行这个进程时,它就将pgd存放在CR3控制寄存器中
mmap指向一个vm_area_struct(区域结构)的链表,其中每个vm_area_struct都描述了当前虚拟地址空间的一个区域(area)
vm_area_struct(一个具体区域的区域结构)包含下面的字段:
vm_start:指向这个区域的起始处。
vm_end:指向这个区域的结束处。
vm_prot:描述这个区域内包含的所有页的读写许可权限。
vm_flag:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的(还描述了其他一些信息)。
vm_next:指向链表中下一个区域结构。
四、exec() -----进程地址空间与ELF文件的纽带
当我们利用fork()创建了一个子进程时,子进程虽然有自己独立的进程地址空间,但它却还是生活在父进程的阴影中,它所有的东西都copy自父进程,它就是父进程的副本,父进程的影子。如果子进程一直这样,那么我们创建它是毫无意义。创建一个进程,就是要让它完成一件新事情---对滴,这就是exec()的用武之地。
/* hello.c gcc hello.c -o hello */ #include<stdio.h> int main() { printf("hello\n");
while(1){} return 0;
}
exec()的功能就是加载和运行可执行文件,格式如下:
execve("\usr\bin\hello",NULL,NULL);
我们首先来看一下ELF可执行文件格式
- ELF头部:描述文件的总体格式。它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。
- 段头部表:描述可执行文件与虚拟存储器(也就是进程地址空间)的映射关系,可执行文件连续的片(chunk)被映射到连续的虚拟存储器段。
- .init:定义了一个小函数,叫做_init,程序的初始化代码会调用它。
- .text:已编译程序的机器代码。
- .rodata:只读数据。
- .data:已初始化的全局C变量。
- .bss:未初始化的全局变量。
- .symtab:符号表,存放程序中定义和引用的函数和全局变量的信息。
- .debug:调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。
- .strtab:字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
exec是如何将ELF可执行程序映射到进程的用户地址空间区域的呢?
加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为新程序的文本、数据、bss和栈区域创建新的区域结构。
- 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。exec做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。
五、动态链接库在ELF文件格式中与进程地址空间中的表现形式
1>动态链接器在ELF文件中的表现形式
exec()加载和运行可执行文件hello时,它是怎么知道hello需要链接动态链接库libc.so的呢?它又是怎样找到动态链接库libc.so的呢?
当exec加载部分链接的可执行文件hello时,它注意到hello包含一个.interp段(如下图所示),这说明该ELF文件需要动态链接。
.interp段
".interp"的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径,动态连接器本身就是一个共享目标(比如,在Linux系统上的LD-LINUX.SO)。
.dynamic段
.dynamic段保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
.dynamic段的结构定义在“elf.h”中:
typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;
有了.interp段和.dynamic段的信息,加载器就可以工作了。
加载器不再像它通常那样将控制传递给应用程序,而是加载和运行这个动态链接器。
然后,动态链接器通过执行下面的重定位完成链接任务:
- 重定位libc.so的文本和数据到某个存储器段。
- 重定位hello中所有对由libc.so定义的符号的引用。
最后,动态链接器将控制传递给应用程序(也就是hello)。从这个时刻开始,动态链接库的位置就固定了,并且在程序执行的过程中都不会改变。
2>动态链接库在进程地址空间的表现形式
执行上面的hello程序,获取进程的PID,然后查看进程的地址空间,如下图:
从图中可以看到ELF可执行文件hello的各个段、动态链接库libc-2.17.so、动态链接器ld-2.17.so在进程地址空间中的映射位置。
注意一下,动态链接库libc-2.17.so和hello的各个段不是相邻的,它应该是位于运行时堆和用户栈之间,如下图所示。