環境:VS2008
咱們都知道,連接器在生成可執行程序時,會忽略那些沒有用到的符號。可是昨天遇到一個連接問題,看起來與這條基本策略並不相符。首先看一個靜態連接庫的結構:
lib
|
|---------------------|
a.cpp b.cpp
| |
|-------| |-----------|
fun1 fun2 fun3 fun4
| ↑___________|
↓
GetModuleFileNameEx(psapi.lib)
這個庫裏只存在兩個依賴:b.cpp中的fun3依賴於a.cpp中的fun2,a.cpp中的fun1依賴於psapi.lib中的GetModuleFileNameEx。
我在一個app中使用了fun4,除此以外別無其它,根據開頭提到的策略,顯然我並不須要連接psapi.lib。可是事實並不是如此,連接器提示了錯誤:
error LNK2001: 沒法解析的外部符號 _GetModuleFileNameExW@16
通過反覆測試確認,正是fun1所依賴GetModuleFileNameEx致使了連接錯誤。這看起來很難以想象,fun4對fun1並無任何依賴關係,連接器爲什麼會報告錯誤?
就個人經驗來講,連接器會使用這項策略確定是毋庸置疑的,最有可能的,是咱們存在某個認知錯誤。因此我決定驗證一下,這裏的驗證分爲兩部分:
1、連接器要求解析一個符號是否意味着連接器須要在生成的PE文件中包含相關符號的代碼?
遇到LNK2001錯誤時,咱們的第一感受是連接器須要在生成的程序中包含這個符號,也就是:
1. 若是該符號在靜態庫中,那麼會把該符號相關的代碼包含到PE文件中;
2. 若是該符號在動態庫中,那麼須要把該符號記錄到導入表中,以使相關的動態庫會在程序啓動時加載到進程中並進行地址映射;
可是連接器對一個顯然沒有依賴關係的函數中的符號提示了LNK2001錯誤,這讓我對以前的「感受」產生了懷疑:或許連接器要解析一個符號並不意味着它會在生成的可執行程序中包含相關的代碼。用一個簡單的對比試驗就能知道結果:
#include <windows.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
void fun_infile_unuse()
{
TCHAR buf[MAX_PATH];
GetModuleFileNameEx(GetCurrentProcess(), NULL, buf, MAX_PATH);
}
void fun_infile_use()
{
OutputDebugStringA("fun_infile_use\r\n");
}
int _tmain(int argc, _TCHAR* argv[])
{
fun_infile_use();
//fun_infile_unuse();
getchar();
return 0;
}
首先,在這段程序裏,無論有沒有調用fun_infile_unuse函數,#pragma comment(lib, "psapi.lib")都是不可缺乏的,不然連接器會提示LINK2001錯誤。
可是,爲調用與不調用fun_infile_unuse兩種狀況分別編譯兩份程序,用LoadPE查看PE文件的導入表,能夠看到:只有調用了fun_infile_unuse的那份程序的導出表中存在psapi.dll的條目,另外一份程序的導出表中則沒有。分別運行兩份程序,用process explorer查看它們加載的庫列表,也能夠看到:沒有調用fun_infile_unuse的那份程序,運行起來並不會去加載psapi.dll。 —— 圖就不貼了,有興趣的本身驗證。
顯然,若是在不調用fun_infile_unuse的程序中不會存在psapi.dll的導入表項的話,那麼,它也應該不會在程序中包含fun_in_fun_unuse的代碼。
這證明了個人懷疑。也就是說:連接器須要解析一個符號,並不意味着它真的須要「連接」這個符號。而連接器在真正連接符號生成程序時,確實會遵循「忽略未使用過的符號」的原則。
不過這尚未完,當咱們獲得這個結論後,也就意味着,連接器在查找符號時,並非按照調用上的依賴關係來進行遍歷的。那麼,連接器是按照什麼關係來遍歷符號的呢?這是下一個問題。
2、連接器依據怎樣的關係來肯定要「解析」的符號範圍?
我不打算對這個問題作嚴格的推理,只是用幾個測試來對比驗證個人一個猜測。這些測試用例分別是:
1. app
|
main.cpp
|---------|---------|
fun1 fun2 main
| ↑_________|
↓
GetModuleFileNameEx(psapi.lib)
2. app
|
|-----------------|
other.cpp main.cpp
| |
| |---------|
fun1 fun2 main
| ↑_________|
↓
GetModuleFileNameEx(psapi.lib)
3. lib app
| |
a.cpp main.cpp
| |
|-------| |
fun1 fun2 main
| ↑__________|
↓
GetModuleFileNameEx(psapi.lib)
4. lib app
| |
|---------------------| |
a.cpp b.cpp main.cpp
| | |
|-------| |-----------| |
fun1 fun2 fun3 fun4 main
| ↑__________|
↓
GetModuleFileNameEx(psapi.lib)
5. lib app
| |
|---------------------| |
a.cpp b.cpp main.cpp
| | |
|-------| |-----------| |
fun1 fun2 fun3 fun4 main
| ↑___________| ↑__________|
↓
GetModuleFileNameEx(psapi.lib)
對上述幾個用例的測試結果以下:
用例(編號) : 1 2 3 4 5
是否LINK2001 : 是 是 是 否 是
注:在我作的真實測試中,還對命名空間的包含關係作了測試,結果發現沒有影響,從命名空間的涵義來講,也不該該有影響,因此相關測試沒有列進來。
咱們都知道,靜態連接庫是由一組obj組成的,而obj與cpp是一一對應的。因此這裏有一個推測:連接器以根據調用關係來搜索符號,可是在處理時是以obj爲節點單位的。
1. 對project內的全部cpp(obj),連接器要求解析其中全部使用到的符號。
2. 對外部庫,若是使用了其中符號,則定位該符號所在obj,並要求解析該obj中的全部符號。以此類推。
完。
第二部分是不嚴謹的測試,也不打算進一步測試,由於須要確認的只是第一部分。若是有對第二部分的內容感興趣的,那我很是期待看到你的結論:yedaoq@126.com