您的位置  > 互联网

创建与使用静态库、动态库的底层格式,内存布局

1.什么是图书馆

库是已经编写的现有的、成熟的、可重用的代码。 实际上,每个程序都依赖于许多基本的底层库。 每个人都不可能从头开始编写代码,因此库的存在具有非凡的意义。

本质上,库是可执行代码的二进制形式,可以由操作系统加载到内存中执行。 有两种类型的库:静态库(.a、.lib)和动态库(.so、.dll)。

所谓静态和动态就是指链接。 回顾一下将程序编译为可执行程序的步骤:

图:编译过程

2.静态库

之所以称为【静态库】是因为在链接阶段,程序集生成的目标文件.o会被链接并与引用的库一起打包成可执行文件。 因此,相应的链接方法称为静态链接。

试想一下,静态库和汇编生成的目标文件链接在一起成为一个可执行文件,那么静态库一定是类似于.o文件格式。 其实静态库可以简单地看成是目标文件(.o/.obj文件)的集合,即很多目标文件压缩打包后形成的文件。 静态库特性总结:

- 静态库到函数库的链接是在编译期间完成的。

-程序运行时与函数库无关,易于移植。

- 浪费空间和资源,因为所有相关的目标文件和涉及的函数库都链接到一个可执行文件中。

接下来,编写一些简单的四算C++类,并将其编译成静态库供其他人使用。 头文件如下:

.h头文件

#pragma once
class StaticMath
{
public:
    StaticMath(void);
    ~StaticMath(void);
 
    static double add(double a, double b);//加法
    static double sub(double a, double b);//减法
    static double mul(double a, double b);//乘法
    static double div(double a, double b);//除法
    void print();
};

使用Linux下的ar工具和VS下的lib.exe将目标文件压缩在一起,并编号和索引,以便于搜索和检索。 创建静态库的一般步骤如图所示:

图:创建静态库的流程

2.1.Linux下创建和使用静态库 2.1.1.Linux静态库命名规则

Linux静态库命名规范必须是“lib[].a”:lib为前缀,中间为静态库名,扩展名为.a。

2.1.2. 创建静态库(.a)

从上面的过程我们可以知道Linux中创建静态库的过程如下:

-首先将代码文件编译成目标文件.o(.o)

g++ -c StaticMath.cpp

注意参数-c,否则会直接编译成可执行文件。

-然后,使用ar工具将目标文件打包成.a静态库文件

ar -crv libstaticmath.a StaticMath.o

生成静态库.a。

较大的项目会编写文件(CMake等项目管理工具)生成静态库,输入多个命令太麻烦。

2.1.3. 使用静态库

编写使用上面创建的静态库的测试代码:

测试代码:

#include "StaticMath.h"
#include 
using namespace std;
int main(int argc, char* argv[])
{
    double a = 10;
    double b = 2;
    cout << "a + b = " << StaticMath::add(a, b) << endl;
    cout << "a - b = " << StaticMath::sub(a, b) << endl;
    cout << "a * b = " << StaticMath::mul(a, b) << endl;
    cout << "a / b = " << StaticMath::div(a, b) << endl;
    StaticMath sm;
    sm.print();
    system("pause");
    return 0;
}

Linux下使用静态库只需在编译时指定静态库的搜索路径(-L选项)和静态库名称(不需要lib前缀和.a后缀、-l选项)即可。

# g++ .cpp -L../-

--L:表示要连接的库所在目录

--l:指定链接所需的动态库。 编译器在搜索动态链接库时有隐含的命名规则,即在给定名称前添加lib,后跟.a或.so来确定库的名称。 。

2.2. 2.2.1. 创建和使用静态库创建静态库 (.lib)

如果使用VS命令行生成静态库,也可以分两步生成程序:

-首先,使用带有编译器选项 /c 的 Cl.exe 编译代码 (cl/.cpp),创建名为“.obj”的目标文件。

-然后,使用库管理器Lib.exe链接代码(.obj),创建静态库.lib。

当然,我们一般不会这样使用。 使用VS项目设置更方便。 创建win32控制台程序时,检查静态库类型; 打开项目“属性面板”-“配置属性”-“常规”,配置类型选择静态库。

图:vs静态库项目属性设置

构建项目以生成静态库。

2.2.2. 使用静态库

Linux下的测试代码如下。 有3种使用方法:

方法一:

在VS中使用静态库方法:

