您现在的位置是:首页 >技术教程 >C++:Article : 链接器(三):库与可执行文件的生成网站首页技术教程
C++:Article : 链接器(三):库与可执行文件的生成
链接器:库与可执行文件
链接器可操作的最小单元是目标文件,也就是说无论我们见到的是静态库,动态库,可执行文件,都是基于目标文件构建出来的
给定目标文件以及链接选项,链接器可以生成两种库,静态库和动态库。
💚💚💚
一言以蔽之我们通过三张图来区分动态库和静态库
💚💚💚
静态库的特点
💚💚💚
动态库的特点
1. 静态库
静态库在windows 环境下是以 .lib为后缀文件, 在 linux 环境下是以 .a为后缀文件。
💚💚💚
你可以将静态文件理解成由:一堆目标文件打包而成,使用者只需要使用其中的函数无需关注该函数来自哪个目标文件
找到函数来自哪个目标文件是由链接器来完成的,而且静态库中的所有目标文件并不会都被使用到,而是链接器用到哪个就使用哪个。
1.1 静态链接下,可执行文件如何生成
在上一节中我们知道,可以将静态链接简单的理解为,链接器将使用到的目标文件集合进行拼装,拼装之后就可以生成可执行文件。
从上图中我们可以看出,可执行文件的特点:
- 可执行文件和目标文件一样,也是由代码段和数据段组成。
- 每个目标文件的数据段被合并到可执行文件的数据段,每个文件的代码段被合并到可执行文件的代码段
- 目标文件的符号表,并没有被合并到 可执行文件中,因为可执行文件不需要符号表。
- 目标文件和可执行文件本质上没有区别,但是可执行文件和目标文件的区别又在于:可执行文件有一个入口函数,即main 函数,入口函数, 在执行过程中会用到可执行文件所用代码段和数据段内容,main是被操作系统 Operating System 调用。
2. 动态库
- 由于静态链接会将用到的目标文件合并到可执行文件中,如果有多个可执行文件需要用到一个静态库,那么就会出现这样一个现象:
- 生成的所有可执行文件当中都有一份一模一样的静态库中的代码,那么这个对内存和硬盘来说是一个极大地浪费。
- 💚💚💚💚
- 所以动态链接变应运而生。
2.1 动态库特点以及与静态库使用方式差异
- 动态库(Dynamic Library)又叫共享库(Shared Library)动态链接库等。
- 在Windows下就是我们常见的 DLL库,在 Linux 下以 .so库为后缀,同时以后 lib 为前缀
- 本质上动态库还是由 :代码段,数据段,符号表组成,但是动态库和静态库的使用方和使用时间是不一样的。
- 如上图所示,可执行文件链接静态库是将,静态库所有文件都拷贝一份给可执行文件,并和可执行文件合并。
- 动态链接:可执行文件链接动态库,仅仅是将动态库用到的目标文件的引用合并到可执行文件中。
2.2 动态库和静态库使用时间
静态库其实是在编译期间(Compile time)链接使用,那么动态链接是在什么时候使用了 ?
- 加载时动态链接(load-time dynamic linking)
- 运行时动态链接(run-time dynamic linking)
3. load-time dynamic linking(加载时动态链接)
- load-time翻译过来就是加载,比如:游戏中经常出现一句话:“加载中,请稍候!”,这里的其实就是指程序的加载,而所谓程序的加载就是把可执行文件从磁盘搬到内存的过程,动态链接就出现在这个过程。
- 当把可执行文件赋值到内存后,且在程序开始运行之前,操作系统会查找可执行文件依赖的动态库信息(主要是:动态库的名字和存放路径),当找到该动态库后,就将动态库从磁盘搬到内存,并进行符号决议。
- 如果符号决议这个过程没问题,那么一切准备工作就绪,程序就可以开始执行了,如果找不到相应的动态库或符号决议失败,那么就会有相应的错误信息,报告给用户,程序就会运行失败。
💚💚💚
总结:从总体上看,加载时动态链接可以分为两个阶段:
阶段一:将动态库信息写入可执行文件。
阶段二:加载可执行文件时依据动态库信息进行动态链接。
3.1:阶段一:将动态库信息写入可执行文件
在编译链接生成可执行文件时,需要将使用的动态库加入到链接选项中,比如在 Linux 下 引用 libMath.so 就需要将 libMath.so加入到链接选项中。
- 如果libMath.so 放到了 /usr/lib 下,那么使用命令 gcc …-|Math -L/usr/lib 进行编译链接,就可以让生成的可执行文件中保存了依赖的动态库信息,你可以 在 Linux 使用 ldd 命令来查看。
3.2:阶段二:加载可执行文件时依据动态库信息进行动态链接
由于在阶段一生成的可执行文件中保存了动态库信息,当可执行文件加载完成后,就可以依据此信息进行动态库的查找以及顾浩决议了
很显然,你已经知道了静态库和动态库的区别:使用动态库的可执行文件当中仅仅保留相应信息,动态库的链接过程被推迟到了程序启动加载时。
4:第二种动态链接:run-time dynamic linking(运行时动态链接)
- run-time dynamic linking运行时动态链接:不需要在编译链接时提供动态库信息,也就是说,在可执行文件被启动运行之前,可执行文件对所依赖的动态库信息一无所知,只有当程序运行到需要调用动态库所提供的代码时才会启动动态链接过程。
- 相比加载时动态链接,运行时动态链接将链接这个过程推迟后推迟,推迟到了程序运行时。
- 当代码中需要使用某个动态库所提供的函数,我们需要使用特定的 API 来运行时加载动态库,在 Windows 下可以通过 LoadLibrary 或者 LoadLibraryEx,这些API被调用后,同样是首先去找这些动态库,将其从磁盘 烤白到 内存,然后 查找程序依赖的函数是否在动态库中定义,这些过程完成后,动态库中的代码就可以被正常使用了。
5:动态链接下可执行文件的生成
- 在静态链接下,链接器通过将各个目标文件的代码段合并并拷贝到可执行文件,因此静态链接下可执行文件当中包含了所依赖的所有代码和数据。
- 然后动态链接并不是将动态库中的代码和数据拷贝到可执行文件中,而是将动态库的必要信息写入了可执行文件,,这样昂可执行文件在加载时就可以根据此信息进行动态链接,我们可以将该信息仅仅认为是动态库的名字,当然真实情况肯定比这要复杂些。
- 在前面的章节中我们将可执行文件简单的换分为:数据段和代码段两段,在这里我们继续丰富下可执行文件中的内容,如下图所示:
💚💚💚
在数据段和代码段的基础上,新增了两段。即:dynamic段和 GOT(Global offset table)段,这两段内容就就是我们刚刚说的 必要信息。
5.1:dynamic段
dynamic段中保存了可执行文件依赖哪些库,动态链接符号表的位置以及重定位表的 位置信息。
5.2:GOT(Global offset table)
6. 动态库VS静态库
6.1 动态库的优点
- 在计算机历史中,最开始程序只能静态链接,但是人们很快发现,静态链接生成的可执行文件在磁盘空间浪费问题,因为对于每个程序来说都需要依赖 lib 库,在静态链接下每个可执行文件当中都有一份 libc代码和数据的拷贝。
- 对于现代计算机来说,比如PC 机,通常会运行成百上千个程序(进程),且程序只有被加载到内存中才可以使用,如果使用静态链接那么在内存中就会有成百上千份同样的libc代码,这对以宝贵的内存资源是极大的挑战和浪费。
所以为了解决这个问题,程序员提出了 动态库
-
动态库在内存中只需要一份 libc 代码,所有的程序共享这一份代码,因此极大的节省了内存资源,这也是为什么动态库又叫共享库。
-
共享库还有一个强大之处就是:如果我们修改了动态库的代码,我们只需要重新编译动态库就可以而无需重新编译我们自己的程序
(这是因为可执行文件仅仅保留动态库的必要信息,重新编译动态库不给你不会改变这些,只要你不修改动态库的名字和动态库导出的可供可执行文件使用的函数即可)。 -
编译好的新动态库只需要简单的替换原有动态库,下一次运行程序就可以使用新的动态库,所以动态库的这种特性就极大的方便了程序升级和bug 修复。
-
动态库的优点可不止这些:我们知道动态链接可以出现在运行时(run-time dynamic link), 动态链接的这种特性可以用于扩展程序能力,那么如何扩展了 ?答案就是插件
-
动态库的强大 还体现在多语言上。
💚💚💚
6.1.1 插件
实现插件时,我们只需要实现规定好的几个函数,插件就可以运行,那么这个是怎么做到的呢 ?答案就是 : 动态链接
我们可以将插件以动态的方式实现。
试想一下:由于动态链接是在运行时,才知道使用了什么库,什么函数,在编译期间可执行文件对此是一无所知。
那么在编译链接可执行文件又是如何知道插件中定义了哪些函数呢 ?
这个就要求 所有插件实现函数都必须有一个统一的格式,程序在运行时间就可以按照这个函数(统一格式)来调用,这样我们写的插件就可以运行起来。
6.1.2 动态库用在多语言上
我们知道使用 Python 可以快速进行开发,但是 Python 的性能无法同 C++相比 (因为 Python 是解释型语言:程序执行速度慢。由于每个语句都是执行的时候才翻译,解释器是一句一句的翻译这样解释性语言,每执行一次就要翻译一次,效率比较低。适用一些脚本,网页等快速开发的功能)
那么是否有一种办法可以做到:既可以兼具 Python 的快速开发能力,也可以具备 C++的高性能了 ?
答案是肯定的。
💚💚💚💚
我们可以将C++代码编译成动态链接库,这样 Python就可以直接调用动态链接库中的函数,其实 不退单 Python Perl 以及 java等语言都是可以通过动态库的形式调用 C++代码的,所以动态库的使用使得同一个项目不同语言的混合编程称为可能 。
6.2 :动态库缺点
6.2.1 :启动性能稍弱
由于动态库是程序在加载时或运行时,才进行链接的,因此同静态链接相比,使用动态链接的程序在性能上要稍弱 静态链接。
💚💚💚💚
这是因为加载时动态链接,这会 减慢程序启动速度。
运行时动态链接:当首次调用动态库函数时,程序会被暂停,当链接过程结束后才可以继续。
这点性能损耗,相比动态库的优点还是微乎其微的。
6.2.2:兼容性问题(版本依赖问题)
动态链接下的可执行文件不可以被独立运行(load-time dynamic link), 换句话说:如果没有提供所依赖的动态库或者动态库的版本和 可执行文件所依赖的版本不兼容,程序是无法启动的,这会给程序的安装部署带来很大的麻烦。
7.静态库优点
优点
静态链接时最古老也是最简单的链接技术,它的最大优点是:使用简单,编译好的可执行文件是完整的,这就消除了动态链接下 依赖的问题,没有了 依赖问题,这就给程序的安装和部署带来极大的方便。
比如 像微信这种数量级的系统,后端会部署在成千上万台机器上,这么多机器的安装部署以及升级会给运维带来极大的挑战,静态链接下的可执行文件由于不依赖任何其他库,部署会及其方便,仅仅用一个新的可执行文件覆盖就可以了。
缺点
静态库会导致可执行文件过大,且多个程序静态链接同一个精态库的话会导致 磁盘浪费问题。
8. Linux下创建静态库
库是一组预先编译好的方法集合,是计算机上的一类文件,提供给使用者或者一些开箱即用的变量,函数或类。
静态库:windows 下扩展名 .lib 而 Linux 下的扩展名为 .a 。
下面我们分析下载 Linux 系统下创建静态库步骤
-
Linux 静态库名规范: lib[your_library_name].a : lib 为前缀,中间是静态库名,扩展名为 .a 。
-
将代码文件编译成目标文件 .o (hello.o) 请注意:带参数 -c ,否则直接编译为 可执行文件
g++ -c hello.cpp -
通过 ar 工具将目标文件打包成 .a 库文件
ar -crv libhello.a hello.o
下面我们编写三个文件,分别是 hello.h hello.cpp main.cpp ,文件内容如下:
hello.h
#ifndef HELLO_H
#define HELLO_H
void hello_fun(const char* name);
#endif // HELLO_H
hello.cpp
#include<iostream>
#include "hello.h"
void hello_fun(const char* name)
{
printf("Hello %s!",name);
}
main.cpp
#include"hello.h"
int main()
{
hello_fun("C++ static library");
return 0;
}
8.1 Linux 下使用静态库
- 方法一: gcc main.cpp libhello.a -o main / g++ main.cpp libhello.a -o main
运行发现命令:gcc main.cpp libhello.a -o main 报错了,但是 g++ main.cpp libhello.a -o main就没有问题,这是什么原因了 ? 我们待会在分析
- 方法二 :
gcc -c main.cpp
gcc -o hello main.o libhello.agcc -c main.cpp
g++ -o main.exe main.o libhello.a
9. Linux 系统下创建动态库
动态库:
Windows 下扩展名:.dll (Dynamic-Link Libraries)
Linux 下扩展名 :.so (Shared Object)
命令:
gcc -shared -fPIC -o libhello.so hello.o
g++ -shared -fPIC -o libhello.so hello.o
9.1. 使用.so动态库
链接动态库到可执行文件
g++ -o main.exe main.cpp libhello.so