C# 调用win32 dll

C++ Tool.h

extern "C" __declspec(dllexport) bool  VerifySum(unsigned char *buf, int begin, int verify_Len);
extern "C" __declspec(dllexport) bool VerifyCRC16(unsigned char *buf, int begin, int verify_len);

#pragma once
class Tools
{
public:
    Tools();
    ~Tools();
};

 

(1)编写dll时,为什么有 extern “C”

原因:因为C和C++的重命名规则是不一样的。这种重命名称为“Name-Mangling”(名字修饰或名字改编、标识符重命名,有些人翻译为“名字粉碎法”,这翻译显得有些莫名其妙)

据说,C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的编译器他们的Name-Mangling规则也是不同的。这样的话,不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数。

C标准规定了C语言Name-Mangling的规范(林锐的书有这样说过)。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。

影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。如extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号,而extern “c” __cdecl则不会附加额外的符号。

dll中的函数在被调用时是以函数名或函数编号的方式被索引的。这就意味着采用某编译器的C++的Name-Mangling方式产生的dll文件可能不通用。因为它们的函数名重命名方式不同。为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即是对每一个导出函数声明为extern “C”,而且采用_stdcall调用约定,接着还需要对导出函数进行重命名,以便导出不加修饰的函数名。

注意到extern “C”的作用是为了解决函数符号名的问题,这对于动态链接库的制造者和动态链接库的使用者都需要遵守的规则。

动态链接库的显式装入就是通过GetProcAddress函数,依据动态链接库句柄和函数名,获取函数地址。因为GetProcAddress仅是操作系统相关,可能会操作各种各样的编译器产生的dll,它的参数里的函数名是原原本本的函数名,没有任何修饰,所以一般情况下需要确保dll’里的函数名是原始的函数名。分两步:一,如果导出函数使用了extern”C” _cdecl,那么就不需要再重命名了,这个时候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,这时候dll中的函数名被修饰了,就需要重命名。二、重命名的方式有两种,要么使用*.def文件,在文件外修正,要么使用#pragma,在代码里给函数别名。

此处我们测试两种情况

首先: 使用 extern “C” 和不使用extern “C” 

代码:

Tools.h

extern "C" __declspec(dllexport) bool  VerifySum(unsigned char *buf, int begin, int verify_Len);

 __declspec(dllexport) bool VerifyCRC16(unsigned char *buf, int begin, int verify_len);

#pragma once
class Tools
{
public:
    Tools();
    ~Tools();
};

此时,我们使用VC++ 自带工具DEPENDS.EXE来查看生成的动态链接库的公开的方法列表

X:\Program Files (x86)\Microsoft Visual Studio\Common\Tools\DEPENDS.EXE 

X 代表您程序安装的盘符

此时可以看到未使用 extern “C” 的VerifyCRC16 此时为 “?VerifyCRC16@@YA_NPAEHH@Z”

被C++重命名成这样的了,此时我们可以通过Visual Studio 自带的工具 undname.exe  来查看原始的名字

使用方式用命令行(cmd) 输入

cd  “C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin”

undname ?VerifyCRC16@@YA_NPAEHH@Z

输出如下图所示:

使用 #pragma   

关于 #pargma comment 使用参考MSDN http://msdn.microsoft.com/en-us/library/7k30y2k5(v=vs.80).aspx

Tools.h

extern "C" __declspec(dllexport) bool  VerifySum(unsigned char *buf, int begin, int verify_Len);

#pragma comment(linker,"/export:VerifyCRC16=?VerifyCRC16@@YA_NPAEHH@Z")
 __declspec(dllexport) bool VerifyCRC16(unsigned char *buf, int begin, int verify_len);

#pragma once
class Tools
{
public:
    Tools();
    ~Tools();
};

查看

 

(2)_declspec(dllexport)和_declspec(dllimport)的作用

       _declspec还有另外的用途,这里只讨论跟dll相关的使用。正如括号里的关键字一样,导出和导入。_declspec(dllexport)用在dll上,用于说明这是导出的函数。而_declspec(dllimport)用在调用dll的程序中,用于说明这是从dll中导入的函数。

       因为dll中必须说明函数要用于导出,所以_declspec(dllexport)很有必要。但是可以换一种方式,可以使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。

而使用_declspec(dllimport)却不是必须的,但是建议这么做。因为如果不用_declspec(dllimport)来说明该函数是从dll导入的,那么编译器就不知道这个函数到底在哪里,生成的exe里会有一个call XX的指令,这个XX是一个常数地址,XX地址处是一个jmp dword ptr[XXXX]的指令,跳转到该函数的函数体处,显然这样就无缘无故多了一次中间的跳转。如果使用了_declspec(dllimport)来说明,那么就直接产生call dword ptr[XXX],这样就不会有多余的跳转了。(参考《加密与解密》第三版279页)

(3)__stdcall带来的影响

       这是一种函数的调用方式。默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给Win32汇编使用(或者其他的__stdcall调用方式的程序),那么就可以使用__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式,所以可以方便的使用win32汇编产生的dll。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。

 

C++ Tool.cpp

#include "Tools.h"
#include "stdio.h"

//unsigned char *buf,
bool VerifySum(unsigned char *buf, int begin, int verify_Len)
{
    if (buf == nullptr)
        return false;
    unsigned char suma = 0, sumb = 0;
    
    printf("Buf[0]:%x\n", *buf);
    for (int i = begin; i < verify_Len + begin; i++)
    {
        
        suma += *(buf + i);
        sumb += suma;

        printf("suma:%dsumb:%d\n" ,suma , sumb);
    }
    unsigned char temp1 = *(buf + begin + verify_Len);
    unsigned char temp2 = *(buf + begin + verify_Len + 1);

    printf("temp1:%x   temp2:%x\n",temp1,temp2 );
    if (suma == temp1 && sumb ==temp2)
        return true;
    else
        return false;
}
bool VerifyCRC16(unsigned char *buf, int begin, int verify_len)
{
    return false;
}


Tools::Tools()
{
    
}


Tools::~Tools()
{
}

c# 控制台

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] buf = { 0x55, 0xaa, 0x81, 0x00, 0x81, 0x02 };

            if (Tools.VerifySum(buf,2, 2))
            {
                Console.WriteLine("true");
            }
            else
                Console.WriteLine("false");
            //Marshal.FreeHGlobal(ptr);
            Console.ReadLine();
        }
    }

    public class Tools
    {
        [DllImport("Verify.dll", EntryPoint = "VerifySum",CallingConvention = CallingConvention.Cdecl)]
        public static extern bool VerifySum(byte[] buf, int beginindex, int len);

    }
}

 

 

遇到的问题 

 

[DllImport("Verify.dll", EntryPoint = "VerifySum")]
未添加
,CallingConvention = CallingConvention.Cdecl 时 出现错误
托管调试助手“PInvokeStackImbalance”在“xxxxxx” 中检测到故障。其他信息: 对 PInvoke 函数“...
解决办法添加:
CallingConvention = CallingConvention.Cdecl
参考MSDN http://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.callingconvention(v=vs.80).aspx?query=ThisCall

posted @ 2015-01-15 16:45  王登斌  阅读(719)  评论(0)    收藏  举报