一、引言
在计算机编程的世界中,库是一个非常重要的改变。它的出现提供了一种共享和重用代码的可能性,复杂的程序因为动态库的出现而变得简洁和方便。然而,库并不是单一的:它们可以是动态的,也可以是静态的,每一种类型都有其使用场景。在本文中,我们将深入探讨动态库和静态库的概念,每种类型都有其优点和使用场景。讨论的范围将会集中两种最为常见的平台——Windows和Linux,主要内容还是帮助读者创建一个在自己平台下使用的动态库。
二、静态库和动态库基础知识
在顺利创建并使用动态库之前,让我们先来了解一下关于这两个库的概念。
2.1 动态库
动态库在程序实际运行时才会被加载到内存,多个程序可以共用这个动态库;Windows动态库以.dll
结尾,而在Linux下则以.so
结尾,其优点在于:
- 节省内存。多个程序如果使用到了同一个动态库,仅需加载一次内存,从而达到节省内存的作用;
- 模块化设计。动态库是一个模块化设计,每个库专注其特定的功能,增加了代码的可读性和维护性;
- 简化更新和修复过程:因为是运行时才加载,如果符号保持不变,更新功能只需要替换掉原来的动态库即可;
- 剥离常用函数,使得维护变得容易。这一个和模块化不同,模块化的功能单一,而剥离这个功能,主要是为了维护不同功能;
- 跨语言兼容。不同语言也可以通过动态库使用对应的功能,使得其变得与语言无关;
- 降低磁盘空间。同一个功能只需要存储一份动态库,而不需要每个程序都带有相应的代码段;
2.2 静态库
静态库在程序进行编译之时就被链接到程序中,每个程序都独占这部分功能代码。Windows中以.lib
结尾,而Linux则以 .a
结尾。一般而言[1],静态库含有对应功能的所有实现,其优点在于:
- 独立性。静态库被链接到应用程序,将内容直接“注入”应用程序,不再需要存放着内容的动态库
.dll
,程序的部署和分发变得简单,无需担心目标系统是否具有对应的动态库; - 兼容性。版本冲突基本上不会出现,因为每个程序在编译之时就已经完成了版本冲突检查,如果有兼容性问题,编译器就被暴露出来了;
- 性能。使用静态库的应用程序无需在运行时进行加载,降低了程序开销;
- 安全性。静态库在编译时已经确定,攻击者更加难以通过替换库中的函数进恶意程序注入;(这就是为什么破解替换动态库就可以完成,大概率是因为替换掉了验证部分函数)
下面是一张比较动态库和静态库优缺点的表格:
动态库 | 静态库 | |
---|---|---|
优点 | 1. 节省内存 2. 支持模块化设计 3. 代码重用 4. 简化更新和修复过程 5. 跨语言兼容性 6. 减少磁盘空间的使用 | 1. 独立性 2. 兼容性 3. 性能 4. 安全性 |
缺点 | 1. 可能导致版本冲突 2. 运行时需要加载和链接库,可能影响性能 | 1. 如果库代码更新,所有使用此库的程序都需要重新编译和链接 2. 程序文件大小通常比动态链接的程序更大 |
三、Windows下动态库和静态库的创建
3.1 如何创建一个动态库?(VS2022为例)
新建一个项目,选择Dynamic-Link Library(DLL)
,VS自动帮我们写了工程配置和部分用于优化的代码。对应源代码和头文件:
#include "pch.h"
#include "addition.h"
int AddNumbers(int a, int b)
{
return a + b;
}
#ifndef ADDITION_H
#define ADDITION_H
__declspec(dllexport) int AddNumbers(int a, int b);
#endif
在Windows平台上,默认情况下,函数和变量不会被自动导出为动态链接库(DLL)的一部分。如果你想要将函数或变量导出为DLL可见的导出项,需要显式地使用
__declspec(dllexport) 关键字进行标记。
在界面上选择目标库的架构和构建模式(Debug或者Release)。如下:
这里我选择了x64 Debug
进行库的生成。在对应目录下可以找到如下内容:
一共生成了四个文件分别是,dll
exp
lib
pdb
:
dll
(Dynamic Link Library)动态链接库:包含已编译的代码和数据,程序运行时将会动态加载;exp
(Exported File)导出文件。是关于dll的导出文件,描述导出函数和数据的名称和属性,含有导出数据和函数的符号信息,其他程序可以根据此文件进行符号解析和导入;lib
(Library):以lib
结尾的文件按功能可以分为两部分,分别是导出库和一般意义上的静态链接库,不过Windows大多数情况都是以导出库形式导出所需要的动态库和函数;PDB
(Program Database) PDB文件时调试符号文件,包含编译器生成的符号信息,用于映射源代码和二进制代码之间的关系,调试器能根据此文件,正确解析符号并提供详细的调试信息,比如函数名、行号等;
动态库导出将代码声明为导出,使用者将库中的函数标记为导入,以便使用其功能。
再次强调一下,在windows生成动态库过程中的lib
和linux下a
不一样,虽然他们都叫做静态库,前者是导出库,后者是含有具体代码的源文件二进制代码。
2.2 如何创建静态库?(以VS2022为例)
步骤和动态库基本相同:
idb
(Intermediate Debug)中间调试文件,主要是为了加快重复生成静态库速度而出现的;pdb
同动态库lib
静态库,我们需要的
四、Linux下创建动态和静态库
4.1 如何创建一个静态库?(Linux下的CMake为例)
CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(buildLib)
set(LIBRARY_OUTPUT_PATH ../lib)
add_library(myLib STATIC library.cpp)
头文件:
//library.h
#ifndef ___LIBRARY_H
#define ___LIBRARY_H
void hello();
int myadd(int,int);
#endif
源文件:
//library.cpp
#include <iostream>
#include "library.h"
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int myadd(int i,int j)
{
return i+j;
}
相对简单一些,CMakeLists.txt同级目录下,可以看到生成的静态库libmyLib.a
:
4.2 如何创建一个动态库(Linux下的CMake为例)
Linux和Windows对于生成库的默认行为不同,前者在默认情况下是全部导出的,后者则是需要显式说明导出的符号。全部导出的好处是,可以减少繁琐的导出或者导入函数,缺点是体积变差。关键字 __declspec
用于标识一个符号是否需要输出,如果在Windows下你需要全部输出,则设置变量CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
为ON
。下面是一个头文件示例(充分考虑了跨平台特性):
// library.h
#ifndef LIBRARY_H
#define LIBRARY_H
// Check if we are on Windows
#ifdef _WIN32
#define LIBRARY_API __declspec(dllexport)
#define LIBRARY_LOCAL
// Check if we are on Unix (Linux, MacOS, etc.)
#elif __GNUC__ >= 4
#define LIBRARY_API __attribute__ ((visibility ("default")))
#define LIBRARY_LOCAL __attribute__ ((visibility ("hidden")))
#else
#define LIBRARY_API
#define LIBRARY_LOCAL
#endif
#ifdef BUILD_DLL
LIBRARY_API void hello();
LIBRARY_API int myadd(int,int);
#else
void hello();
int myadd(int,int);
#endif
#endif // LIBRARY_H
这个头文件做了一些宏处理,使用库和编译库都可以使用同一个头文件。
源文件:
//library.cpp
#include <iostream>
#include "library.h"
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int myadd(int i,int j)
{
return i+j;
}
CMakeLists.txt文件如下:
cmake_minimum_required(VERSION 3.15)
project(buildLib)
set(LIBRARY_OUTPUT_PATH ../lib)
add_library(myLib SHARED library.cpp)
请注意,如果你在Windows下使用这个CMakeLists.txt,如果你不手动添加一些导出关键字,生成动态库下不会出现Windows平台所需要的
lib
文件,除非你手动指定了导出的符号。
生成的文件如下:
五、小结
动态和静态库都是编程上常见的技术,它们各自有各自的特点。在Windows和Linux下他们都有相应的概念,对于Windows而言,为了简化动态库.dll
的使用,Windows提出了一种.lib
文件单独解决和揭示应用程序中使用符号的问题,而Linux则将这部分工作放入了.so
中。需要特别注意的是Windows在使用动态库要使用到的.lib
不一定与Linux.a
一样,它有可能是为了解决动态库使用问题的。这就是为什么我们在Windows平台使用库的使用需要用到两个文件,一个是.dll
另一个则是lib
;在Linux下,只需要在.a
和.so
选择一个即可进行编译。为了保证动态库的使用效率,Windows默认情况下将动态库的所有符号都进行了隐藏,也就是默认不输出;而Linux则是将所有符号进行了输出,所幸的是,它们都有相应的关键字进行可见性的控制。之前遇到Windows生成不了.lib
从而导致没有办法使用其中的库,其实就是因为没有相应标记输出的符号,如果没有输出符号,Windows当然也不会为你生成对应的代码。
[1]为什么我说是一般情况?在Windows中,.lib
文件除了可以用作静态链接库外,还有另一种用途,就是用作动态链接库(.dll
)的“导入库”。