《程序员的自我修养--链接、装载与库》 -- 阅读笔记

《程序员的自我修养-链接、装载与库》– 笔记

前言

只挑了几张章看。

第一章

  1. 总线分为南桥北桥。北桥用于连接高速设备;南桥用于连接低速的设备,比如键盘、鼠标、usb等,由南桥将它们汇总后连接到北桥上。
  2. SMP:对称多处理器。简单说就是每个CPU在系统中所处的位置和所发挥的功能都是一样的,是相互对称的。实际上,速度的提高不能与CPU的数量完全成正比,不一定所有的程序都能分解成若干个不相干的子问题。
  3. 多核处理器:将多个处理器打包,共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器进行外包装。
  4. 系统软件:传统意义上的系统软件是只管理计算机本身的软件,以区分普通的应用程序。系统软件分为平台性的和用于程序开发的。平台性的包括操作系统、驱动程序、运行库和系统工具;用于程序开发的包括编译器、链接器、汇编器等开发工具和开发库。
  5. 计算机体系结构中每个层次之间通信的协议称为接口。除了应用程序和硬件,其他都是属于中间层,中间层是对下面那层的包转和扩展。
  6. 运行库使用操作系统提供的系统调用接口,系统调用接口在实际中往往以软件中断来实现。
  7. 操作系统内核层使用硬件提供的接口与硬件进行通信,这种接口称作硬件规格。
  8. 多任务系统:所有的应用都以进程的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间。
  9. CPU分配方式:在多任务操作系统中,CPU由操作系统统一进行分配,每个进程根据优先级的高低都有机会获得CPU,如果一个进程运行超过了一定时间,则操作系统就会暂停该进程,将CPU资源分配给其他进程,这种CPU分配方式成为抢占式。
  10. 硬盘的基本存储单位是扇区,每个扇区一般是512字节。一个硬盘有多个盘片,每个盘片有两面,每面按照同心圆划分为若干个磁道,每个磁道有若干个扇区。那么一个硬盘的大小为2x盘片数x磁道数x扇区数x单个扇区存储字节数 = 总的字节。
  11. MMU(Memory Management Unit): 内存管理单元,CPU发出的是VA(Virtual Address),经过MMU转换得到PA(Physical Address)。一般MMU都集成在CPU内部。
  12. 线程: 又称为轻量级进程(LightWeight Process,LWP)。线程可以访问进程内存中的数据,同时线程也有自己的私有空间,包括栈、线程局部存储、寄存器。线程可以共享进程的文件,线程A打开的文件可以由线程B读写。
  13. 只有在线程数小于处理器数的情况下,线程的并发才是真正的并发,这个时候所有的线程运行在不同的处理器上。在单处理上,线程的并发是模拟出来的状态。操作系统会让线程程序轮流执行,每次每个线程执行一小段时间。这样的一个不断在处理器上切换不同线程的行为称为线程调度。
  14. 线程调度中,线程通常至少有三种状态。分别是运行、就绪、等待。
  15. IO密集型线程:频繁等待的线程;CPU密集型线程:很少等待的线程,频繁的进行大量的计算的线程。IO密集型线程总是比CPU密集型线程容易得到优先级的提升。
  16. 线程优先级改变的方式:用户指定优先级、根据等待状态的频繁程度提升或降低优先级、长时间得不到执行而被提升优先级。
  17. fork产生的新任务的速度非常快,因此fork并不复制原任务的内存空间,而是与原任务共享一个写时复制(COW)的内存空间。
  18. fork只能产生本任务的镜像,因此必须要使用exec配合才能启动别的新任务。
  19. fork和exec通常用于产生新任务,clone常用于产生新线程。
  20. 单指令操作称为原子的,单指令的执行是不会被系统中断的。在i386系统中inc指令可以直接进行加一操作。
  21. 同步:是指在一个线程访问数据未结束的时候,其他线程不得对同意数据进行访问。同步常用的方法是加锁,线程在访问共享数据时,先获取锁,访问结束后释放锁。
    1. 二元信号量是最简单的一种锁,具有两种状态:占用和非占用。但它只适合被唯一一个线程独占访问的资源。
    2. 对于允许多个线程并发访问的资源,通常使用多元信号量简称信号量。一个初始值为N的信号量允许N个线程并发访问。
    3. 互斥量和二元信号量相似,都只适合被唯一个线程独占的资源。区别在于信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求由获取的线程负责释放。
    4. 临界区:临界区和前面三种的区别在于只允许本进程访问。
    5. 读写锁:读写锁有两种获取方式:共享和独占。
    6. 条件变量:一个条件变量可以被多个线程等待。使用条件变量可以让多线程一起等待某个事件的发生,当事件发生时,所以线程被唤醒并恢复执行。
  22. 线程安全
    CPU乱序执行导致线程安全的保障工作变得异常艰难,阻止CPU换序时必须的,现在并不存在可移植的阻止换序的方法。通常情况下时调用CPU一条barrier指令,一条barrier指令可以阻止CPU将该指令之前的指令交换到barrier之后。

