读《程序员的自我修养 —— 装载与动态链接》乱摘

2016.05.14 – 05.18
《程序员的自我修养 —— 链接、装载与库》的“装载与动态链接”部分。
- 余甲子 石凡 潘爱民编
个人选读笔记 - 学点表皮。

05.14
PART II 装载与动态链接

1 可执行文件的装载与进程

1.1 进程虚拟地址空间的大小

每个进程拥有自己独立的虚拟地址空间,该虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的(地址线 —— C语言中的指针所占空间)。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,如32位的硬件平台决定了虚拟地址空间的地址为0到 2321 ,即0x00000000 ~ 0xFFFFFFFF(4GB)。

程序在运行时处于操作系统的监管下,操作系统为了达到监管程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的虚拟地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。[如在C语言程序中,引用了0x00000000虚拟地址,该虚拟地址不在操作系统分配给该程序运行时的虚拟地址空间之内(0x00000000这个虚拟地址映射了一个物理内存地址,该物理地址保存了操作系统或其他进程中的内容;或者说0x00000000这个虚拟地址在虚拟地址层面就是用作操作系统的虚拟地址的),故而会引起操作系统的干涉]

Linux进程虚拟空间分布(虚拟地址空间层面的分布/规定
这里写图片描述

[虚拟地址空间大小跟物理内存空间大小不必相等;管理好虚拟空间和物理内存空间的映射关系即可]

1.2 程序动态装载 —— 页映射

程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的方法就是将程序运行所需要的指令和数据全都装入内存中,这样程序就可以顺利运行,这就是最简单的静态装载。但是很多情况下程序所需要的内存数量大于物理内存的数量,当内存的数量不够时,根本的解决办法就是添加内存。欲在不添加内存的情况下让更多的程序运行起来,尽可能有效地的利用内存,就要利用程序运行时的局部性原理,即将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘里,等需要用这些数据时再将该部分载入内存中(覆盖原在内存中但已不用的部分),这就是程序动态装载的基本原理 —— 利用程序的局部性原理;用到哪个模块就将哪个模块载入内存,如果不用就暂时不装入而存放在磁盘中。

页映射将内存和所有磁盘中的数据和指令按照“页(Page)”为单位划分成若干个页,以后所有的装载和操作的单位就是页。目前来看,硬件规定的页的大小有4096字节、8192字节、2MB、4MB等,最常见的Intel IA32处理器都是用4Kb的页。

页映射机制图简示
这里写图片描述

1.3 描述虚拟地址空间数据结构和页错误

这里写图片描述

1.4 进程虚存空间分布

在Linux下,可用以下方式查看进程虚拟空间分布:
./elf &  
[1] 21963
vi /proc/21963/maps

(1) ELF文件链接视图和执行视图
ELF文件中,段的权限往往只有为数不多的几种组合,基本上是三种:

  • 以代码段为代表的权限为可读可执行的段。
  • 以数据段和BSS段为代表的权限为可读可写的段。
  • 以只读数据段为代表的权限只读的段。

对于相同权限的段,把它们合并到一起当作一个段映射。ELF可执行文件引入一个叫做“Segment”的概念 —— 包含属性类似的多个“Section”(Segment实际上是从装载的角度重新划分了ELF的各个段),映射(虚拟页面)时以“Segment”为单位进行映射。
这里写图片描述

在将目标文件链接成可执行文件的时候,链接器会尽量把相同权限属性的段分配在同一空间。比如可读可执行的都放在一起,这种段的典型是代码段;可读可写的段都放在一起,这种段的典型是数据段。把在ELF中这些属性相似的、又连在一起的段叫作一个“Segment”(程序头),而系统正是按照“Segment”而不是“Section”来映射可执行文件的。描述“Segment”的结构叫程序头,它描述了ELF文件该如何被操作系统映射到进程的虚拟空间(readelf -l *)[ELF文件中有一个专门的数据结构 —— elf.h中的Elf32_Phdr来描述程序头表(保存segment的信息)]。从Section角度来看ELF文件就是链接视图,从“Segment”的角度来看就是执行视图

(2) 堆和栈
在操作系统里面,VMA(Virtual Memory Area,由多个页组成)除了被用来映射可执行文件中的各个“Segment”以外,它还可以有其他的作用,操作系统通过使用VMA来对进程的地址空间进行管理。进程在执行的时候还需要用到栈、堆等空间,事实上它们在进程的虚拟空间中也是一VMA存在的 —— 操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间;基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为如下几种VMA区域:

  • 代码VMA,权限只读、可执行。
  • 数据,权限可读写、可执行。
  • 堆VMA,权限可读写、可执行。
  • 栈VMA,权限可读写、不可执行。
  • vdso VMA,它是一个内核模块,进程可以通过访问这个VMA来跟内核进行一些通信。

这里写图片描述
[VMA含义图示]

这里写图片描述
[ELF与Linux进程虚拟空间映射关系]

(3) 进程栈初始化
进程刚开始启动的时候,须知道一些进程运行的环境,最基本的就是系统环境变量和进程的运行参数。很常见的一种做法是操作系统在进程启动前将这些信息提前保存到进程的虚拟空间栈中。假设系统中有两个环境变量:
HOME=/home/user
PATH=/usr/bin
若运行该程序的命令是:prog 123
且假设堆栈底部地址为0Xbf802000,那么进程初始化后的堆栈如下图所示。
这里写图片描述
进程在启动以后,程序的库部分会把栈里的初始化信息中的参数信息传递给main()函数,即main()函数中的两个argc和argv两个参数(命令行参数数量和命令行参数字符串指针)。

05.15

1.5 Linux内核装载ELF过程简介

用户状态。bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

内核状态。新的进程在进入execve()系统调用之后,Linux内核就开始进行真正的装载工作。在内核中,execve()的入口是sys_execve()[arch\i386\kernel\Process.c]。sys_execve()进行一些参数检查复制后,调用do_execve()。do_execve()检查文件的前128个字节(特别是前4个字节 —— 魔数)判断可执行文件格式,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并调用相应的装载处理过程[如ELF可执行文件的装载处理过程叫load_elf_binary()]。load_elf_binary()的主要步骤是:

  1. 检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(segment)的数量。
  2. 寻找动态链接的“.interp”段,设置动态链接器路径。
  3. 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
  4. 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI地址。
  5. 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,

这个程序入口就是ELF文件的文件头中e_entery所指的地址;对于动态链接的ELF可执行文件,程序入口点事动态链接器。
当load_elf_binary()执行完毕后,返回至do_execve()再返回至sys_execve()时,上面的第5)步中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。所以当sys_execve()系统调用从内核状态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完毕。

2 动态链接

这里写图片描述
[静态链接时,库文件在内存/磁盘中的副本]

这里写图片描述
[动态链接时,库文件在内存/磁盘中的副本]

(模块)。在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体;但在动态链接下,一个程序被分成了若干文件,有程序的主要部分,即用户编写的程序和程序所依赖的共享对象,可以把这些部分称为模块,即动态链接下的可执行文件和共享对象都可以看作是程序的一个模块。

静态链接输出一个可执行文件;动态链接将可执行程序分成了多个模块 —— 程序文件的目标文件和动态链接库(可以这样认为:它们在装载时会被动态链接器链接成一个可执行文件)。

为解决空间浪费和更新困难两个问题的一个办法是把程序的模块相互分割开来,形成独立的文件,不再将它们静态地链接在一起。简单的讲,即不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接 —— 将链接这个过程推迟到运行时再进行,这就是动态链接的基本思想。在Linux系统中,ELF动态链接文件被称为(动态)共享对象(DSO,Dynamic Shared Objects),它们一般都是以“.so”为扩展名的文件;在Windows中,动态链接文件被称为动态链接库(Dynamical Linking Library),它们通常以“.dll”的文件形式存在。

2.1 Linux下一个简单的动态链接例子

(1) 动态符号
动态共享对象的生成。
这里写图片描述
用[gcc -fPIC -shared -o bk_lib.so bk_lib.c]命令生成bk_lib.so共享对象。bk_lib.so保存了完整的符号信息(运行时动态链接还需使用符号信息),把bk_lib.so作为链接的输入文件之一,链接器在解析符号时就可以知道:foobar是一个定义在bk_lib.so的动态符号。这样链接器就可以对foobar的引用做特殊的处理,使它成为一个对动态符号的引用。

