最近看了本《程序员的自我修养》,其中很多内容着实不错,明确了很多从前模糊的概念,所以贴上关于编译,链接及库的一些笔记,供菜鸟分享。

-------------------------------------------------------------------------------------------------

*****************编译和链接*****************
    从源代码到可执行代码可以分解为4个步骤,分别是预处理(prepressing)、编译(compilation)、汇编(assembly)和链接(linking)。

    预编译主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”等,主要处理规则如下:
    ● 展开所有宏定义,并且将所有的“#define”删除。
    ● 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
    ● 处理“#include”预编译指令,将被包含的文件插入到该指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其它文件。
    ● 删除所有注释。
    ● 添加行号和文件名标识,以便产生调试用的行号信息。
    ●保留所有#pragma编译器指令。

    编译就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。这个过程是构建整个程序的核心部分,也是最复杂的部分。

    现代的编译和链接过程也并非想象中那么复杂。比如我们在程序模块main.c中使用另外一个模块func,c中的函数foo()。我们在main.c模块中每一处调用foo()的时候都必须确切知道foo()的地址,但由于每个模块都是单独编译的,在编译器编译main.c的时候并不知道foo的函数地址,所以它暂时把这些调用foo()的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,我们必须手工把每个调用foo()的指令进行修正,这是相当繁琐的工作。
    将地址修正的过程也称“重定位(relocation)”,每个要修正的地方称为一个“重定位入口”。

*****************目标文件里有什么*****************
    一般C语言的编译后执行语句都编译成机器代码保存在.text段中;已初始化的全局变量和局部静态变量都保存在.data段里;未初始化的全局变量和局部静态变量一般放在.bss段。

    程序的指令和数据为何分开存放?

    ● 当程序被装载后,数据和指令分别被映射到两个虚存区域,这两个虚存区域的权限可以被分别设置成可读写和只读的。
    ● 对于现代的CPU来说,他们有着极其强大的缓存(cache)体系。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
    ● 最重要的原因是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只要保存一份该程序的指令部分。对于其他的只读数据也是一样的,程序中的图标、图片、文本等资源也是属于可共享的。

    链接的接口目标文件中的符号
    在链接的过程中,我们将函数和变量统称为符号(symbol);函数名或变量名就是符号名(symbol name)。
    每个目标文件都会有一个相应的符号表(symbol table),这个表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是它们的地址。将符号表中的所有符号进行分类,可得:
    ● 定义在本目标文件中的全局符号,可以被其他目标文件引用。比如其中定义的一些函数和全局变量。
    ● 在本目标文件中引用的全局符号,却并没有定义在本目标文件,这一般称为外部符号(external symbol)。最典型的就是Hello World中的printf。
    ● 段名,由编译器产生,它的值就是该段的起始地址。
    ● 局部符号,这类符号只在编译单元内部可见,如函数中的局部变量。这些局部符号对于链接过程没有作用,链接器往往忽略它们。
    ● 行号信息。
    对于我们而言,最值得关注的就是全局符号,即上面的第一和第二类。因为链接过程只关心全局符号的相互粘合。

    符号修饰与函数签名
    很久以前,编译器编译源代码产生目标文件时,符号名与相应的变量或函数的名字是一样的。比如一个汇编源代码里面包含一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中相对应的符号名也是foo。当C语言出现时,已经存在很多用汇编语言编写的库和目标文件。如果一个C程序要使用这些库的话,则该C程序就不可以使用这些库中定义的函数和变量的名字作为符号名,否则将产生冲突。
    为了防止类似的符号名冲突,UNIX下的C语言规定,C语言源代码文件中的所有全局变量和函数经过编译后,相应的符号名前加上下划线“_”。而Fortran语言的源代码经过编译后,所有的符号名前加上下划线,后面也加上下划线。
    这种方法减少了多种语言目标文件之间冲突的概率,但同一种语言编写的目标文件还有可能会产生符号冲突,当程序很庞大时,不同的模块由多个部门开发,它们之间的命名规范若不严格,则有可能导致冲突。于是像C++这样的后来者考虑了这个问题,增加了命名空间(namespace)来解决符号冲突。

    复杂的C++拥有类、继承、虚机制、重载、命名空间等特性,这使得符号管理更为复杂。
    C++允许函数重载,C++还在语言级别支持命名空间。由此我们引入了“函数签名(Function Signature)”,函数签名包含了一个函数的信息,包括函数名、参数类型、函数所在类和命名空间及其他信息。具体的函数签名方法视不同的编译器而定,VISUAL C++编译器是这么做的:

