制作C/C++动态链接库(dll)若干注意事项

摘要:
当在定义了_STATIC_CPPLIB的情况下使用/MD时,它将导致应用程序与静态多线程标准C++库而非动态版本链接,同时仍通过msvcrt.lib动态链接到主CRT。将/DLL选项传递到链接器。链接器查找DllMain函数,但并不需要该函数。动态链接多线程库动态链接的运行时库,此时将msvcrt.lib安置到obj文件中,它连接到dll的方式是静态链接,实际上工作的库是msvcrxx.dll。所有的C库函数保存在动态链接库msvcrXX.dll中,由msvcrXX.dll处理多线程问题。
一、CC++ 运行时库编译选项简单说明

问题:我的dll别人没法用

运行时库是个很复杂的东西,作为开发过程中dll制作需要了解的一部分,这里主要简单介绍一下如何选择编译选项。

在我们的开发过程中时常会遇到这样的问题:

1. 我的VS版本比较高(比如:VS2012),我想制作一个dll,封装了几个函数给别人用。

2. 打包后发现我的dll引用了msvcr110.dll或者msvcr110d.dll,这个dll别人电脑可能没有。

3. 于是别人使用时出现了诸如:“无法在DLL“XXXX.dll”中找到名为“XXXX()”的入口点”等问题。

最终结果就是,反复检查发现都没有错,用工具查看也发现函数确实已经导出了,但是别人就没法用。

这里可能就需要对编译选项进行修改了。

解释:如何避免上述问题

在VS中打开:项目属性——>配置属性——>C/C++——>代码生成——>运行时。其中可以看到多个选项,如下图所示:

image

在微软的msdn中对CRT库进行了简单解释:

https://msdn.microsoft.com/zh-cn/library/2kzt1wy3(VS.80).aspx

https://msdn.microsoft.com/zh-cn/library/abx4dbyh(v=vs.110).aspx

下面是我黏贴的表格:

选项说明
/MD

使应用程序使用运行时库的多线程并特定于 DLL 的版本。定义 _MT_DLL,并使编译器将库名 MSVCRT.lib 放入 .obj 文件中。

用此选项编译的应用程序静态链接到 MSVCRT.lib。该库提供允许链接器解析外部引用的代码层。实际工作代码包含在 MSVCR80.DLL 中,该库必须在运行时对于与 MSVCRT.lib 链接的应用程序可用。

当在定义了 _STATIC_CPPLIB (/D_STATIC_CPPLIB) 的情况下使用 /MD 时,它将导致应用程序与静态多线程标准 C++ 库 (libcpmt.lib) 而非动态版本 (msvcprt.lib) 链接,同时仍通过 msvcrt.lib 动态链接到主 CRT。

/MDd定义 _DEBUG_MT_DLL,并使应用程序使用运行时库的调试多线程并特定于 DLL 的版本。它还使编译器将库名 MSVCRTD.lib 放入 .obj 文件中。
/MT使应用程序使用运行时库的多线程静态版本。定义 _MT 并使编译器将库名 LIBCMT.lib 放入 .obj 文件中,以便链接器使用 LIBCMT.lib 解析外部符号。
/MTd定义 _DEBUG_MT。此选项还使编译器将库名 LIBCMTD.lib 放入 .obj 文件中,以便链接器使用 LIBCMTD.lib 解析外部符号。
/LD

创建 DLL。

将 /DLL 选项传递到链接器。链接器查找 DllMain 函数,但并不需要该函数。如果没有编写 DllMain 函数,链接器将插入返回 TRUE 的DllMain 函数。

链接 DLL 启动代码。

如果命令行上未指定导出 (.exp) 文件,则创建导入库 (.lib);将导入库链接到调用您的 DLL 的应用程序。

/Fe(命名 EXE 文件)解释为命名 DLL 而不是 .exe 文件;默认程序名成为 basename.dll 而不是 basename.exe。

除非显式指定 /MD,否则将暗指 /MT

/LDd

创建调试 DLL。定义 _MT_DEBUG

就从VS的dll库的编译选项来说就前面四项,/MD、/MDd、/MT和/MTd。其中/MDd、/MTd后面的“d”表示编译生成的是Debug版本,也就是用于编译生成Debug版本的程序;不加“d”表示是Release版本的程序,如果此时你把项目配置成Debug版本,编译会不通过。

动态链接多线程库(MD/MDd)

动态链接的运行时库,此时将msvcrt.lib安置到obj文件中,它连接到dll的方式是静态链接,实际上工作的库是msvcrxx.dll。所有的 C 库函数保存在动态链接库 msvcrXX.dll中, 由msvcrXX.dll处理多线程问题。也就是说,这种编译方式下我们是通过msvcrXX.dll这个动态链接库去链接CRT。

此时我们编译的dll引用了msvcrXX.dll文件,这个文件在使用的时候必须能够被计算机查询到。比如:我的电脑编译引用了msvcr110.dll,那么我的dll给别人调用的时候,对方计算机一定要有msvcr110.dll,否则dll库就无法使用。因此这种编译方式,必须将msvcr110.dll一同携带过去,并且要在对方计算机上进行注册。

使用此方式编译时,使用Depends查看,我们可以看到dll 引用了msvcr110.dll,如下图:

