1. 程式人生 > >【C/C++開發】強制連結靜態庫所有符號(包括未被使用的)

【C/C++開發】強制連結靜態庫所有符號(包括未被使用的)

C++程式在連結一個靜態庫時,如果該靜態庫裡的某些方法沒有任何地方呼叫到,最終這些沒有被呼叫到的方法或變數將會被丟棄掉,不會被連結到目標程式中。這樣做大大減小生成二進位制檔案的體積。但是,某些時候,即使靜態庫裡的某些方法沒有任何地方使用到,我們也希望將這些沒有使用到的程式碼編譯進最終的二進位制檔案中。

為什麼會有這樣的需求?的確,存在這種需求的是少數情況,但是一旦你遇到下面的需求,就變得必須了。比如:

  1. 動態外掛機制。程式碼中沒有直接呼叫某方法,但是希望能在執行時動態載入執行某方法。
  2. 執行程式碼覆蓋率統計。需要統計靜態庫所有程式碼的覆蓋情況,而不只是被使用到的程式碼覆蓋情況。

如果是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:[email protected]@@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 ??4[email protected]@[email protected]@@Z
      328 [email protected][email protected]@Turtle?5run?4[email protected]
      328 [email protected]@YAHXZ
      328 [email protected]@@QAEXXZ
     19CE [email protected]@YAXXZ
     19CE [email protected]@@QAEXXZ
     2D16 [email protected][email protected]?5run?4[email protected]
     2D16 [email protected]@YAHXZ
     2D16 [email protected]@@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靜態庫所有符號時,程式碼覆蓋率結果為:

noinclude

通過上面的方法,自動生成LinkAllSymbols.h並#include "LinkAllSymbols.h",覆蓋率結果為:

included

github

所有程式碼見:https://github.com/coderzh/LinkAllSymbols

強制連結靜態庫所有符號(包括未被使用的)