DLL两种加载的详解
目前以lib后缀的库有两种,一种为静态链接库(Static Libary,以下简称“静态库”),另一种为动态连接库(DLL,以下简称“动态库”)的导入库(Import Libary,以下简称“导入库”)。
静态库是一个或者多个obj文件的打包,所以有人干脆把从obj文件生成lib的过程称为Archive,即合并到一起。比如你链接一个静态库,如果其中有错,它会准确的找到是哪个obj有错,即静态lib只是壳子。
动态库一般会有对应的导入库,方便程序静态载入动态链接库,否则你可能就需要自己LoadLibary调入DLL文件,然后再手工GetProcAddress获得对应函数了。有了导入库,你只需要链接导入库后按照头文件函数接口的声明调用函数就可以了。
导入库和静态库的区别很大,他们实质是不一样的东西。静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
这也是实际上很多开源代码发布的惯用方式:
1. 预编译的开发包:包含一些.dll文件和一些.lib文件。其中这里的.lib就是导入库,而不要错以为是静态库。但是引入方式和静态库一样,要在链接路 径上添加找到这些.lib的路径。而.dll则最好放到最后产生的应用程序exe执行文件相同的目录。这样运行时,就会自动调入动态链接库。
2. 用户自己编译: 下载的是源代码,按照readme自己编译。生成很可能也是.dll + .lib(导入库)的库文件
3. 如果你只有dll,并且你知道dll中函数的函数原型,那么你可以直接在自己程序中使用LoadLibary调入DLL文件,GetProcAddress
DLL:
动态链接库 (DLL) 是作为共享函数库的可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个 DLL 副本的内容。
动态链接与静态链接的不同之处在于它允许可执行模块(.dll 文件或 .exe 文件)仅包含在运行时定位 DLL 函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库获取所有被引用的函数(下面说明如何把不使用的函数也装载进来),并将库同代码一起放到可执行文件中。
使用动态链接代替静态链接有若干优点。DLL 节省内存,减少交换操作,节省磁盘空间,更易于升级,提供售后支持,提供扩展 MFC 库类的机制,支持多语言程序,并使国际版本的创建轻松完成。
API 就是应用程序编程接口。它是能用来操作组件、应用程序或者操作系统的一组函数。典型的情况下,API 由一个或多个提供某种特殊功能的 DLL 组成。
DLL 是一个文件,其中包含了在 Microsoft? Windows? 下运行的任何应用程序都可调用的函数。运行时,DLL 中的函数动态地链接到调用它的应用程序中。无论有多少应用程序调用 DLL 中的某个函数,在磁盘上只有一个文件包含该函数,且只在它调入内存时才创建该 DLL。
您听到最多的 API 可能是 Windows API,它包括构成 Windows 操作系统的各种 DLL。每个 Windows 应用程序都直接或间接地与 Windows API 互动。Windows API 保证 Windows 下运行的所有应用程序的行为方式一致。
注意 随着 Windows 操作系统的发展,现已发布了几个版本的 Windows API。Windows 3.1 使用 Win16 API。Microsoft? Windows NT?、Windows 95 和 Windows 98 平台使用 Microsoft? Win32? API。
除 Windows API 外,其他一些 API 也已发布。例如,邮件应用程序编程接口 (MAPI) 是一组可用于编写电子邮件应用程序的 DLL。
API 传统上是为开发 Windows 应用程序的 C 和 C++ 程序员编写的,但其他的编程语言(包括VBA)也可以调用 DLL 中的函数。因为大部分 DLL 主要是为 C 和 C++ 程序员编写和整理说明的,所以调用 DLL 函数的方法与调用 VBA 函数会有所不同。在使用 API 时必须了解如何给 DLL 函数传递参数。
警告 调用 Windows API 和 其他 DLL 函数可能会给您的应用程序带来不良影响。从自己的代码中直接调用 DLL 函数时,您绕过了 VBA 通常提供的一些安全机制。如果在定义或调用 DLL 函数时出现错误(所有程序员都不可避免),可能会在应用程序中引起应用程序错误(也称为通用性保护错误,或 GPF)。最好的解决办法是在运行代码以前保存该项目,并确保了解 DLL 函数调用的原理。
LIB 创建标准库、导入库和导出文件,在生成 32 位程序时可将它们与 LINK 一起使用。LIB 从命令提示运行。
可在下列几种模式下使用 LIB:
生成或修改 COFF 库
将成员对象提取到文件中
创建导出文件和导入库
这些模式是互斥的;每次只能以一种模式使用 LIB。
1 静态链接库的优点
(1) 代码装载速度快,执行速度略比动态链接库快;
(2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
2 动态链接库的优点
(1) 更加节省内存并减少页面交换;
(2) DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
(3) 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
(4)适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
3 不足之处
(1) 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
(2) 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统撕掉。这在早期Windows中很常见。
强制链接静态库所有符号(包括未被使用的)
C++程序在链接一个静态库时,如果该静态库里的某些方法没有任何地方调用到,最终这些没有被调用到的方法或变量将会被丢弃掉,不会被链接到目标程序中。这样做大大减小生成二进制文件的体积。但是,某些时候,即使静态库里的某些方法没有任何地方使用到,我们也希望将这些没有使用到的代码编译进最终的二进制文件中。
为什么会有这样的需求?的确,存在这种需求的是少数情况,但是一旦你遇到下面的需求,就变得必须了。比如:
动态插件机制。代码中没有直接调用某方法,但是希望能在运行时动态加载执行某方法。
执行代码覆盖率统计。需要统计静态库所有代码的覆盖情况,而不只是被使用到的代码覆盖情况。
如果是gcc编译,比较好办,只需要加上--whole-archive链接选项。但是在Windows平台,微软的编译器没有这样的选项,一个最接近的选项是/OPT:NOREF。
文档见:https://msdn.microsoft.com/en-us/library/bxwfs976.aspx
说明:/OPT:REF eliminates functions and data that are never referenced; /OPT:NOREF keeps functions and data that are never referenced.
/OPT:NOREF在Debug下是默认打开的,而且只能强制保留本工程未被使用到的函数和变量。对于引用的静态库的未被使用的函数和变量是不生效的。甚至有人认为这是微软的BUG在这个帖子里热烈讨论过:LINK.EXE BUG: /OPT:NOREF option doesn't work!
遇到同样问题的可不止我一个人,比如StackOverFlow里就有人问:What is the Visual studio equivalent to GNU ld option --whole-archive
有人建议他用/INCLUDE 选项强制链接未使用的符号,也有人说使用/OPT:NOREF(显然不行)。
使用/INCLUDE 指定某个符号强制链接是可以的。但是,假如静态库中有成百上千个符号需要强制/INCLUDE,怎么办?
所以,最好的方法,也是上面讨论/OPT:NOREF BUG的帖子里有人提到的方法,就是在代码中使用:
#pragma comment(linker, '/include:?emptyreference@Noisy@@QAEXXZ')
通过上面的方法,可以让链接器强制include一个符号,include:后面的是符号名称。如果要强制include静态库中所有符号,需要把静态库中的所有符号找出来,然后通过上面的方法强制include。
人手工找出所有Symbols,然后添加上面的代码是不太靠谱的。一方面Symbols的格式可读性太差不好维护,另一方面假如静态库符号信息修改了,这个维护代价就更大了。所以,必须让这个过程自动完成。
查看静态库所有符号列表,Linux里可以使用nm ,Windows平台可以使用dumpbin。
执行dumbin.exe需要注意,必须在Visual Studio的开发命令行环境才能执行。不过有个小技巧可以让你不必在Developer Command Prompt执行,就是假如是VS2013环境,建一个批处理,在开头加上:
@echo offif defined VS120COMNTOOLS (call '%VS120COMNTOOLS%\vsvars32.bat')
我们使用dumpbin /LINKERMEMBER xxx.lib,可以列出所有的符号名字,比如查看静态库MyLib.lib所有符号:
d:\Code\Cpp\LinkAllSymbols\Debug>dumpbin.exe /linkermember:1 MyLib.lib
Microsoft (R) COFF/PE Dumper Version 12.00.30501.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file MyLib.lib
File Type: LIBRARY
Archive member name at 8: /
557D4C17 time/date Sun Jun 14 17:40:39 2015
uid
gid
0 mode
ED size
correct header end
9 public symbols
328 ??4Turtle@@QAEAAV0@ABV0@@Z
328 ??_C@_0M@KEAKLOKJ@Turtle?5run?4?$AA@
328 ?Download@@YAHXZ
328 ?Run@Turtle@@QAEXXZ
19CE ?FishRun@@YAXXZ
19CE ?Run@Fish@@QAEXXZ
2D16 ??_C@_08EMEDHABH@Dog?5run?4?$AA@
2D16 ?Foo@@YAHXZ
2D16 ?Run@Dog@@QAEXXZ
Summary
28B4 .debug$S
F0 .debug$T
102 .drectve
15 .rdata
C .rtc$IMZ
C .rtc$TMZ
15A .text$mn
因此,只需要执行dumpbin,并且在输出结果中抽取出所有的符号名称,然后自动生成#pragma comment(linker, '/include:xxx')代码即可。
于是,我写了一个Python脚本,执行dumpbin,然后通过正则表达式拿到所有符号名称,自动生成强制include了所有符号的头文件。关键代码如下:
import re regex = re.compile(r'\s+.*\s([\?_]+.*)') exclude = [] def gen_header_file_for_lib(lib_path, header_path): cmd = ['dumpbin.exe','/linkermember:1', lib_path] lines = execute_command(cmd) symbols = find_matches(lines, regex, exclude) with open(header_path, 'w') as f: header_guard = 'LINK_ALL_SYMBOLS_H_' f.write('#ifndef ' + header_guard + '\n') f.write('#define ' + header_guard + '\n') f.write('// Generated by GenLinkerSymbols.py. Do not modify! \n\n') for symbol in symbols: pragma_line = '#pragma comment(linker, '/include:' + symbol + '')' f.write(pragma_line + '\n') f.write('\n#endif // ' + header_guard + '\n') print('Link symbols count: %s' % len(symbols)) def find_matches(lines, regex, exclude_list): def match(line): m = regex.match(line) if m: return m.group(1).split()[0] return None def exclude_filter(line): if not line: return False for exclude in exclude_list: if line.find(exclude) >= 0: return False return True matches = filter(exclude_filter, map(match, lines)) return list(set(matches))
结合Visual Studio工程配置里的Post-Build Event,就可以在编译静态库之后自动更新头文件了。比如:
python ..\GenSymbolsHeader.py $(OutDir)$(TargetName)$(TargetExt) ..\Include\LinkAllSymbols.h
在使用该静态库的工程代码中,只需要#include 'LinkAllSymbols.h' 就可以了。
对比
使用OpenCppCoverage进行代码覆盖率测试,对比如下:
正常情况下,不强制在linker时include静态库所有符号时,代码覆盖率结果为:
通过上面的方法,自动生成LinkAllSymbols.h并#include 'LinkAllSymbols.h',覆盖率结果为: