2017-2018-1 20155328 《信息安全系统设计基础》第十四周学习总结

本周学习任务是找出自己本学期学得最差的一章,重新学,总结新的收获。我认为自己学得最差的一章是教材第七章《链接》。

第七章新收获

  • 静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。链接器的两个任务:符号解析、重定位

  • 编译器和汇编器生成可重定位目标文件。

    • 一个典型的可重定位目标文件格式如下:

- .txt:已编译程序的机器代码。

- .rodata:只读数据。

- .data:已初始化的全局和静态C变量。

- .bss:未初始化的全局和静态C变量。

- .symtab:一个符号表。

- .rel.text.:一个.text节中位置的列表。

- .rel.data:被模块引用或定义的所有全局变量的重定位信息。

- .debug:一个调试符号表。

- .line:原始C源程序中的行号和.text节中机器指令之间的映射。

- .strtab:一个字符串表。
  • 符号和符号表:每个重定位目标模块m都有一个符号表,他包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

    • 由模块m定义并能被其他模块引用的全局变量;

    • 由其他模块定义并被模块m引用的全局符号;

    • 只被模块m定义和引用的局部符号;

  • 符号解析:

    • 连接器解析符号引用的方法:将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。

    • 对于局部符号,编译器只允许每个模块中每个局部变量有一个定义。

    • 对于全局符号的引用就会复杂,当编译器遇到一个不是在本模块定义的符号,回假设它由其他的模块定义的,生成一个链接器符号表条目,并把它交给链接器处理。

  • 在重定位中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:重定位节和符号定义;重定位节中的符号引用。

  • 当加载器运行时,创建如下图所示的内存映像。

  • 位置无关代码:可以加载而无需重定位的代码。

  • 链接器如何使用静态库来解析引用

在符号解析的阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件。)在这次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时U和D都是空的。

- 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。

- 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不再发生变化。在此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。

- 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,从而构建输出的可执行文件。

因此命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能解析,链接会失败。关于库的一般准则是将它们放在命令行的结尾。

如果各个库的成员是相互独立(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以按照任何顺序放置在命令行的结尾处。另一方面,如果库不是相互独立的,那么它们必须排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的。

  • 动态链接共享库

静态库有一些明显的缺点:

- 静态库在更新时,使用该库的程序需要与更新的库进行重新链接。

- 由于使用静态库的程序在链接时都会拷贝静态库里被应用程序引用的目标模块,像printf和scanf这样的函数的代码在运行时都会被复制到每个运行进程的文本段中,这造成了冗余,浪费了稀缺的存储器资源。

为了解决静态库的这些缺陷,共享库(share library)出现了。共享库是一个目标,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。

下面是对GCC常用参数的整理:

-std=standard   #Specify the standard to which the code should conform. 

-E              #预处理后即停止,不进行编译,预处理后的代码送往标准输出
-S              #编译后即停止,不进行汇编,可以使用 -o 选项选择其他名字
-c              #编译或汇编源文件,但是不作连接,编译器输出对应于源文件的目标文件,可以使用 -o 选项选择其他名字

-o              #指定输出文件名。该选项不在乎 GCC 产生什么输出,无论是可执行文件,目标文件,汇编文件还是预处理后的 C 代码

-MM             #和 -M 选项类似,但是输出结果仅涉及用户头文件

-llibrary       #连接名为 library 的库文件,连接器在标准搜索目录中寻找这个库文件,库文件的真正名字是 liblibrary.a
                #搜索目录除了一些系统标准目录外,还包括用户以 -L 选项指定的路径
-static         #在支持动态连接的系统上,阻止连接共享库
-shared         #生成一个共享目标文件,他可以和其他目标文件连接产生可执行文件
-fPIC           #如果支持这种目标机,编译器就输出位置无关目标码,适用于动态连接,即使分支需要大范围转移

-Idir           #在头文件的搜索路径列表中添加 dir 目录
-Ldir           #在 -l 选项的搜索路径列表中添加 dir 目录

-w              #禁止所有警告信息
-Wall           #生成所有警告信息

-g              #以操作系统的本地格式产生调试信息
-rdynamic       #把所有符号(而不仅仅只是程序已使用到的外部符号,但不包括静态符号,比如被static修饰的函数)
                #都添加到动态符号表(即.dynsym表)里,以便那些通过dlopen()或backtrace()
                #(这一系列函数使用.dynsym表内符号)这样的函数使用。

-O0             #不优化                
-O1             #优化,对于大函数,优化编译占用稍微多的时间和相当大的内存 
-O2             #多优化一些,除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作
-O3             #优化的更多,除了打开 -O2 所做的一切,它还打开了 -finline-functions 选项
                #-finline-functions,把所有简单的函数集成进调用者,编译器探索式地决定哪些函数足够简单,值得这种集成

-m32            #产生32位代码