进程的创建与可执行程序的加载
一 进程的创建
进程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”段。
动态链接的实现步骤: