windows下动态链接库(dll)深入浅出(一)http://blog.csdn.net/xingfu0539/article/details/6247027

用于声明导入导出函数 
__declspec(dllexport)   声明一个导出函数,一般用于dll中 
__declspec(dllimport)   声明一个导入函数,一般用于使用某个dll的exe中 


转自:http://blog.csdn.net/xingfu0539/article/details/6247027

前记:最近项目上要用到动态链接库,一直都没有正式地学过,在网上down了这本书,正好拿来和大家分享一下,也算是自己做个总结吧。

 

 

1.概论

(1)DLL可以看做仓库,它提供了一些可以直接拿来用的变量、函数或类。在仓库的历史上经历了“无库-静态链接库-动态链接库”的时代。

(2)无论是静态还是动态都是共享代码的方式,如果采用静态链接库,lib的指令都被直接包含在最终生成的EXE文件里,而DLL则不必如此。同时,静态链接库不能包含其他的动态或静态链接库,动态链接库可以包含其他的动态或静态链接库。

(3)DLL的编制与具体的编程语言及编译器无关,比如windows提供的系统DLL,在任何开发环境中都能被调用,不在乎是VB  VC  还是Delphi;动态链接库随处可见,比较著名的三个kernel32.dll  user32.dll gdi32.dll

(4)VC支持三种DLL,分别是Non-MFC DLL 、MFC Regular DLL 、MFC Extension DLL。Non-MFC DLL不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序调用;MFC规则DLL包含一个继承自CWinApp的类,但无其消息循环;MFC扩展DLL采用MFC的动态链接库动态版本创建,它只能被MFC类库所编写的应用程序所调用。

2.编写第一个静态链接库程序

 

我用的环境是VS2010,直接新建一个Win32项目,接下来会让你选择是建立一个什么样的Win32程序,原代码如下所示:

  1. //lib.cpp  
  2. #include "lib.h"  
  3. int add(int x,int y)  
  4. {  
  5.     return x + y;  
  6. }  
  7. //lib.h  
  8. #ifndef LIB_H  
  9. #define LIB_H  
  10. extern "C" int add(int x,int y);  
  11. #endif  
 

新建名为libtest工程,包含着两个文件,生成libtest.lib文件,这就是我们所需要的静态链接库。

把.h文件和.lib文件交给别人就可以开发使用你开发的静态链接库了。

 

再新建一个工程来测试如何调用这个静态链接库,

  1. // main.cpp : 调用LIB中的函数  
  2. #include <stdio.h>  
  3. #include "../lib.h"  
  4. #pragma comment(lib,"..//debug//libTest.lib") //指定与静态库一起连接  
  5. int main(int argc, char* argv[])  
  6. {  
  7.    printf("2 + 3 = %d",add(2,3));  
  8. }  
 

这样我们就能调用libtest.dll了。

 

在上面的代码中我遇到了几个知识点,后来查了查资料,算是补充吧,主要是自己的基础太差。

 

1.extern “C”

 

 

概述

  extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

  (1) 被extern "C"限定的函数或变量是extern类型的

  extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:

  extern int a;

  仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

  通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

  与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

  (2) 被extern "C"修饰的变量和函数是按照C语言方式编译和连接的

