共计 1905 个字符,预计需要花费 5 分钟才能阅读完成。
title: 操作系统_03_可执行文件加载和运行流程 date: 2022-04-18 10:54:36.0 updated: 2022-05-05 22:32:42.348 url: /archives/os03 categories:
- 可信计算
- 操作系统 tags:
- 文件系统
- 操作系统
操作系统_03_可执行文件加载和运行流程
以下内容绝大部分来自参考文献,转载请附带参考文献。
在 linux 系统中,一个ELF文件主要用来表示3种类型的文件
- 可执行文件 : 被操作系统中的加载器从硬盘中读取,加载到内存中去执行。
- 目标文件(.o) :被链接器读取,用来产生一个可执行文件或者共享文件。
- 共享文件(.so) :在动态链接的时候,由 ld-linux.so 来读取。
我们的源代码通过预处理,编译,汇编,链接后形成可执行文件,那么当我们在cmd窗口敲出指令$test argv1 argv2\n 后,操作系统是怎么将我们的可执行文件加载并运行的呢?
首先知道,计算机的操作系统的启动程序是写死在硬件上的,每次计算机上电时,都将自动加载启动程序,之后的每一个程序,每一个应用,都是不断的 fork 出来的新进程。那么我们的可执行文件,以linux 系统为例,也是由shell 进程 fork 出一个新进程,在新进程中调用exec函数装载我们的可执行文件并执行。
- execve()
当shell中敲入执行程序的指令之后,shell进程获取到敲入的指令,并执行execve()函数,该函数的参数是敲入的可执行文件名和形参,还有就是环境变量信息。execve()函数对进程栈进行初始化,即压栈环境变量值,并压栈传入的参数值,最后压栈可执行文件名。初始化完成后调用 sys_execve()
- sys_execve()
该函数进行一些参数的检查与复制,而后调用 do_execve()
- do_execve()
该函数在当前路径与环境变量的路径中寻找给定的可执行文件名,找到文件后读取该文件的前128字节。读取这128个字节的目的是为了判断文件的格式,每个文件的开头几个字节都是魔数,可以用来判断文件类型。读取了前128字节的文件头部后,将调用 search_binary_handle()
- search_binary_handle()
该函数将去搜索和匹配合适的可执行文件装载处理程序。Linux 中所有被支持的可执行文件格式都有相应的装载处理程序。
以Linux 中的ELF 文件为例,接下来将会调用elf 文件的处理程序:load_elf_binary()
- load_elf_binary()
该函数执行以下三个步骤:
a)创建虚拟地址空间:实际上指的是建立从虚拟地址空间到物理内存的映射函数所需要的相应的数据结构。(即创建一个空的页表)
b)读取可执行文件的文件头,建立可执行文件到虚拟地址空间之间的映射关系
c)将CPU指令寄存器设置为可执行文件入口(虚拟空间中的一个地址)
load_elf_binary()函数执行完毕,事实上装载函数执行完毕后,可执行文件真正的指令和数据都没有被装入内存中,只是建立了可执行文件与虚拟内存之间的映射关系,以及分配了一个空的页表,用来存储虚拟内存与物理内存之间的映射关系。
- 程序返回到execve()中
此时从内核态返回到用户态,且寄存器的地址被设置为了ELF 的入口地址,于是新的程序开始启动,发现程序入口对应的页面并没有加载(因为初始时是空页面),则此时引发一个缺页错误,操作系统根据可执行文件和虚拟内存之间的映射关系,在磁盘上找到缺的页,并申请物理内存,将其加载到物理内存中,并在页表中填入该虚拟内存页与物理内存页之间的映射关系。之后程序正常运行,直至结束后回到shell 父进程中,结束回到 shell。
总结
加载时只是做了一个虚拟空间和可执行文件之间的映射,并没有实际分配物理内存,更没有从磁盘读取文件内容到内存。代码段,数据段,堆,栈等这时候都只是存在于虚拟空间。
程序开始执行后,先访问虚拟空间的代码段,操作系统的内存管理单元(MMU)通过TLB和Page Table查找虚拟页面(Page)所对应的物理内存页框(Page Frame)时,发现还没有对应的物理页框,也就是发生了缺页中断(Page Fault),这时候操作系统才会分配物理内存,并将程序的可执行文件读入内存中,开始执行程序代码。堆栈中的内存也是在需要访问的时候,发生Page Fault,然后才分配物理内存。
这样做好处很明显,需要用到的时候才分配物理内存,可以节省很多不必要的浪费。
参考文献