Linux可执行文件如何装载进虚拟内存

开篇先抛出几个问题,之后逐个击破:

  1. 什么是进程的虚拟地址空间?为什么进程要有自己的虚拟地址空间,这样做有什么好处?
  2. 我们都听说过页映射,什么是页映射,操作系统为什么要以页映射方式将程序映射到进程地址空间,这样做有什么好处?程序运行过程中发生页错误如何处理?
  3. 什么是进程?从操作系统的角度来看,进程是如何被建立的?
  4. 进程虚拟地址空间的分布是什么样的?
  5. Linux是如何装载并运行ELF程序的?

虚拟地址空间

what:虚拟地址空间就是我们常说的虚拟内存,虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存的使用也更有效率。当处理器读取或写入内存位置时,都会使用虚拟地址。在读取或写入操作过程中,处理器会将虚拟地址转换为物理地址。

why:使用虚拟内存有如下好处:

  • 程序员无需操心如何存储数据或者程序等内容

  • 程序可以使用一系列连续的虚拟地址来访问物理内存中不连续的大内存区域,用户看到的是连续地址,而无需关心更底层物理地址的排布。

  • 通过使用虚拟内存,程序可以使用大于实际可用物理内存的空间,当物理内存不够用时,操作系统会将物理内存页保存在磁盘文件,数据页或者代码页会根据需要在物理内存和磁盘之间移动。

  • 不同进程使用的虚拟地址彼此隔离,用户无需担心会影响到其它程序内存地址中的数据,操作系统的内存管理模块会将虚拟地址映射到物理地址。

更多详解虚拟内存的内容可以看我之前的文章:1, 2, 3

页映射

background:程序运行时所需要的指令和数据必须放在内存中才可以正常执行,最简单的办法就是将运行所需要的指令和数据全部装进内存,但是很多时候程序需要的内存可能大于实际可用的物理内存,为了解决这种不够用的问题引入了动态装入的概念,可以将程序最常用的部分驻留在内存中,而将一些不常用的数据存在磁盘中。

what:页映射不是一次性将所有的程序和数据装入内存,而是将内存和磁盘中的数据和指令按页为单位分成若干份,以后所有的装载和操作的单位就是页。页的大小不固定,但是一般都是4096字节。

how:如下图,举个例子,可执行程序所需要的指令和数据总和占8个页,编号为VP0-VP7,而实际的物理内存只有4个页,编号为PP0-PP3,4个页的物理内存无法同时将8个页的程序都装载进去,所以需要动态装入,假设程序入口地址在VP0,这时内核发现VP0不在内存中,所以将VP0分配给了PP0,将VP0的内容装入了PP0,运行一段后程序需要用到VP2,内核又将VP2分配给了PP1,之后又用到VP4和VP6,内核又分别分配给了PP2和PP3。这时候程序只需要VP0、VP2、VP4和VP6这四个页就可以一直运行下去,如果程序又需要VP5,那内核就必须会放弃正在使用的四个内存页中的一个才可以把VP5装载进去继续执行,至于选择哪个,操作系统内核会有多种换出算法来处理这种问题。

image.png

why:其实上面已经介绍了原因,如果一次性把所有指令和数据都加载到内存中,物理内存可能不够用,所以需要使用动态装入,所以引入了页映射的方法。

进程如何被建立

这里首先需要弄清楚程序和进程的区别?程序(可执行文件)是一个静态的概念,它就是预先编译好的指令和数据集合的一个文件,进程则是一个动态的概念,它是程序运行的一个过程。

从操作系统角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间,很多时候一个程序被执行都伴随着一个新的进程被创建,之后装载相应的可执行文件并运行。上述经历了什么步骤?

  1. 创建一个独立的虚拟地址空间:这里的创建空间并不是真正的创建空间,而是创建映射函数所需要的数据结构,方便后面映射需要。
  2. 读取可执行文件头,建立虚拟空间和可执行文件的映射关系:上面的映射数据结构是为了建立虚拟空间到物理内存的映射关系,这一步是虚拟空间与可执行文件的映射关系。
  3. 将CPU的指令寄存器设置成可执行文件入口,启动运行:这里可以简单的理解为操作系统执行了一条跳转指令,跳转到可执行文件的入口地址。

页错误:当程序执行一个地址的指令时,发现是个空页面,所以就认为是个页错误,这时候控制权交由操作系统,操作系统有专门的错误处理程序处理这种情况,查询第二步骤建立的映射数据结构,找到空页面所在的虚拟内存区域,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中的该虚拟页与物理页建立映射关系,控制权返还给进程,进程从页错误的位置继续执行。

