在 C++ 調用 C 語言編譯器編譯的庫時,是不是經常遇到下面這個報錯:
error LNK2019: 無法解析的外部符號 "int __cdecl add(int,int)" (?add@@YAHHH@Z),函數 main 中引用了該符號
正如 《Effective C++》 開篇所説, C++ 是一個 C 語言、OO 風格、模板、STL 風格組成的語言聯邦,C++ 是可以直接引入 C 語言代碼編譯的庫的,而 C 語言和 C++ 由於鏈接器符號設計的差異,引發了一些問題,下面一起討論一下。
1. C++ 與 C 的符號差異
與 C++ 不同,C 語言是不支持命名空間、類和函數重載的,因此它的符號名稱簡單直接,只需函數名即可標識。而對於 C++,為了唯一標識共享了相同名稱的函數或方法,C++ 鏈接器在為函數入口點建立符號時,會使用名稱修飾(Name Mangling)來包含函數的輸入參數的信息。
名稱修飾會將函數名、函數的從屬信息、函數的參數列表進行組合,最後生成可以保證唯一性的符號名稱,比如上述報錯信息中的 ?add@@YAHHH@Z 就是 C++ 鏈接器為簽名為 int add(int,int) 的函數生成的修飾名稱。
注意,由於名稱修飾沒有統一的標準,不同編譯器有自己的生成規則,不同編譯器為同一個源文件中的同一個符號生成的修飾名稱很可能是不一樣的。
2. C++ 中編譯動態庫
比如,下面有一個 mylib 的頭和源文件,代碼如下:
// mylib.h
namespace hello {
int add(int a, int b);
double add(double a, double b);
}
// mylib.cpp
#include "mylib.h"
int hello::add(int a, int b) { return a + b; }
double hello::add(double a, double b) { return a + b; }
在 Linux 環境下,使用 g++ 執行編譯,設置導出動態庫為 libmylib.so,然後查看導出的動態庫中的符號:
# linux 下編譯為 libmylibc.so
g++ -shared -fPIC -o libmylib.so mylib.cpp
# 查看導出的動態符號
objdump -T libmylib.so
# win下編譯生成動態庫,同時生成導入庫
# cl /LD /MD mylib.cpp /link /OUT:mylib.dll /IMPLIB:mylib.lib
# win下查看動態符號
# dumpbin /EXPORTS mylib.dll
結果如下:
動態庫導出的符號中有兩個函數,分別是 hello::add(int, int) 和 hello::add(double, double),其符號名稱被編譯器修飾為了 _ZN5hello3addEii 和 _ZN5hello3addEdd,符號名中包含了命名空間、函數名及參數類型等信息,其符號類型為 DF 也就是函數符號 Function Symbol。
最前面一列的一長串 8 個字節的 0000000000001115 為這個符號在動態庫中的地址偏移量,在動態鏈接過程中,動態鏈接器會根據名稱修飾後的符號名稱(如 _ZN5hello3addEii)找到動態庫中對應的符號地址,加上動態庫在內存中的基地址,計算出符號在進程地址空間中的絕對地址,可執行文件就可以拿到函數具體的地址了。
後面的 g 表示全局符號 Global Symbol 意為這個符號是對外可見的,.text 表示符號所在的節 Section 為代碼段,之後的 8 個字節為代碼段長度。
3. 使用 extern "C" 避免名稱修飾
當我們在 C++ 中調用 C 語言庫時,為了解決符號差異問題,可以使用 extern "C" 指示編譯器按照 C 語言的方式生成符號,避免名稱修飾。
修改 mylib.h 如下:
// mylib.h
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
// mylib.cpp
#include "mylib.h"
int add(int a, int b) { return a + b; }
然後執行編譯,對於 extern "C" 的我們導出為 libmylibc.so
# 編譯為 libmylibc.so
g++ -shared -fPIC -o libmylibc.so mylib.cpp
# 查看導出的動態符號
objdump -T libmylibc.so
結果如下:
可以看到這個符號就叫 add,沒有經過名稱修飾,也就是説如果聲明瞭 extern "C" ,確實影響了鏈接器生成符號時的名稱修飾行為。
4. 為什麼調用 C 標準庫函數時不需要 extern "C"
在 C++ 編譯器中,即便 C 風格的函數不需要名稱修飾,鏈接器默認也會為其創建帶修飾的名稱。如果希望避免名稱修飾,需要使用 extern "C" 關鍵字來告知鏈接器不要修飾符號名稱,那麼鏈接器會創建不帶修飾的符號名稱。
在編寫 C++ 代碼時,我們經常調用 C 語言標準庫中的函數,如 strcpy、memcpy 和 printf。為什麼這些函數不需要顯式地使用 extern "C" 呢?實際上,這些 C 標準庫的頭文件(如 stdio.h 和 string.h 可以點進去看看)在 C++ 環境中已經被包裹在 extern "C" 中。因此,C++ 編譯器會自動處理這些符號,避免名稱修飾。
網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出,如果本文幫助到了你,別忘了點贊支持一下,你的點贊是我更新的最大動力!~
參考文檔:
- 高級C/C++編譯技術
PS:本文同步更新於在下的博客 Github - SHERlocked93/blog 系列文章中,歡迎大家關注我的公號 CPP下午茶,直接搜索即可添加,持續為大家推送 CPP 以及 CPP 周邊相關優質技術文,共同進步,一起加油~