-项目“属性面板”--“公共属性”--“框架和引用”--“添加引用”,将显示“添加引用”对话框。 “项目”选项卡列出了当前解决方案中的各个项目以及所有可以引用的库。 在“项目”选项卡中,选择。 单击“确定”。

-添加.h头文件目录,目录路径必须修改。 打开项目“属性面板”-“配置属性”-“C/C++”-“常规”。 在“附加包含目录”属性值中,键入 .h 头文件所在目录的路径或浏览到该目录。

编译运行OK。

图:静态库测试结果(vs)

如果引用的静态库不是同一个解决方案下的子工程,而是使用了第三方提供的静态库lib和头文件,则无法设置上述方法。 还有2种可行的设置方法。

方法二:

打开项目“属性面板”--“配置属性”--“链接器”--“命令行”,输入静态库的完整路径。

方法三:

-“属性面板”--“配置属性”--“链接器”--“常规”,在附加依赖库目录中输入静态库所在目录;

-“属性面板”--“配置属性”--“链接器”--“输入”,输入附加依赖库中的静态库名称.lib。

3.动态库

通过上面的介绍,我们发现静态库很容易使用和理解,而且还达到了代码复用的目的。 那么为什么我们需要动态库呢?

3.1. 为什么需要动态库?

为什么需要动态库,其实是由于静态库的特性。

-浪费空间是静态库的一个问题。

-还有一个问题是静态库会给程序的更新、部署、发布页面带来麻烦。 如果静态库liba.lib更新了,使用它的应用程序需要重新编译并发布给用户(对于玩家来说,可能是一个小小的改动,但会导致整个程序重新下载并全面更新) 。

动态库不会在程序编译时链接到目标代码中,而是在程序运行时加载。 如果不同的应用程序调用同一个库,它们只需要在内存中拥有一个共享库的实例,避免了空间浪费的问题。 动态库是在程序运行时加载的,这也解决了静态库会给程序的更新、部署、发布页面带来麻烦的问题。 用户只需要更新动态库,增量更新。

动态库特点总结:

-动态库将某些库函数的链接加载推迟到程序运行时。

-可以实现进程间的资源共享。 (所以动态库也称为共享库)

- 轻松升级一些程序。

- 甚至可以真正实现程序代码中完全由程序员控制的链接加载(显式调用)。

与Linux可执行文件格式不同,创建动态库时存在一些差异。

- 系统下的可执行文件格式为PE格式。 动态库需要一个函数来进行初始化入口。 通常,声明函数时需要使用 () 关键字。

- Linux下gcc编译的可执行文件默认是ELF格式,不需要初始化入口,也不需要特殊声明函数,更容易编写。

与创建静态库不同,不需要打包工具(ar、lib.exe),而动态库可以直接使用编译器创建。

3.2.Linux下创建和使用动态库 3.2.1.Linux动态库的命名规则

动态链接库的名称采用.so的形式,前缀为lib,后缀为“.so”。

- 每个共享库都有一个相对于实际库文件的特殊名称“”。 程序启动后,程序用这个名字告诉动态加载器要加载哪个共享库。

- 在文件系统中,只有一个到实际动态库的链接。 对于动态库来说,每个库实际上都有另一个名称供编译器使用。 它是一个链接文件(lib++.so),指向实际的库映像文件。

3.2.2. 创建动态库(.so)

编写四种算术运算动态库代码:

.h头文件

#pragma once
class DynamicMath
{
public:
        DynamicMath(void);
        ~DynamicMath(void); 
        static double add(double a, double b);//加法
        static double sub(double a, double b);//减法
        static double mul(double a, double b);//乘法
        static double div(double a, double b);//除法
        void print();
};

-首先,生成目标文件。 此时添加编译选项-fpic

g++ -fPIC -c DynamicMath.cpp

-fPIC 创建一个与地址无关的编译器(图片、代码),可以在多个应用程序之间共享。

-然后,生成动态库,此时添加链接器选项-

g++ -shared -o libdynmath.so DynamicMath.o

-指定生成动态链接库。

事实上,上述两步可以合并为一条命令:

g++ -fPIC -shared -o libdynmath.so DynamicMath.cpp

3.2.3. 使用动态库

编写使用动态库的测试代码:

测试代码:

#include "../DynamicLibrary/DynamicMath.h"
#include 
using namespace std;
int main(int argc, char* argv[])
{
    double a = 10;
    double b = 2;
    cout << "a + b = " << DynamicMath::add(a, b) << endl;
    cout << "a - b = " << DynamicMath::sub(a, b) << endl;
    cout << "a * b = " << DynamicMath::mul(a, b) << endl;
    cout << "a / b = " << DynamicMath::div(a, b) << endl;
    DynamicMath dyn;
    dyn.print();
    return 0;
}

