[原创] VC导入库的导入名修改工具(用于从DLL导入STDCALL函数)

fwleiming 2014-02-13 04:32:33
说简单点,就是正常情况下比如写了一个这样的def文件

LIBRARY MyDLL
EXPORTS
function
dosomething

此时如果用lib.exe工具从这个def文件生成lib文件,那么这个lib文件将从DLL导入function和dosomething,同时向链接器提供_function和_dosomething。这样的话对于__cdecl方式导出的函数是没有问题的。但是如果DLL的导出符号仍然是function和dosomething,但是调用方式却是__stdcall,就会遇上了问题:如果function有一个int型参数,那么链接器将去寻找_function@4而不是_function,因为找不到符号,所以链接失败了。

如果DLL中导出的符号名是function@4,此时生成的lib就会从DLL导入function@4并向链接器提供_function@4,但是实际上以__stdcall方式调用,但是最后导出的时候不带@nn的情况是非常多的。最典型的情况就是Windows API。而且实话说吧因为@nn这样的实在不怎么好看,所以我自己写DLL的时候也会通过使用DEF文件的方式或者#pragma comment(linker, “/EXPORT:xxxxxx”)这样的预编译语句让导出的函数不带如此的后缀。

于是这样需求就变得简单:让生成的导入库从DLL引入的时候是不带后缀的,但是向链接器提供的符号是带后缀的。

这并非不可能,调用Windows API的时候就是这样:Windows SDK里面那些LIB文件,就是实现这样的功能。对其格式进行研究,根本就是和普通的导入库一样。除了……呃,除了某些细节上的参数。

于是我的目的就是修改这些细节上的参数。Microsoft PE and COFF Specification里也提供了这方面的详细资料。

方法的话,遍历LIB文件中的每个member,LIB文件的格式可以从上面提到的资料中找到,不会太复杂。注意每个member的第一个字节似乎是对其到偶数边界的,没有对齐的用\n来补。在遍历的过程中分析前四个字节就知道这个member是不是声明了一个导入函数,如果是,根据导入的格式分析其头部信息,然后获得DLL名、函数名等。

在这个导入member的“头部信息”中,有关于导入名应该如何计算的属性。但是LIB.exe工具却没有这方面的参数可以调整,默认就是先前面给你加一个_下划线,然后方式是NoPrefix就是再把这个下划线去掉。也就是说,def文件中如果写的是function,那么它就先加一个下划线,变成_function,作为向链接器提供的符号。然后导入名生成方式是NoPrefix,也就是链接器在链接的时候会再从_function去掉前缀的下划线变回function作为从DLL导入的名。

这个导入名的计算方式有

IMPORT_ORDINAL:通过标号导入,此时不会计算导入名
IMPORT_NAME:原原本本的,和向链接器提供的符号保持一致
IMPORT_NAME_NOPREFIX:不带前缀的,LIB.EXE生成的就是这种。前缀是指?、@或者可选的下划线_
IMPORT_NAME_UNDECORATE:不带前缀并且只取@之前的部分,此时如果向链接器提供的符号名是_function@4,那么导入名就是function,也就是我们需要的。

有了这些资料,思路就非常清楚,比如有以下两个函数

__declspec(dllimport) int __stdcall add2(int, int);
__declspec(dllimport) int __stdcall add3(int, int, int);


然后DLL里的导出名分别是add2和add3而不是add2@8和add3@12,此时可以写这样一个DEF文件
LIBRARY doadd
EXPORTS
add2@8
add3@12

此时生成的lib文件中,向链接器提供的符号这一边已经是所需要的_add2@8和_add3@12了。然后这个时候要修正的是导入名计算方式,因为现在是IMPORT_NAME_NOPREFIX,要改成IMPORT_NAME_UNDECORATE才行。这个时候就要用十六进制编辑器来修改,但是手工做这种事情实在太麻烦,这种事情应该给程序去做。所以就写了个工具,来解决这种问题。


图片为修改一个函数的导入名计算方式后的截图。改完以后保存,就可以实现目的了。

这里给出这个自制工具的下载:(上传时间:2014年2月13日。工具有修改的话,这里可能不会同步更新。)

这里下载工具
http://download.csdn.net/detail/fwleiming/6919785

运行需要.net framework 2.0环境。修改完成后记得保存。

里面的DLL其实是COM组件形式的,我在资源里面嵌入了tlb文件,接口应该算是比较简单,想自己在程序里调用的话,要注意最初SetSource的时候送入的指针要在整个操作过程中有效(因为在.net环境中,对象的内存地址是有可能被GC给移动的)

p.s. 这坑爹的论坛我又不是发求助帖怎么还一定要积分……
...全文
510 7 打赏 收藏 转发到动态 举报
写回复
用AI写文章
7 条回复
切换为时间正序
请发表友善的回复…
发表回复
赵4老师 2014-02-14
  • 打赏
  • 举报
回复
正如楼主所说,如果能避免人工手动静态或动态跟踪而 编写一个VM来执行跟踪某Export函数的执行以自动获取函数调用方式以及函数的参数字节数 的话,整个世界就清净了!
fwleiming 2014-02-14
  • 打赏
  • 举报
回复
引用 5 楼 zhao4zhong1 的回复:
以下内容来自网络,仅供参考: dll 导出函数名的那些事 关键字: VC++  DLL 导出函数  经常使用VC6的Dependency查看DLL导出函数的名字,会发现有DLL导出函数的名字有时大不相同,导致不同的原因大多是和编译DLL时候指定DLL导出函数的界定符有关系。 VC++支持两种语言:即C/C++,这也是造成DLL导出函数差异的根源 我们用VS2008新建个DLL工程,工程名为"TestDLL" 把默认的源文件后缀 .CPP改为.C(C文件) 输入测试代码如下: 01 int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 为了导出上面这个函数,我们有以下几个方法: 1. 使用传统的模块定义文件 (.def) 新建一个 后缀为.def的文本文件(这里建一个TestDll.Def),文件内容为: LIBRARY TestDll EXPORTS MyFunction 在 Link 时指定输入依赖文件:/DEF:"TestDll.Def" 2. Visual C++ 提供的方便方法 在01行的int 前加入 __declspec(dllexport) 关键字 通过以上两种方法,我们就可以导出MyFunction函数。 我们用Dependency查看导出的函数: 第一种方法导出的函数为: MyFunction 第二种方法导出的函数为: _MyFunction@4 __stdcall会使导出函数名字前面加一个下划线,后面加一个@再加上参数的字节数,比如_MyFunction@4的参数(int iVariant)就是4个字节 __fastcall与 __stdcall类似,不过前面没有下划线,而是一个@,比如@MyFunction@4 __cdecl则是始函数名。 小结:如果要导出C文件中的函数,并且不让编译器改动函数名,用def文件导出函数。 下面我们来看一下C++文件 我们用VS2008新建个DLL工程,工程名为"TestDLL" 默认的源文件后缀为 .CPP (即C++文件)。 输入测试代码如下: 01 int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 为了导出上面这个函数,我们有以下几个方法: 3. 使用传统的模块定义文件 (.def) 新建一个 后缀为.def的文本文件(这里建一个TestDll.Def),文件内容为: LIBRARY TestDll EXPORTS MyFunction 在 Link 时指定输入依赖文件:/DEF:"TestDll.Def" 4. Visual C++ 提供的方便方法 在01行的int 前加入 __declspec(dllexport) 关键字 通过以上两种方法,我们就可以导出MyFunction函数。 我们用Dependency查看导出的函数: 第一种方法导出的函数为: MyFunction 第二种方法导出的函数为: ?MyFunction@@YGHH@Z 可以看到 第二种方法得到的 导出函数名 并不是我们想要的,如果在exe中用显示方法(LoadLibrary、GetProcAddress)调用 MyFunction 肯定会失败。 但是用引入库(*.LIB)的方式调用,则编译器自动处理转换函数名,所以总是没有问题。 解决这个问题的方法是: 用VC 提供的预处理指示符 "#pragma" 来指定链接选项。 如下: #pragma comment(linker, "/EXPORT:MyFunction=?MyFunction@@YGHH@Z") 这时,就会发现导出的函数名字表中已经有了我们想要的MyFunction。但我们发现原来的那个 ?MyFunction@@YGHH@Z 函数还在,这时就可以把 __declspec() 修饰去掉,只需要 pragma 指令即可。 而且还可以使如下形式: #pragma comment(linker, "/EXPORT:MyFunction=_MyFunction@4,PRIVATE") PRIVATE 的作用与其在 def 文件中的作用一样。更多的#pragram请查看MSDN。 小结:如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数。 同时可以用#pragma指令(C 中也可以用)。 总结: C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字也是不同的(一般涉及到C++ 中的重载等)。 如果利用不同编译器分别生成DLL和访问DLL的exe程序,后者在访问该DLL的导出函数时就会出现问题。如上例中函数MyFunction在C++编译器改编后的名字是?MyFunction@@YGHH@Z。我们希望编译后的名字不发生改变,这里有几种方法。 第一种方法是通过一个称为模块定义文件DEF来解决。 LIBRARY TestDll EXPORTS MyFunction LIBRARY 用来指定动态链接库内部名称。该名称与生成的动态链接库名一定要匹配,这句代码不是必须的。 EXPORTS说明了DLL将要导出的函数,以及为这些导出函数指定的符号名。 第二种是定义导出函数时加上限定符:extern "C" 如:#define DLLEXPORT_API extern "C" _declspec(dllexport) 但extern "C"只解决了C和C++语方之间调用的问题(extern "C" 是告诉编译器,让它按C的方式编译),它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。 同时如果导出函数的调用约定发生改变,即使使用extern "C",编译后的函数名还是会发生改变。例如上面我们加入_stdcall关键字说明调用约定(标准调用约定,也就是WINAPI调用约定)。 #define DLLEXPORT_API extern "C" _declspec(dllexport) 01 DLLEXPORT_API int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 编译后函数名MyFunction改编成了_MyFunction@4 通过第一种方法模块定义文件的方式DLL编译后导出函数名不会发生改变。 DLL(动态库)导出函数名乱码含义 C++编译时函数名修饰约定规则: __stdcall调用约定: 1、以"?"标识函数名的开始,后跟函数名; 2、函数名后面以"@@YG"标识参数表的开始,后跟参数表; 3、参数表以代号表示: X--void D--char E--unsigned char F--short H--int I--unsigned int J--long K--unsigned long M--float N--double _N--bool .... PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复; 4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前; 5、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。 其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如 int Test1(char *var1, unsigned long)-----"?Test1@@YGHPADK@Z" void Test2()-----"?Test2@@YGXXZ" __cdecl调用约定: 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。 __fastcall调用约定: 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。 如果要用DEF文件输出一个"C++"类,则把要输出的数据和成员的修饰名都写入.def模块定义文件 所以... 通过def文件来导出C++类是很麻烦的,并且这个修饰名是不可避免的
这一串都没说到我要的要点…… 是这样的,生成DLL的时候弄出个LIB,这个说谁都会我觉得是没问题。 要面对的情况是,只有DLL没有LIB,要根据DLL的导出名和已知的调用方式来生成LIB。 我做的工具是面向DLL调用者的不是面向DLL编写者的。 DLL调用者如果没有拿到LIB文件或者拿到的LIB文件不是VC的,这种情况要在VC中隐式导入的话,就需要VC能用的LIB文件。 自己编写一个DEF文件然后生成LIB是最经典的方法,但是这种方法无法解决像Windows API的那种情况:DLL中的导出名只含有函数名,但是链接器链接的时候需要带有后缀修饰的符号名。 你可以自己尝试一下,来个最简单的, __declspec(dllimport) void __stdcall Sleep(unsigned int); void Entry() { Sleep(10000); } #pragma comment(linker, "/nodefaultlib:kernel32.lib") #pragma comment(linker, "/subsystem:console /entry:Entry") 保存成一个.c文件,然后自己从DEF文件用LIB.EXE工具做一个导入库来搞定Sleep的导入,这个函数都在Kernel32.dll里面。如果导入的时候调用方式没弄正确,比如把它作为cdecl处理,这时虽然可以正常链接生成EXE,但是在Sleep调用完毕之后,是无法正常从Entry函数返回的,因为堆栈的平衡被破坏,此时应该是能看到程序崩溃的。 尝试一下这个情景,我想你会知道我到底想要实现什么、解决什么问题了。
赵4老师 2014-02-14
  • 打赏
  • 举报
