• 【链接 1】与静态链接库链接


    本文内容基于《CSAPP》第7章,只是符号解析的一部分,从使用的角度阐述了静态库的由来和使用,仅仅是个人见解,可能从编译的角度看有不严谨的地方,如发现错误,还请指正,谢谢!

    1 静态库

    首先我们要知道,链接器将一组可重定位目标文件链接起来可以组成一个可执行文件,如

    $ ld -o prog ./a.o   ./b.o
    

    但对于一些基础的操作,如C标准库中提供的printf、scanf、rand等一些列常用的函数,如果每次编译,我们都要操作带有这些函数的可重定位目标文件,那么一次简单的编译过程就会变成下面这样:

    $ gcc -o a.out main.c /usr/lib/printf.o   /usr/lib/scanf.o /usr/lib/rand.o ...
    

    这样一来,不仅每次都要编写冗长的命令行,而且程序员还必须维护一个包含所需的源文件或目标文件的文件夹。

    但实际上,我们在编译我们的程序时,并没有考虑过这样的问题,对于一个仅仅使用了标准库中函数的源文件而言,也并不需要程序员手动的进行额外的链接操作。如对于下面main.c这个源文件而言,

    // main.c
    #include<stdio.h>
    
    int main()
    {
        printf("Hello World!");
        return 0;
    }
    

    我们只需要简单的执行

    $ gcc -o a.out main.c
    

    这是因为,标准库中的函数都被编译成了独立的目标模块,然后相关模块会被封装成一个单独的静态库文件,如libc.a包含了C标准库中的标准I/O、字符串操作等函数,libm.a包含了C标准库中的整数数学函数,在执行链接操作时,编译器的驱动程序会将这些标准静态库传送给链接器,链接器会从中选择适当的模块同我们自己编写的目标模块(main.o)链接起来得到可执行文件。

    在Linux系统中,静态库以一种称为存档(archive)的文件格式存储,后缀名.a,它由一个头和一系列的目标模块构成,头负责描述每个成员目标模块的位置和大小。

    2 使用静态库

    既然有标准库,那我们也可以把自己编写的函数、全局变量、宏等封装成静态库。

    例如我们实现两个自定义的整型操作函数,分别定义在下面两个源文件中,

    // add.c
    int add(int a, int b){
        return a+b
    }
    
    // sub.c
    void sub(int a, int b){
        return a-b;
    }
    

    创建静态库需要使用AR工具,使用以下命令:

    $ gcc -c add.c  sub.c
    $ ar rcs libcal.a  add.o sub.o
    

    如此便得到了一个静态库libcal.a,在源文件中引用,即可使用静态库中定义的符号(非static函数、全局变量等)。

    // main2.c
    #include "cal.h"
    
    int main()
    {
        int a = 0, b = 3, c = 0;
        c = add(a, b);
        printf("%d", c);
        return 0;
    }
    

    编译该源文件,

    $ gcc -c main2.c
    $ gcc -static -o prog2c main2.o 
    

    或者等价地使用,

    $ gcc -c main2.c
    $ gcc -static -o prog2c main2.o -L. -lcal
    

    链接器运行时,它就会判定main2.o引用了add.o定义的add符号,所以复制add.o到可执行文件,此外,他也会从/usr/lib/libc.a中复制printf所在的目标文件到可执行文件。

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

    命令行上库和目标文件的顺序非常重要,如果我们对上一条命令做一些小小的改动,使之变为

    $ gcc -static -o prog2c ./libcal.a main2.o
    

    这条命令的执行就会报错“undefined reference to 'add'”,之所以出现这样的情况,是链接器解析外部引用的方式导致的。

    链接器是按照命令行上从左到右的顺序来扫描文件的,在扫描文件时,链接器会维护三个集合:E(这个集合中的文件会被合并起来形成可执行文件)、U(未解析的符号)以及D(在前面输入文件中已定义的符号集合),三个集合初始为空。

    • 对于命令行上的每个文件f,链接器会首先判断这一文件是目标文件还是静态库文件。若该文件是一个目标文件,则放入E中,并修改U和D来反映f中的符号定义和引用。
    • 但如果f是一个静态库文件,那么链接器就试图对U中未解析的符号和f的成员所定义的符号进行匹配。如果f中的某一成员m定义了一个符号来解析U中的一个引用,那么就将m加入E中,再相应地修改U和D中的内容来反映m中的符号定义和引用,对f中的所有成员逐个进行匹配操作直至U和D不再发生变化,连接器便开始处理下一个文件。
    • 当链接器扫描完所有命令行中的文件后,若U是空的,那么连接及就会合并和重定位E中的文件,得到一个可执行文件;否则,链接器就会报错并终止。

    现在,是不是理解了上面的错误了呢,链接器扫描到libcal.a时,U中尚是空的,故直接继续扫描后面的main2.o,然后,main2.o中的add符号未解析,被加入到U中,随后,结束扫描,U中非空,链接器报错。

    需要注意的是,库和库之间也可能存在依赖关系,故使用多个库时要注意其先后顺序,若存在相互依赖的关系,则可以选择在命令行上重复库,如下面一条命令中,libx.a调用了liby.a中的函数,liby.a又调用了libx.a中的函数,

    $ gcc foo.c libx.a liby.a libx.a
    

    当然,把两者合并为单独的一个静态库也不失为一种好方法。

  • 相关阅读:
    MySQL(DQL部分)
    Java函数式接口
    Java简易版生产者消费者模型
    浅谈Java三种实现线程同步的方法
    Linux常用命令
    计算机组成原理笔记
    资煌麻辣烫——事后诸葛亮
    资煌麻辣烫——冲刺集合
    资煌麻辣烫——测试总结
    资煌麻辣烫——冲刺总结
  • 原文地址:https://www.cnblogs.com/chuaner/p/14488548.html
Copyright © 2020-2023  润新知