进程虚拟空间分布

如果您读过我之前的文章应该就知道,一个正常的进程,可执行文件中不只包含数据段和代码段,还有好多个段,这里通过readelf可以查看:

$ readelf -S test
There are 9 section headers, starting at offset 0x1208:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000000004000e8  000000e8
       0000000000000056  0000000000000000  AX       0     0     1
  [ 2] .rodata           PROGBITS         000000000040013e  0000013e
       0000000000000006  0000000000000000   A       0     0     1
  [ 3] .eh_frame         PROGBITS         0000000000400148  00000148
       0000000000000078  0000000000000000   A       0     0     8
  [ 4] .data             PROGBITS         0000000000601000  00001000
       0000000000000008  0000000000000000  WA       0     0     8
  [ 5] .comment          PROGBITS         0000000000000000  00001008
       0000000000000029  0000000000000001  MS       0     0     1
  [ 6] .symtab           SYMTAB           0000000000000000  00001038
       0000000000000150  0000000000000018           7     7     8
  [ 7] .strtab           STRTAB           0000000000000000  00001188
       000000000000003a  0000000000000000           0     0     1
  [ 8] .shstrtab         STRTAB           0000000000000000  000011c2
       0000000000000042  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific

通过上面的结果可以看出这里的段叫Section,拿这个举例,ELF文件映射是以系统页为单位,每个段在映射时的长度都是系统页的整数倍,假设程序有8个段,每个段都占512字节,占用了8个页,但是一个页却有4K的大小,空间利用率只有1/8,造成极大的空间浪费。实际上,从操作系统装载可执行文件的角度看,可以发现它实际上并不关心可执行文件各个段所包含的实际内容,它主要就是关心段的权限(可读、可写、可执行),ELF文件中段的权限主要就有几种组合:

  1. 以代码段为代表的权限为可读可执行的段

  2. 以数据段和BSS段位代表的权限为可读可写的段

  3. 以只读数据段位代表的权限为只读的段

对于相同权限的段,可以把它们(section)合并到一起当作一个段(segment)进行映射。拿前面的例子,之前8个section需要8个页,而这种方式8个section可能会被合并成2个segment,占用2个页。

segment的概念实际上是从装载的角度重新划分了ELF的各个段,在将目标文件链接成可执行文件的时候,链接器尽量把相同权限属性的段分配在同一空间,多个section变成一个segment,而系统就是按这种segment来映射可执行文件的。

segment和section是从不同的角度划分一个ELF文件,称为不同的视图,从section的角度来看ELF文件就是链接视图,从segment的角度来看ELF文件就是执行视图,当我们在谈到ELF装载时,段专门指segment,其它情况下,段指的是section。

通过readelf命令可以查看可执行文件的segment。

$ readelf -l test

Elf file type is EXEC (Executable file)
Entry point 0x400123
There are 3 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000001c0 0x00000000000001c0  R E    0x200000
  LOAD           0x0000000000001000 0x0000000000601000 0x0000000000601000
                 0x0000000000000008 0x0000000000000008  RW     0x200000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10

 Section to Segment mapping:
  Segment Sections...
   00     .text .rodata .eh_frame
   01     .data
   02

这里有很少的segment,描述segment属性的结构叫程序头,这里只需要知道ELF可执行文件中有一个专门的数据结构叫程序头表,它用来保存segment的信息,因为目标文件不需要被装载,所以没有程序头表,而可执行文件和共享库文件都有头表,他们会被用于装载,这里的各个segment都是通过匿名虚拟内存区域(VMA)来映射。

可以通过cat来查看VMA:

