动态库(Dynamic Library)是包含可以由多个程序同时使用的代码和数据的文件。在Windows上,它们通常被称为DLL(动态链接库),而在Linux和macOS上,它们通常被称为共享对象(.so文件)。当程序使用动态库时,有两种主要的调用方式:静态调用(也称为隐式链接)和动态调用(也称为显式链接)。此外,还有一种与动态调用相关的特性叫做延迟加载。
静态调用(隐式链接)
在静态调用中,编译器和链接器会处理库的引用。需要在编译时告诉链接器我们想要使用哪些库,通常通过命令行参数或IDE设置。对于Windows平台上的DLL,这通常意味着添加对.lib文件的引用;对于Linux/macOS平台上的共享对象,则是直接链接.so文件。
Windows 示例 (C++)
假设我们有一个名为mydll.dll的动态库,其中定义了一个函数int add(int a, int b)。为了在程序中使用这个函数,我们需要一个相应的导入库mydll.lib。
// main.cpp - 使用静态调用方式// 假设 we have an import library mydll.lib for mydll.dll
#pragma comment(lib, "mydll.lib") // 告诉链接器需要链接 mydll.libextern "C" int add(int a, int b); // 声明从 DLL 导出的函数int main() {int result = add(5, 3); // 直接调用 DLL 中的函数printf("Result: %d\n", result);return 0;
}
Linux/macOS 示例 ©
在Linux或macOS上,如果我们有一个名为libmydll.so
的共享库,我们可以这样链接并调用它:
// main.c - 使用静态调用方式#include <stdio.h>// 假定 libmydll.so 提供了 add 函数
extern int add(int a, int b);int main() {int result = add(5, 3); // 直接调用共享库中的函数printf("Result: %d\n", result);return 0;
}// 编译时需要链接共享库
// gcc main.c -o main -L. -lmydll
动态调用(显式链接)
在动态调用中,我们会在运行时手动加载库,并获取库中函数的地址。下面是如何在不同平台上执行此操作的示例。
Windows 示例 (C++)
// main.cpp - 使用动态调用方式#include <windows.h>
#include <iostream>typedef int (*AddFunc)(int, int);int main() {HMODULE hModule = LoadLibrary(L"mydll.dll"); // 加载 DLLif (!hModule) {std::cerr << "Failed to load DLL" << std::endl;return 1;}AddFunc add = (AddFunc)GetProcAddress(hModule, "add"); // 获取函数指针if (!add) {FreeLibrary(hModule);std::cerr << "Failed to get function address" << std::endl;return 1;}int result = add(5, 3); // 通过函数指针调用std::cout << "Result: " << result << std::endl;FreeLibrary(hModule); // 卸载 DLLreturn 0;
}
Linux/macOS 示例 ©
// main.c - 使用动态调用方式#include <dlfcn.h>
#include <stdio.h>typedef int (*AddFunc)(int, int);int main() {void *handle = dlopen("./libmydll.so", RTLD_LAZY); // 加载共享库if (!handle) {fprintf(stderr, "%s\n", dlerror());return 1;}AddFunc add = (AddFunc)dlsym(handle, "add"); // 获取函数指针const char *dlsym_error = dlerror();if (dlsym_error) {fprintf(stderr, "%s\n", dlsym_error);dlclose(handle);return 1;}int result = add(5, 3); // 通过函数指针调用printf("Result: %d\n", result);dlclose(handle); // 卸载共享库return 0;
}
延迟加载是一种特殊的动态库加载机制,它结合了静态调用的简单性和动态调用的灵活性,延迟加载允许我们在首次调用函数时才加载库,而不是在程序启动时就加载。这可以提高启动速度,并减少内存占用。在Windows上,可以通过链接器选项/DELAYLOAD来实现。在Linux上,可以通过RTLD_LAZY标志来实现。
Windows 示例 (C++)
// main.cpp - 使用延迟加载#pragma comment(linker, "/DELAYLOAD:mydll.dll") // 延迟加载 DLL
#pragma comment(lib, "delayimp.lib") // 需要链接 delayimp.libextern "C" int __declspec(dllimport) add(int a, int b); // 声明从 DLL 导出的函数int main() {int result = add(5, 3); // 第一次调用时加载 DLLprintf("Result: %d\n", result);return 0;
}
在上面的代码中,/DELAYLOAD:mydll.dll指令告诉链接器不要在程序启动时加载mydll.dll,而是等到第一次调用add函数时再加载它。delayimp.lib是一个特殊的库,它提供了必要的支持以实现延迟加载。
Linux/macOS
使用 dlopen 和 RTLD_LAZY
这是最直接的方法,通过显式链接(动态调用)来实现延迟加载。你可以使用 dlopen 函数并传递 RTLD_LAZY 标志来加载库。这种方式允许你在运行时根据条件选择性地加载库,并且符号解析会在首次引用时进行。
void *handle = dlopen("libexample.so", RTLD_LAZY);
if (!handle) {fprintf(stderr, "%s\n", dlerror());exit(EXIT_FAILURE);
}
编译器和链接器支持的延迟加载
一些编译器和链接器提供了内置的支持来实现延迟加载。例如,GNU 编译器集合 (GCC) 和 GNU 链接器 (ld) 提供了 -Wl,–no-as-needed 和 -Wl,-z,lazy 选项来控制库的加载行为。
-Wl,–no-as-needed:确保所有指定的库都被包含在最终的二进制文件中,即使它们没有被直接引用。
-Wl,-z,lazy:告诉链接器以延迟方式加载符号,即符号解析将在首次引用时进行,而不是在加载时立即解析。
你可以在编译和链接时使用这些选项,例如:
gcc -o myprogram myprogram.o -Wl,--no-as-needed -Wl,-z,lazy -lexample
使用 -Wl,–as-needed 和 -Wl,–unresolved-symbols=ignore-all
另一种方法是结合使用 -Wl,–as-needed 和 -Wl,–unresolved-symbols=ignore-all 选项。–as-needed 确保只有在真正需要的时候才加载库,而 --unresolved-symbols=ignore-all 允许链接器忽略未解析的符号,直到它们在运行时被引用。
gcc -o myprogram myprogram.o -Wl,--as-needed -Wl,--unresolved-symbols=ignore-all -lexample
动态链接器配置
Linux 的动态链接器(如 ld.so 或 ld-linux.so)也提供了一些环境变量来控制库的加载行为:
LD_BIND_NOW:如果设置为非空值,强制所有符号在加载时立即解析,相当于 RTLD_NOW。
LD_BIND_LAZY:如果设置为非空值,使符号解析延迟到首次引用时,相当于 RTLD_LAZY。
可以通过设置这些环境变量来影响整个系统的库加载行为,或者仅对特定的应用程序生效:
export LD_BIND_NOW=1 # 强制立即解析所有符号
export LD_BIND_LAZY=1 # 延迟解析符号
总结
静态调用
优点:
简单易用,不需要额外的代码来加载或卸载库。
函数调用更高效,因为地址在程序启动时就已经确定了。
缺点:
如果库不存在或版本不兼容,程序可能无法启动。
即使程序运行时不使用某些库功能,这些库也会被加载到内存中。
动态调用
优点:
可以根据条件选择性地加载库,节省资源。
可以更容易地支持插件架构或多版本库共存。
缺点:
代码更加复杂,需要管理库的加载、卸载和错误处理。
每次调用库函数时都需要通过指针进行间接调用,这可能会稍微降低性能。
优点
提高了程序的启动速度,因为不必要的库不会在启动时加载。
减少了内存占用,只有实际需要的库才会被加载。
缺点
如果库加载失败,错误可能会在程序运行过程中出现,而不是在启动时。
一些平台或编译器可能不支持延迟加载,或者需要特定的编译选项来启用。