我们的源代码通过预处理,编译,汇编,链接后形成可执行文件,(关于源代码到可执行文件的介绍见我的另一篇博客:程序从代码到可执行文件的过程简述)那么当我们在cmd窗口敲出指令$test argv1 argv2\n 后,操作系统是怎么将我们的可执行文件加载并运行的呢?
首先知道,计算机的操作系统的启动程序是写死在硬件上的,每次计算机上电时,都将自动加载启动程序,之后的每一个程序,每一个应用,都是不断的 fork 出来的新进程。那么我们的可执行文件,以linux 系统为例,也是由shell 进程 fork 出一个新进程,并装载我们的可执行文件并执行的。
1.execve()
当在shell 中敲入执行程序的指令后,shell 进程获取到敲入的指令,并调用 execve() 函数,该函数的参数是敲入的可执行文件名与形参,还有就是环境变量信息。execve()函数对进程栈进行初始化,即:压栈环境变量值,并压栈传入的参数值,最后压栈可执行文件名。初始化完成后调用 sys_execve()
2.sys_execve()
该函数进行一些参数的检查与复制,而后调用 do_execve()
3.do_execve()
该函数在当前路径与环境变量的路径中寻找给定的可执行文件名,找到文件后读取该文件的前128字节。读取这128个字节的目的是为了判断文件的格式,每个文件的开头几个字节都是魔数,可以用来判断文件类型。读取了前128字节的文件头部后,将调用 search_binary_handle()
4.search_binary_handle()
该函数将去搜索和匹配合适的可执行文件装载处理程序。Linux 中所有被支持的可执行文件格式都有相应的装在处理程序。以Linux 中的ELF 文件为例,接下来将会调用elf 文件的处理程序:load_elf_binary()
5.load_elf_binary()
该函数执行以下三个步骤:
a)创建虚拟地址空间:实际上指的是建立从虚拟地址空间到物理内存的映射函数所需要的相应的数据结构。(即创建一个空的页表)
b)读取可执行文件的文件头,建立从虚拟地址空间到可执行文件之间的映射关系
c)将CPU指令寄存器设置为可执行文件入口(虚拟空间中的一个地址)
load_elf_binary()函数执行完毕,事实上装载函数执行完毕后,可执行文件真正的指令和数据都没有被装入内存中,只是建立了虚拟内存与可执行文件之间的映射关系,以及分配了一个空的页表,用来存储虚拟内存与物理内存之间的映射关系。
6. 程序返回到 execve()中
此时从内核态返回到用户态,且寄存器的地址被设置为了ELF 的入口地址,于是新的程序开始启动,发现程序入口对应的页面并没有加载(因为初始时是空页面),则此时引发一个页错误,操作系统根据虚拟内存与可执行文件之间的映射关系,在磁盘上找到缺的页,并申请物理内存,将其加载到物理内存中,并在页表中填入该虚拟内存页与物理内存页之间的映射关系。之后程序正常运行,直至结束后回到shell 父进程中,结束回到 shell。