决战未解析的外部符号

译 者: zhzhtst
时 间: 2007-08-18,02:08
链 接: http://bbs.pediy.com/showthread.php?t=49875

决战未解析的外部符号

相信大家在编程时都遇到过这样的错误提示信息:未能解析的外部符号。Matt Pietrek在MSJ杂志1996年7月的Under the Hood专栏中对许多读者请教的这个问题做了回答。

注:附件中是本文的PDF格式,可读性好

决战“未解析的外部符号”
作者:Matt Pietrek

我用Delphi写了一些32位代码并把它编译成了OBJ文件。我想把这个Delphi代码与由Visual C++ 编译的C++代码混合起来用。但是,我却得到了类似下面的未解析符号的链接器错误:                                    D2.OBJ : error LNK2001: unresolved external symbol "MessageBeep"
有问题的函数看起来好像只有Windows API函数。然而,当我用Borland C++ 编译时,一切工作正常。这到底是为什么呢?


啊,链接器错误。这是我特别喜欢的方面。我花费了很多时间来解决这些非常耗时间的问题。现在,我已经有了一套行之有效的办法来快速解决这些“未解析的外部符号”之类的问题。同时,如果你知道一些这方面的基本知识,那我解释起来就不困难了。
可能这只是我的感觉,但是我确实发现现在的程序员都使用非常好用的开发环境,很少有人知道它们的高级语言代码是如何变成可执行的机器代码的。像OBJ和LIB之类的文件对大多数程序员来说都是黑盒。当一切工作正常时,你可能确实不需要知道从你的代码被送到编译器到在磁盘上产生一个可执行文件这段时间内到底发生了什么。但是,如果出现了一些什么问题,这些黑盒很可能是就是你找到问题所在的惟一线索。
我要告诉你两个关于C/C++ 编程的基本事实。我一直把问题归结到这些事实上,并且总能找到答案(至少是在遇到“未解析的外部符号”这样的链接器错误时是这样)。第一个事实是,如果你跨编译单元(一个文件就是一个编译单元)引用符号,链接器看到的符号名必须完全匹配。
现在给出一个具体的例子。假定你的一个源文件A.C中有函数Foo的实现代码,并且,由A.C生成的OBJ文件中这个函数的名字还是Foo。用链接器的说法就是,名字Foo是文件A.OBJ中的一个公共符号。现在,假定你在另外一个名字为B.CPP的文件中调用函数Foo。当你在B.CPP中调用函数Foo时,编译器并不知道函数Foo的实现代码在哪里。在这种情况下,编译器在B.OBJ文件中生成一个记录。这个记录告诉链接器,它需要用函数Foo的真实地址来修正对函数Foo的调用。这个记录被称为外部符号定义,因为函数Foo的位置是在调用它的源文件的外部。链接器的主要工作之一就是匹配,或者说是“解析(resolve)”公共符号(像包含在文件A.OBJ中的公共符号)的外部定义(像文件B.OBJ中的符号Foo)。
在这个例子中,对链接器最重要的并不是在你的源文件中调用了什么函数。相反,惟一重要的事就是公共符号的名字必须与外部名字完全匹配。如果它们并不完全匹配,你就会得到令人害怕的“未解析的外部符号”这样的链接器错误。
第二个基本事实是,编译器背着你偷偷改变了符号的名字。例如,当生成OBJ文件时,C编译器在符号的名称前加一个下划线再放入OBJ文件中。因此,在A.C中的函数Foo在A.OBJ中的公共符号是_Foo。另外一个例子是当你使用C++时,编译器把函数的参数信息也添加到了函数名上。在Visual C++ 中,函数“void Foo(int i)”变成了“?Foo@@YAXH@Z”。这种重命名方法被称为名字粉碎(mangling或decorating),主要是为了让链接器区分重载的函数。(重载函数是名字相同,但参数不同的函数。记住这些,你就会理解链接器是怎样处理重载的C++函数的。)
现在,我们的两个事实说明公共符号与外部符号的名字在链接阶段必须匹配,还有就是,编译器改变了符号名。当你遇到“未解析的外部符号”这样的链接器消息时,要立即采取的行动再明显不过了:找出OBJ或LIB文件中的公共符号名,然后与链接器不能接受的符号名比较。它们几乎总是不相同的,解决这个问题的方法就是让这些符号名匹配。
回到前面的例子,假定函数Foo在B.CPP文件中的原型如下:
void Foo(int i);
如果我链接A.OBJ与B.OBJ时,会有一个链接器错误,为什么呢?因为在A.OBJ文件中,Foo的公共名字是_Foo,但是在B.OBJ文件(由B.CPP生成)中被粉碎后的函数名字是?Foo@@YAXH@Z。这清楚地表明了那两个事实:编译器在两个源文件中都改变了符号名,从而导致符号名不匹配。

