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 off
if 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",覆蓋率結果為: