程序员的自我修养:链接、装载与库 6 可执行文件的装载与进程

文章探讨了进程在不同架构下的虚拟地址空间,包括32位和64位平台的大小限制。重点介绍了操作系统如何管理和限制进程的虚拟空间,并讨论了静态装入与动态装入,如覆盖装入和页映射等动态装载方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 进程虚拟地址空间

每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如32位的硬件平台决定了虚拟地址空间的地址为0到 2 32 − 1 2^{32}-1 2321,即0x00000000~0xFFFFFFFF,也就是我们常说的4GB虚拟空间大小:而64位的硬件平台具有64位寻址能力,它的虚拟地址空间达到了 2 64 2^{64} 264字节,即0x0000000000000000~0xFFFFFFFFFFFFFFFF,总共17179869184GB。

从程序的角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节:64位平台下的指针为64位,即8字节。

在下文的介绍中以32位的地址空间为主,64位的与32位类似。

32位平台下的4GB虚拟空间程序不可以任意使用。操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。我们经常在Windows下碰到的“进程因非法操作需要关闭”或Linux下的“Segmentation fault”很多时候是因为进程访问了未经允许的地址。

在Linux操作系统中,32位进程的虚拟地址空间默认分为两部分:操作系统使用高地址的1GB(0xC0000000到0xFFFFFFFF),用户进程使用低地址的3GB(0x00000000到0xBFFFFFFF)。因此,用户进程的代码、数据以及通过动态分配(如malloc)申请的空间,总计最多可使用3GB的虚拟地址。

然而,3GB的空间在某些现代应用中显得不足,例如大型数据库、数值计算、图形处理、虚拟现实和游戏等场景。虽然64位处理器能够将虚拟地址空间扩展到极高的容量(17179869184GB),但升级硬件并非总是可行,尤其是一些软件只能运行在32位平台上。

对于Windows操作系统来说,它的进程虚拟地址空间划分是操作系统占用2GB,那么进程只剩下2GB空间。2GB空间对些程序来说太小了,所以Windows有个启动参数可以将操作系统占用的虚拟地址空间减少到1GB,即跟Linux分布一样。方法如下:修改Windows系统盘根目录下的Boot.ini,加上“/3G”参数

1.1 PAE

32位CPU的虚拟地址空间最大为4GB,因此程序的虚拟地址空间无法超过4GB。然而,自1995年起,Intel在Pentium Pro处理器中引入了36位物理地址,支持最多64GB的物理内存访问。

通过扩展地址线至36位,Intel修改了页面映射机制,使32位CPU能够访问超过4GB的物理内存。这种地址扩展技术称为PAE(Physical Address Extension)。扩展的物理地址空间通常由操作系统管理,对于普通应用程序而言,其虚拟地址空间仍然限制在32位范围内。为了使用超出4GB的物理内存,操作系统提供了一种窗口映射机制,将额外的物理内存映射到进程的虚拟地址空间。

以书架和书本的例子来解释这个机制:

  • 书架的容量有限:想象一个图书馆(计算机系统)里的书架(虚拟地址空间)只能放下32本书(对应32位地址空间的4GB限制)。然而,图书馆里实际藏有100本书(物理内存大于4GB)。显然,这些书不能同时摆在书架上。
  • 窗口映射的机制:图书馆管理员(操作系统)设计了一个机制:把书架分成固定大小的格子,比如一层有4个格子,每个格子可以放一本书(比如256MB)。当需要查看某本书时,管理员会从存储室(更大的物理内存空间)中拿出那本书,放进一个空格子里。这样,虽然书架总共只能放32本书,但通过不断替换,可以访问更多书。
  • 映射机制的意义:虽然每次只能看到书架上的书(虚拟地址空间),但实际上你可以通过管理员来取用整个存储室的藏书(物理内存)。这就是AWE(Address Windowing Extensions)的原理——利用虚拟地址的固定窗口,将超出4GB的物理内存映射到虚拟地址空间中。Linux和UNIX系统中使用mmap()系统调用来实现。

2 装载的方式

程序执行时,所需的指令和数据必须加载到内存中才能运行。最简单的方式是静态装入,即将程序运行所需的全部内容一次性加载到内存中。但当程序所需内存超过物理内存时,增加内存虽能解决问题,但成本较高。因此,为了更高效地利用内存,提出了动态装入方法。

动态装入基于程序的局部性原理:将常用部分保留在内存中,较少使用的数据存放在磁盘中。覆盖装入(Overlay)和页映射(Paging)是典型的动态装入方法,其核心思想是按需加载模块——用到时装入内存,不用时保留在磁盘中,从而节省内存资源。

2.1 覆盖装入

省略 178

2.2 页映射

页映射是虚拟存储机制的重要组成部分,其在动态装载中得到了广泛应用。与覆盖装入类似,页映射并不将程序的所有数据和指令一次性加载到内存,而是将内存和磁盘数据划分为固定大小的页(Page),以页为单位进行装载和操作。

当前硬件支持的页大小包括4096字节、8192字节、2MB、4MB等,其中Intel IA32处理器常用的页大小为4096字节。例如,在512MB物理内存中页的数量为512*1024*1024/4096=131072个页。

为了演示页映射的基本机制,假设我们的32位机器有16 KB的内存,每个页大小为4096字节,则共有4个页,如下表所示。

页编号地址
F00x00000000-0x00000FFF
F10x00001000-0x00001FFF
F20x00002000-0x00002FFF
F30x00003000-0x00003FFF

假设程序的指令和数据总大小为32 KB,划分为8个页(P0~P7)。由于内存大小为16 KB,无法同时加载整个程序,因此采用动态装入机制逐页加载。

程序入口地址位于P0时,装载管理器检测到P0尚未加载,便将内存框F0分配给P0,并将P0的数据装入F0。执行过程中,当程序需要访问P5时,装载管理器将P5的数据加载到内存框F1。随后,当访问P3和P6时,P3和P6分别被加载到内存框F2和F3中。程序的页与内存框的映射关系如图6-4所示。
在这里插入图片描述

如果程序此时仅需要访问P0、P3、P5和P6,程序可以继续正常运行。然而,当程序需要访问P4时,装载管理器必须从当前加载的4个页(P0、P3、P5、P6)中选择一个页释放,以便为P4腾出内存空间。这种页替换过程通常基于特定的算法:

  • FIFO(先入先出算法):选择最早装入的页,例如释放F0;
  • LRU(最近最少使用算法):选择一段时间内访问频率最低的页,例如释放F2。

假设选用FIFO算法,释放P0所占用的F0页框,则F0将用于加载P4。程序随后继续按照动态装载的逻辑运行。

所谓的装载管理器实际上是现代操作系统的存储管理模块。当前主流操作系统均通过这种方式加载可执行文件。例如,Windows通过此机制加载PE文件,Linux加载ELF文件。接下来将从操作系统的角度分析可执行文件的加载过程。

3 从操作系统角度看可执行文件的装载

在这里插入图片描述

3.1 进程的建立

182

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值