回复
以下内容来自网络,仅供参考: dll 导出函数名的那些事 关键字: VC++  DLL 导出函数  经常使用VC6的Dependency查看DLL导出函数的名字,会发现有DLL导出函数的名字有时大不相同,导致不同的原因大多是和编译DLL时候指定DLL导出函数的界定符有关系。 VC++支持两种语言:即C/C++,这也是造成DLL导出函数差异的根源 我们用VS2008新建个DLL工程,工程名为"TestDLL" 把默认的源文件后缀 .CPP改为.C(C文件) 输入测试代码如下: 01 int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 为了导出上面这个函数,我们有以下几个方法: 1. 使用传统的模块定义文件 (.def) 新建一个 后缀为.def的文本文件(这里建一个TestDll.Def),文件内容为: LIBRARY TestDll EXPORTS MyFunction 在 Link 时指定输入依赖文件:/DEF:"TestDll.Def" 2. Visual C++ 提供的方便方法 在01行的int 前加入 __declspec(dllexport) 关键字 通过以上两种方法,我们就可以导出MyFunction函数。 我们用Dependency查看导出的函数: 第一种方法导出的函数为: MyFunction 第二种方法导出的函数为: _MyFunction@4 __stdcall会使导出函数名字前面加一个下划线,后面加一个@再加上参数的字节数,比如_MyFunction@4的参数(int iVariant)就是4个字节 __fastcall与 __stdcall类似,不过前面没有下划线,而是一个@,比如@MyFunction@4 __cdecl则是始函数名。 小结:如果要导出C文件中的函数,并且不让编译器改动函数名,用def文件导出函数。 下面我们来看一下C++文件 我们用VS2008新建个DLL工程,工程名为"TestDLL" 默认的源文件后缀为 .CPP (即C++文件)。 输入测试代码如下: 01 int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 为了导出上面这个函数,我们有以下几个方法: 3. 使用传统的模块定义文件 (.def) 新建一个 后缀为.def的文本文件(这里建一个TestDll.Def),文件内容为: LIBRARY TestDll EXPORTS MyFunction 在 Link 时指定输入依赖文件:/DEF:"TestDll.Def" 4. Visual C++ 提供的方便方法 在01行的int 前加入 __declspec(dllexport) 关键字 通过以上两种方法,我们就可以导出MyFunction函数。 我们用Dependency查看导出的函数: 第一种方法导出的函数为: MyFunction 第二种方法导出的函数为: ?MyFunction@@YGHH@Z 可以看到 第二种方法得到的 导出函数名 并不是我们想要的,如果在exe中用显示方法(LoadLibrary、GetProcAddress)调用 MyFunction 肯定会失败。 但是用引入库(*.LIB)的方式调用,则编译器自动处理转换函数名,所以总是没有问题。 解决这个问题的方法是: 用VC 提供的预处理指示符 "#pragma" 来指定链接选项。 如下: #pragma comment(linker, "/EXPORT:MyFunction=?MyFunction@@YGHH@Z") 这时,就会发现导出的函数名字表中已经有了我们想要的MyFunction。但我们发现原来的那个 ?MyFunction@@YGHH@Z 函数还在,这时就可以把 __declspec() 修饰去掉,只需要 pragma 指令即可。 而且还可以使如下形式: #pragma comment(linker, "/EXPORT:MyFunction=_MyFunction@4,PRIVATE") PRIVATE 的作用与其在 def 文件中的作用一样。更多的#pragram请查看MSDN。 小结:如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数。 同时可以用#pragma指令(C 中也可以用)。 总结: C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字也是不同的(一般涉及到C++ 中的重载等)。 如果利用不同编译器分别生成DLL和访问DLL的exe程序,后者在访问该DLL的导出函数时就会出现问题。如上例中函数MyFunction在C++编译器改编后的名字是?MyFunction@@YGHH@Z。我们希望编译后的名字不发生改变,这里有几种方法。 第一种方法是通过一个称为模块定义文件DEF来解决。 LIBRARY TestDll EXPORTS MyFunction LIBRARY 用来指定动态链接库内部名称。该名称与生成的动态链接库名一定要匹配,这句代码不是必须的。 EXPORTS说明了DLL将要导出的函数,以及为这些导出函数指定的符号名。 第二种是定义导出函数时加上限定符:extern "C" 如:#define DLLEXPORT_API extern "C" _declspec(dllexport) 但extern "C"只解决了C和C++语方之间调用的问题(extern "C" 是告诉编译器,让它按C的方式编译),它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。 同时如果导出函数的调用约定发生改变,即使使用extern "C",编译后的函数名还是会发生改变。例如上面我们加入_stdcall关键字说明调用约定(标准调用约定,也就是WINAPI调用约定)。 #define DLLEXPORT_API extern "C" _declspec(dllexport) 01 DLLEXPORT_API int _stdcall MyFunction(int iVariant) 02 { 03 return 0; 04 } 编译后函数名MyFunction改编成了_MyFunction@4 通过第一种方法模块定义文件的方式DLL编译后导出函数名不会发生改变。 DLL(动态库)导出函数名乱码含义 C++编译时函数名修饰约定规则: __stdcall调用约定: 1、以"?"标识函数名的开始,后跟函数名; 2、函数名后面以"@@YG"标识参数表的开始,后跟参数表; 3、参数表以代号表示: X--void D--char E--unsigned char F--short H--int I--unsigned int J--long K--unsigned long M--float N--double _N--bool .... PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复; 4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前; 5、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。 其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如 int Test1(char *var1, unsigned long)-----"?Test1@@YGHPADK@Z" void Test2()-----"?Test2@@YGXXZ" __cdecl调用约定: 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。 __fastcall调用约定: 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。 如果要用DEF文件输出一个"C++"类,则把要输出的数据和成员的修饰名都写入.def模块定义文件 所以... 通过def文件来导出C++类是很麻烦的,并且这个修饰名是不可避免的
fwleiming 2014-02-14
  • 打赏
  • 举报