引用动态库并编译成可执行文件(与静态库相同):

g++ TestDynamicLibrary.cpp -L../DynamicLibrary -ldynmath

然后运行:./a.out,发现报错! ! !

你可能猜到是因为动态库和测试程序不在同一个目录下,那么我们来验证一下是否是这样:

发现或报告错误! ! ! 那么,执行过程中如何定位共享库文件呢?

1)当系统加载可执行代码时,它可以知道它所依赖的库的名称,但它还需要知道绝对路径。 在这种情况下,需要系统动态加载器(/)。

2)对于elf格式的可执行程序,由ld-linux.so*完成,依次搜索elf文件各段——环境变量——/etc/ld.so.cache文件列表——/lib/,/usr/ lib目录找到库文件并将其加载到内存中。

如何让系统找到它:

-如果安装在/lib或者/usr/lib下,ld默认可以找到,不需要其他操作。

- 如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中。 步骤如下:

--编辑/etc/ld.so.conf文件,添加库文件所在目录的路径

--运行,该命令将重建/etc/ld.so.cache文件

我们将创建的动态库复制到/usr/lib下,然后运行测试程序。

3.3. 3.3.1下创建和使用动态库。 创建动态库(.dll)

与Linux相比,系统下创建动态库稍微麻烦一些。 首先需要一个函数来进行初始化入口(创建win32控制台程序时,检查DLL类型会自动生成这个文件):

.cpp入口文件

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

通常声明导出函数时需要使用 () 关键字:

.h头文件

#pragma once
class DynamicMath
{
public:
    __declspec(dllexport) DynamicMath(void);
    __declspec(dllexport) ~DynamicMath(void);
    static __declspec(dllexport) double add(double a, double b);//加法
    static __declspec(dllexport) double sub(double a, double b);//减法
    static __declspec(dllexport) double mul(double a, double b);//乘法
    static __declspec(dllexport) double div(double a, double b);//除法
    __declspec(dllexport) void print();
};

生成动态库需要设置项目属性,打开项目“属性面板”-“配置属性”-“常规”,配置类型选择动态库。

图:v动态库项目属性设置

编译工程,生成动态库。

3.3.2. 使用动态库

创建win32控制台测试程序:

.cpp测试程序

#include "stdafx.h"
#include "DynamicMath.h"
#include 
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    double a = 10;
    double b = 2;
    cout << "a + b = " << DynamicMath::add(a, b) << endl;
    cout << "a - b = " << DynamicMath::sub(a, b) << endl;
    cout << "a * b = " << DynamicMath::mul(a, b) << endl;
    cout << "a / b = " << DynamicMath::div(a, b) << endl;
    DynamicMath dyn;
    dyn.print();
    system("pause");
    return 0;
}

方法一:

-项目“属性面板”--“公共属性”--“框架和引用”--“添加引用”,将显示“添加引用”对话框。 “项目”选项卡列出了当前解决方案中的各个项目以及所有可以引用的库。 在“项目”选项卡中,选择。 单击“确定”。

-添加.h头文件目录,目录路径必须修改。 打开项目“属性面板”-“配置属性”-“C/C++”-“常规”。 在“附加包含目录”属性值中,键入 .h 头文件所在目录的路径或浏览到该目录。

编译运行OK。

图:动态库测试结果(vs)

方法二:

-“属性面板”--“配置属性”--“链接器”--“常规”,在附加依赖库目录中输入动态库所在目录;

-“属性面板”--“配置属性”--“链接器”--“输入”,在附加依赖库中输入动态库编译出来的.lib。

这里你可能会有一个疑问,为什么动态库会有.lib文件呢? 即无论是静态链接库还是动态链接库,最终都有一个lib文件,那么两者有什么区别呢? 事实上,两者是完全不同的东西。

.lib 的大小为 190KB,.lib 的大小为 3KB。 静态库对应的lib文件称为静态库,动态库对应的lib文件称为【导入库】。 事实上,静态库本身包含了实际的执行代码、符号表等,而对于导入库来说,实际的执行代码位于动态库中。 导入库只包含地址符号表等,以保证程序找到对应的函数。 一些基本的地址信息。

4.动态库的显式调用

上面介绍的动态库的使用方法和静态库类似,都是隐式调用。 编译时指定相应的库和搜索路径。 事实上,动态库也可以显式调用。 [在C语言中],显式调用动态库很容易!