C源程序。
这里写图片描述
用[gcc -o bk_program1 bk_program1.c ./bk_lib.so],[gcc -o bk_program2 bk_program2.c ./bk_lib.so]两个命令生成可执行相应的可执行程序。

(2) 动态链接程序运行时虚拟地址空间分布
这里写图片描述
用户程序(bk_program1)和共享库(bk_lib.so,libc-2.15.so)以及动态链接器(ld-2.15.so)都被操作系统映射到了进程的虚拟地址空间。在系统开始运行bk_program1之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给bk_program1,然后开始执行

这里写图片描述
这里写图片描述
[共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象]

3 Linux共享库的组织

05.18
共享库版本。Linux有一套规则来命名系统中的每一个共享库,它规定共享库的文件名规则必须如下:
libname.so.x.y.z。最前面使用前缀“lib”、中间是库的名字和后缀“.so”,最后面跟着的是三个数字组成的版本号。“x”表示主版本号,“y”表示次版本号,“z”表示发布版本号。主版本号表示库的重大升级,不同主版本号的库之间是不兼容的,依赖于旧的主版本号的程序要改动相应的部分,并且重新编译,才可以在新版的共享库中运行;或者,系统必须保留旧版本的共享库,使得那些依赖于旧版共享库的程序能够正常运行。次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。发布版本号表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行更改。[诸如C语言库也不使用这种规则]

共享库系统路径。目前大多数包括Linux在内的开源操作系统都遵守一个叫做FHS(File Hierarchy Standard)的标准,这个标准规定了一个系统中的系统文件应该如何存放。共享库作为系统中重要文件,它们的存放方式也被FHS列入了规定范围。FHS规定,一个系统中主要有3个存放共享库的位置:

  • /lib,这个位置主要存放系统最关键和基础的共享库,比如动态链接器、C语言运行库、数学库等,这些库主要是那些/bin或/sbin下的程序所要用到的库,还有系统启动时需要的库。
  • /usr/lib,这个目录下主要保存的是非系统运行时所需要的关键性的共享库,主要是一些开发时用到的共享库,这些共享库不会被用户的程序或shell脚本直接用到。这个目录还包含了开发时可能会用到的静态库、目标文件等。
  • /usr/local/lib,这个目录用来存放本身跟操作系统并不十分相关的库,主要是一些第三方应用程序的库。

共享库查找过程。在Linux系统中,程序所依赖的共享对象全由动态链接器负责装载和初始化。ELF中任何一个动态链接的模块所依赖的模块路径保存在“.dynamic”段里,由DT_NEED类型的项表示。动态链接器对于模块的查找有一定的规则:如果DT_NEED保存的是绝对路径,那么动态链接器就按照这个路径去查找;如果DT_NEED保存的是相对路径,那么动态链接器会在/lib、/usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库。ld.so.conf是一个文本配置文件,它可能包含其它的配置文件,这些配置文件内包含着目录信息。Linux系统中有一个叫ldconfig的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的符号链接(SO-NAME),这样每个共享库的符号链接就能正确的指向共享库文件;该程序还会将这些符号链接搜集起来,集中存放到/etc/ld.so.cache里,并建立一个符号链接缓存。当动态链接器查找共享库时,可以直接从ld.so.cache里查找(ld.so.cahce文件内容的组织结构对查找很高效)。

SO-NAME。SO-NAME由共享库的文件名去掉次版本号和发布版本号组成,它是一个软链接 —— 指向目录中主版本号相同、此版本号和发布版本号最新的共享库。
链接名。当在编译器中使用共享库的时候,只需要在编译器中的参数中指定共享库的名字即可。

共享库的创建和安装
创建:使用“gcc + 适当的参数”。
安装:ldconfig工具。

4 余留

老爷子居然需要再读一遍来理解动态链接(虚拟地址空间的作用)。

[2016.05.13 - 21:19]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值