【Linux操作系统分析】进程的创建与可执行程序的加载
进程的创建与可执行程序的加载
一 进程的创建
进程0是所有进程的祖先。进程1被创建并选择后调用execve()系统调用转入可执行程序init,init进程一直存活,创建和监控在操作系统外层执行的所有进程的活动。
当fork()被调用时,主要由函数do_fork()函数来处理。do_fork()函数的执行流程如下:
do_fork()的主要作用是为子进程分配PID,检查各个标志位,以决定新创建的子进程的被创建后所处的状态和执行队列,以及调用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。
do_fork()结束后,创建了可运行的完整的子进程,调用程序把子进程描述符thread字段的值装入CPU寄存器,特别是把thread.esp(子进程内核态对战的地址)装入esp寄存器,把函数ret_from_fork()的地址装入eip寄存器,这个汇编语言函数调用schedule_tail()函数,用存放在栈中的值再装载所有的寄存器,并强迫CPU返回到用户态。然后在fork()系统调用结束时,新进程将开始执行。系统调用的返回值放在eax寄存器中:返回给子进程的值是0,返回给父进程的值是子进程的PID。
至此,fork()系统调用结束,父进程和子进程暂时共享同一个用户态对战,但是当父子进程中有一个试图去改变栈,则写时复制复制机制将拷贝出一份新的用户态堆栈给父进程。
二 可执行程序的加载
前面讲到fork()系统调用创建出了一个新的进程,然后紧接着,这个新的进程一般会用来调用execve()系统调用执行指定的ELF文件,即当进入execve()系统调用之后,就开始了可执行程序的加载。
.
上面绿色的步骤为装载文件的主要函数,其主要步骤为:
- 检查ELF可执行文件格式的有效性,比如摩数、程序头表中段的数量。
- 需找动态链接的“.interp”段,设置动态连接器路径
- 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码,数据,只读数据
- 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址
- 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的连接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址:对于动态链接的ELF可执行文件,程序入口点是动态连接器
当load_elf_binary()执行完毕,返回至do_execve()再返回至sys_execve()时,上面第5步已经把系统调用的返回地址改成了被装载的ELF程序的入口地址。所以当sys_+execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。
三 附录
1 fork()和exec()族函数
- fork():创建一个新进程,该进程几乎是当前进程的完全拷贝。
- exec()族函数:启动另外的进程以取代当前运行的进程。
1.1 fork()函数
forkTest.c
int main() { pid_t pid; pid = fork(); if(pid == 0) { printf("Child process!\n"); } else if(pid > 0) { sleep(1); printf("Parent process!\n"); } else printf("fork failure!\n"); exit(0); }
运行截图:
由运行结果可知,fork()创建了一个子进程,父进程和子进程各打印了一条信息。
1.2 exec()族函数
execTest.c
- 例子中用execl系统调用
- 在相同的文件夹中已经编译好一个helloworld可执行文件。
- execTest.c文件,在上例中fork()函数创建的子进程分支中增加了一个execl()系统调用,调用同文件夹下的helloworld可执行文件。
#include<stdlib.h> #include<stdio.h> #include<sys/types.h> #include<unistd.h> int main() { pid_t pid; pid = fork(); if(pid == 0) { execl("./helloworld", "helloworld", NULL); printf("Child process!\n"); } else if(pid > 0) { sleep(1); printf("Parent process!\n"); } else printf("fork failure!\n"); exit(0); }
运行截图:
由运行结果可以看到,execl()函数调用了一个新的进程,完全取代当前调用该函数的进程。上例中,fork出来的子进程并没有打印出“Child process!”,正说明了这一点。
2 fork和exec系统调用在内核中的执行过程
2.1 C代码中嵌入汇编代码
asmTest.c
#include <stdio.h> int main() { /* val1+val2=val3 */ unsigned int val1 = 1; unsigned int val2 = 2; unsigned int val3 = 0; printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3); asm volatile( "movl $0,%%eax\n\t" /* clear %eax to 0*/ "addl %1,%%eax\n\t" /* %eax += val1 */ "addl %2,%%eax\n\t" /* %eax += val2 */ "movl %%eax,%0\n\t" /* val2 = %eax*/ : "=m" (val3) /* output =m mean only write output memory variable*/ : "c" (val1),"d" (val2) /* input c or d mean %ecx/%edx*/ ); printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3); return 0; }执行截图:
2.2 C代码中嵌入系统调用汇编代码
sys_asmTest.c
#include <stdio.h> #include <time.h> int main() { time_t tt; struct tm *t; int ret; /* (gdb) disassemble time Dump of assembler code for function time: 0x0804f800 <+0>: push %ebp 0x0804f801 <+1>: mov %esp,%ebp 0x0804f803 <+3>: mov 0x8(%ebp),%edx 0x0804f806 <+6>: push %ebx 0x0804f807 <+7>: xor %ebx,%ebx 0x0804f809 <+9>: mov $0xd,%eax 0x0804f80e <+14>: int $0x80 0x0804f810 <+16>: test %edx,%edx 0x0804f812 <+18>: je 0x804f816 <time+22> 0x0804f814 <+20>: mov %eax,(%edx) 0x0804f816 <+22>: pop %ebx 0x0804f817 <+23>: pop %ebp 0x0804f818 <+24>: ret End of assembler dump. */ #if 0 time(&tt); printf("tt:%ld\n",tt); #else /* 没有使用常规寄存器传参的方法 */ asm volatile( "mov $0,%%ebx\n\t" /* 不使用参数tt */ "mov $0xd,%%eax\n\t" "int $0x80\n\t" "mov %%eax,%0\n\t" : "=m" (tt) ); printf("tt:%ld\n",tt); t = localtime(&tt); printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); /* 使用常规寄存器传参的方法 */ asm volatile( "mov %1,%%ebx\n\t" /* 使用参数tt */ "mov $0xd,%%eax\n\t" "int $0x80\n\t" "mov %%eax,%0\n\t" : "=m" (ret) : "b" (&tt) ); printf("tt:%ld\n",tt); t = localtime(&tt); printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); #endif return 0; }运行截图:
2.3 fork()系统调用的执行过程
查看fork()系统调用的汇编代码(部分):
2.4 exec()系统调用的执行过程
对exec()进行反汇编:
Dump of assembler code for function execl: 0xb7ed85f0 <+0>: push %ebp 0xb7ed85f1 <+1>: push %edi 0xb7ed85f2 <+2>: push %esi 0xb7ed85f3 <+3>: push %ebx 0xb7ed85f4 <+4>: sub $0x102c,%esp 0xb7ed85fa <+10>: mov 0x1044(%esp),%edx 0xb7ed8601 <+17>: lea 0x20(%esp),%ecx 0xb7ed8605 <+21>: call 0xb7f4af83 0xb7ed860a <+26>: add $0xec9ea,%ebx 0xb7ed8610 <+32>: lea 0x1048(%esp),%eax 0xb7ed8617 <+39>: mov %ecx,0x18(%esp) 0xb7ed861b <+43>: test %edx,%edx 0xb7ed861d <+45>: mov %edx,0x20(%esp) 0xb7ed8621 <+49>: je 0xb7ed8724 <execl+308> 0xb7ed8627 <+55>: lea 0x4(%eax),%ebp 0xb7ed862a <+58>: mov (%eax),%eax 0xb7ed862c <+60>: mov $0x1,%esi 0xb7ed8631 <+65>: lea 0x20(%esp),%edi 0xb7ed8635 <+69>: mov $0x400,%edx 0xb7ed863a <+74>: test %eax,%eax 0xb7ed863c <+76>: mov %eax,(%edi,%esi,4) 0xb7ed863f <+79>: je 0xb7ed865d <execl+109> 0xb7ed8641 <+81>: lea 0x0(%esi,%eiz,1),%esi 0xb7ed8648 <+88>: add $0x1,%esi 0xb7ed864b <+91>: cmp %esi,%edx 0xb7ed864d <+93>: je 0xb7ed86a0 <execl+176> 0xb7ed864f <+95>: mov %ebp,%eax 0xb7ed8651 <+97>: lea 0x4(%eax),%ebp ---Type <return> to continue, or q <return> to quit--- 0xb7ed8654 <+100>: mov (%eax),%eax 0xb7ed8656 <+102>: test %eax,%eax 0xb7ed8658 <+104>: mov %eax,(%edi,%esi,4) 0xb7ed865b <+107>: jne 0xb7ed8648 <execl+88> 0xb7ed865d <+109>: mov -0xd4(%ebx),%eax 0xb7ed8663 <+115>: mov 0x1040(%esp),%ecx 0xb7ed866a <+122>: mov (%eax),%eax 0xb7ed866c <+124>: mov %edi,0x4(%esp) 0xb7ed8670 <+128>: mov %ecx,(%esp) 0xb7ed8673 <+131>: mov %eax,0x8(%esp) 0xb7ed8677 <+135>: call 0xb7ed82e0 <execve> 0xb7ed867c <+140>: cmp 0x18(%esp),%edi 0xb7ed8680 <+144>: mov %eax,%esi 0xb7ed8682 <+146>: je 0xb7ed868c <execl+156> 0xb7ed8684 <+148>: mov %edi,(%esp) 0xb7ed8687 <+151>: call 0xb7e36ef0 <free@plt+48> 0xb7ed868c <+156>: add $0x102c,%esp 0xb7ed8692 <+162>: mov %esi,%eax 0xb7ed8694 <+164>: pop %ebx 0xb7ed8695 <+165>: pop %esi 0xb7ed8696 <+166>: pop %edi 0xb7ed8697 <+167>: pop %ebp 0xb7ed8698 <+168>: ret 0xb7ed8699 <+169>: lea 0x0(%esi,%eiz,1),%esi 0xb7ed86a0 <+176>: cmp 0x18(%esp),%edi 0xb7ed86a4 <+180>: mov $0x0,%eax 0xb7ed86a9 <+185>: lea (%edx,%edx,1),%ecx 0xb7ed86ac <+188>: mov %ecx,0x1c(%esp) 0xb7ed86b0 <+192>: lea 0x0(,%edx,8),%ecx ---Type <return> to continue, or q <return> to quit--- 0xb7ed86b7 <+199>: cmovne %edi,%eax 0xb7ed86ba <+202>: mov %edx,0x14(%esp) 0xb7ed86be <+206>: mov %ecx,0x4(%esp) 0xb7ed86c2 <+210>: mov %eax,(%esp) 0xb7ed86c5 <+213>: call 0xb7e36e70 <realloc@plt> 0xb7ed86ca <+218>: mov 0x14(%esp),%edx 0xb7ed86ce <+222>: test %eax,%eax 0xb7ed86d0 <+224>: je 0xb7ed8710 <execl+288> 0xb7ed86d2 <+226>: cmp 0x18(%esp),%edi 0xb7ed86d6 <+230>: je 0xb7ed86e8 <execl+248> 0xb7ed86d8 <+232>: mov %eax,%edi 0xb7ed86da <+234>: mov 0x1c(%esp),%edx 0xb7ed86de <+238>: mov %ebp,%eax 0xb7ed86e0 <+240>: jmp 0xb7ed8651 <execl+97> 0xb7ed86e5 <+245>: lea 0x0(%esi),%esi 0xb7ed86e8 <+248>: shl $0x2,%edx 0xb7ed86eb <+251>: mov %edx,0x8(%esp) 0xb7ed86ef <+255>: mov %edi,0x4(%esp) 0xb7ed86f3 <+259>: mov %eax,(%esp) 0xb7ed86f6 <+262>: mov %eax,0x14(%esp) 0xb7ed86fa <+266>: call 0xb7e9f750 0xb7ed86ff <+271>: mov 0x14(%esp),%ecx 0xb7ed8703 <+275>: mov %ebp,%eax 0xb7ed8705 <+277>: mov 0x1c(%esp),%edx 0xb7ed8709 <+281>: mov %ecx,%edi 0xb7ed870b <+283>: jmp 0xb7ed8651 <execl+97> 0xb7ed8710 <+288>: cmp 0x18(%esp),%edi 0xb7ed8714 <+292>: mov $0xffffffff,%esi 0xb7ed8719 <+297>: jne 0xb7ed8684 <execl+148> ---Type <return> to continue, or q <return> to quit--- 0xb7ed871f <+303>: jmp 0xb7ed868c <execl+156> 0xb7ed8724 <+308>: mov -0xd4(%ebx),%eax 0xb7ed872a <+314>: mov 0x1040(%esp),%ecx 0xb7ed8731 <+321>: mov (%eax),%eax 0xb7ed8733 <+323>: mov %ecx,(%esp) 0xb7ed8736 <+326>: mov %eax,0x8(%esp) 0xb7ed873a <+330>: lea 0x20(%esp),%eax 0xb7ed873e <+334>: mov %eax,0x4(%esp) 0xb7ed8742 <+338>: call 0xb7ed82e0 <execve> 0xb7ed8747 <+343>: mov %eax,%esi 0xb7ed8749 <+345>: jmp 0xb7ed868c <execl+156> End of assembler dump.
3 task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置。
3.1 task_struct进程控制块结构
3.2 ELF文件格式
3.3 ELF文件格式与进程地址空间的关系
ELF文件中,段的权限往往只有为数不多的几种组合,基本上是三种:
- 以代码段为代表的权限为可读可执行的段
- 以数据段和BSS段为代表的权限为可读可写的段
- 以只读数据段为代表的权限为只读的段
对于相同权限的段,把它们合并在一起当作一个段进行映射。
如.text和.init,它们包含的分别的是程序的可执行代码和初始化代码,并且它们的权限相同,都是可读并且可执行。假设.text为4097字节,.init为512字节,这两个段分别映射的话需要占用三个页面,因为一个页面的大小为4KB。如果把它们合并成一起映射的话只需占用两个页面。
ELF可执行文件中引入了一个概念叫做“Segment”,一个Segment包含一个或多个属性类似的Section。Segment实际上从装载的角度重新划分了ELF的各个段。
4 动态链接库在ELF文件格式中与进程地址空间中的表现形式
表现形式:动态连接器和动态链接重定位表。
在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体;但是在动态连接下,一个程序被分成了若干个文件,有程序的主要部分,即可执行文件和程序所依赖的共享对象(.so文件)。
动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行程序之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把
控制权交给程序,然后开始执行。
动态连接器的位置是由ELF可执行文件决定的。在ELF可执行中,有一个专门的段叫做“.interp”段。
动态链接的实现步骤: