这篇博客记录编译得到可执行目标文件后,加载和运行的过程。
编译得到可执行目标文件后,就可以将“可执行目标文件”加载“运行地址”所指的内存位置,然后运行了。下面记录Linux虚拟内存运行的运行过程。
2.1 程序的加载过程
当在windows下双击.exe可执行目标文件,或者在linux下面./a.out时候执行时候,首先进行程序的加载,步骤如下:
首先:从父进程复制出一个子进程。图形界面、命令行程序就是父进程。当双击图标,执行程序时会从父进程复制出子进程,复制的目的其实就是从父进程的 “虚拟内存”复制出一个子进程的“虚拟内存”,准确讲应该是复制出“虚拟内存”的task进程数据结构,用于建立子进程的虚拟内存。有了子进程的虚拟内存,就可以将新的程序加载到虚拟内存中了。
如上图所示,上图的复制过程,调用fork()函数实现(见UNIX环境高级编程笔记)。进程的虚拟内存空间被分为了两部分,一部分是内核空间,另一个部分是应用空间。每个进程的虚拟内存和物理内存是一一对应的,每个进程虚拟对内对应不同的物理内存,这也是为什么每个进程的虚拟地址都是0x08048000,而不会与物理地址重复。
然后,程序运行起来就编程进程了。程序加载时候,linux提供的加载器将程序的代码段和数据段加载到进程的应用空间中,加载到0x08048000地址处。所有的进程都共享内核空间。
2.1 程序的运行过程
程序被加载到虚拟内存后,程序就可以运行了,运行后就是动态的进程了。进程运行过程如下:
(a)cpu的pc指向_start(将start第一条指令——start所在位置的虚拟地址存放到pc)
(b)从_start开始执行启动代码。
(c)启动代码调用_init等函数进行初始化。其中很重要的就是弄出堆和栈这两个东西
(d)启动代码调用main函数,main函数再调用各个子函数,我们自己写的代码就开始运行了。
(e)main函数调用return关键字,返回到启动代码。main函数将返回值return给启动代码后,启动代码会调用exit函数,接着将返回值返回给OS。该返回值主要是为了获取子进程的“进程状态”,从而回收子进程的资源。
如果子进程调用exec加载新程序后,子进程空间和新加载程序的虚拟内存是怎样的?
比如,执行下面代码后,子进程的空间是怎样的?
int main(int argc,char **argv)
{
pid_t ret = 0;
int fd = 0;
ret = fork();
if (ret > 0) // parent pid
{
}
else if(ret == 0) // child pid
{
extern char **environ;
execve("./new",argv,environ);
}
//while(1);
return 0;
}
加载新程序之前,子进程中的所有内容(包括堆和栈),都是从父进程复制(继承)而来,子进程的.text、.data、…、堆栈与父进程的一模一样。当exec加载新程序后,新程序的.text、.rodata、.data等会覆盖子进程原有的.text、.rodata、.data、.bss,然后开始执行新程序。执行新程序时还是从.text中的“启动代码”开始执行的,当执行启动代码中设置堆栈的代码时,会重新设置新程序自己的堆栈指针,此时所代表的就是新程序自己的堆栈,只不过堆栈空间还是子进程的堆栈空间,并且该空间不会被清零(memset 或 bzero清零)。