声明、链接输入和运行库是三种东西
跳转到“声明、链接输入和运行库是三种东西”原来的三篇笔记分别讨论 _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;
#endifdemo_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.ag++ -std=c++17 link_consumer.cpp ./libdemo.dll.a -o link_consumer.exe./link_consumer.exeg++ -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.sog++ -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 服务或任意二进制库兼容。