理解和创建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)的“导出库”。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JS JSP ASP .NET J2AM API接口和返回的版本 目前所有版本的JS JSP ASP .NET J2AM 都是提供源代码的,对于一些脚本语言来说,直接解压缩之后就可以使用了,不需要什么安装步骤。另外一些需要编译的语言,则提供了编译用的 shell 文件(Linux/Unix 下使用)和 bat 文件(Windows 下使用),或者直接提供编译好的二进制库文件。 不过为了让读者能够更清楚如何安装,我们还是对每种语言的安装都做详细的讲解,你可以在安装列表里找到你感兴趣的语言的安装方法。 示例 如果你已经把 JS JSP ASP .NET J2AM 安装好了,那么接下来就让我们开始第一个小程序吧。按照惯例,第一个演示程序几乎总是 HelloWorld,我们也不想打破这个惯例,不过对于 PHPRPC 来说,有服务器端就要有客户端,否则我们就没有什么好演示的啦,所以我们的第一个演示程序实际上是两个,一个是服务器端,另一个是客户端。我们都先用 PHP 语言来写好了。 服务器端 view plaincopy to clipboardprint? <?php include ("php/phprpc_server.php"); function HelloWorld() { return 'Hello World!'; } $server = new PHPRPC_Server(); $server->add('HelloWorld'); $server->start(); ?> 客户端 view plaincopy to clipboardprint? <?php include ("php/phprpc_client.php"); $client = new PHPRPC_Client('http://127.0.0.1/server.php'); echo $client->HelloWorld(); ?> 对于服务器端程序,我们应该将它命名为 server.php(这是因为客户端调用时用的是这个名字,而不是 PHPRPC 的什么规定),然后把它放在本地 Web 服务器的根目录下,并保证服务器可以正常运行 PHP 程序,之后在浏览器或命令行下运行客户端程序,你就可以看到结果了。 这两个程序几乎简单到无需解释的地步,所以如果你已经明白它们的意思,那么就可以直接跳过下面的解释,继续看后面的例子。 服务器端第 1 句是将 它的服务器端程序包含到你的程序里,之后的 2 - 4 句是定义一个远程调用的函数,你会发现它与本地函数没有任何区别。第 5 句是创建服务器端对象,第 6 句是添加要发布的方法,这里添加的就是刚刚定义的 HelloWorld 函数,在 PHP 中,添加的发布方法是函数名的字符串表示,在其它语言中可能略有不同。第 7 句是启动服务。 客户端就更简单了,第 1 句是将 它的客户端程序包含到你的程序里。第 2 句是创建客户端对象,其中的参数就是服务器端的地址。第 3 句是对远程方法(函数)的调用,之后通过 echo 将它显示出来。如果顺利的话,执行后你就会看到输出的 Hello World!。 上面的例子是发布的是函数,下面我们来看一下类中的静态方法如何发布: view plaincopy to clipboardprint? <?php include ("php/phprpc_server.php"); class Hello { static function HelloWorld() { return 'Hello World!'; } } $server = new PHPRPC_Server(); $server->add('HelloWorld', 'Hello'); $server->start(); ?> 这个服务器端只要它的名字与发布的地址与上面那个发布函数的例子一样的话,上面的那个客户端就可以得到同样的结果,也就是说,在客户端看来是没有任何区别的。 它并不是只可以在 PHP 中使用,它同样支持其它语言的服务器和客户端,而且还可以无差别的相互调用。 现在我们来看一下如何在 Java 中调用这个 PHP 的服务器方法: view plaincopy to clipboardprint? import org.phprpc.*; interface IHello { public String helloWorld(); } public class HelloWorld
JS JSP ASP .NET J2AM API接口和返回的版本 目前所有版本的JS JSP ASP .NET J2AM 都是提供源代码的,对于一些脚本语言来说,直接解压缩之后就可以使用了,不需要什么安装步骤。另外一些需要编译的语言,则提供了编译用的 shell 文件(Linux/Unix 下使用)和 bat 文件(Windows 下使用),或者直接提供编译好的二进制库文件。 不过为了让读者能够更清楚如何安装,我们还是对每种语言的安装都做详细的讲解,你可以在安装列表里找到你感兴趣的语言的安装方法。 示例 如果你已经把 JS JSP ASP .NET J2AM 安装好了,那么接下来就让我们开始第一个小程序吧。按照惯例,第一个演示程序几乎总是 HelloWorld,我们也不想打破这个惯例,不过对于 PHPRPC 来说,有服务器端就要有客户端,否则我们就没有什么好演示的啦,所以我们的第一个演示程序实际上是两个,一个是服务器端,另一个是客户端。我们都先用 PHP 语言来写好了。 服务器端 view plaincopy to clipboardprint? <?php include ("php/phprpc_server.php"); function HelloWorld() { return 'Hello World!'; } $server = new PHPRPC_Server(); $server->add('HelloWorld'); $server->start(); ?> 客户端 view plaincopy to clipboardprint? <?php include ("php/phprpc_client.php"); $client = new PHPRPC_Client('http://127.0.0.1/server.php'); echo $client->HelloWorld(); ?> 对于服务器端程序,我们应该将它命名为 server.php(这是因为客户端调用时用的是这个名字,而不是 PHPRPC 的什么规定),然后把它放在本地 Web 服务器的根目录下,并保证服务器可以正常运行 PHP 程序,之后在浏览器或命令行下运行客户端程序,你就可以看到结果了。 这两个程序几乎简单到无需解释的地步,所以如果你已经明白它们的意思,那么就可以直接跳过下面的解释,继续看后面的例子。 服务器端第 1 句是将 它的服务器端程序包含到你的程序里,之后的 2 - 4 句是定义一个远程调用的函数,你会发现它与本地函数没有任何区别。第 5 句是创建服务器端对象,第 6 句是添加要发布的方法,这里添加的就是刚刚定义的 HelloWorld 函数,在 PHP 中,添加的发布方法是函数名的字符串表示,在其它语言中可能略有不同。第 7 句是启动服务。 客户端就更简单了,第 1 句是将 它的客户端程序包含到你的程序里。第 2 句是创建客户端对象,其中的参数就是服务器端的地址。第 3 句是对远程方法(函数)的调用,之后通过 echo 将它显示出来。如果顺利的话,执行后你就会看到输出的 Hello World!。 上面的例子是发布的是函数,下面我们来看一下类中的静态方法如何发布: view plaincopy to clipboardprint? <?php include ("php/phprpc_server.php"); class Hello { static function HelloWorld() { return 'Hello World!'; } } $server = new PHPRPC_Server(); $server->add('HelloWorld', 'Hello'); $server->start(); ?> 这个服务器端只要它的名字与发布的地址与上面那个发布函数的例子一样的话,上面的那个客户端就可以得到同样的结果,也就是说,在客户端看来是没有任何区别的。 它并不是只可以在 PHP 中使用,它同样支持其它语言的服务器和客户端,而且还可以无差别的相互调用。 现在我们来看一下如何在 Java 中调用这个 PHP 的服务器方法: view plaincopy to clipboardprint? import org.phprpc.*; interface IHello { public String helloWorld(); } public class HelloWorld { public static void main ( String [] args ) { PHPRPC_Client client = new PHPRPC_Client("http://127.0.0.1/server.php"); IHello clientProxy = (IHello)client.useService(IHello.class); System.out.println(clientProxy.helloWorld()); } } 当我们把这个例子编译之后,在命令行中输入以下命令就可以看到执行结果了: java -classpath .;phprpc_client.jar HelloWorld 这个 Java 的客户端看上去比 PHP 的要稍微复杂一些,不过仍然很好理解。在 Java 客户端中,我们使用了接口来描述远程方法,之后我们通过 useService 方法返回一个远程代理对象,该对象实现了我们定义的接口,之后我们就可以直接调用远程方法 helloWorld 了。如果你比较细心的话,你还会发现我们在 PHP 中定义的方法和在 Java 中定义的接口的名字的大小写有点不同,但是仍然可以正常调用。是的,PHPRPC 发布的方法是不区分大小写的。所以不论你所使用的语言是否区分大小写,都可以按照自己(或语言)的习惯来定义方法名。 在本章的最后,我们再来看一下在 JavaScript 如何调用它的服务。顺便再强调一下,服务器端不止是可以用 PHP 来编写,你同样可以使用其它语言(比如 Java,.NET,Ruby,Python 等),这里我们只是以 PHP 为例而已。在这个例子中你还会看到如何使用加密传输。 view plaincopy to clipboardprint? <html> <head> <title>HelloWorld</title> [removed][removed] [removed] var client = new PHPRPC_Client('http://127.0.0.1/server.php', ['HelloWorld']); client.setEncryptMode(2); client.HelloWorld(function (result) { alert(result); }); [removed] </head> <body> </body> </html> 这个 JavaScript 是在网页中运行的,这里建议大家要把 head 和 body 标签都写全,即使它们对你来说看上去没有什么用处,但是在有些浏览器中,如果这些标签没有写全,或者写的不正确,程序就不能正确运行。 在这个例子中,我们会发现在创建 PHPRPC_Client 对象时,除了要写服务器地址以外,还要将远程方法的名字作为参数,因为远程方法可能不止一个,所以这个参数是数组类型的。 client.setEncryptMode(2); 这句是设置加密传输,参数 2 表示双向加密,就是参数和结果都加密(只不过这个 HelloWorld 比较特殊,它没有参数)。 接下来就是调用远程方法 HelloWorld 了,我们会发现它跟 PHP 和 Java 客户端的调用不太一样。是的,在 JavaScript 中远程调用都是采用异步方式的,也就是说要获得结果,需要用回调函数,回调函数作为远程方法的最后一个参数,回调函数也有四个参数,这里我们只使用第一个参数,也就是返回结果 result,通过 alert(result); 我们就可以在浏览器中看到最后弹出的 Hello World! 提示框了。 通过上面的例子,我们已经看到在 PHP、Java 和 JavaScript 这三种语言中使用 PHPRPC 都很简单,但因为语言的不同,在写法和用法上又有一些差别。不过你不用担心其它的语言跟它们也会有很大的差别,因为在接下来的章节中你很快就会发现其它语言跟这上面三种语言实现的写法和用法上的相似或相同之处了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值