回复
引用 3 楼 zhao4zhong1 的回复:
听说可以用dll生成对应的lib
x64下大概是没问题,x64的VC编译器套件对函数名似乎cdecl或者stdcall都不会进行修饰。 x86下理论上可行,实践上有很大的困难。因为仅从DLL的导出表获取的导出名当中,是没办法得知它的调用方式的,可能是cdecl可能是stdcall,这两种调用方式有着不同的调用名,而且对于stdcall调用方式,还必须确切知道它的参数字节数(修饰名中包含这个信息)。虽然我也知道可以通过函数执行完毕后的ret语句的参数来得知确切参数的数量,但是要从函数入口跟踪到函数执行完毕是很困难的,代码中不乏有一些地址并不是静态地写在立即数里,比如jmp dword [xxxx]比如call dword [xxxx]又比如通过虚表调用虚函数的情况,更有理论上存在的利用条件跳转实现无条件跳转的情况,要编写一个VM来执行跟踪这些代码的执行以自动获取stdcall调用方式的函数的参数字节数,这并不是可以容易做到的事情。 另外,一种我在研究LIB导入文件格式的时候在MSVCRT.LIB中发现的情况,向链接器提供的是C++的修饰名,但是从DLL导入的却是C那样的符号。
赵4老师 2014-02-14
  • 打赏
  • 举报
回复
听说可以用dll生成对应的lib
fwleiming 2014-02-13
  • 打赏
  • 举报
回复
引用 1 楼 u012421456 的回复:
似乎目前对俺来说没啥用 这个帖子是不是该发到VC/MFC板块啊
感觉不太好选…… 因为这个工具一个重要的用途是有DLL但是没有对应的LIB的情况, 比如GCC等其他编译器生成的DLL,虽然说最后服务对象还是VC编译器。 那个板块看名字的话给人感觉以MFC啊ATL啊以及windows下什么什么开发这类东西的讨论为主 虽然我是希望多一点人看到这个帖子啦这样可以促进交(xuàn)流(yào)(误) 怎么说这个也是困扰了我有那么些年头的问题,虽然到头来要说是彻底解决的话还是自己给解决的,之前用implib sdk来作为解决方法,不过发现这工具有一些局限性很烦人,比如DLL的文件名的长度限制,之类。
百曉生 2014-02-13
  • 打赏
  • 举报
回复
似乎目前对俺来说没啥用 这个帖子是不是该发到VC/MFC板块啊

24,856

社区成员

发帖
与我相关
我的任务
社区描述
C/C++ 工具平台和程序库
社区管理员
  • 工具平台和程序库社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