编译方式

  未加extern “C”声明时的编译方式

  首先看看C++中对类似C的函数是怎样编译的。

  作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:

  void foo( int x, int y );

  该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangledname”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y)编译生成的符号是不相同的,后者为_foo_int_float。

  同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

  未加extern "C"声明时的连接方式

  假设在C++中,模块A的头文件如下:

  // 模块A头文件 moduleA.h

  #ifndef MODULE_A_H

  #define MODULE_A_H

  int foo( int x, int y );

  #endif

  在模块B中引用该函数:

  // 模块B实现文件 moduleB.cpp

  #i nclude "moduleA.h"

  foo(2,3);

  实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

  加extern "C"声明后的编译和连接方式

  加extern "C"声明后,模块A的头文件变为:

  // 模块A头文件 moduleA.h

  #ifndef MODULE_A_H

  #define MODULE_A_H

  extern "C" int foo( int x, int y );

  #endif

  在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

  (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

  (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

  如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

  所以,可以用一句话概括extern“C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):

  实现C++与C及其它语言的混合编程。

  明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。

 

 

习惯用法

 

extern "C"的惯用法

  (1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

  extern "C"

  {

  #include "cExample.h"

  }

  而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

  笔者编写的C++引用C函数例子工程中包含的三个文件的源代码

如下:

  1. /* c语言头文件:cExample.h */  
  2. #ifndef C_EXAMPLE_H  
  3. #define C_EXAMPLE_H  
  4. extern int add(int x,int y);  
  5. #endif  
  6. /* c语言实现文件:cExample.c */  
  7. #i nclude "cExample.h"  
  8. int add( int x, int y )  
  9. {  
  10. return x + y;  
  11. }  
  12. // c++实现文件,调用add:cppFile.cpp  
  13. extern "C"  
  14. {  
  15. #include "cExample.c"  
  16. }  
  17. int main(int argc, char* argv[])  
  18. {  
  19. add(2,3);  
  20. return 0;  
  21. }  

  如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

  (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

  笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:

 

 

  1. //C++头文件 cppExample.h  
  2. #ifndef CPP_EXAMPLE_H  
  3. #define CPP_EXAMPLE_H  
  4. extern "C" int add( int x, int y );  
  5. #endif  
  6. //C++实现文件 cppExample.cpp  
  7. #include "cppExample.h"  
  8. int add( int x, int y )  
  9. {  
  10. return x + y;  
  11. }  
  12. /* C实现文件 cFile.c 
  13. /* 这样会编译出错:#include "cExample.cpp" */  
  14. extern int add( int x, int y );  
  15. int main( int argc, char* argv[] )  
  16. {  
  17. add( 2, 3 );  
  18. return 0;  
  19. }  
 

 

  总结 C和C++对函数的处理方式是不同的.extern "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明。

 

 

2.#pragma comment

 

 

  #pragma comment( comment-type ,["commentstring"] )

  comment-type是一个预定义的标识符,指定注释的类型,应该是compiler,exestr,lib,linker之一。

  commentstring是一个提供为comment-type提供附加信息的字符串。

  注释类型:

  1、compiler:

  放置编译器的版本或者名字到一个对象文件,该选项是被linker忽略的。

  2、exestr:

  在以后的版本将被取消。

  3、lib:

  放置一个库搜索记录到对象文件中,这个类型应该是和commentstring(指定你要Linker搜索的lib的名称和路径)这个库的名字放在Object文件的默认库搜索记录的后面,linker搜索这个这个库就像你在命令行输入这个命令一样。你可以在一个源文件中设置多个库记录,它们在object文件中的顺序和在源文件中的顺序一样。如果默认库和附加库的次序是需要区别的,使用Z编译开关是防止默认库放到object模块。

  4、linker:

  指定一个连接选项,这样就不用在命令行输入或者在开发环境中设置了。

  只有下面的linker选项能被传给Linker.

  /DEFAULTLIB ,/EXPORT,/INCLUDE,/MANIFESTDEPENDENCY, /MERGE,/SECTION

  (1) /DEFAULTLIB:library

  /DEFAULTLIB 选项将一个 library 添加到 LINK 在解析引用时搜索的库列表。用 /DEFAULTLIB指定的库在命令行上指定的库之后和 .obj 文件中指定的默认库之前被搜索。忽略所有默认库 (/NODEFAULTLIB) 选项重写 /DEFAULTLIB:library。如果在两者中指定了相同的 library 名称,忽略库 (/NODEFAULTLIB:library) 选项将重写 /DEFAULTLIB:library

  (2)/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]

  使用该选项,可以从程序导出函数,以便其他程序可以调用该函数。也可以导出数据。通常在 DLL 中定义导出。entryname 是调用程序要使用的函数或数据项的名称。ordinal 在导出表中指定范围在 1 至 65,535 的索引;如果没有指定 ordinal,则 LINK 将分配一个。NONAME 关键字只将函数导出为序号,没有 entryname。

  DATA 关键字指定导出项为数据项。客户程序中的数据项必须用 extern __declspec(dllimport)

 来声明。

  有三种导出定义的方法,按照建议的使用顺序依次为:

  源代码中的 __declspec(dllexport).def 文件中的 EXPORTS 语句LINK 命令中的 /EXPORT 规范所有这三种方法可以用在同一个程序中。LINK 在生成包含导出的程序时还创建导入库,除非生成中使用了 .exp 文件。

  LINK 使用标识符的修饰形式。编译器在创建 .obj 文件时修饰标识符。如果 entryname 以其未修饰的形式指定给链接器(与其在源代码中一样),则 LINK 将试图匹配该名称。如果无法找到唯一的匹配名称,则 LINK 发出错误信息。当需要将标识符指定给链接器时,请使用 Dumpbin 工具获取该标识符的修饰名形式。

  (3)/INCLUDE:symbol

  /INCLUDE 选项通知链接器将指定的符号添加到符号表。

  若要指定多个符号,请在符号名称之间键入逗号 (,)、分号 (;) 或空格。在命令行上,对每个符号指定一次 /INCLUDE:symbol。

  链接器通过将包含符号定义的对象添加到程序来解析 symbol。该功能对于添包含不会链接到程序的库对象非常有用。用该选项指定符号将通过 /OPT:REF 重写该符号的移除。

 

  我们经常用到的是#pragma comment(lib,"*.lib")这类的。#pragma comment(lib,"Ws2_32.lib")表示链接Ws2_32.lib这个库。 和在工程设置里写上链入Ws2_32.lib的效果一样,不过这种方法写的 程序别人在使用你的代码的时候就不用再设置工程settings了

 

3.#ifdef  LIB_H

  define  LIB_H

  #endif

 

 

 

使用C/C++语言开发软件的程序员经常碰到这样的问题:有时候是程序编译没有问题,但是链接的时候总是报告函数不存在(经典的LNK 2001错误),有时候是程序编译和链接都没有错误,但是只要调用库中的函数就会出现堆栈异常。这些现象通常是出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。函数调用方式决定了函数参数入栈的顺序,是由调用者函数还是被调用函数负责清除栈中的参数等问题;而函数名修饰规则决定了编译器使用何种名字修饰方式来区分不同的函数,如果函数之间的调用约定不匹配或者名字修饰不匹配就会产生以上的问题。本文分别对C和C++这两种编程语言的函数调用约定和函数名修饰规则进行详细的解释,比较了它们的异同之处,并举例说明了以上问题出现的原因。

 

 

函数调用约定(Calling Convention)

 

     函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。

 

 

1.__cdecl

 

     编译器的命令行参数是/Gd。__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall或__fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数,由于每次函数调用都要由

 

编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的API wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰方式。

 

 

2.__fastcall

 

     编译器的命令行参数是/Gr。__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。

 

 

3.__stdcall

 

     编译器的命令行参数是/Gz,__stdcall是Pascal程序的缺省调用方式,大多数Windows的API也是__stdcall调用约定。__stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number

 

4.thiscall

 

     thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。

 

5.naked call

 

     采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESI,EDI,EBX,EBP寄存器的代码,在退出函数时恢复这些寄存器的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked   call不是类型修饰符,故必须和_declspec共同使用。

 

     VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting...菜单-》C/C++ =》Code   Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等单独确定函数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为__stdcall。

 

 

 

函数名字修饰(Decorated Name)方式

 

     函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中,为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的名字修饰的情况是在汇编程序中调用C或C++的函数。如果函数名字,调用约定,返回值类型或函数参数有任何改变,原来的名字修饰就不再有效,必须指定新的名字修饰。C和C++程序的函数在内部使用不同的名字修饰方式,下面将分别介绍这两种方式。

 

1. C编译器的函数名修饰规则

 

     对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number。   

 

举例如下:

__cdecl调用约定(默认方式)

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int add(int x,int y);        //或者int __cdecl add(int x,int y);但完全没必要添加一个__cdecl

#endif

 

//lib.c

//文件:lib.c

#include "lib.h"

int add(int x,int y)        // 或者int __cdecl add(int x,int y)

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = _add

 00000018(    5) 0000001E(    6)

 

__stdcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __stdcall add(int x,int y); //①

#endif

 

//lib.c

//文件:lib.c

#include "lib.h"

int __stdcall add(int x,int y)//②注意①处和本行的形式必须一模一样,尤其②的调用约定__stdcall不能漏掉

//否则会出现重复定义错误(error C2373: 'add' : redefinition; different type modifiers)

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = _add@8

 00000018(    5) 0000001E(    6)

 

 

__fastcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __fastcall add(int x,int y); //

#endif

 

//lib.c

//文件:lib.c

#include "lib.h"

int __fastcall add(int x,int y)//

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = @add@8

 00000020(    5) 00000026(    6)

 

 

 

2. C++编译器的函数名修饰规则

 

     C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示:

X--void   

D--char   

E--unsigned char   

F--short   

H--int    

I--unsigned int   

J--long   

K--unsigned long(DWORD)

M--float   

N--double   

_N--bool

U--struct

....

指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟着参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明:

int Function1(char *var1,unsigned long);

 其函数修饰名为“?Function1@@YGHPADK@Z”,而对于函数声明:

 

void Function2();

 其函数修饰名则为“?Function2@@YGXXZ” 。

 

类似地,以上的三个完整的例子在C++编译器下:

__cdecl调用约定(默认方式)

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int add(int x,int y);        //或者int __cdecl add(int x,int y);但完全没必要添加一个__cdecl

#endif

 

//lib.cpp

//文件:lib.cpp

#include "lib.h"

int add(int x,int y)        // 或者int __cdecl add(int x,int y)

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = ?add@@YAHHH@Z (int __cdecl add(int,int))

 00000018(    5) 0000001E(    6)

 

附:调用约定保持不变,如果采用以下的代码,

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

extern "C" int add(int x,int y);//声明为C编译、连接方式的外部函数

#endif

 

//lib.cpp(编译器自动识别文件为C++文件,然后采用C++编译器进行编译,#ifdef __cplusplus常常用来进行这种判断,MSDN说__cplusplus一个预定义宏(Predefined Macros … __cplusplus Defined for C++ programs only.))

//文件:lib.cpp

#include "lib.h"

int add(int x,int y)        //

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = _add

 00000018(    5) 0000001E(    6)

可以看到extern “C”使得其作用的函数采用C名字修饰方式进行编译。

 

另外,不要在C程序(源程序文件以.c作为后缀)中使用extern “C”,否则也会出现错误。看下例:

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

extern "C" int add(int x,int y);

#endif

//lib.c

//文件:lib.c

#include "lib.h"

int add(int x,int y)       

{

         return x + y;

}

由于C编译器不认识extern “C”,对lib.c进行编译时报错:

…lib.h(4) : error C2059: syntax error : 'string' //…lib.h(4)à extern "C" int add(int x,int y)

 

 

__stdcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __stdcall add(int x,int y); //①

#endif

 

//lib.cpp

//文件:lib.cpp

#include "lib.h"

int __stdcall add(int x,int y)//②注意①处和本行的形式必须一模一样,尤其②的调用约定__stdcall不能漏掉

//否则会出现重复定义错误(error C2373: 'add' : redefinition; different type modifiers)

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = ?add@@YGHHH@Z (int __stdcall add(int,int))

 00000018(    5) 0000001E(    6)

 

__fastcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __fastcall add(int x,int y); //

#endif

 

//lib.cpp

//文件:lib.cpp

#include "lib.h"

int __fastcall add(int x,int y)//

{

         return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbol index:       12 Base line number:     4

 Symbol name = ?add@@YIHHH@Z (int __fastcall add(int,int))

 00000020(    5) 00000026(    6)

 

对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。下面就以类CTest为例说明C++成员函数的名字修饰规则:

 

 

class CTest

{

......

private:

     void Function(int);

protected:

     void CopyInfo(const CTest &src);

public:

     long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);

     long InsightClass(DWORD dwClass) const;

......

};

 

 

 

对于成员函数Function,其函数修饰名为“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示这是一个私有函数。成员函数CopyInfo只有一个参数,是对类CTest的const引用参数,其函数修饰名为“?CopyInfo@CTest@@IAEXABV1@@Z”。DrawText是一个比较复杂的函数声明,不仅有字符串参数,还有结构体参数和HDC句柄参数,需要指出的是HDC实际上是一个HDC__结构类型的指针,这个参数的表示就是“PAUHDC__@@”,其完整的函数修饰名为“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass是一个共有的const函数,它的成员函数标识是“@@QBE”,完整的修饰名就是“?InsightClass@CTest@@QBEJK@Z”。

 

无论是C函数名修饰方式还是C++函数名修饰方式均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

 

3.查看函数的名字修饰

 

     有两种方式可以检查你的程序中的函数的名字修饰:使用编译输出列表或使用Dumpbin工具。使用/FAc,/FAs或/FAcs命令行参数可以让编译器输出函数或变量名字列表。使用dumpbin.exe /SYMBOLS命令也可以获得obj文件或lib文件中的函数或变量名字列表。此外,还可以使用 undname.exe 将修饰名转换为未修饰形式。

 

 

函数调用约定和名字修饰规则不匹配引起的常见问题

     函数调用时如果出现堆栈异常,十有八九是由于函数调用约定不匹配引起的。比如动态链接库a有以下导出函数:

 

long MakeFun(long lFun);

 

动态库生成的时候采用的函数调用约定是__stdcall,所以编译生成的a.dll中函数MakeFun的调用约定是_stdcall,也就是函数调用时参数从右向左入栈,函数返回时自己还原堆栈。现在某个程序模块b要引用a中的MakeFun,b和a一样使用C++方式编译,只是b模块的函数调用方式是__cdecl,由于b包含了a提供的头文件中MakeFun函数声明,所以MakeFun在b模块中被其它调用MakeFun的函数认为是__cdecl调用方式,b模块中的这些函数在调用完MakeFun当然要帮着恢复堆栈啦,可是MakeFun已经在结束时自己恢复了堆栈,b模块中的函数这样多此一举就引起了栈指针错误,从而引发堆栈异常。宏观上的现象就是函数调用没有问题(因为参数传递顺序是一样的),MakeFun也完成了自己的功能,只是函数返回后引发错误。解决的方法也很简单,只要保证两个模块的在编译时设置相同的函数调用约定就行了。

 

     在了解了函数调用约定和函数的名修饰规则之后,再来看在C++程序中使用C语言编译的库时经常出现的LNK 2001错误就很简单了。还以上面例子的两个模块为例,这一次两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编译的(C语言方式),所以a.dll的载入库a.lib中MakeFun函数的名字修饰就是“_MakeFun@

 

error LNK2001: unresolved external symbol ?MakeFun@@YGJJ@Z

 

解决的方法和简单,就是要让b模块知道这个函数是C语言编译的,extern "C"可以做到这一点。一个采用C语言编译的库应该考虑到使用这个库的程序可能是C++程序(使用C++编译器),所以在设计头文件时应该注意这一点。通常应该这样声明头文件:

 

#ifdef _cplusplus

extern "C" {

#endif

 

long MakeFun(long lFun);

 

#ifdef _cplusplus

}

#endif

 

这样C++的编译器就知道MakeFun的修饰名是“_MakeFun@

 

     许多人不明白,为什么我使用的编译器都是VC的编译器还会产生“error LNK

 

 

 

 

 

“error LNK2001: unresolved external symbol”深入浅出

错误现场再现:在工程A(Win32 Static Library)创建一个静态库,然后在另一个工程B(Win32 Console Application)中使用这个库。

A //库工程(Win32 Static Library)

//文件:lib.h

#ifndef LIB_H

#define LIB_H

extern "C" int add(int x,int y);//声明为C编译、连接方式的外部函数

#endif

 

//文件:lib.cpp

#include "lib.h"

int add(int x,int y)

{

        return x + y;

}

或者采用以下另一种形式(但习惯上人们都在.cpp文件中实现C程序,而基本不用.c后缀。):

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int add(int x,int y);//声明为C编译、连接方式的外部函数

#endif

 

//文件:lib.c

#include "lib.h"

int add(int x,int y)

{

        return x + y;

}

Build该工程,生成staticlib.lib库文件。

 

B //调用程序工程(Win32 Console Application)

//文件appCaller.cpp

//文件:appCaller.cpp

#include <stdio.h>

#include "../静态链接库/lib.h"     //包含进相应的程序,可以与lib工程共用。

#pragma comment( lib, "..//静态链接库//debug//staticlib.lib" )   //指定与静态库一起连接

int main(int argc, char* argv[])

{

    printf( "2 + 3 = %d", add( 2, 3 ) );

}

编译(Compile)该文件(先不要马上Build,尽管完全可以这样做。),除了可能出现几个warning之外,没有其他问题,这就表示编译通过。

 

之后进行Build(先前已经进行了编译,所以这里主要的工作就只剩下了链接(Link),不过编译器中没有专门的命令。),结果显示错误:

--------------------Configuration: StaticlibCaller - Win32 Debug--------------------

Linking...

appCaller.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)

Debug/StaticlibCaller.exe : fatal error LNK1120: 1 unresolved externals

Error executing link.exe.

 

StaticlibCaller.exe - 2 error(s), 0 warning(s)

 

"int __cdecl add(int,int)" (?add@@YAHHH@Z):

其中的"int __cdecl add(int,int)"是指lib.h中的函数,这个是应用程序自己的函数,也即unresolved external symbol无法解析的符号是指自身要用到的符号无法解析,这个解析是要到相应的库文件(staticlib.lib)中去解析,如果在库文件中找不到这个符号,就报错,表示连接失败。

       而(?add@@YAHHH@Z)是指左边的函数的完整的C++编译器修饰名。

 

解决办法:

//文件:lib.c改为lib.cpp,然后使用extern “C”处理头文件中的add函数,重新编译生成库。完了原样处理文件appCaller.cpp就可以了。

当然还有别的方法,这里限于篇幅不一一例举。

 

 

 

下面给出几个较复杂的函数及其相应的C++修饰名:

"public: virtual long __stdcall CBaseRenderer::FindPin(wchar_t const *,struct IPin * *)" (?FindPin@CBaseRenderer@@UAGJPB_WPAPAUIPin@@@Z)

 

"public: virtual long __stdcall CBaseVideoRenderer::JoinFilterGraph(struct IFilterGraph *,wchar_t const *)" (?JoinFilterGraph@CBaseVideoRenderer@@UAGJPAUIFilterGraph@@PB_W@Z)

 

"public: virtual long __stdcall CBaseFilter::QueryVendorInfo(wchar_t * *)" (?QueryVendorInfo@CBaseFilter@@UAGJPAPA_W@Z)

 

"public: __thiscall CBaseVideoRenderer::CBaseVideoRenderer(struct _GUID const &,char *,struct IUnknown *,long *)" (??0CBaseVideoRenderer@@QAE@ABU_GUID@@PADPAUIUnknown@@PAJ@Z) referenced in function "public: __thiscall Ogre::CTextureRenderer::CTextureRenderer(struct IUnknown *,long *)" (??0CTextureRenderer@Ogre@@QAE@PAUIUnknown@@PAJ@Z)

 

假设将以上的appCaller.cpp改为:

//文件appCaller.cpp

#include <stdio.h>

//#include "../静态链接库/lib.h"

#pragma comment( lib, "..//静态链接库//debug//staticlib.lib" )//指定与静态库一起连接

//int add(int x,int y);

extern "C" char getChar1(void);

char getChar2(void);

int main(int argc, char* argv[])

{

    //printf( "2 + 3 = %d", add( 2, 3 ) );

        char ch1 = getChar1();

        char ch2 = getChar2();

}

 

编译通过,链接出错,原因在于函数getChar1()和getChar2()压根就没有定义过,所以也没法用:

--------------------Configuration: StaticlibCaller - Win32 Debug--------------------

Linking...

appCaller.obj : error LNK2001: unresolved external symbol "char __cdecl getChar2(void)" (?getChar2@@YADXZ)

appCaller.obj : error LNK2001: unresolved external symbol _getChar1

Debug/StaticlibCaller.exe : fatal error LNK1120: 2 unresolved externals

Error executing link.exe.

 

StaticlibCaller.exe - 3 error(s), 0 warning(s)

注意两个函数的symbol的区别所在(一个C名字修饰,一个C++名字修饰)。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值