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

2016.05.14 –

《程序员的自我修养 —— 链接、装载与库》的装载与动态链接部分。

- 余甲子 石凡 潘爱民编

个人选读笔记 - 学点表皮。

05.14

PART II 装载与动态链接

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

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

每个进程拥有自己独立的虚拟地址空间,该虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的(地址线 —— C语言中的指针所占空间)。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,如32位的硬件平台决定了虚拟地址空间的地址为0到232–1,即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工具。

3 Linux共享库的组织

4 Windows下的动态链接

5 余留

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

[2016.05.13 - 21:19]

时间: 2024-10-25 12:21:31

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

程序员的自我修养-装载、链接与库【原创】

读了这本书之后,对于最基本的程序的编译.链接与装载,还有运行库.API.页映射.VMA等很多基本的概念有了拨云见雾的感觉,确实是一本好书.搞C++开发也近五年了,读了这本书之后发现自己对很多基础知识还是比较薄弱,还有待加强,打好地基才能撑得起高楼大厦.正好最近有在学思维导图,所以就用XMind画了一幅读书笔记,以便自己日后回忆,也共享出来,希望对其他想读或读过这本书的同学一点帮助! (这里传不了附件, 只能上传图片了,xmind文件放到我文件列表了,有需要的可以下载.http://files.c

读《程序员的自我修养》感受

这书不错,链接-装载-库 我觉得是很底层的东西.比如很多人闭着眼睛都能写出来的hello world(当然不包括brianfuck,如果你会,你真的闹残了吗= =), 其实链接编译器做了很多,不然就哪来的printf,这IO初始化也是CRT(c runtime)库完成的.堆栈的初始化,还有系统装载让程序运行等等.涉及很多. 书里后面就讲了一个CRT库,自己写一个,感觉不错,学了很多.比如malloc,free的实现,话说还是跨平台的.当然库很小,功能不多,不过写这个也可以学学算法.内存的分配,这

程序员的自我修养—链接、装载与库pdf

下载地址:网盘下载 内容简介 编辑 <程序员的自我修养:链接.装载与库>对装载.链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书.同时,还可作为各行业从事软件开发的工程师.研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材. 媒体评论 编辑 这是一本深人阐述链接.装载和库等问题的优秀图书,读来让人愉悦,你从巾可以清晰地了解程序的前世今生,彻底理解敲人的代码如何变成程序任系统中运行.通读本书不管对于开发

《程序员的自我修养》第三章学习笔记

1,  编译器编译源代码生成的文件叫做目标文件. 从结构上说,是编译后的可执行文件,只不过还没有经过链接 3.1 目标文件的格式 1,可执行文件的格式: Windows下的PE  和   Linux下的ELF 2,从广义上说,目标文件与可执行文件的格式几乎是一样的,所以广义上可以将目标文件与可执行文件看成是一种类型的文件. 3,可执行文件,动态链接库,静态链接库都按照可执行文件格式存储(Windows下是 PE-COFF格式,Linux下是ELF格式). 4,Linux下命令: $: file 

读书笔记:程序员的自我修养-----第一章(综述)

题前:30--45天读完,一周至少3篇读书笔记.不能坚持,不再联系,不再找你. 一. hello world 程序引出的问题,看40天后,再回来看看自己的答案,提升多少. Q1:程序为什么要被编译器编译之后才可以运行?   A1 : 系统执行的机器语言,即二进制文件,程序是文本文件需要编译之后,由链接器链接需要的基本库生成二进制文件. Q2: 编译器在把C语言程序转换成可以执行的机器码的过程中作了什么,怎么做的?   A2: 预处理,汇编器生成汇编文件,编译器生成目标文件,链接器链接生成可执行文

程序员的自我修养笔记

1,为什么内存需要分段和分页机制? 早起的计算机中,程序都是直接运行在物理内存上的.这样做有几个问题: 1)地址空间不隔离,计算机的安全性和稳定性没有办法保证,由于所有的程序都可以访问物理内存,恶意的程序可以很容易修改其他程序的内容,达到破坏的目的. 2)内存使用效率低,当前执行的程序(列入进程A)必须被整个装载到内存中执行,如果需要执行另一个程序时,发现内存空间不足,则需要将进程A的数据整体换出到磁盘. 3)程序运行的地址不确定 为了解决上述的三个问题,引入了虚拟地址.分段和分页的概念. 有了

《程序员的自我修养》读书笔记 -- 第三章

第三章 目标文件里有什么 3.1 目标文件的格式 1.目标文件就是源代码编译后还未进行链接的中间文件.因为目标文件与可执行文件的内容和结构很相似,所以一般跟可执行文件的存储形式相同,Linux下统称为ELF可执行文件.动态链接库与动态链接库也使用可执行文件格式存储. 2.ELF文件标准里面把ELF文件归为4类: l  可重定位文件(这类文件包含代码和数据,可被用来链接成可执行文件,静态链接库属于此类.如linux下的.o文件和windows下的.obj ) l  可执行文件(这类文件包含可直接执

一个程序员的自我修养

在网上看到一篇程序员的自我修养,深以为然,不禁摘录一些,勉励自己 一个好的开发人员,应该能够全面.高效.严谨的去处理任何软件程序和业务问题,成为一个好的开发,是一个很有意思的话题,不过无论这个话题如何开展,基础两个字必不可少,虽然代码量是衡量开发能力的重要指标,但仅能够熟练的进行代码编写是不够的,更要能深刻的理解技术原理和业务逻辑,扎实的个人基础和技术基础往往会促进代码的编写,更游刃有余的解决问题. 下面说的一些基础,可能绝大部分开发人员都不会在意甚至忽略,但恰恰这些才是开发大厦的基石. 1.科

程序员的自我修养:(1)目标文件

程序员的自我修养:(1)目标文件 1.目标文件 1.1 编译与链接 在使用像Visual Studio或Qt Creator等IDE时,通常有一个叫做"构建"的按钮.当编辑完成要运行和测试时点一下它,程序就能跑起来了,所以我们很少关心编译和链接.其实,编译和链接合并在一起就称为 构建(Build).简单的一次按键,实际背后却是异常复杂的过程: 预编译(Preprocessing) 编译(Compilation) 扫描:算法类似有限状态机(FSM),将字符转换成Token. 语法分析:分