cat /proc/72/maps
7f445f4b0000-7f445f4c7000 r-xp 00000000 00:00 516559             /lib/x86_64-linux-gnu/libgcc_s.so.1
7f445f4c7000-7f445f4c8000 ---p 00017000 00:00 516559             /lib/x86_64-linux-gnu/libgcc_s.so.1
7f445f4c8000-7f445f6c6000 ---p 00000018 00:00 516559             /lib/x86_64-linux-gnu/libgcc_s.so.1
7f445f6c6000-7f445f6c7000 r--p 00016000 00:00 516559             /lib/x86_64-linux-gnu/libgcc_s.so.1
7f445f6c7000-7f445f6c8000 rw-p 00017000 00:00 516559             /lib/x86_64-linux-gnu/libgcc_s.so.1
7f445f6d0000-7f445f86d000 r-xp 00000000 00:00 516611             /lib/x86_64-linux-gnu/libm-2.27.so
7f445f86d000-7f445f870000 ---p 0019d000 00:00 516611             /lib/x86_64-linux-gnu/libm-2.27.so
7f445f870000-7f445fa6c000 ---p 000001a0 00:00 516611             /lib/x86_64-linux-gnu/libm-2.27.so
7f445fa6c000-7f445fa6d000 r--p 0019c000 00:00 516611             /lib/x86_64-linux-gnu/libm-2.27.so
7f445fa6d000-7f445fa6e000 rw-p 0019d000 00:00 516611             /lib/x86_64-linux-gnu/libm-2.27.so
7f445fa70000-7f445fc57000 r-xp 00000000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7f445fc57000-7f445fc60000 ---p 001e7000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7f445fc60000-7f445fe57000 ---p 000001f0 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7f445fe57000-7f445fe5b000 r--p 001e7000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7f445fe5b000-7f445fe5d000 rw-p 001eb000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7f445fe5d000-7f445fe61000 rw-p 00000000 00:00 0
7f445fe70000-7f445ffe9000 r-xp 00000000 00:00 540399             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f445ffe9000-7f445fff6000 ---p 00179000 00:00 540399             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f445fff6000-7f44601e9000 ---p 00000186 00:00 540399             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f44601e9000-7f44601f3000 r--p 00179000 00:00 540399             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f44601f3000-7f44601f5000 rw-p 00183000 00:00 540399             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f44601f5000-7f44601f9000 rw-p 00000000 00:00 0
7f4460200000-7f4460226000 r-xp 00000000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7f4460226000-7f4460227000 r-xp 00026000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7f4460427000-7f4460428000 r--p 00027000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7f4460428000-7f4460429000 rw-p 00028000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7f4460429000-7f446042a000 rw-p 00000000 00:00 0
7f4460440000-7f4460442000 rw-p 00000000 00:00 0
7f4460450000-7f4460452000 rw-p 00000000 00:00 0
7f4460460000-7f4460462000 rw-p 00000000 00:00 0
7f4460600000-7f4460601000 r-xp 00000000 00:00 205513             /mnt/d/wzq/wzq/util/test/a.out
7f4460800000-7f4460801000 r--p 00000000 00:00 205513             /mnt/d/wzq/wzq/util/test/a.out
7f4460801000-7f4460802000 rw-p 00001000 00:00 205513             /mnt/d/wzq/wzq/util/test/a.out
7fffc7576000-7fffc7597000 rw-p 00000000 00:00 0                  [heap]
7fffcf186000-7fffcf986000 rw-p 00000000 00:00 0                  [stack]
7fffcfe84000-7fffcfe85000 r-xp 00000000 00:00 0                  [vdso]

上面的我们可以先忽略,看最下面,主要有堆和栈,这两个VMA几乎在所有的进程中都存在,而[vdso]是一个内核模块,程序通过这个模块和内核进行通信。

小总结:如图【图片来自网络,侵权删】:

image.png

操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映射文件的映射成一个VMA,一个进程主要可以分成以下几种VMA区域:

  1. 代码VMA:权限只读可执行,有映射文件

  2. 数据VMA:权限可读写可执行,有映射文件

  3. 堆VMA:权限可读写可执行,无映射文件,匿名,向上扩展

  4. 栈VMA:权限可读写不可执行,无映射文件,匿名,向下扩展

Linux如何装载并运行ELF程序

Linux内核装载ELF文件主要有两步:

  1. 通过fork系统调用创建一个新的进程
  2. 通过execve系统调用执行指定的ELF文件,附带环境变量和参数
    1. 检查ELF可执行文件的有效性,比如魔数(通过魔数可以确定文件格式)、Segment的数量等
    2. 寻找动态链接的段,设置动态链接器路径
    3. 根据ELF可执行文件的程序头表描述,对ELF文件进行映射,比如代码、数据、只读数据
    4. 初始化ELF进程环境
    5. 将系统调用的返回地址修改为ELF可执行文件的入口地址。

参考资料

《程序员的自我修养:链接装载与库》

https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/virtual-address-spaces

https://blog.csdn.net/chenlycly/article/details/53367336

https://www.zhihu.com/question/290504400

https://zh.wikipedia.org/wiki/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98

https://www.cnblogs.com/Tan-sir/p/7488796.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容