静态链接,动态链接,静态库,共享库这些概念的详解

静态链接,动态链接,静态库,共享库,这些名词的资料在网上一大堆,但是我看了这些资料,总觉得有些地方怪怪的,有些文章描述某个概念特别好,但是在描述其他的概念的时候就有些凌乱。于是,我把这些资料给整理一下,厘清这些名词的真正含义。

本文一开始将各个概念全部罗列一遍(一开始的时候不讲各个之间的关系,或者比较少讲),然后才将各个概念联系起来(这就是外面那些资料缺少的地方,那些资料都没有讲清楚)

编译过程

要了解这些概念,编译过程是前提条件。
编译过程分成下面几个过程:预处理–>汇编–>编译–>链接。这些概念大家也可以到网上去搜索,一大堆。
本文中的这些概念就是发生在最后一个环节:链接。千万要记住这一点,很重要。

静态链接

关于静态链接和动态链接,网上这篇文章写得通俗易懂:静态链接和动态链接的区别,这里对这篇文章进行了摘抄:
1. 什么是静态链接:主函数中调用了库中的某个函数(one()),并且把该函数编码进了可执行文件中。见下图:

2. 静态链接存在的第一个问题:若需要调用库中的函数过多,会造成可执行文件体积巨大。见下图:

3. 静态链接存在的第二个问题:因为库中的函数代码嵌入了可执行程序,所以如果库更新了,想要更新可执行文件中库部分的代码,就只能重新编译。见下图:

动态链接

为了解决静态链接的以上2个问题,可以让库中的函数不要直接编译进可执行程序,而是放在内存中,可执行程序在使用(one)函数时,直接读内存的地址就可以用了。这样既减小了可执行程序的大小,又可以方便的使用库函数更新。这种方式即为——动态链接。见下图:

动态链接的优点:
1. 执行文件很小。
2. 当依赖的库函数发生升级了,执行文件不用重新编译。
动态链接的缺点:
一个可执行应用,在A服务器上进行了编译–>链接操作,在链接的时候所依赖的那些函数库是存在A服务器上的。当把这个可执行应用拿到B服务器上去运行,因为在B服务器上该可执行应用所依赖的函数库可能缺少或者位置不正确,而导致报错。所以,在使用这个可执行应用之前,需要在B服务器上进行make一下生成所对应的可执行应用才可以。这也就是 configure–>make–>make install的真正含义所在。

静态库和共享库(动态库,动态链接库)概述

  1. 静态库,在windows的后缀为 .lib(library),在linux的后缀为 .a(archive)
  2. 共享库,又称为动态库,又称为动态链接库,(这3个叫法在网上到处混着乱飞,就是指同一个),在windows的后缀为 .dll(dynamic link library),在linux的后缀为 .so(shared object)

在讲下面之前,借用《C编程一站式学习》中的“链接详解”这个章节中的例子来举例说明,顺便说一下《C编程一站式学习》这本书写得不错,但是“链接详解”这个章节还是没有说清楚,反而越说越糊涂。

/* stack.c */
char stack[512];
int top = -1;
/* push.c */
#include <stdio.h>

extern char stack[512];
extern int top;
void push(char c)
{
    printf("....");
    stack[++top] = c;
}
/* pop.c */
extern char stack[512];
extern int top;
char pop(void)
{
return stack[top--];
}
/* is_empty.c */
extern int top;
int is_empty(void)
{
return top == -1;
}
/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
/* main.c */
#include <stdio.h>
#include "stack.h"
int main(void)
{
push('a');
return 0;
}

上面这个例子也是很浅显易懂的。

静态库

gcc -c stack.c push.c pop.c is_empty.c
//上面这个步骤是compile,还不涉及link,结果生成了各自的.o,通过反汇编能看到,凡是使用printf()这个函数的地方都用了 0X0 这样的占位符先占着位置(printf()是出自libc.so这个大名鼎鼎的共享库),凡是彼此之间相互用到的也都用了0X0 这样的占位符先占着位置。
ar cr libstack.a stack.o push.o pop.o is_empty.o //创建libstack.a这个静态库

