环境: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: 无法解析的外部符号 [email protected]
经过反复测试确认,正是fun1所依赖GetModuleFileNameEx导致了链接错误。这看起来很不可思议,fun4对fun1并没有任何依赖关系,链接器为何会报告错误?
就我的经验来说,链接器会使用这项策略肯定是毋庸置疑的,最有可能的,是我们存在某个认知错误。所以我决定验证一下,这里的验证分为两部分:
一、链接器要求解析一个符号是否意味着链接器需要在生成的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的代码。
这证实了我的怀疑。也就是说:链接器需要解析一个符号,并不意味着它真的需要“链接”这个符号。而链接器在真正链接符号生成程序时,确实会遵循“忽略未使用过的符号”的原则。
不过这还没有完,当我们得到这个结论后,也就意味着,链接器在查找符号时,并不是按照调用上的依赖关系来进行遍历的。那么,链接器是按照什么关系来遍历符号的呢?这是下一个问题。
二、链接器依据怎样的关系来确定要“解析”的符号范围?
我不打算对这个问题做严格的推理,只是用几个测试来对比验证我的一个猜想。这些测试用例分别是:
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中的所有符号。以此类推。
完。
第二部分是不严谨的测试,也不打算进一步测试,因为需要确认的只是第一部分。如果有对第二部分的内容感兴趣的,那我非常期待看到你的结论:[email protected]