4.1. Linux下显式调用动态库

# ,提供以下接口:

-void *(const char *, int mode):函数以指定的模式打开指定的动态链接库文件,并返回一个句柄给调用进程。

-void*dlsym(void*, const char*):dlsym根据动态链接库操作()和()返回符号对应的地址。 使用该函数不仅可以获取函数地址,还可以获取变量地址。

-(void *):用于关闭指定句柄的动态链接库。 只有当这个动态链接库的使用次数为0时,才会真正被系统卸载。

-const char *(void):当动态链接库操作函数执行失败时,可以返回错误信息。 当返回值为NULL时,表示操作函数执行成功。

4.2. 下面显式调用动态库

应用程序必须进行函数调用以在运行时显式加载 DLL。 要显式链接到 DLL,应用程序必须:

- 调用(或类似函数)加载DLL并获取模块句柄。

- 调用以获取指向要由应用程序调用的每个导出函数的函数指针。 由于应用程序通过指针调用DLL函数,编译器不会生成外部引用,因此不需要与导入库链接。

- 使用完 DLL 后调用。

4.3. 显式调用C++动态库时的注意事项

对于C++来说,情况稍微复杂一些。 显式加载 C++ 动态库的困难部分是因为 C++ 名称; 部分原因是没有合适的 API 来加载类。 在C++中,你可能需要使用库中的类,这需要创建类的实例,这并不容易做到。

名称可以用“C”解析。 C++ 有一个特定的关键字来声明使用 C 的函数:“C”。 用“C”声明的函数将使用函数名称作为符号名称,就像 C 函数一样。 因此,只有非成员函数可以声明为“C”并且不能重载。 尽管存在这些限制,“C”函数仍然非常有用,因为它们可以像 C 函数一样动态加载。 添加“C”限定符并不意味着函数中不能使用 C++ 代码。 相反,它仍然是一个完整的C++函数,可以使用任何C++特性和各种类型的参数。

另外,有几篇相关文章介绍如何从C++动态库获取类,但我不推荐这样做:

-“在DLL中调用类”:

-《C++ 迷你指南》:

在C++动态库中“显式”使用Class是非常麻烦和危险的,所以能用“隐式”就不要用“显式”,能用静态就不要用动态。

5.附:Linux库相关命令5.1.g++(gcc)编译选项

--:指定生成动态链接库。

--:指定生成静态链接库。

--fPIC:表示编译成与位置无关的代码,用于编译共享库。 目标文件需要创建为与位置无关的代码,这意味着当可执行程序加载它们时,它们可以放置在可执行程序内存中的任何位置。

--L.:表示要连接的库所在目录。

--l:指定链接所需的动态库。 编译器查找动态链接库时有一个隐含的命名规则,即在给定的名称前面添加lib,在其后面添加.a/.so来确定库的名称。

--Wall:生成所有警告消息。

--ggdb:该选项将尽可能生成可供gdb使用的调试信息。

--g:编译器在编译时生成调试信息。

--c:仅激活预处理、编译和汇编,即使程序成为目标文件(.o文件)。

--Wl,:将参数()传递给链接器ld。 如果中间有逗号,则会被分割成多个选项,传递给链接器。

5.2.nm命令

有时您可能需要检查库中有哪些函数。 nm命令可以打印出库中涉及的所有符号。 库可以是静态的也可以是动态的。 nm列出的符号有很多,常见的有以下三种:

- 一种在库中被调用但库中未定义(说明需要其他库支持),用U表示;

- 一种是库中定义的函数,用T表示,最常见;

——一是所谓的“弱国家”符号。 虽然它们是在库中定义的,但它们可能会被其他库中的同名符号覆盖,用 W 表示。

$nm.h

5.3.ldd命令

ldd命令可以查看可执行程序所依赖的共享库。 例如我们编写的四个算术运算动态库依赖于以下库:

六、总结

两者的区别在于代码加载的时间不同。

- 程序编译时,静态库将连接到目标代码。 程序运行时将不再需要静态库,因此其体积更大。

- 动态库不是在程序编译时链接到目标代码,而是在程序运行时加载。 因此,程序运行时需要存在动态库,因此代码量较小。

动态库的优点是,如果不同的应用程序调用同一个库,它们只需要在内存中拥有一个共享库的实例。 带来好处的同时,也带来了问题! 比如经典的DLL Hell问题,如何避免动态库管理问题,可以自行查找相关资料。

7.相关教程

C++ 教程: