【连接 1】与静态连接库连接

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

1 静态库

首先咱们要知道,连接器将一组可重定位目标文件连接起来能够组成一个可执行文件,如shell

$ 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;
}

咱们只须要简单的执行code

$ gcc -o a.out main.c

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

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

2 使用静态库

既然有标准库,那咱们也能够把本身编写的函数、全局变量、宏等封装成静态库。数学

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

// 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

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

相关文章
相关标签/搜索