装载的方式
程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将程序运行所需要的指令和数据全都装入内存中,这样程序就可以顺利运行,这就是最简单的静态装入的办法。但是很多情况下程序所需要的内存数量大于物理内存的数量,当内存的数最不够时,根本的解决办法就是添加内存。相对于磁盘来说,内存是昂贵且稀有的,这种情况自计算机磁盘诞生以来一直如此。所以人们想尽各种办法,希望能够在不添加内存的情况下让更多的程序运行起来,尽可能有效地利用内存。后来研究发现,程序运行时是有局部性原理的,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘里面,这就是动态装入的基本原理。
覆盖装入(Overlay)和页映射(Paging)是两种很典型的动态装载方法,它们所采用的思想都差不多,原则上都是利用了程序的局部性原理。动态装入的思想是程序用到哪个模块,就将哪个模块装入内存,如果不用就暂时不装入,存放在磁盘中。
覆盖装入
覆盖装入在没有发明虚拟存储之前使用比较广泛,现在已经几乎被淘汰了。虽然这种方法很蹩脚,在被虚拟存储惯坏了的现代PC机程序员眼里可能不屑一顾,但是它在计算机发展的初期的确为程序能够在内存受限的机器下正常运行提供了一种解决方案。它所体现的一些思想还是很有意义的。值得一提的是,在一些现代嵌入式的内存受限环境下,特别是诸如DSP等,这种方法或许还有用武之地。覆盖装入通过划分程序为多个模块,并动态加载程序中需要执行的部分(覆盖模块)到相同的内存空间,从而降低内存需求。覆盖装入的工作流程:
程序划分,程序员或编译器将程序划分为多个模块:
常驻模块:程序的主逻辑。覆盖模块:根据逻辑和功能划分,避免功能之间的相互依赖。
创建覆盖表:定义模块的层次关系及其加载时的内存地址。程序执行:加载常驻模块和第一个需要执行的覆盖模块到内存。根据程序运行的需求,加载或替换覆盖模块。
页映射
页映射是虚拟内存管理的核心机制,用于在操作系统中实现程序地址空间与物理内存的动态关联。通过将虚拟地址分割成固定大小的“页”(Page),并映射到物理内存的“页框”(Page Frame),操作系统可以灵活管理内存资源,实现高效的内存利用和按需加载。这个学过操作系统已经很熟悉了,至于怎么换页,有多种算法选择,比如先进先出,最少使用等,不过多介绍。
从操作系统看可执行文件的加载
从操作系统的角度来看,可执行文件的装载(Loading of Executable Files)是将程序从存储设备加载到内存并准备执行的过程。这个过程包括读取可执行文件、解析其内容、分配资源,以及在必要时建立与动态链接库的关联。
装载流程
1.触发装载
用户请求执行:用户通过命令行(如 ./program)或图形界面双击可执行文件触发程序执行。系统调用启动:内核接收请求,调用 execve() 系统调用启动程序。
2.打开并验证文件
打开文件:操作系统通过文件系统接口打开可执行文件。文件格式验证:
读取文件头,验证格式是否正确。检查魔数(如 ELF 的 0x7f 45 4C 46)和其他标识。如果文件格式非法,返回错误(如 “文件不可执行”)。
3.解析可执行文件头
文件头信息:从文件头中提取以下关键信息:
文件类型(可执行文件、共享库等)。程序入口点(Entry Point)。段表(Program Header Table)的位置和大小。动态链接器的位置(如果使用动态链接)。
获取装载所需的段信息:
代码段(.text):存储可执行代码。数据段(.data):存储初始化的全局变量。未初始化数据段(.bss):存储未初始化的全局变量。动态链接段(.dynamic):记录动态链接库信息。
4.创建进程地址空间
清空旧地址空间:如果装载是对已有进程执行,操作系统会释放旧地址空间(如旧堆栈、代码段)。分配新的虚拟地址空间:
为新程序创建独立的虚拟地址空间。初始化页表结构。
5.加载段到内存
按需加载(Demand Paging):操作系统通常不一次性加载整个程序,而是通过按需加载优化性能。具体过程如下:
1.读取段表:
遍历段表,逐一处理每个段(如代码段、数据段)。根据段的类型和权限,决定如何加载。
2.将段映射到虚拟内存:
代码段:通过 mmap 映射为只读,确保指令不能被修改。数据段:映射为读写,允许数据的修改。未初始化数据段(.bss):
分配对应的虚拟内存区域。不从磁盘读取内容,直接清零。
3.按需加载内容:
页表初始记录段的虚拟地址到磁盘的映射关系。当程序第一次访问某个页面时触发缺页异常,操作系统将对应页面从磁盘加载到物理内存。
6.设置运行环境
操作系统为程序运行设置必要的环境,包括堆栈、堆和寄存器。
堆栈初始化:
分配用户栈内存(通常为固定大小,如 8 MB)。将参数、环境变量和程序名拷贝到栈中:
参数列表(如 argv)。环境变量(如 PATH)。程序名。
堆初始化:堆通常从数据段后开始,随程序动态分配内存而增长。
7.动态链接
如果程序依赖共享库(动态链接库),动态链接器负责解析和加载这些库。动态链接过程:
加载动态链接器:由可执行文件头指定的动态链接器(如 /lib/ld-linux.so.2)被加载到内存。解析依赖库:动态链接器读取动态段(.dynamic),找到所有依赖的共享库路径。加载共享库:使用 mmap 将共享库映射到进程地址空间。符号解析:查找和绑定动态库中的符号到程序中(如函数 printf 的地址)。延迟绑定(Lazy Binding):动态链接器可能延迟解析符号,仅在程序第一次调用相关函数时解析。
8.设置入口点
操作系统根据文件头记录的入口点(Entry Point)地址,将程序计数器(PC)设置为入口点。将堆栈指针(SP)指向初始化后的用户栈。
9.运行程序
操作系统将控制权交给程序的入口点。从入口点开始,程序按指令逐步执行。