ar 这个命令仅仅是压缩,什么都不涉及,不会对那些.o有任何修改,记住就是压缩!既然是压缩,那么原来怎么样就是怎么样了。
通过上面ar生成的 libstack.a 就是静态库,那为什么要取这样的名字 lib + stack + .a,这个是规范,要给下面讲到的链接所用,死记即可。
静态库的特点如下:
1,仅仅是.o的打包而已
2,.o中假如是 .c之间相互用到的,那么是0X0占位符
3,.o中假如有来自共享库(见下面,这里暂时不管)的,那么也是0X0占位符

静态库和静态链接、动态链接的结合

这个部分就是外面网上好多没有提到的,这样就导致了看到后面一头雾水,就是压根不明白到底什么是什么。

默认“动态链接”

gcc main.c -L. -lstack -Istack -o main  #这是一步compile和link合在一起的步骤

-L:大写的L,表示静态库的文件夹(看清每个字,理解每个字,是文件夹,而且是静态库)
-l:小写的l,表示链接的时候要链接 libstack.so或者libstack.a 这个文件,具体找到共享库(.so)还是静态库(.a),libstack.so先找到就用这个共享库,找不到则在-L指定的文件夹下面找libstack.a这个静态库

那是不是找到.so动态库就是动态链接?找到.a静态库就是静态链接?
否,否,否!!!
千万不要被他们之间摸棱两可的命名给混淆了,接着看:

gcc的链接,默认的情况下采用“动态链接”,比如上面的那条命令。那么按照“动态链接”的概念来进行链接,main.o当链接到“静态库”的时候,假如只是单纯的把main.o中的push()替换成“静态库”中的push()函数地址,因为libstack.a是不可能更改的,里面的那些彼此之间的函数调用以及调用的printf()仅仅是0X0地址,那肯定不对的。所以,尽管是“动态链接”,但是遇到libstack.a这个静态库的时候,只有华山一条路,只能采用“静态链接”的方式,将所调用的push()函数给拷贝到应用程序main中,同时调整拷贝过来的push()中所用到的libc.so中的printf()函数地址为间接地址(也就是说printf()还是“动态链接”的)。这个过程想想都能明白的。

总结一下:
“动态链接”,当链接到“静态库”的时候,采用“静态链接”,全部拷贝过来。但是对于main自己本身所用到的“动态库”和“静态库”中所用到的“动态库”则采用“动态链接”。

强制使用“静态链接”

gcc -static main.c -L. -lstack -Istack -o main

-static,表示使用“静态链接”的方式。那么不管是遇到“静态库”还是“动态库”,还是“静态库”中用到“动态库”,将一律拷贝到应用程序中,更改调用地址,也就是说一律采用“静态链接”的方式。

总结一下:
“静态链接”,不管遇到“静态库”还是“动态库”,统统采用“静态链接”的方式。

共享库(动态库,动态链接库)

gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
//compile成.o,通过反汇编可以看到调用的函数地址为0x0(%ebx),本质上还是仅仅占位符,可以看成0X0
gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o
//将上面生成的.o编译成共享库libstack.so,通过反汇编可以看到上面的0x0已经被替换成间接地址了。

到这里为止,可以看到“共享库”和“静态库”有本质差别的,差别就在于调用函数的地址上,“共享库”提供了一个简介地址可以在后面的链接中以及运行中找到调用函数。

共享库和静态链接、动态链接的结合

还是和上面一样,“共享库”和“静态链接”、“动态链接”没有本质上的瓜葛。

gcc main.c -g -L. -lstack -Istack -o main
./main
./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory

从前面讲到的知道,这是“动态链接”,而且链接的是“动态库”libstack.so,而且还要链接printf()的动态库libc.so。那么最终生成的main这个可执行程序中所涉及的函数全部都是间接寻址。

当执行./main的时候报错,说libstack.so找不到,这个时候要记得:.L是指的“静态库”的文件夹,而对于“动态库”的寻找,一般是在/usr/lib下面,所以我们还需要将生成的libstack.so放在/usr/lib下面,这也就是 make install 的作用,将生成的“动态库”放在 /usr/lib下面。

假如我们还是想用“静态链接”来链接,那么就改用下面的来执行:

gcc -static main.c -L. -lstack -Istack -o main

这样,尽管都是“动态库”,但是还是强制使用了“静态链接”。

全部总结一下:
当“动态链接”遇到“静态库”,则采用“静态链接”。
当“动态链接”遇到“动态库”,则采用“动态链接”。
当“静态链接”遇到“静态库”、“动态库”,一律采用“静态链接”。

本文结束,希望你能彻底搞清这些名词之间的关系,“静态链接”和“静态库”,都有“静态”2个字,其实是2个方面,2者严格意义上完全不一样。“动态链接”和“动态链接库”,更加具有混淆性,也是2个方面,千万别被字面上的含义弄混淆。

顺便说一下:ldd和nm是2个用来分析链接的常用工具。

参考资料

C编程一站式学习

Makefile疑难解答和技巧合集

Makefile文档

Makefile是GNU make中最重要的一个文件,官方的文档:GNU make,当然也是比较复杂和难懂的。

Makefile教程

很早以前,咱们国人写过这么一篇《跟我一起写 Makefile》,这篇文章写得是比较不错的,通过通俗易懂的例子教你怎么写Makefile,但是是一页一页的html,看起来累死,你也可以到网上去搜索相应的pdf,下载下来后,在写作和分析别人写得Makefile的时候,可以作为参考手册。

Makefile基础

Makefile的大致框架如下:

target(目标) ... : prerequisites(依赖目标) ...
    <TAB>command(命令)

上述的翻译:目标、依赖目标、命令 是来自《跟我一起写 Makefile》,大家在看这本书的时候,一定要记住,千万要看清目标和依赖目标,千万别看错了。我个人觉得prerequisites还是翻译成“前置条件”比较好,尽管从意义上来看在这里的prerequisites确实是依赖的目标,但是翻译成了“前置条件”不至于把读者给看晕和看错。

Makefile的调试

Makefile挺难的,更难的是那些变量值、那些调用过程,一个复杂点的Makefile会把你给完全绕晕。这个时候,对于Makefile的调试就非常重要了。对于Makefile的调试,主要看这篇文章,写得相当不错:Makefile常用调试方法
我调试的时候,常用下面的技巧:
1. make的时候,用参数 “-n” “--just-print” “--dry-run” “--recon”,这4个参数是完全一样的。比如 make -n
2. 但是上面只能看到具体执行了哪个命令,假如Makefile相当庞大和复杂,那么就不知道这个命令是由哪个规则调用的,这个时候就需要用下面的参数: --debug=basic,也就是完整的命令是:make -n --debug=basic,出来的结果非常不错,强烈推荐这个。
3. 对于某个变量的值是多少,可以用$(error, ….) $(warning, ….)这2个函数来打印。

Mafefile的函数参考

《跟我一起写 Makefile》这本书写得比较早,有些函数在它上面找不到了,需要在官网http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions参考,官网是全面的。

Makefile的模式规则

其实挺简单的,就是类似正则表达式,比如下面的:

all: bin/a bin/b
bin/%: ...

那么 bin/a 就会匹配 bin/%,bin/b 也会匹配 bin/%

再比如:

all: bin/a/image bin/b/image
bin/%/image: ...

那么 bin/a/image 就会匹配 bin/%/image,bin/b 也会匹配 bin/%/image

再来一个例子:

