Compile Link   

第一章 温故而知新

1. 内存不够怎么办

解决方法:

第二章 编译和链接

程序编译过程

第三章 目标文件里有什么

ELF 文件

#第四章 静态链接 #

  1. 链接器为目标文件分配地址和空间这句话中的“地址和空间”其实有两个含义:第一个是在输出的执行文件中的空间;第二个是在装载后的虚拟地址中的虚拟地址空间。对于.text和.data,它们在文件中和虚拟地址中都要分配空间,因为它们在这两者中都存在;而对于“.bss”这样的段来说,分配空间的意义只局限于虚拟地址空间

  2. 链接过程

空间与地址分配:扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中的所有符号定义和符号引用收集起来,统一放到一个全局符号表。

符号解析与重定位:使用上面第一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

  1. 在Linux下,ELF可执行文件默认从地址 0x08048000开始分配。

第六章 可执行文件的装载和执行

  1. 进程的建立 a. 首先是创建虚拟地址空间,实际只是分配一个页目录。 b. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系(当操作系统捕获到缺页错误时,它应当知道程序当前所需要的页在可执行文件中的位置)。 c. 将CPU指令寄存器设置成可执行文件入口(ELF文件头中保存有入口地址),启动运行。

  2. ELF文件的执行视图 ELF可执行文件中有一个专门的数据结构叫做程序头表用来保存“Segment”的信息。因为ELF目标文件不需要装载,所以它没有程序头表,而ELF的可执行文件和共享库文件都有。 readelf -l SectionMapping.elf 一个“Segment”包含一个或多个属性类似的“Section”。比如我们可以将“.text”和 “.init”合并在一起看作是一个”Segment”,那么装载的时候就可以将它们看作一个整体一起映射,也就是说映射以后在进程虚存空间中只有一个相对应的VMA,而不是两个,这样做的好处是可以很明显地减少页面内部碎片,从而节省了内存空间。

第七章 动态链接

要解决空间浪费和更新困难这个两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接(也叫延迟绑定)。也就是说,把链接这个过程推迟到运行时再进行,这就是动态链接的基本思想。

1. DSO: 动态共享对象(Dynamic Shared Objects).

2. 动态链接的优点

    a. 动态链接的方式使得开发过程中各个模块更加独立,耦合度更小,便于不同的开发者和开发组织之间独立进行开发和测试。

    b.程序在运行时可以动态地选择加载各种程序模块,这个优点就是后来被人们用来制作程序的插件。

3. 动态链接的过程

    a. 符号解析:lib.so保存的是一些符号,用于链接时候确定这个符号是静态符号还是动态符号。

    b. 共享对象的最终装载地址在编译时不确定的。即共享对象在编译时不能假设自己在进程虚拟地址空间中的位置。这就需要装载时重定位。

4. 地址无关代码

    装载时重定位是解决动态模块中有绝对地址引用的办法之一,但是它有一个很大的缺点是指令部分无法在多个进程之间共享。故希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟书记部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前被称为地址无关代码(PIC)的技术。

 a. 模块内部的函数或跳转等:采用相对地址调用,或者是基于寄存器的相对调用,对于这种指令是不需要重定位的。

 b. 模块内部数据访问:一个模块前面一般是若干个页的代码,后面紧跟着若干页的数据,这些页之间的相对位置是固定的。故我们只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。

 c. 模块间数据访问:在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用,基本机制如下图:

当指令要访问变量b时,程序会先找到GOT,然后根据GOT中变量所对应的项目找到变量的目标地址。每个变量都对应一个4个字节的地址,链接器在装载模块时为查找每个变量所在的地址,然后填充GOT中的各个项,以确保每个地址所指向的地址正确。由于GOT本身是放在数据段的,所以他可以在模块装载时被修改,并且每个进程都可以独立有副本,相互不受影响。 d. 模块间调用、跳转 对于模块间调用和跳转,我们也可以采用上面类型三的方法来解决。与上面类型有所不同的是,GOT中相应的项目保存的是目标函数的地址,当模块要调用目标函数时,可以通过GOT中的项进行间接跳转。基本原理如下图所示

如何区分一个DSO是否为PIC,下面命令有输出就是

readelf -d foo.so grep TEXTREL
ELF共享库在编译时,默认把定义在模块内部的全局变量当作定义在其他模块的全局变量,也就是前面类似四。
  1. 延迟绑定

    在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上时一种浪费。所以ELF采用了延迟绑定的做法,基本思想就是当函数第一次被用到时才进行绑定(符号查找和重定位),否则就不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大地加快程序的启动速度,特别有利于一些有大量模块的程序。

    ELF采用PLT(Procedure Linkage Table)的方法来实现延迟绑定。当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项来进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而时通过一个叫PLT项的结构来实现跳转。

6.程序的执行过程 操作系统读取可执行文件的头部,检查文件的合法性,然后从部中的“Program Header”中读取每个“Segment”的虚拟地址,文件地址和属性,并将耽误比赛的进程虚拟空间的相应位置,这些步骤跟前面的静态链接情况下的装载基本无异,在静态链接情况下操作系统接着就可以把控制权转交给可执行文件的入口地址,然后程序开始执行,一切看起来非常直观。 但是在动态链接情况下,操作系统还不能在装载完可执行文件之后就把控制权交给可执行文件,因为我们知道可执行文件依赖于很多共享对象。这时候可执行文件里对于很多外部符号的引用还处于无效率地址的状态,即还没有跟相应的共享对象的实际位置链接起来。所以在映射完可执行文件之后,操作系统会先启动一个动态链接器。 在linux下,动态链接器ld.so实际上是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间中。操作系统在加载完动态链接,随后就将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址)。当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作,然后根据当前的环境参数开始对可执行文件进行动态链接工作。当所有的动态链接工作完成以后,动态链接器会将控制权转交给到执行文件的入口地址路,程序开始执行。

  1. 动态链接时进程堆栈初始化信息

    在进程初始化的时候,堆栈里面保存了关于进程执行环境和命令行参数等信息。事实上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组。
    
  2. 全局符号介入

一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象又被称为共享对象全局介入。由于存在这种重名符号被直接覆盖的问题,当程序使用大量共享对象时应该非常小心符号的重名问题,如果两个符号重名又执行不同功能,那么程序运行时可能会将所有该符号名的引用解析到第一个被加入全局符号表的使用该符号名的符号,从而导致程序莫名其妙的错误。

  1. 运行时加载

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接,有时候也叫做运行时链接。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。而且一般的共享对象不需要进行任何修改就可以进行运行时装载,这种共享对象往往被叫做动态装载库。主要包括以下函数:

dlopen/dlsym/dlerror/dlclose

第八章 共享库的查找过程

  1. 共享库系统路径 目前大多数包括Linux在内的开源操作系统都遵守一个叫做FHS的标准,这个标准规定了一个系统文件应该如何存放,包括各个目录的结构、组织和应用。 a. /lib,这个位置主要存放系统最关键和基础的共享库,比如动态链接器、C语言运行库、数学库等,这些库主要是那些/sbin和/sbin下的程序所用到的库,还有系统启动时需要的库。 b./usr/lib,这个目录下主要保存的是一些非系统运行时所需要的关键性的共享库,主要是一些开发时用到的共享库。 c./usr/local/lib,这个目录主要是用于放置一些跟操作系统本身并不十分相关的库,主要是一些第三方的应用程序的库。

  2. 环境变量 a. LD_LIBRARY_PATH,进程在启动时,动态链接器在查找共享库时,会首先查找该变量指定的目录。这个环境变量可以很方便地让我们测试新的共享库或使用非标准的共享库。 b. LD_DEBUG,这个变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,比如我们如下设置: $LD_DEBUG=files ./HelloWorld.out

  3. 共享库的创建 a. -shared 表示输出结果是共享库类型的; b. -fPIC 表示使用地址无关代码(Position Independent Code)技术来生产输出文件。 c. -W1 这个参数可以将指定的参数传递给链接器。

第十章 内存

  1. 程序的执行环境 内存、运行库、系统调用

  2. 段错误(segment fault)原因如下: a. 程序员将指针初始化为NULL,之后却没有给它一个合理的值就开始使用指针。 b. 程序员没有初始化栈上的指针,指针的值一般会是随机数,之后就直接开始使用指针。

  3. 栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧,包含如下信息: a. 函数的返回地址和参数。 b. 临时变量:包含函数的非静态局部变量以及编译器自动生成的其他临时变量。 c. 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

参考资料

Linux下缩小可执行程序: http://blog.sina.com.cn/s/blog_602f87700100t0t5.html