理解和创建Windows和Linux下的动态和静态库区别

一、引言

在计算机编程的世界中,库是一个非常重要的改变。它的出现提供了一种共享和重用代码的可能性,复杂的程序因为动态库的出现而变得简洁和方便。然而,库并不是单一的:它们可以是动态的,也可以是静态的,每一种类型都有其使用场景。在本文中,我们将深入探讨动态库和静态库的概念,每种类型都有其优点和使用场景。讨论的范围将会集中两种最为常见的平台——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_SYMBOLSON。下面是一个头文件示例(充分考虑了跨平台特性):

// 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)的“导出库”。