返回「计算机、信息技术与工程」

Windows链接

更多
Markdown 结构化数据
本文目录 21 个章节

Windows链接

# Windows链接

  1. 静态链接(static)
  • .lib(静态库) → 代码被拷进最终产物
  • 不产生对该库 DLL 的运行时依赖
  1. 动态链接(dynamic)分两种
  • 隐式动态链接:.lib(导入库) + .dll(运行时) → 产物导入表依赖 DLL
  • 显式动态链接:运行时 LoadLibrary/GetProcAddress → 不在导入表里硬依赖

1)从“编译到运行”的链路全貌

A. 编译(Compile)

  • 输入:.c/.cpp

  • 输出:.obj(目标文件)

  • 里面有什么:

    • 机器码(函数实现)
    • 符号表(这个 obj 里定义了哪些函数/变量、引用了哪些外部符号)
  • 此时 还没有把所有外部函数地址确定,比如你调用 GetProcessMemoryInfo,编译器只会记一个“外部符号引用”。

  • 输入:很多 .obj + 若干 .lib(库)+ 以及一些默认系统库

  • 输出:.exe.dll

  • 链接器做两件大事:

    1. 符号解析:把“外部符号引用”匹配到“某个地方的定义”
    2. 地址重定位:把最终要调用/访问的地址关系安排好

链接有两条主要路径:

  • 你链接到的是 静态库 .lib(里面真的包含实现代码)

  • 链接器会把需要的 .obj 代码复制到最终 exe/dll 里

  • 结果:

    • 最终产物 不需要那个库的 DLL(因为代码已经被带进来)
    • 文件可能更大
    • 每个使用方都有一份拷贝(除非 LTO/COMDAT 合并等优化)

2)动态链接(Dynamic link,基于 DLL)

动态链接又分两种“建立依赖”的方式:

  • 隐式动态链接(Implicit dynamic linking)

    • 你在链接时使用一个 导入库 import lib(也是 .lib)
    • 链接器不会把实现代码拷进来,而是生成 Import Table(导入表)
    • 程序启动/模块加载时,Windows Loader 会加载对应 .dll 并把函数地址填入 IAT
    • 如果 DLL 缺失,可能导致加载失败/进程启动失败(取决于该模块什么时候被加载)
  • 显式动态链接(Explicit dynamic linking)

    • 运行时你自己调用:

      • LoadLibrary("xxx.dll")
      • GetProcAddress("Func")
    • 这时链接器不需要 import lib(或只需要 kernel32 的这些 API)

    • 如果 DLL 缺失,你可以选择优雅降级,不会因为导入表缺失而加载失败


2)“库文件”到底分哪些?(Windows 视角)

1)DLL:动态链接库(运行时实体)

  • .dll 是真正的动态库文件,装着实现代码
  • 运行时会被加载到进程地址空间里

2)LIB:.lib 不是一种“库类型”,而是两种不同东西的容器名(关键)

在 MSVC/Windows 生态里,.lib 可能是:

A) 静态库(static library)

  • 里面包含真实的 .obj 代码(实现)
  • 用于静态链接:代码会被拷进最终 exe/dll

B) 导入库(import library)

  • 里面通常不包含实现代码(或只包含很小的跳板/描述信息)
  • 它告诉链接器:某些符号来自某个 DLL
  • 用于隐式动态链接:最终 exe/dll 里会生成导入表,运行时需要对应 DLL

所以:“.lib 既可能是静态库,也可能是导入库” 光看扩展名看不出来,必须结合来源/用途/工具检查(dumpbin 等)。


Android与Windows的区别

是的,这里确实体现了 Windows vs Android 在“动态链接”工程化形态上的差异。但本质机制并不矛盾:两边都可以“链接到动态库”,只是Windows 把“链接到 DLL”通常分成了两件文件(.lib + .dll),而 Android/Linux 通常直接用 .so 及其符号/SONAME 体系,不需要“import lib”这种额外壳。

下面按你关心的点说明:import lib 是什么、为什么 Android 没这个概念、两边动态链接流程差异、CMakeLists 写法差异


1)为什么 Android 平台很少听说 “import lib”?

Android/Linux 的常规做法

  • 动态库是 .so,里面既有实现代码,也包含动态符号表(可供其他模块在链接/运行时解析)。

  • 链接一个共享库时,链接器用的是:

    • libxxx.so(或 libxxx.solinker stub
    • 生成可执行文件/共享库里的 DT_NEEDED 依赖条目
  • 运行时由 dynamic loader(linker/ld-android.so)加载 .so,按 ELF 机制做重定位与符号解析。

Windows 的常规做法

  • 动态库是 .dll,但 MSVC 的链接器通常不直接用 .dll 做链接输入。

  • MSVC 使用一个配套的 导入库(import library)xxx.lib

    • 它像“链接期说明书”,告诉链接器:这些符号来自哪个 DLL、如何生成导入表(IAT)。
  • 运行时由 Windows Loader 根据导入表加载 .dll 并填充 IAT。

2)两边动态链接的“工程形态”差异总结

Windows(PE/COFF)

  • 链接期:通常用 xxx.lib(import lib)来链接
  • 运行期:加载 xxx.dll
  • 显式动态加载LoadLibrary + GetProcAddress

Android/Linux(ELF)

  • 链接期:用 libxxx.so(或开发包提供的 stub so)
  • 运行期:加载 libxxx.so
  • 显式动态加载dlopen + dlsym

3)CMakeLists 写法有什么差异?

A. 链接一个“系统库/平台库”的差异

Windows:

target_link_libraries(my_target PRIVATE psapi)
  • 这会让链接器去找 psapi.lib(import lib),最终运行时依赖 psapi.dll
  • 如果你写了 #pragma comment(lib,"psapi.lib"),那是 MSVC 私有的做法;CMake 更推荐用 target_link_libraries

Android:

target_link_libraries(my_target PRIVATE log android)
  • Android NDK 里 log / android 等是系统 so(实际是 liblog.so / libandroid.so)。
  • 不存在“额外 import lib”的概念。

B. 链接一个第三方动态库的差异(你自己带的库)

Android: 你通常直接链接 .so

add_library(foo SHARED IMPORTED)
set_target_properties(foo PROPERTIES
  IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libfoo.so"
)
target_link_libraries(my_target PRIVATE foo)

Windows: 第三方库通常给你一套:foo.dll + foo.lib

  • 你链接的是 .lib,运行时需要 .dll
add_library(foo SHARED IMPORTED)
set_target_properties(foo PROPERTIES
  IMPORTED_IMPLIB  "${CMAKE_SOURCE_DIR}/libs/foo.lib"   # 链接期用
  IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/bin/foo.dll"   # 运行时用(可选写)
)
target_link_libraries(my_target PRIVATE foo)

关键差别:Windows 下 CMake 要区分 IMPORTED_IMPLIB(导入库)和 IMPORTED_LOCATION(DLL 本体)。Android 下通常只要 IMPORTED_LOCATION 指向 .so


C. “我不想硬依赖某个动态库”的写法差异(SDK 很常用)

Windows 显式加载:

  • 不在 CMake 链接 psapi(也不 #pragma comment(lib)

  • 运行时:

    • LoadLibraryW(L"psapi.dll")
    • GetProcAddress(...)
  • 好处:缺失时可降级,不影响模块加载/进程启动。

Android 显式加载:

  • 不在 CMake 链接 libxxx.so

  • 运行时:

    • dlopen("libxxx.so", RTLD_NOW)
    • dlsym(...)
  • 同样是可降级策略。