Linux可执行文件与进程的虚拟地址空间
一个可执行文件被执行的同时也伴随着一个新的进程的创建。Linux会为这个进程创建一个新的虚拟地址空间,然后会读取可执行文件的文件头,建立虚拟地址空间与可执行文件的映射关系,然后将CPU的指令指针寄存器设置成可执行文件的入口地址,然后CPU就会从这里取指令执行。
一个可执行文件包含可被CPU执行的指令和待处理的数据,上CPU之前,指令和数据全部被翻译成成二进制的形式。在可执行的文件的内部,划分出了一些专门的段,如代码段,数据段,BSS段等。代码段中存放的是可执行的二进制指令,数据段存放初始化过的变量,BSS段存放未初始化的变量,从装载的角度,把这些段称为segment。
32位的虚拟地址空间
64位的虚拟地址空间
Proc目录下的进程虚拟地址空间布局
Linux在装载可执行文件的时候,会将这些segment映射到进程的地址空间中。映射的时候,这里面的segment会对应一个VMA。Linux将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA)。在/proc目录下,可以查看一个进程的虚拟地址空间,通过命令
cat /proc/pid/maps
这里面的每一行都对应一个VMA,每一个VMA都通过vm_area_struct结构体来描述。结构体中的vm_start和vm_end是VMA的起始地址和结束地址,还有其他的一些域来描述VMA的权限等。我们需要关注的是前三个VMA,这是ELF可执行文件的segment映射过来的。可以看到,这里面并没有标明哪个是TEXT段,哪个是DATA段和BSS段。但是可以发现,前三个VMA的权限都不一样。
虚拟地址空间存储区的分布
所以,操作系统实际上并不关心可执行文件各个段所包含的的实际内容,OS只关心一些跟装载相关的问题,最主要的是段的权限(可读,可写,可执行)。
ELF文件中,段的权限往往只有为数不多的几种组合,基本上就3种:
-
以代码段为代表的权限为可读可执行的段 -
以数据段和BSS段为代表的权限为可读可写的段 -
以只读数据段为代表的权限为只读的段
ELF可执行文件中有两个概念,分别是段(segment)和节(section)。通过readelf -S name.elf可以查看ELF可执行文件的节头表,这里面有所有节的信息
在将目标文件链接成可执行文件的时候,链接器会尽量把相同权限属性的段分配在同一空间。比如可读可执行的段都放在一起,这种段的典型是代码段;可读可写的段都放在一起,这种段的典型是数据段。在ELF中,把这些属性相似的,又连在一起的段叫做一个“segment”,而系统正是按照“segment”而不是“section”来映射可执行文件的。
可以使用命令 readelf -l name.elf来查看ELF的段。在ELF的程序头表,保存着segment的信息
最下面是是段与节的归属关系:
可以看到这个可执行文件中共有9个segment。从装载的角度看,我们只关心两个“LOAD”型的segment,因为只有它是需要被映射的,其他诸如“NOTE”,”GNU_STACK”都是在装载时起辅助作用的。下面的0到8分别对应着上面的一个segment,两个LOAD类型的segment分别对应着02和03,可以看到每个LOAD类型的segment里面都包含了许多的section。
ELF将相同或者相似属性的section合并为一个segment并映射到一个VMA中,是为了减少页面内部碎片,以节省内存空间的使用。因为在有了虚拟存储机制以后,装载的时候采用页映射的方式。Intel系列的处理器,页尺寸最小是4096个字节,也就是4KB。当写的程序很小的时候,每个section可能只有几十或者几百个字节,如果每个section都占用一个页的话,对内存的浪费是海量的。所以在将目标文件链接成可执行文件的时候,链接器会尽量把相同或相似权限属性的section分配在同一空间,在程序头表中,将一个或多个属性类似的section合并为一个segment,然后在装载的时候,将这个segment映射到进程虚拟地址空间中的一个VMA中。
ELF可执行文件与进程虚拟地址空间的映射关系
很明显,属性相同或相似的section会被归类到一个segment,并且被映射到同一个VMA。
总的来说,“segment”和“section”是从不同的角度来划分同一个ELF文件。这个在ELF文件中被称为不同的视图(view),从section的角度来看ELF文件就是链接视图(Linking View),从segment的角度来看就是执行视图(Execution View)。当我们在谈到ELF装载时,段专门指segment,而在其他的情况下,段指的是section。
在实际的映射过程中,只发现有代码段映射的VMA,有数据段映射的VMA,却没有BSS段映射的VMA。
如果仔细观察程序头表,查看两个LOAD型的segment,会发现一些映射的细节。
FileSiz表示segment在ELF文件中所占的大小,MemSiz表示segment在进程虚拟地址空间中所占的大小。可以发现,MemSiz比FileSiz多出了0x20个字节,十六进制的20对应的十进制是32。再来看一下这个ELF可执行文件中BSS段的大小。
可以看到,BSS段的大小正好是十进制的32,。这说明在实际映射的时候,数据段在内存中所分配的空间大小超过实际的大小,超出去的这部分空间就是BSS段,并没有为BSS段进行专门的映射,这就是为什么在查看程序头表时,只看到了两个LOAD类型的段,而不是三个,BSS段已经被合并到了数据类型的段里面。
这样做的好处就是在构造ELF可执行文件时,不需要再额外设立BSS的segment了,只需把数据segment的内存扩大,那些额外的部分就是BSS。而这部分多出的BSS空间,会被全部填充为0 。在C语言中,没有初始化的全局变量和一些静态变量会被默认初始化为0 ,这就是原因,因为它们会被分配到BSS段上,被一次性初始化为0。
最后我们通过一个打印变量地址的小程序进行验证,仔细观察没有初始化的全局变量和一些静态变量的线性地址。
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !