跳转到内容
新建笔记

DLL、导入库与 SO:从符号链接到运行时加载

声明、链接输入和运行库是三种东西

跳转到“声明、链接输入和运行库是三种东西”

原来的三篇笔记分别讨论 _imp_XXX、Windows TCP 示例缺 -lws2_32,以及“为什么 DLL 还需要 LIB,SO 却可以直接链接”。它们可以沿同一条依赖链理解:源文件需要正确声明,链接器需要能够解析符号的输入,加载器需要实际可用的运行库。

文件或配置主要作用不能替代什么
头文件/API 声明让编译器检查名称、类型、调用约定不自动提供库的实现
静态实现库将选中的实现目标代码加入链接产物不自动消除其余动态依赖
Windows 导入库为常规链接提供 DLL 导入所需信息不等于包含完整 DLL 实现
DLL / ELF SO装载后提供动态库实现不保证声明、ABI 或全部依赖已经匹配
包含目录、库目录、运行搜索目录分别影响编译、链接和加载三种目录不能任意互换

.lib 既可能是静态实现库,也可能是导入库。导入库在文件结构上可以是静态归档,但它通常保存导入信息及相关目标片段,而不是把 DLL 的全部功能复制到程序里。MinGW 常见名称为 libname.dll.a;扩展名仍不能独自证明 ABI 兼容。

MSVC 的常规 DLL 链接通常使用导入库。GNU ld 的 MinGW/Cygwin 版本还支持直接链接 DLL,因此“Windows 只有 DLL 一定无法链接”并不成立;显式运行时加载也是另一条路径。是否适用取决于实际工具链、导出和调用约定。GNU ld 的 Windows 支持

Linux ELF 链接器通常直接读取共享对象的动态符号等信息,不需要 Windows 风格的单独导入库。最终程序记录所需库与重定位等信息,运行时由动态加载器处理;这里不应把所有机制都称作 PE 的“导入表”。成功链接也不能保证目标机器上找得到库、符号版本兼容、插件和传递依赖齐全。

一个实际可构建的小型共享库

跳转到“一个实际可构建的小型共享库”

以下三个文件采用 C++17。extern "C" 约束导出接口的语言链接,不能承诺任意编译器、架构或复杂 C++ 对象都能跨 ABI 通用。这里接口只传递两个无符号整数,测试输入为 19 和 23。

demo_api.h:

#ifndef DEMO_API_H
#define DEMO_API_H
#if defined(_WIN32)
#if defined(BUILDING_DEMO_DLL)
#define DEMO_API __declspec(dllexport)
#else
#define DEMO_API __declspec(dllimport)
#endif
#else
#define DEMO_API
#endif
extern "C" DEMO_API unsigned int demo_add(unsigned int a, unsigned int b) noexcept;
#endif

demo_library.cpp:

#include "demo_api.h"
extern "C" DEMO_API unsigned int demo_add(unsigned int a, unsigned int b) noexcept
{
return a + b;
}

link_consumer.cpp:

#include "demo_api.h"
#include <cassert>
#include <cstdio>
int main()
{
const unsigned int answer = demo_add(19U, 23U);
assert(answer == 42U);
std::printf("answer=%u\n", answer);
}

Windows MinGW:生成 DLL 和导入库

跳转到“Windows MinGW:生成 DLL 和导入库”

在这三个文件的目录中执行:

g++ -std=c++17 -DBUILDING_DEMO_DLL -shared demo_library.cpp -o demo.dll -Wl,--out-implib,libdemo.dll.a
g++ -std=c++17 link_consumer.cpp ./libdemo.dll.a -o link_consumer.exe
./link_consumer.exe
g++ -std=c++17 link_consumer.cpp ./demo.dll -o direct_consumer.exe
./direct_consumer.exe

两种消费方式均应输出 answer=42。第一种明确传入导入库,第二种明确传入 DLL,以验证 GNU ld 的能力。它们运行时都仍需要 demo.dll;后者不意味着把 DLL 变成了静态实现。

在匹配的 MSVC 开发命令环境中,等价路线通常是用 /LD 生成 DLL/导入库,再把导入库加入消费端的链接输入;本页没有将 MinGW 的执行记录当作 MSVC 实测。Qt 的完整第三方目标配置另见头文件、静态目标和 DLL。

Linux GCC:直接链接 ELF 共享对象

跳转到“Linux GCC:直接链接 ELF 共享对象”

以下是在 Linux shell 中对同一组三文件使用的命令:

终端窗口
g++ -std=c++17 -fPIC -shared demo_library.cpp -Wl,-soname,libdemo.so -o libdemo.so
g++ -std=c++17 link_consumer.cpp -L . -ldemo -Wl,-rpath,'$ORIGIN' -o link_consumer
./link_consumer

-L . 供本次链接查库;$ORIGIN 在这里供加载器表示可执行文件所在目录,外层单引号阻止 shell 提前替换。真实包还要设计版本化文件名、依赖、安装目录与更新策略,不能把这个实验目录当作完整发布方案。Linux 动态加载器

只有 DLL 时,显式加载需要哪些信息

跳转到“只有 DLL 时,显式加载需要哪些信息”

即便没有导入库,调用者仍需知道准确的导出名称、函数签名、调用约定和所有权约定。不能通过猜测函数指针类型来代替头文件或正式 API 文档。

下面的 Windows 文件 runtime_loader.cpp 不链接 libdemo.dll.a。参数是上述实验生成的 DLL 路径;本例命令行示范使用 ASCII 路径。先转换为绝对路径,再指定加载搜索策略。函数地址只在 DLL 仍被持有时使用。

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <cstdio>
#include <filesystem>
int main(int argc, char** argv)
{
if (argc != 2) {
std::fputs("usage: runtime_loader <demo.dll>\n", stderr);
return 1;
}
std::error_code error;
auto dll = std::filesystem::absolute(argv[1], error);
if (error) {
std::fputs("Cannot form absolute DLL path\n", stderr);
return 2;
}
dll.make_preferred();
HMODULE module = LoadLibraryExW(dll.c_str(), nullptr,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
if (!module) {
std::fprintf(stderr, "LoadLibraryEx failed: %lu\n", GetLastError());
return 2;
}
const FARPROC symbol = GetProcAddress(module, "demo_add");
if (!symbol) {
const DWORD cause = GetLastError();
FreeLibrary(module);
std::fprintf(stderr, "GetProcAddress failed: %lu\n", cause);
return 3;
}
using Add = unsigned int (__cdecl *)(unsigned int, unsigned int) noexcept;
const auto add = reinterpret_cast<Add>(symbol);
const unsigned int answer = add(19U, 23U);
const bool unloaded = FreeLibrary(module) != 0;
if (!unloaded || answer != 42U)
return 4;
std::printf("runtime answer=%u\n", answer);
}

MinGW 可用 g++ -std=c++17 runtime_loader.cpp -o runtime_loader.exe 构建,再运行 ./runtime_loader.exe ./demo.dll,应输出 runtime answer=42。若项目启用了 GCC 的 -Wcast-function-type,这个由通用 FARPROC 转成已知签名的 Windows API 用法会触发警告;本页严格验证仅对这个目标增加 -Wno-cast-function-type,没有取消其他错误检查。

按 Windows API 的约定转换 GetProcAddress 的结果,必须与真实函数匹配。DLL 卸载后不能再调用先前取得的地址,也不能让依赖它的对象继续使用代码。LoadLibraryExW 成功仅证明装载成功,不能证明所有后续 API 调用都正确。Microsoft 运行时动态链接、LoadLibraryExW

原文还提出从 .def 生成导入库,例如 MSVC lib /def:your_module.def /out:your_module.lib /machine:x64。其中名称是占位符;只有导出表和目标架构正确,生成的导入库才有意义。它不能补出 DLL 中不存在的函数,也不能修复错误签名、C++ 对象布局或运行库不兼容。Microsoft 创建导入库

Winsock 的未解析符号:保留原 gcc 修复场景

跳转到“Winsock 的未解析符号:保留原 gcc 修复场景”

原记录在 Windows TCP 服务器构建时遇到 undefined reference,补上了 -lws2_32。这是链接阶段缺少所需库的典型修复;不要把它描述成只要包含头文件就能解决。

下面的 wcc.c 只初始化 Winsock、创建一个尚未连接的套接字并关闭它,不监听端口、不向远端发送数据。它能验证声明、链接和最小资源清理路径,不能验证原服务器协议正确。

#include <winsock2.h>
#include <stdio.h>
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#endif
int main(void)
{
WSADATA data;
const int initialized = WSAStartup(MAKEWORD(2, 2), &data);
if (initialized != 0) {
fprintf(stderr, "WSAStartup failed: %d\n", initialized);
return 1;
}
SOCKET socket_handle = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket_handle == INVALID_SOCKET) {
const int cause = WSAGetLastError();
WSACleanup();
fprintf(stderr, "socket failed: %d\n", cause);
return 2;
}
const int closed = closesocket(socket_handle);
const int close_error = closed == SOCKET_ERROR ? WSAGetLastError() : 0;
const int cleaned = WSACleanup();
if (closed == SOCKET_ERROR || cleaned == SOCKET_ERROR) {
fprintf(stderr, "cleanup failed; close error=%d\n", close_error);
return 3;
}
puts("Winsock create/close passed");
return 0;
}

原命令 gcc wcc.c -o wcc.exe -lws2_32 对应这里的库依赖;CMake 目标可以用 target_link_libraries(target PRIVATE ws2_32) 表达。MSVC 的 #pragma comment(lib, "Ws2_32.lib") 是工具链相关的做法,不是 ISO C/C++ 的通用链接语法。Microsoft Winsock 工程配置

__imp_ 一类前缀提示导入相关符号,但诊断仍要看完整名称与引用位置。先按函数官方文档确认库,再检查目标架构、实际链接命令、导出名称/调用约定、静态与 DLL 宏配置。对于 GCC 风格的链接命令,把需要的库放在引用它的对象之后;重复试加无关库或换文件后缀没有诊断价值。

本页验证范围包括 MinGW 的导入库/直接 DLL/显式加载、Linux 本地共享对象,以及 Winsock 的创建与关闭。缺链接输入和缺导出等失败路径单独核对;没有据这些小程序宣称原 TCP 服务或任意二进制库兼容。