第二章 编译和链接

  1. 源文件到可执行文件经历的四个过程:
    1. 预编译:主要处理源代码文件中以“#”开始的预编译指令,生成.i或.ii文件。预编译后的.i文件不包含任何宏定义,因为所有的宏定义已经被展开,并且包含的文件也被插入到.i文件中。所以当无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件内存来定位问题。
    2. 编译:该过程就是将预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。
    3. 汇编:汇编器是将汇编代码文件转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
    4. 链接:将所有相关的目标文件链接成最后的可执行文件。
  2. 语义分析:语义分析包含静态语义动态语义。静态语义是指在编译期可以确定的语义。动态语义是指在运行期才能确定的语义。
  3. 中间代码:是语法树的顺序表示,非常接近目标代码。中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后段将中间代码转换成目标机器代码。
  4. 代码生成器:代码生成器将中间代码转换成机器代码。
  5. 目标代码优化器:目标代码优化器对机器代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余指令等。
  6. 链接:链接过程包括了地址和空间重定位、符号决议和重定位等步骤。
    1. 重定位:重新计算各个目标的地址过程称为重定位。
    2. 符号决议:符号用来表示一个地址,这个地址可能是一段子程序(函数)的起始地址,也可以是一个变量的地址。符号决议也叫做符号绑定、地址绑定、指令绑定等。在静态链接中一般叫做符号决议,在动态链接中一般叫做符号绑定。
  7. 静态链接:最基本的静态链接过程为:每个模块的源文件经过编译器生成目标文件,然后目标文件和库一起链接成最后的可执行文件。最常见的库是运行时库,及是支持程序运行的基本函数的集合。

    第三章

  8. 目标文件的格式:windows下的PE和Linux下的ELF,他们都是COFF格式的变种。目标文件与可执行文件一般采用一种格式存储。动态链接库和静态链接库文件都按照可执行文件存储。
    1. 文件头:描述整个文件属性
    2. 代码段:源代码编译后的机器指令经常被放在代码段,.code或.text。
    3. 数据段:存储已经初始化的全局变量和局部静态变量数据,.data。
    4. .bss:存储未初始化的全局变量和静态局部变量。.bss段只是为未初始化的全局变量和局部静态变量预留出位置,并没有内容,所以在文件中不占据空间。
  9. 链接过程时只关心全局符号的相互结合,而局部符号、段名、行号等都是对其他文件不可见的,在链接过程中也是无关紧要的。

    第四章 静态链接

  10. 链接器将多个目标文件链接成最后的可执行文件,目前通常都是将相似段合并的链接方式,采用这种方式的链接器都采用一种两步链接的方法。即第一步进行空间与地址分配:链接器将会获取目标文件的段长度,并且将它们合并,计算出输出文件中哥哥段合并后的长度,并建立映射关系。第二步是符号解析与重定位,这步是链接过程中的核心,主要是利用第一步收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析和重定位、调整代码位置等。
  11. VMA:虚拟内存地址。LMA:加载地址。通常情况下这两种地址是相同的。但是在一些嵌入式系统中,是不相同的。
  12. 在链接前,目标文件中的所有段的虚拟地址都是0,因为虚拟空间还没分配。当链接之后,可执行文件所有段都被分配到了相应的虚拟地址。
  13. 在Linux下,ELF可执行文件默认从地址0x08048000开始分配。
  14. 重定位
    1. 0xE8是近址相对位移调用指令的操作码,后面四个字节是被调函数的相对于调用指令的下一条指令的偏移量。
    2. 重定位表:ELF文件中的重定位表保存与重定位相关的信息。对于可重定位的ELF文件来说,必须包含可重定位表。对于每个需要重定位的ELF段都有一个对应的重定位表,而一个重定位表往往也是一个ELF文件中的段。比如.text段里有需要重定位的地方,那么就会有一个相应的叫.rel.text的段保存了代码段的重定位表。
    3. 每个要被重定位的地方叫做一个重定位入口。重定位入口的偏移表示该入口在被重定位的段中的位置。
  15. 指令修正方式:绝对寻址修正和相对寻址修正的区别在于就是绝对寻址修正后的地址为符号的实际地址;相对寻址修正后的地址为符号距离被修正位置的地址差。
    1. 绝对寻址修正:修正后的地址的计算方式为A+S(A为保存在被修正位置的值,S为符号的实际地址)。
    2. 相对寻址修正:修正后的地址计算方式为A+S-P(P为被修正的位置,即相对于段开始的偏移量或虚拟地址)。
  16. COMMON块:COMMON机制的原因是编译器和链接器允许不同类型的弱符号存在,最本质的原因还是链接器不支持符号类型。COMMON类型的链接规则是针对符号都是弱符号的情况。当有多个符号名相同的多个弱符号存在时,链接后的符号的大小为占空间最大的类型相同。
  17. 思考:在目标文件中,编译器为什么不直接将为初始化的全局变量,为它在BSS段分配空间,而是将其标记为COMMON类型的变量?:当弱符号存在多个文件中时,当编译器将每个文件编译成多个目标文件,弱符号在每个文件中的类型不相同时,锁占的空间也不相同,所以当编译器在编译单个模块时,无法知道最终弱符号所占空间的大小。所以编译器无法在BSS段为其分配空间。但是在链接过程中,链接器读取所有的输入目标文件后,每个弱符号的大小都可以确定,所以链接器在最终输出文件的BSS段为其分配空间。所以,总的来说,为初始化的全局变量还是放在BSS段。
  18. ABI:(Application Binary Interface)是指符号修身标准、变量内存布局、函数调用方式等与可执行代码二进制兼容相关的内容。影响ABI的因素有很多,硬件、编程语言、编译器、链接器、操作系统等。
  19. 静态库:可以简单的看作是一组目标文件的集合。
  20. BFD库:(Binary File Discriptor)是一个GNU项目,目的是希望通过统一的借口来处理不同格式目标文件格式。