代码:
   int func(int)    >>>>签名后>>>>    ?func@@YAHH@Z
    float func(float)    >>>>签名后>>>>    ?func@@YAMM@Z
    int C::func(int)    >>>>签名后>>>>    ?func@C@@AAEHH@Z
    int C::C2::func(int)    >>>>签名后>>>>    ?func@C2@C@@AAEHH@Z
    int N::func(int)    >>>>签名后>>>>    ?func@N@@YAHH@Z
    int N::C::func(int)    >>>>签名后>>>>    ?func@C@N@@AAEHH@Z
    VC++的函数签名规则并没有对外公开,虽然我们一般也无需去了解,但有时在链接、调试程序的时候可能需要将一个修饰后的名称转换成修饰前的。Microsoft提供了一个UnDecorateSymbolName ()的API,可实现该功能。

    C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的关键字:extern c。
    其用法:
代码:
    extern "c"
    {
        int func( int ) ;
        int var ;
    }
    或
    extern "C" int func( int ) ;
    extern "C" int var;
    C++编译器会把在extern "C"内的代码当作C语言代码处理,其中C++的名称修饰机制将不会起作用。
    有时有些头文件声明了一些C语言的函数和全局变量,但这个头文件可能会被C代码或C++代码包含。对于C++来说,必须使用extern "C"来声明;而若是被C代码包含,C又是不支持extern "C"语法的。如果为了同时兼容C和C++而定义2套头文件,未免过于麻烦。
    有一个很好的方法可以解决上述问题,就是使用C++的宏"_cplusplus” ,C++编译器会在编译C++程序时默认定义这个宏。可以使用这个宏来判断当前编译单元是否为C++代码:
代码:
    #ifdef _cplusplus
    extern "C" {
    #endif

    .......在此放置函数声明
    #ifdef _cplusplus
     {
    #endif    
    这种技巧几乎在所有的系统头文件里被用到。
*****************静态链接*****************
    现在的链接器一般采用一种叫两步链接(Two-pass Linking)的方法:
    (1)空间与地址分配
    (2)符号解析与重定位
     空间分配一般采用相似段合并的方法,即将相同性质的段合并到一起。合并后输入文件中各个段在链接完成后的虚拟地址就已经确定了,比如“.text”段起始地址为0x08048094。
    之后,链接器开始计算各个符号的虚拟地址,因为各个符号在段内的相对位置是固定的。

    当一个"*.c"被编译成目标文件时,编译器并不知道其中外部符号的地址,因为它们定义在其他目标文件中。所以编译器就暂时把这些地址先搁置(比如直接填0),到链接时再对相关指令进行修正。

    那么链接器是怎么知道哪些指令需要调整?这些指令的哪些部分要被调整?怎么调整?比如MOV和CALL指令的调整方式就有所不同。
    在目标文件中有一个叫重定位表(Relocation Table)的结构专门用来保存与重定位相关的信息。一个重定位表就是一个段,故重定位表也可成为重定位段。
    每个要重定位的地方叫重定位入口(Relocation Entry)。
    重定位过程中,每个重定位入口都是对一个符号的引用,那么当链接器对某个符号进行重定位时,它就要确定这个符号的目标地址。这时链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
    在链接器扫描完所有的输入目标文件之后,那些未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误。

    不同的汇编指令对于地址的格式和方式都不一样,x86中的jmp,call,mov等指令的寻址方式千差万别。
ELF文件的重定位入口所修正的指令寻址方式只有2种:
    ● 绝对近址32位寻址
    ● 相对近址32位寻址

    我们知道在一个C语言运行时库中包含了很多跟系统功能相关的代码,比如输入输出、文件操作、时间日期、内存管理等。把零散的目标文件直接提供给库的使用者,很大程度上会造成文件传输或管理上的不便,于是人们用压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便查找和检索。
    每个运行库中的函数被独立地放在一个目标文件中,这样可以尽量减少空间的浪费,那些没用到的函数就不会一起链接进输出文件。
    VC++中提供了个叫"lib.exe"的工具,该程序可以用来创建、提取、列举.lib文件中的内容。使用"lib /LIST libcmt.lib"就可以列举出libcmt中所有的目标文件。libcmt.lib中包含949个目标文件。