image

静态链接多线程库(MT/MTd)

静态链接多线程库,此时编译器把LIBCMT.lib 安置到OBJ文件中,让链接器使用LIBCMT.lib 处理外部符号。这种方式说简单点就是让我们的dll能够直接链接到CRT,无需引用msvcrXX.dll这个动态链接库。这样我们就不需要担心对方电脑是否拥有对应版本的msvcrXX.dll,因此这是一种方便的好办法。

在这种方式下,编译的dll通过Depends查看就是这样的:

image

注意:几个注意点

光从上面来看,MT的方式很好用,但是必须要注意一个问题:

不要混合使用库的静态版本和动态版本。在一个进程中有多个库副本会导致问题,因为副本中的静态数据不与其他副本共享。(还应该避免在一个进程中混合使用这些库的调试版本和非调试版本)。

还有,在实际使用中能用MD就用MD的方式,因为这种方式软件更小,调用同一个dll时在内存中使用的是同一个副本。这就不会有堆空间释放问题:

不同的模块各自有一份C运行时库代码,各个C运行库会有各自的堆,导致了各个模块会有各自的堆。如果在A堆中申请空间,到B堆中释放就会有崩溃,在模块A申请的空间,必须在模块A中释放。

二、函数调用方式_cdecl与_stdcall的选择

_stdcall调用

_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。形式如下:

#define SW_REV extern "C" _declspec(dllexport)
SW_REV int __stdcall add(int a, intb);

int  __stdcall add(int a, intb)
{
    return a+b;
}

_cdecl调用

_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。

在不加修饰的情况下,VC++默认使用这种调用方式,形式如下(默认,可省略):

#define SW_REV extern "C" _declspec(dllexport)
SW_REV int add(int a, intb);

int add(int a, intb)
{
    return a+b;
}

什么时候区分?

其实两种方式都一样,区别在于调用。例如:有些语言要求调用时只认_stdcall,那么就只能按照要求使用_stdcall版本的dll。很多情况下,调用方式都可选择,说白了,就是一定要保持一致,例如C#调用上述_stdcall方式的dll:

[DllImport("SwLib.dll", EntryPoint = "add", CallingConvention =CallingConvention.StdCall)]
        private static extern int add(int a, int b);

这里CallingConvention需要选择CallingConvention.StdCall,如果我将其改成 CallingConvention.Cdecl,程序就会报错,指出堆栈调用不对称:

image

PS:写这个东西真的好费劲,上次写到现在好久了,有好多想记录一下的东西,发现自己懒得写,好懒!

免责声明:文章转载自《制作C/C++动态链接库(dll)若干注意事项》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇WinServer2019的IIS上无法安装framework3.5的问题angular-cli小白入门选集下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

APP漏洞自动化扫描专业评测报告

一、前言 目前在业界有很多自动化检测APP安全性的在线扫描平台。为了了解目前国内移动APP在线漏洞扫描平台的发展情况,我进行了一次移动安全扫描平台的评测分析;主要从漏洞项对比、扫描能力对比以及扫描结果这三个方向来对比。 希望此次的调研结果可以为读者提供更加可靠的安全漏洞扫描服务建议。 二、分析对象 这一章主要介绍需要对比的扫描平台和需要测试的APP样本。...

预编译头文件

一、预编译头文件使用经验: 如果预编译头文件被正确使用时,它确实大大提高我们编程的效率(你工作中,有多少时间是在等编译完成?很多吧,这个时候一般都很无聊,无奈,浪费时间)。但是他太容易用错了. 下面是几种常见的错误用法. 1) 在预编译头文件里include自己的头文件(当然, 如果你的头文件不经常变化, 也可以) 原因:自己的头文件一般会经常变, 便利后...

VS2010 永久配置OpenCv2.4.9 及转换到COFF 期间失败:文件无效或损坏,解决方法

1、下载OpenCv2.4.9(win pack):http://opencv.org/releases.html 下载完成后,进行解压(win7 64位系统) 2、环境配置,配置如下图所示: 找到path后,在后面加上: E:opencvuildx64vc10in E:opencvuildx86vc10in   3、配置工程依赖库(新建工程,都需重新...

Java如何实现跨平台

在前面讲解编程语言的时候我们看到,通过引入编译器,解决了使用机器语言编程带来的问题。但这有待来了另一个问题:不同的平台(你可以理解成CPU不同、操作系统不同)所能理解的二进制机器指令是不一样的,编译器只能针对某个特定目标平台进行编译,一旦编译完成生成的可执行程序只能在目标平台上运行,换到其它平台就不能运行了。 针对这一问题,Java使用了另一种方案,这也是...

用QT在Windows下编写dll程序

转自:http://blog.csdn.net/yyzsyx/article/details/6086052 因为QT必须有调用QApplication的exec方法,这样才能产生消息循环,QT的程序才可以运行。所以说如果我们使用了QT编写了dll程序,在普通的 windows程序中是不能调用的。在调用的时候会出现错误。当然QT提供了解决方法:那就是QTW...

【转】DELPHI 线程类

原文地址:http://yyimen.blog.163.com/blog/static/179784047201211811178223/ Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对TThread类的几个成员作一简单介绍,再说明一下Execute的实现和Synchronize的用法就...