第六章 可执行文件的装载与进程

  1. PAE:(Physical Address Extension)
  2. 动态装载:覆盖装入和页映射是两种典型的动态装载方法。都是利用了程序局部性原理。页映射的方式就是将内存和磁盘中的数据和指令按照“页”为单位进行划分,以后所有的装载和操作的单位都是页。
  3. 在链接时,链接器会尽量把相同权限属性的段分配在同一空间。比如.text段的属性是可读可执行,.init段的属性也是可读可执行,在链接的时候就会将这两个section放在一起,组成一个“Segment”,一个“Segment”包含了一个或多个属性类似的“Section”。这样做的好处是可以有效的减少页面内部碎片。在ELF中把这些属性相似、又连在一起的“Section”叫做一个“Segment”,而系统正是按照“Segment”来映射可执行文件的。
  4. “Segment”和“Section”是从不同的角度来划分同一个ELF文件。从“Section”的角度来看ELF文件就是链接视图;从“Segment”角度来看是执行视图。在ELF装载时,“段”专门指“Segment”;在其他情况下,“段”指”Section“。

    第七章 动态链接

  5. 共享对象的最终装载地址在编译时是不确定的。而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给进程的共享对象。
  6. 实现共享对象任意地址装载:使用装载时重定位,而不是链接时重定位。即链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时进行。一旦共享模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址的引用进行重定位。比如program1需要用到的共享对象libc.so。program1要用到libc.so中的foobar函数,且相对于代码段起始地址为0x100,当libc.so装载到0x10000000时,假设代码段位于模块的最开始处,则foobar函数的地址为0x10000100。此时系统将遍历重定位表,将所有对foobar的地址引用都重定位到0x10000100。
  7. 地址无关代码(PIC, Position-Independent Code):装载时重定位无法解决共享对象指令中对绝对地址的重定位问题。地址无关代码技术是指将指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样就可以保证指令部分不变,数据部分在每个进程中都拥有一个副本。从而可以实现程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变。
  8. 延迟绑定:在函数第一次被用到时才进行绑定(符号查找、重定位等)。ELF使用PLT来实现。
  9. ELF将GOT分成了两个表,一个是.got,一个是.got.plt。.got用来保存全局变量引用的地址。.got.plt用来保存函数引用的地址。
  10. .interp段:(interpreter, 解释器)。动态链接器的位置是由ELF文件的.interp段来指定的。ELF文件中的interp段保存了动态连接器的路径。
  11. 全局符号介入:一个共享对象里面的全局符号被另外一个共享对象的同名全局符号覆盖的现象被称为全局符号介入。Linux下的动态链接器规定,当一个符号加入全局符号表时,如果相同的符号已经存在,则后面加入的符号被忽略。
  12. 动态连接器本身是静态链接的,因为它不能依赖于其他共享对象,动态链接器本身是用来帮助其他ELF文件解决共享对象依赖问题的,如果自身也依赖共享对象,那谁来帮他解决共享对象依赖呢,就会进入一个死循环,所以它本身不依赖其他共享对象。
  13. 动态链接器本身是PIC的吗?:理论上说是不是PIC对动态链接器来说并不关键。但通常是PIC的,使用PIC会更加简单一些。
  14. 动态链接器和普通的共享对象的装载地址一样,没有什么区别,内核在装载它是会为其选择一个合适的装载地址。