在这种情况下,你可以用extern “C”机制来解决这个问题。也就是说,把B.CPP中的函数原型改成
extern  void Foo(int i);

extern "C"告诉编译器不要粉碎函数Foo的名字,按C编译器的做法来(在OBJ文件中,放一个“_”在函数名前使它变成_Foo)。这样,两个名字匹配,从而解决了错误。
怎样才能知道OBJ文件中的外部符号名称,从而改好自己的代码呢?Visual C++ 附带了一个DUMPBIN程序,它可以显示由Visual C++创建的OBJ文件和LIB文件的内容(还有其它东西)。如果你运行DUMPBIN,记得要带上/symbols参数才能看到所有的符号名。Borland编译器附带了一个程序叫TDUMP,它可以用于Borland生成的OBJ文件和LIB文件。要想更容易地解决问题而不使用DUMPBIN或TDUMP,继续往下读。我在本月专栏后面的部分提供了自己的工具。
如果要使基于Delphi的代码与Visual C++ 共同工作,又该如何呢?很明显,几乎所有的Win32函数都被定义成__stdcall类型。除了还指示参数传递习惯外,__stdcall类型的函数的名字已经被Visual C++ 修改得Delphi和Borland C++ 都不认识了。准确地说,Visual C++ 在__stdcall类型的函数的名字前加了一个“_”,在名字的最后加上了“@xxx”。xxx是所有实际通过堆栈传递给函数的参数的大小。因此,MessageBeep(UINT uType)变成了_MessageBeep@4。同样,GetMessageA,它带了四个参数,变成了_GetMessageA@16。一些程序员把这种重命名方法叫做__stdcall名字粉碎,但它与C++名字粉碎是截然不同的。
虽然Visual C++ 认为__stdcall类型的函数的名字已经被粉碎了,但Borland编译器并不这么认为。因此,Delphi生成的OBJ引用MessageBeep,而MessageBeep不在Visual C++ 使用的USER32.LIB导入库中,导入库中的公共符号是_MessageBeep@4。Mirosoft链接器认为这两个名字不匹配,因此产生了一个链接器错误。如果你混合Borland C++ 代码与Microsoft Visual C++ 代码,你会遇到同样的问题。
使事情更复杂的是,当__stdcall类型的函数的名字出现在DLL的导出表中时,Microsoft并不粉碎它。在内部,Visual C++在你的OBJ文件中把MessageBeep函数粉碎成_MessageBeep@4,但是USER32.DLL(MessageBeep函数的代码就在其中)导出的名字却是MessageBeep。这允许Borland编译的代码(它不粉碎__stdcall类型的函数的名字)可以正确地链接Win32 DLL的导出函数。也就是说,当把名字放入DLL的导出表中时,Visual C++ 去掉了前导的“_”和后续的 “@xxx”。
怎样才能混合使用这两个厂商的代码呢?不幸的是,没有什么我们能做的。你的第一反应可能是在Delphi代码中调用函数_MessageBeep@4。同样不幸的是,在Delphi(或C++)中,字符“@”是不合法的,因此这样的代码不能编译。直到编译器厂商开始行动之前,我们只有忍耐。


不知出于什么原因,我不能在Microsoft和Borland的32位编译器之间混合使用OBJ文件和LIB文件。然而,在16位编译器上可以正常工作。这到底是为什么呢?


让我们先把目光对准OBJ文件,然后再说LIB文件。从PC出现到第一个Microsoft Win32编程工具出现,几乎所有编译器生成的OBJ文件都是Intel OMF格式。与OMF格式的OBJ文件打交道并不是一件轻松的事,因此,我并没有打算详细描述它。最初的Windows NT开发小组使用的OBJ文件格式被称为通用目标文件格式(Common Object File Format,COFF),而COFF格式是UNIX System V的正式机器代码格式。使用COFF相对容易。COFF格式的OBJ与可移植可执行(Portable Executable,PE)文件的格式非常接近,而可移植可执行文件格式又是Win32的可执行文件格式。COFF格式的链接器从COFF格式的文件创建EXE或DLL需要做的工作比从Intel OMF格式的文件要少。
就像有OMF和COFF格式的OBJ文件一样,LIB文件也有OMF格式与COFF格式之分。幸运的是,这两种格式的LIB文件都是仅仅把相应格式的一些OBJ文件放在一起组成的单个文件。专用记录中的附加信息可以让链接器快速从LIB文件中找到所需的OBJ文件。
混合使用不同编译器厂商的OBJ文件和LIB文件的问题是,并非每个厂商都把它的32位编译器转换到了COFF格式。Borland和Symantec仍旧使用OMF格式的OBJ文件和LIB文件,但是Microsoft的32位编译器生成COFF格式的OBJ文件和LIB文件。MASM 6.11默认情况下生成OMF格式的文件令人感到困惑,但使用/coff开关可以生成COFF格式的OBJ文件。
当链接不同格式的文件时,每个人可以猜猜链接器会做什么。例如,如果需要,Visual C++ 链接器可以把OMF格式的OBJ文件转换成COFF格式,但它遇到OMF格式的LIB文件时就拒绝工作。Borland的TLINK始终拒绝使用COFF格式的OBJ文件和LIB文件,Symantec C++ 7.2也是如此。Watcom 10.5好像选择的是COFF。结果混合不同编译器生成的文件经常造成混乱。链接器产生的模糊的错误信息并帮不了什么忙。
即使你不混合使用不同编译器生成的OBJ文件,你仍然会在混合使用由不同编译器生成的EXE和DLL时遇到问题。问题来自不同的导入库,这些导入库是一些非常小的OBJ文件的集合,能够告诉链接器某个特定的函数在正在链接的EXE或DLL之外的哪个DLL中。如果你提供了一个DLL,但不知道使用这个DLL的用户使用的是哪个编译器,这样,不同的LIB文件格式就会导致问题。大多数情况下你都得提供两种不同格式的导入库,一种是COFF格式,另一种是OMF格式。问题是,你怎样才能创建这些导入库呢?
如果你曾为Windows 3.x编过程序,你可能使用过编译器附带的一个叫做IMPLIB的工具。IMPLIB接受一个DLL作为输入,生成一个OMF格式的导入库。IMPLIB是通过读取它处理的DLL的导出节来达到上述效果的。因此,如果你使用像Borland C++ 或Symantec C++ 之类的编译器,你可以在任何你想链接的DLL上运行IMPLIB,这样就能得到合适格式的LIB文件。
可惜!32位版的Visual C++ 并没有附带像IMPLIB之类的工具。这是为什么呢?一个很好的解释就是由于文章前面提到的__stdcall类型的函数的名字粉碎。DLL导出的函数名字并不包含任何有关此函数所带参数个数的信息,因此,假定有这样一个IMPLIB,它也不知道怎样生成合适的__stdcall类型的名字(例如,_MessageBeep@4)。
幸运的是,在有些情况下,你可以使用一些鲜为人知技巧。不过这有些乱,并且仅适用于_cdecl类型的函数,不适用于__stdcall类型的函数。如果你想链接到某个DLL上,就创建一个相应的DEF文件。在这个DEF文件中,有一个EXPORTS节,所有需要包含在生成的LIB文件中的函数的名字都要在这个节中。不要在名字前加一个“_”字符,因为它会被自动加上。创建完DEF文件后,运行Microsoft的32位LIB工具,带上/MACHINE和/DEF选项。例如,要为MYDLL.DLL创建一个导入库,你可以先创建MYDLL.DEF文件,然后运行
LIB /MACHINE:i386 /DEF:MYDLL.DEF
如果一切顺利,这会创建一个名字叫MYDLL.LIB的COFF格式的导
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值