CSAPP阅读笔记(5)-连接

  这个连接是指程序在编译完成后的连接过程。按书中的说法,了解程序源代码转化为二进制可执行文件的过程,有助于程序员构建大型项目。从我自己的角度来看,了解这些算是对程序有了更进一步的认识,也算是编译原理的后一部分延续。这一章名为连接,实质上读完了使人对整个的编译连接过程以及程序如何运行有了初步的理解。废话不多说,开始。
  (本章所用例子均处于Unix类操作系统中)
  我们知道,像C++这种编译型语言,源代码转化的过程一般为以下三步:预编译、编译、连接。第一步将各种预编译头替换为实质代码或数值,例如#include<stdio.h>,就会将stdio.h文件内容替换到当前引用的源码中来。第二步,将第一步的生成结果作为输入,用编译器以单个的源文件为单位,生成可重定位的目标文件,这种文件已经是二制格式了,但仍不可直接运行。第三步,连接,这一步将工程内各个分散的可重定位目标文件连接到同一个文件中,当然需要有些特殊的处理过程(后面详细说明),然后加入可执行程序必须具备的头部信息。经此三步,一个真正的可执行程序就产生了。
  第一步,先了解下Unix平台下可重定位目标文件格式和可执行目标文件格式。如下图所示:
(a)可重定位的目标文件格式
(b)可执行的目标文件格式
  从(a)(b)这两张图可以看到,两种格式大体是相同的,都具有.text .rodata .data .bss .symtab .debug .line .strtab段,ELF头部和节头部表。不同的是,(a)中多了.rel.text和.rel.data这两部分,rel其实是重定位的英文缩写,表明这两个节中包含了与重定位相关的信息,也就是一些外部数据引用或函数引用等的一些信息,在连接时会使用到。在(b)中,多了.init这一节,这里面是程序初始化时运行的一段代码,这里面大致包括一些加载程序以及main的入口等。
  关于各节的用途说明如下:
  .text: 存放程序的代码部分
  .rodata:存放只读数据,例如常量字符串
  .data: 初始化的全局变量,占据一定的磁盘空间。例如,int a[100000] = {0};
  .bss:  未初始化的全局变量,不占据磁盘空间。例如,int a[1000000];
  .symtab:符号表,存储函数或者全局变量的定义和引用
  .rel.text:.text中出现的需要在连接时确定其引用位置的函数引用
  .rel.data:需要在连接时确定的全局变量引用
  .debug: debug版创建,包括局部变量以及源代码等信息
  .line:  debug版创建,记录生成代码与源代码之间的映射关系
  .strtab: 存储.symtab和.debug中出现的字符串
  关于各节在进程虚存空间中的映射关系如下图所示:
  如图所示,基本上进程中用户空间可用的地址范围占据全部的75%,0xc0000000~0xffffffff为内核空间,内核空间用户代码是不允许访问的,所以有时候我们在运行程序时出现指针非法操作0xc0000005的错误,就是这个原因。注意下0x08048000往上是只读的段,包括.text .init .rodata这些节,之后是可读写的.data .bss这些全局变量,再往上就是堆空间,而栈空间从0xc0000000-1往下增长。此外,0x40000000往上是共享库(也就是动态库)加载的地址空间。由此可见,堆空间和栈空间被共享库给隔开,所以从这一层面说栈会耗尽堆空间是不对的。(一般来说栈和堆是相生的,即此消彼长。这是因为程序的其他部分在运行时是定长的,只有堆栈动态变化,由于目前物理内存一般都小于虚存空间,故堆栈空间会相互竞争)
  第二步,开始看连接过程。连接分两种,一种是静态连接,一种是动态连接。
  首先看静态连接。静态连接,即是把各个可重定位目标文件中有用的.text节全部复制到生成的可执行文件中,最终运行的程序不依赖于其他任何文件。静态连接的基本算法说明如下:
  定义三个集合,E表示等待合并的.o文件或者是.a中的.o,U表示未定义的符号,D表示已经定义过的符号。算法开始时,E、U、D均为空。
  对于每个需要连接的文件,若其是.o文件,将其放入E中,解析其中已定义的符号放入D中,未定义的符号放入U中;若是.a库文件,则依序遍历其中的.o模块,若.a中某个.o模块可以解析U中的符号,将其加入E,并更新U和D,当E、U、D不再变化时,结束。
  当所有的需要连接的文件均处理完毕后,U仍不为空,则将其中信息打印出来并报连接错误。
  以上就是静态连接的基本过程。
  再来看动态连接。动态连接,即运行时动态加载所需的库文件。编译连接时,只连接其入口函数。这种方式下,程序运行时依赖于其它的共享库文件。连接算法与静态连接基本相同,只不过细节处理不一样。
  比较一下静态和动态,可以发现,静态连接在运行时不依赖于库文件,访问库函数速度较快,但对于同一机器中的多个使用同一库的进程而言,内存中存在大量重复代码段,造成内存空间浪费。动态连接在运行时加载用到的库函数,访问速度较静态方式要慢一些(其实也就是第一次加载文件进入内存时慢一些,后面慢不了多少),对于同一机器中的多个使用同一库的进程来说,内存中仅存在一个代码段。这一技术的实现依赖于虚存管理,由于虚存是到物存的映射,对于不同的进程的共享库的虚存页,可以将其映射到同一物理页,从而达到共享的目的。
  总的来说,这一章不仅让我了解了连接的基本过程,也使我对进程运行的一些基本原理有了初步理解。
原文地址:https://www.cnblogs.com/peteryj/p/1944892.html