all: bin/a/image bin/b/image

bin/a/image: ...

bin/%/image: ...

那么 bin/a/image 会匹配 bin/a/image 和 bin/%/image 这2个目标,这2个目标都执行!

Makefile的中间规则文件

隐含规则,可以参见《跟我一起写 Makefile》,其中上面所说到的模式规则也是属于隐含规则。那么上面所提到的bin/a,bin/a/image所匹配的bin/%, bin/%/image这些就是属于中间规则,假如这些中间规则结果是个文件,那么就叫做中间规则文件。正常情况下,中间规则文件是在规则执行完后要被删除的,比如:

%.txt: foo.log
    # pass

%.log:
    # pass

执行结果如下:

$ make a.txt -n
# pass
# pass
rm foo.log

上面的foo.log这个就是中间规则文件,所以要被删除的。为了防止中间规则文件被删除,那么用下面的来定义,这样就不会删除该中间规则文件了:
.PRECIOUS: foo.log
或者
.SECONDARY: foo.log

Centos7下升级GCC

centos7自带的GCC是比较老的,gcc -v可以看到版本是4.8.5,在yum中也能看到最高版本也是这个版本4.8.5:yum list gcc。

用这个比较老的GCC版本,在运行一些软件的时候,常会报错:
CXXABI_1.3.8 not found
GLIBCXX_3.4.20 not found

用下面2个命令可以看到上述这2个确实不存在
strings /usr/lib64/libstdc++.so.6 | grep GLIBC
strings /usr/lib64/libstdc++.so.6 | grep CXXABI

解决这个问题,只有一条路可以走,那就是升级GCC到更高的版本,GCC官网上的版本已经到了很高的版本9了,我个人觉得没有必要用这么高的版本,折衷一下,选个GCC 6.5.0 这个版本即可。

下面是GCC的升级之路,整个过程大概耗时3个小时,要有心理准备:
1,下载GCC 6.5.0:http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-6.5.0/gcc-6.5.0.tar.gz,或者到官网找更好的mirror下载,https://gcc.gnu.org/mirrors.html,我选了个日本的镜像,很遗憾没有国内的。
2,假设 gcc-6.5.0.tar.gz 在 /opt 下面,tar xzvf gcc-6.5.0.tar.gz && cd gcc-6.5.0
3,升级之前,需要做好下面2个准备工作:
3.a:yum -y install gcc-c++ , 否则在升级的时候会报错:error: C++ preprocessor “/lib/cpp” fails sanity check
3.b:cd /opt/gcc-6.5.0,然后运行 sh contrib/download_prerequisites,必须在/opt/gcc-6.5.0下面,否则运行download_prerequisites的时候就会报错;这个脚本将会下载4个小文件(每个文件1M左右),但是因为网络的因素,下载了半天才下载完。这个必须要运行,否则在GCC升级的时候就会报错什么:GMP 4.2+, MPFR 2.4.0+ and MPC 0.8.0+这些找不到。
4,准备工作做好了,开始吧:./configure –disable-multilib,disable-multilib表示仅仅需要64位的GCC,不需要32位的GCC,假如在centos7安装的时候没有选上32位的GCC,那么在这里必须要 –disable-multilib,否则就会报错。
5,make,这个过程最长了,估计要1个多小时。
6,make install,非常快
7,装好后,gcc -v,确实已经升级到了 6.5.0 版本,但是假如运行前面报错的软件的话,还是会报以前的错误,用刚才的strings命令去看,也是缺少那2个,这是什么原因呢?
8,这是因为,gcc升级安装在了 /usr/local/lib64下面,而运行的软件只认 /usr/lib64 这个位置,所以最后还需要 cp /usr/local/lib64/libstdc++.so.6 /usr/lib64/,这样就全部搞定了

最后说一下,GCC的升级就是比较耗时点,但是难度不大,严格按照我上面的步骤,很容易搞定的。