关于链接错误的一个罕见原因

来源:互联网 发布:淘宝上传视频怎么传 编辑:程序博客网 时间:2024/05/16 07:23
这是一个关于编译链接的问题。如果平铺直叙地描述,“因为某某原因,所以看到这样的场景”,就会比较的无趣,会觉得这个原因听起来就很无趣。但是如果重现当时的场景,讲解一下trouble shooting的整个过程,抽丝剥茧,最后分析出原因,那么应该会比较的有意思。所以,先来看一下问题是什么吧。

问题是,在编译链接一个项目的时候,报了一个错: 
LNK2001: unresolved external symbol: xxxx staticMetaObject(xxxxxx@@xxx)

稍有经验的开发者,看到这样的错误,就知道这是一个很常见的错误,原因往往是因为lib文件里没有这样的方法或者dll里面没有这样的方法。于是,很自然的,第一步先去检查一下lib文件和dll文件到底在不在链接选项LIBPATH里面。

这里先提一下,这个工程是QT Creator创建的一个Console工程,最终目标是生成一个QtPlugin,其实也就是一个dll文件。当然,这个目标dll是依赖于其他dll的。由于是QT的工程,所以很自然的,要去检查一下.pro文件和.pri文件。检查下来,没有发现什么问题。这只是表面上的,更本质更底层的,要去进一步检查qmake所生成的Makefile到底对不对。检查Makefile.debug之后,发现LIBPATH也是对的。

这就很奇怪了。那么,难道是lib文件里面或者dll文件里面,确实是缺少了一个叫做staticMetaObejct的函数吗?所以,第二步,精确分析lib文件和dll文件的构成,换句话说,dump一下看看。怎么做呢?很自然的,在Windows系统下,直接运行如下的命令即可:

dumpbin /exports xxxx/xxx.libdumpbin /exports xxxx/xxx.dll
当然,dumpbin要被加到%PATH%中,这么运行才没问题。其实,检查dll的话,还有另一个方法,即用DependencyWalker打开这个dll文件,也能看到它包含了哪些function. 

这两句命令运行下来,居然还是没有问题!

其实,到此为止,几乎所有的路都被封死了。实在看不出什么了。也许有人说,你看,staticMetaObject,我知道,这是QT的MOC(Meta-Object Compiler)自动生成的代码,所以,是不是你的QtPlugin写的有问题呢?是不是少写了Q_OBJECT宏,或者是少写了Q_DECLARE_METATYPE(XXX)之类的了呢?这确实是会误导人往这方面想的一个因素,但是很可惜,我也这么想过,仔细检查了,这些都没有写错。就算是这些地方的问题,报错应该也报的不是LNK2001的错。所以说,到此为止,似乎所有的路都封死了。

其实,根据以往的经验,还有一条路。第三步,检查所依赖的dll是否暴露了该暴露的类和方法。

根据最上面的出错信息,这个没有被找到的方法其实是处于一个被依赖的dll里面的,如果这个dll的实现里面根本忘记了把这个类暴露(exposed)出来,那么别人自然找不到用不了啦!嗯,好像是这个原因!赶紧检查!。。。。。。相信读者已经猜到了结果。。。这里仍然是正确的!!暴露了!

所有的线索都断了!根本不可能发生的事情?逻辑上所有的漏洞都没有?实在想不出还有什么原因能引起这个LNK2001的错误了!

怎么办呢?

第四步,用最傻的办法,把所有代码全部注释掉,再编译链接,看能不能过!--- 这不可能不能过了吧?!--- 该类所有的function全部被注释掉,仍然不能过!--- 其实,理性分析来看,这很正常。因为人家报的是LNK2001的错,而不是LNK2019的错!注意,这二者的共同点是,都叫做"unresolved external symbol",而二者的区别是,LNK2001里报的这个function不但没有被链接上,也没有被用过,而LNK2019里面报的这个function,仅仅是没有被链接上,但是被用过(be referenced by xxx)。

前面的四步,无论是用理性的分析,还是用最傻的办法,全部失败了。难道这个是无解的难题了吗?聪明的读者,您觉得是哪里错了呢?

最后一步,重建项目,从头做起。其实,并不是真正的重建项目,而是拷贝了一个其他的项目,替换了一些关键字,再重建起来的。也幸亏不是真正重建,而是拷贝了另一个项目!完事之后,编译链接,一切OK!事情至此,可以肯定,必然能够找到root cause了!经过仔细比对,终于水落石出!原因如下:

假设最终的目标dll叫做a.dll, 它依赖于b.dll. 它们都是QtPlugin. 对于任何一个QtPlugin而言,要暴露自己的类供别人使用,必然在自己的实现里面使用 Q_DECL_EXPORT 来声明此类;而作为使用者,当它要使用别的QtPlugin时,被使用的QtPlugin的类应当是被声明为Q_DECL_IMPORT的。对于普通的dll而言,相应的关键字就是__declspec(dllexport) 和 __declspec(dllimport),都是类似的意思。这个想必大家都耳熟能详、毋须多言了。而且,一个业界常用的、比较smart的方法,就是专门写一个头文件,见如下内容,然后来include它,通过对一个宏的判断,来决定到底是export还是import.

#if defined(ABC_LIBRARY)#  define MYCLASS_EXPORT Q_DECL_EXPORT#else#  define MYCLASS_EXPORT Q_DECL_IMPORT#endif

这里的MYCLASS_EXPORT就是被声明在要暴露的类名前的关键字。根据是否存在宏 ABC_LIBRARY,来决定该类是EXPORT出去,还是该类是IMPORT进来的。所以,在自身的实现里,就会存在ABC_LIBRARY这个宏的定义,而在使用者那里,为了达到IMPORT的目的,就不会存在ABC_LIBRARY这个宏。

在QT Creator中,当新建一个dll的Console工程,添加一个类时,以上这段代码是自动生成的。这是QT比较智能的地方,但也是这里出错的玄机。

上面说了这么多,都是在介绍常识性的知识,下面讲具体的root cause. 

root cause就是,

条件一、在b.dll的工程中,它用的是ABC_LIBRARY来判断b中的类是EXPORT还是IMPORT,而在a.dll中,它也是用ABC_LIBRARY进行的判断!(而这只是因为是QT自动生成的 --- 只要是类名相同,就会生成同一个宏进行判断!)

条件二、在a.dll工程的.pro文件中,它是这么写的:

DEFINES += ABC_LIBRARY....LIBS += -L<b.lib path> -lb

这意味着什么呢?

这就意味着:对于本来应该是IMPORT的b.dll中的类,变成了EXPORT的了!既然不是IMPORT的,那么自然就无法链接到了!于是乎,就报出了LNK2001的错误。分析完毕。

总结:

到底应该在哪里改进呢?

难道是上面的DEFINES和LIBS在.pro文件里面的顺序问题吗?不是,DEFINES写在前面很正常。

难道是应该检查上面的那个自动生成的文件吗?也不是,这个本不必检查。

其实,笔者认为,最好还是不要使用相同的类名,尤其是当它们关系还很近的时候。很多时候,可能会发生一些奇怪的事情,而这,本来是没有必要的。最后,要声明一下的是,相同的类名用在有依赖关系的dll里,这可不是笔者的设计。:)

(完)





1 0