Linux操作系统--可执行文件的装载

注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。

一,覆盖装入

使用虚拟存储的操作系统下,该方法基本已经被淘汰,但是在内存受限的嵌入式环境中,诸如dsp等,这种方法还有用武之地。

方法其实是将程序模块的装入交给程序员处理。

这样,程序一般都必须有一个进行内存管理的辅助代码,称为覆盖管理器(Overlay Manager)。覆盖管理器管理那些模块何时应该驻留在内存而何时应该被替换掉。

例如程序有模块main和A、B。A和B相互独立,模块main占用的字节是1024,而A为512,B为256字节。不考虑内存地址对齐、装载地址限制等情况,那么使用覆盖装入的办法,在内存中可以如下进行安排:

Linux操作系统--可执行文件的装载_第1张图片

A和B在内存中“相互覆盖”,共享同一个内存区域。main调用A时,覆盖管理器保证A模块在内存中,main调用B时,覆盖管理器保证B模块在内存中。整个共需要1536字节。

覆盖管理器本身很小,数十到数百字节,一般常驻内存。

如果模块很多,程序员往往需要手工将模块按照他们的调用依赖关系组织成树状结构。例如:

Linux操作系统--可执行文件的装载_第2张图片

覆盖管理器必须保证:

1,树状结构中,从任何一个模块到树根(main)成为调用路径。当该模块被调用时,整个调用路径上的模块都必须存在于内存。

2,禁止跨树间调用。任何模块不允许跨过树状结构进行调用,上述例子中,A不可以调用B、E、F;C不可调用D、B、E、F。因为覆盖管理器不能保证跨树的模块能够存在于内存。很多时候,两个模块都需要一个模块,比如E和C都需要G,此时可以把G并入main模块。

二,页映射

页映射是虚拟存储机制的一部分。页映射机制将内存和硬盘的数据和指令按照页为单位划分成若干,装载的单位是页。硬件规定的页大小有4096字节、8192字节、2MB、4MB等。最常见的IA32处理器一般使用4096字节/页。

假设32位机器有16KB内存,则共有4页,为F0~F3。设程序有32KB,即8页,为P0~P7。很明显,16KB空间无法装入32KB的程序。我们动态装入的过程可能如下: 开始程序执行的入口地址为P0,此时装载管理器(假设装载由其完成)将F0分配给P0,即将P0装入到F0;一段时间后,程序需要P5,于是装载器将P5放到F1,这样,P3和P6分别进入F2和F3,如下所示:

Linux操作系统--可执行文件的装载_第3张图片

很显然,如果此时程序只需要P0、P3、P5和P6这4个页,则程序一直运行。如果访问到了P4,那么装载管理器必须做出决定将哪一个替换出。选择的算法有很多,比如FIFO,LUR。之后程序继续执行。

其实这里的装载器就是现在操作系统的存储管理器。

三,从操作系统角度看可执行文件装载

从上述过程可见,可执行文件的页可以被装入到物理的任意页。如果程序使用物理地址直接访问,那么每次页被装入都要进行重定位。实际上,程序使用MMU进行虚拟地址和物理地址的转换。

可执行文件的装载可以如下描述:

1,创建进程

1)创建一个独立虚拟地址空间

即建立虚拟地址空间和物理空间映射关系的数据结构。i86 的Linux下,其实只是分配一个页目录(Page Directory),甚至不设置页映射关系。该映射关系到程序发生页错误的时候再进行设置。

2)读取可执行文件头,建立虚拟地址空间和可执行文件的映射关系

程序发生页错误的时候,操作系统将从物理内存分配一个物理页,然后将该页从硬盘读取到内存,再设置虚拟页和物理页的映射关系。显然,操作系统必须知道缺页在可执行文件中的位置。这就是通过虚拟地址空间和可执行文件见的映射关系获取的。

3)将CPU的指令寄存器设置成可执行文件的入口地址,启动进程

涉及内核堆栈和用户堆栈的切换,CPU执行权限的切换等。从进程角度,即跳转到ELF可执行文件指明的可执行文件入口地址处执行。

举例:

ELF可执行文件有一个代码段".text",其虚拟地址为0x08048000,在文件中的大小是0x000e1,对齐为0x1000。由于虚拟存储页映射是以页为单位,在32位IA32下一般为4096字节,所以32位ELF的对齐粒度是0x1000。由于该段大小不到1页,所以在虚拟空间将占用一页。

如下,是ELF可执行文件和进程虚拟地址空间的映射关系:

Linux操作系统--可执行文件的装载_第4张图片

ELF文件的.text段,大小为0x000e1,对齐为0x1000,其虚拟地址为0x08048000,所以占用了0x08048000到0x08049000这个页的虚拟地址空间。

Linux将虚拟地址空间中的段称为VMA(虚拟内存区域);Windows下称为虚拟段;即存在于虚拟地址空间的可执行文件中的段的映射。

操作系统将为这种映射关系建立数据结构以保存,例如这里为 虚拟地址空间的称为.text的VMA,其对应的是可执行文件中,偏移为0的段.text,其属性为只读代码段。

其实在这个过程中,仅仅是建立映射的数据结构,之后执行中,当发生页错误,程序将执行权限交给操作系统。操作系统通过这种映射关系,找到在ELF可执行文件中的位置,然后在物理内存上分配给这个虚拟地址空间物理的内存,以进行加载,最后把执行权限交还给进程,程序从页错误位置重新执行。

四,进程虚拟地址空间分布

将ELF中的段划分成页并映射到虚拟地址空间,然后再映射到物理内存,这样,如果一个段不是页的整数倍,必然导致该段存在页的浪费。对于整个ELF文件映射到虚拟地址空间的VMA,那么必然存在着段间碎片。由于一个可执行文件有十几到数十个段,那么段间碎片造成的浪费可想而知。

其实从操作系统的角度看,段可以划分为如下几种:

1,以代码段为代表,权限属性为可读可执行

2,以BBS段和数据段为代表,权限属性为可读可写

3,以只读数据为代码,权限属性为只读

对于相同权限的段,可以当作一个段,放在一起进行映射。例如,一个段为.text,另外一段为.init,那么由于分别是可执行代码段和初始化代码段,都是可读可执行,假设.text大小4097字节,.init大小512字节,则如果分别映射,需要3个页,合并后进行映射,需要2个页的虚拟空间。

ELF可执行文件中的Segment的概念,就是包含一个或者多个相似属性的section。将属性类似的,例如上述的.text和.init当作一个segment进行映射,可以有效减少页面内部碎片。 一般Segment是装载角度,而Section则是链接角度看可执行文件的。也就是说,操作系统是使用Segment,而不是section来进行可执行文件的装载的。因此Segment是ELF的执行视图(Execution View),而Section是链接视图(Linking View)。

使用readelf -S查看Section(属性结构为段表),而使用readelf -l 可以查看Segment(属性结构为程序头Program Header),其描述了ELF文件应该如何被操作系统映射到虚拟地址空间。

readelf -l的结果中的为LOAD的Segment是被操作系统加载的,其他诸如NOTE、TLS、GNU_STACK则是装载时起辅助作用的。

合并section成为Segment,将LOAD属性的segment进行映射的例子如下:

Linux操作系统--可执行文件的装载_第5张图片

ELF目标文件不需要被装载,所以没有保存Segment信息的程序头表(Program Header Table),ELF可执行文件和共享库文件则需要程序头表。其数据结构的定义为:

typedef struct {

Elf32_Word p_type; //类型,如LOAD=1,DYNAMIC、INTERP等

Elf32_Off p_offset; //该Segment在文件中的偏移

Elf32_Addr p_vaddr; //该Segment第一字节在进程虚拟地址空间的位置

Elf32_Addr p_paddr; //Segment的装载地址,即LMA(Load Memory Address),一般与p_vaddr相同

Elf32_Word p_filese; //在ELF文件中占用的空间长度,如果在ELF文件中不存在内容,则可能为0

Elf32_Word p_memsz; //在进程虚拟地址空间占用的长度,也可能为0

Elf32_Word p_flags; //权限属性,R、W、X

Elf32_Word p_align; //对齐属性,2的p_align次方。

}Elf32_Phdr;

该结构体的几个成员与使用 readelf -l打印的文件头表结果对应。

对于p_memsz的值大于p_filesz,则该Segment在内存分配空间大于文件中的大小,这些大的部分被填“0”。这种情况下,BSS Section不用再设立Segment,而是使用数据段Segment的额外的部分存储BSS Section,数据段和BSS的区别只在于数据段从文件中初始化,而BSS段内容初始化为0。也就是BSS Section和数据 Section,一般被合并成为一个Segment。

五,堆和栈

上边我们知道ELF文件的Section被以Segment的形式,映射到虚拟地址空间的VMA。其实进程的堆或者栈,也是以VMA形式存在于虚拟地址空间的。

使用 cat /proc/进程ID/maps 可以查看进程虚拟地址空间的分布。

例如cat /proc/12520/maps

这是其中结果的一部分:

00e42000-00e43000 r-xp 00e42000 00:00 0 [vdso]
03b5f000-03c88000 r-xp 00000000 fd:00 1574509 /lib/libcrypto.so.0.9.8e
03c88000-03c9b000 rwxp 00129000 fd:00 1574509 /lib/libcrypto.so.0.9.8e
03c9b000-03c9f000 rwxp 03c9b000 00:00 0
03ca1000-03d34000 r-xp 00000000 fd:00 1346623 /usr/lib/libkrb5.so.3.3
03d34000-03d37000 rwxp 00092000 fd:00 1346623 /usr/lib/libkrb5.so.3.3
08048000-080a6000 r-xp 00000000 fd:00 2337851 /usr/local/bin/ovs-openflowd
080a6000-080a8000 rw-p 0005e000 fd:00 2337851 /usr/local/bin/ovs-openflowd
080a8000-080ac000 rw-p 080a8000 00:00 0
09cd4000-09d12000 rw-p 09cd4000 00:00 0 [heap]
b7f13000-b7f18000 rw-p b7f13000 00:00 0
bfecb000-bfee0000 rw-p bffea000 00:00 0 [stack]

上述结果中,第一列是VMA地址范围,第二列是VMA权限,p表示私有(COW,Copy on Write),s表示共享,r可读,w可写,x可执行。

第三列是对应Segment在映像文件中的偏移,第四列是映像文件所在设备的主设备号和次设备号。第五列是映像文件的节点号。最后是映像文件的路径。

像 stack和heap这种主设备号和次设备号都是0.表示他们没有映射文件,这种VMA为匿名VMA(Anonymous Virtual Memory Area)。堆由系统库进行管理。栈也称堆栈,对于单线程栈属于一个线程。vdso是很特殊的一种VMA,它的地址已经位于内核空间了(即大于0xC00000000)。事实上是一个内核模块,进程通过对其访问与内核通信。

一个例子:

Linux操作系统--可执行文件的装载_第6张图片

其实 Segment和VMA并不是一一对应的。VMA可以不映射到文件。Segment也不是非得对应一个VMA。有的时候Segment的内容会一部分映射到某个VMA,另外的则映射到堆段。

六,堆的最大大小

使用malloc可以测试最大申请的堆空间大小。

Linux操作系统--可执行文件的装载_第7张图片

一般,Linux下是2.9G左右,Windows下是1.5G。这个大小,其实受很多因素影响,比如栈多少和数量、程序大小、操作系统。

七,节约物理内存

上边,我们知道可以通过对ELF可执行文件的Section进行合并为Segment,以节约虚拟内存空间占用的页多少。那么对于物理内存,如果在虚拟内存段不可合并的情况下,如何进行物理页的节约呢?

有的时候,Unix会采用对于不同的段,共享物理页的方法。unix会把ELF文件头也读入作为一个段,对于需要读取ELF文件头的操作,例如动态链接器,就可以直接从内存里读取了。

采取共享内存页方法,一般是将一个段的最后一物理页,跟另外一个段的开始一页共享。这样这个物理页就映射到了两个虚拟地址空间的段的页。这个物理页一部分属于一个段,另外一部分属于另外一个段。另外一个段不再是页粒度的地址对齐了。

这样,一个物理页可能包含两个甚至更多的段的页。

八,段地址对齐

由于共享物理内存的原因,所以使用共享物理页的,段虚拟地址不再进行页粒度的对齐。

例如VMA0的起始地址是0x08048000,长度是0x709E5,因此其结束地址是0x080B89E5。占用的页面最后一个是0x080B8000,占用到0x080B89E5。VMA1的地址则是0x080B9E5+3,

九,Windows PE和Linux ELF的区别

Windows PE的段起地址和长度都是4096页大小的整数倍,而且section比较少,一般不需要合并成很多Segment,而是将段合并成为数不多的代码段、数据段、只读数据段、BSS段等几个段,不需要考虑段对齐等。Linux比Windows下的装载复杂。

PE文件一般采用Base Address和RVA(Relative Virtual Address)来确定目标地址(Target Address)。还采用Rebasing机制以解决加载地址冲突。PE拓展头和段表是PE文件加载所需要的信息存储的载体。

你可能感兴趣的:(数据结构,windows,linux,XP,读书)