本文为《程序员的自我修养——链接、装载与库》的读书笔记,内容和图片为书本内容的整理。
程序在运行起来后将拥有自己独立的虚拟地址空间(Virtual Address Space),这个空间的大小由计算机的硬件决定。对于32位OS,使用32位指针寻址,寻址空间为0 ~ 2^32-1,即0x00000000 ~ 0xFFFFFFFF,共4GB。对于64位OS,使用64位指针寻址,寻址空间位0x00……0(16个0) ~ 0xFF……F(16个F)。共17 179 869 184GB。
问题:在32位OS中程序能否使用超过4GB的内存空间?
使用PAE(Physical Address Extension)的方法,将地址线拓展到36位后,Intel修改了页映射的方式,通过窗口映射的方法,应用程序可以根据需要申请和映射内存,例如可以从高于4GB的物理空间申请多个大小为256MB的物理空间,在Windows下这种访问内存的方式叫AWE(Address Windowing Extensions)。
覆盖装入的方法将内存管理的任务给了程序员,需要由程序员编写一段辅助代码—(覆盖管理器)来决定程序中的模块何时在内存驻留、何时被替换掉。在复杂的情况下,程序员需要将模块按调用依赖关系组织成树状结构,以正确地释放内存空间。
页映射是虚拟存储机制的一部分,它将内存和磁盘中的数据和指令按照“页(Page)“为单位划分为若干页,由OS来控制如何将页加载进内存,页的选择算法有多种,例如FIFO(先进先出算法)、LUR(最少使用算法),详见操作系统原理。
从OS的角度看,创建一个进程有三个步骤:
在第一步创建一个独立的虚拟地址空间中,OS实际上是创建映射函数所需要的相应的数据结构。在i386下只需分配一个页目录即可,不需要设置页映射关系。
在第二步建立虚拟地址空间与可执行文件的映射关系中,当程序执行最初开始执行时,程序的页还没有装载进内存中,此时会触发页错误(Page Fault),OS使用缺页中断将需要的页装载进内存。在缺页中断的情况下,OS会查询页目录,找到空页面的VMA,计算得到相应页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与物理页建立映射关系,再将控制权还给进程继续执行。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA),Windows中称为虚拟段(Virtual section)。
在第三步将CPU的指令寄存器设置成可执行文件的入口地址启动运行中,涉及内核堆栈和用户堆栈的切换,CPU特权级的切换等操作。
对于可执行文件的装载,OS并不关注可执行文件各个段的实际内容,主要关注与装载相同的问题,例如段的权限。段的权限主要包括三种:
对于ELF文件,可以从链接和装载的角度将其分为两种视图(View):从链接视图看,ELF被分为多个section;从装载视图看,ELF被分为多个segment。OS在装载时会将权限相同的段合并到一个段进行映射,映射后在进程空间空只有一个相对应的VMA,也就是一个segment包含多个section。
对于可读可执行的段统一映射到VMA0,可读可写段统一映射到VMA1,还有一些包含调试信息的section没有被映射。
OS通过VMA堆进程的地址空间进行管理,堆、栈在进程虚拟空间中是以VMA的形式存在的。
进程虚拟空间属性解释:
第三列:VMA对应的segment在映像文件中的偏移。
第四列:映像文件所在的主设备号、次设备号。
第五列:映像文件的节点号。
堆、栈、vdso的主次设备号均为0,表示他们没有映射到文件中,这种VMA叫做匿名虚拟内存区域。vdso是一个特殊的VMA,他的地址位于内核空间,是一个内核模块,进程可以通过该VMA与内核进行通信。
对于x86处理器,一页为4096字节(4K),对于物理内存和进程虚拟空间中之间建立映射关系时,内存长度必须是4096的整数倍,且这段空间在物理内存和进程虚拟空间中的起始地址也要是4096的整数倍。
一种最简单的方式是将每个段分开映射,长度不足一页的占一页,这种方式会产生页碎片浪费空间。
还有一种映射方式是让接壤的两个段共享一个物理页面,然后将该物理页面映射两次。映射两份到虚拟地址空间。在这种映射方式下,一个物理页面可能包括两个段的数据。因为段地址对齐的关系,此时各个段的虚拟地址就不是页面长度的整数倍了。
可以得到一个规律:在ELF中,对于任何一个可装载的segment,它的p_vaddr除以对其属性的余数等于p_offset除以对齐属性的余数。
在用户态上,bash进程会调用fork()系统调用创建一个新的进程,这个新的进程会调用execve()系统调用执行指定的ELF文件,然后bash进程等待新的线程的结束。
ELF装载的主要步骤: