Visual Studio 设置里的Runtime Library

参考链接:https://blog.csdn.net/luoweifu/article/details/49055933

如下图所示,C++的工程里,C/C++ ----> Code Generation ----> Runtime LIbrary下一共有四个选项,这些代表什么意思呢
在这里插入图片描述

概念解析

在这方面,经常看到以下名词:

  • Runtime Library
  • MSVCRT
  • libcmt.lib
  • LIBC.LIB
  • crtdll.dll
  • CRT

这些名词看完这篇文章就知道了,后面会再解释



什么是Runtime Library

Runtime Library就是运行时的库,也就是程序运行的时候需要的库文件,说白了就是一堆.lib或.dll文件的集合(Windows平台下),在Windows下有lib和dll两种文件,比如C Runtime Library提供了相关的lib文件和dll文件,具体名字之后再讲

比如安装VS2010时,就会下载对应C++的CRT源码,放在安装目录VC\crt\src下,这个版本的CRT既包括老的C的库文件,也包括C++添加进去的新的库文件,所以VC\crt\src下,既有C的文件(如output.c、stdio.h等),也有C++的文件(如iostream、string)。

其实Runtime Library就是各自编程语言对应的库文件,比如说.NET Runtime、CRT,都是一样的,就是一堆库文件的集合

既然是库文件,那么使用库文件的时候,自然有两种使用的方法:

  • Static Linking
  • Dynamic Linking

对于库文件,如果用的Static Linking,则是把对应的.lib库文件复制到自己的程序里,如果用的Dynamic Linking,则是把对应的.dll库文件load到自己的程序里,Static Linking更快,但需要拷贝库文件,Dynamic Linking虽然慢,但所有用库文件的程序都可以共享一份.dll文件

在C Runtime Library出来之前,对于库文件,都是用的.lib的Static Linking去做的,之后微软把库文件做成了.dll,让大家用Dynamic Linking进行使用。



Runtime Library的主要作用

主要两点:

  • 使用编程语言一般都有的功能:如memcpy、printf、malloc和相关C++标准库(STL)的支持。
  • 提供EntryPoint函数,启动函数的主要是为了进行程序初始化,对全局变量进行赋初值等

不采用宽字符集的控制台程序的入口点为mainCRTStartup(void)。下面我们以该函数为例来分析运行时库究竟为我们添加了怎样的入口程序。这个函数在crt0.c中被定义,下列的代码经过了笔者的整理和简化:

void mainCRTStartup(void)
{
    
    
 int mainret;
 /*获得WIN32完整的版本信息*/
 _osver = GetVersion();
 _winminor = (_osver >> 8) & 0x00FF ;
 _winmajor = _osver & 0x00FF ;
 _winver = (_winmajor << 8) + _winminor;
 _osver = (_osver >> 16) & 0x00FFFF ;
 _ioinit(); /* initialize lowio */
 /* 获得命令行信息 */
 _acmdln = (char *) GetCommandLineA();
 /* 获得环境信息 */
 _aenvptr = (char *) __crtGetEnvironmentStringsA();
 _setargv(); /* 设置命令行参数 */
 _setenvp(); /* 设置环境参数 */
 _cinit(); /* C数据初始化:全局变量初始化,就在这里!*/
 __initenv = _environ;
 // 可以看到,这里调用了常用的main函数,同时把命令行的参数作为main函数的参数传递了进去
 mainret = main( __argc, __argv, _environ ); /*调用main函数*/
 exit( mainret );
}

除了crt0.c外,C运行时库中还包含wcrt0.c、 wincrt0.c、wwincrt0.c三个文件用来提供初始化函数。wcrt0.c是crt0.c的宽字符集版,wincrt0.c中包含 windows应用程序的入口函数,而wwincrt0.c则是wincrt0.c的宽字符集版。



MT、MTd、MD、MDd、(ML、MLd 已废弃)的区别与原理

这些都是VS里的Runtime Library的选项,其实不同的选项就代表了不同版本的C/C++语言对应的库的版本,这些字母分别对应以下版本:
在这里插入图片描述

也就是说,/ML和/MLd是最早的C++的库文件,分别对应Release和Debug平台,而且此时的C++库文件不支持多线程,是使用Static Link的方式Link进对应的C++程序的,这意味着每一份程序,都会拷贝其代码整合进去。

接下来,C++提供了/MT和/MTd的库文件,此时的库文件就支持多线程了,同样是使用的Static Linking的方法Link进对应的C++程序的。

最后提供的/MD和MDd库文件,同样支持多线程,不过它使用的是Dynamic Link的方式,这样多个程序也能共享一份dll,使用的时候就把它加载到程序里,熟悉dll的人肯定知道,使用dll的时候需要对应的dll和对应的lib文件,lib文件用于快速找到dll中对应的函数位置。


概念解析
现在再来看看之前不懂的概念名词

  • Runtime Library:就是运行时的库,说白了就是一项编程语言一般要用到的基本的库文件,C++有自己的CRT,C#有自己的.NET Runtime
  • MSVCRT.dll:多线程的C+++的动态库文件
  • libcmt.lib:多线程的C++的静态库文件
  • LIBC.LIB:单线程的C++的静态库文件
  • crtdll.dll:C语言使用的动态库
  • CRT:C++自己的运行时库

三种模式的比较
(1). 静态链接的单线程库
静态链接的单线程库只能用于单线程的应用程序, C 运行时库的目标代码最终被编译在应用程序的二进制文件中。通过 /ML 编译选项可以设置 Visual C++ 使用静态链接的单线程库。

(2). 静态链接的多线程库
静态链接的多线程库的目标代码也最终被编译在应用程序的二进制文件中,但是它可以在多线程程序中使用。通过 /MT 编译选项可以设置 Visual C++ 使用静态链接的多线程库。
该选项生成的可执行文件运行时不需要运行时库dll的参加,会获得轻微的性能提升,但最终生成的二进制代码因链入庞大的运行时库实现而变得非常臃肿。当某项目以静态链接库的形式嵌入到多个项目,则可能造成运行时库的内存管理有多份,最终将导致致命的“Invalid Address specified to RtlValidateHeap”问题。

(3). 动态链接的运行时库
动态链接的运行时库将所有的 C 库函数保存在一个单独的动态链接库 MSVCRTxx.DLL 中, MSVCRTxx.DLL 处理了多线程问题。使用 /MD 编译选项可以设置 Visual C++ 使用动态。
链接时将按照传统VC链接dll的方式将运行时库MSVCRxx.DLL的导入库MSVCRT.lib链接,在运行时要求安装了相应版本的VC运行时库可再发行组件包(当然把这些运行时库dll放在应用程序目录下也是可以的)。 因/MD和/MDd方式不会将运行时库链接到可执行文件内部,可有效减少可执行文件尺寸。当多项目以MD方式运作时,其内部会采用同一个堆,内存管理将被简化,跨模块内存管理问题也能得到缓解。

/MDd 、 /MLd 或 /MTd 选项使用 Debug runtime library( 调试版本的运行时刻函数库 ) ,与 /MD 、 /ML 或 /MT 分别对应。 Debug 版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,加强了对错误的检测,因此在运行性能方面比不上 Release 版本。

结论:/MD和/MDd将是潮流所趋,/ML和/MLd方式请及时放弃,/MT和/MTd在非必要时最好也不要采用了。

前面提到了,动态链接的运行时库,内部会采用同一个堆,下面两张图是/MT和/MD的内存模型:
在这里插入图片描述
在这里插入图片描述

回顾之前遇到的问题

之前在学习Hazel游戏引擎的时候遇到了这么一个问题,这里回顾一下:

我有一个Hazel.dll,游戏引擎作为dll,dll工程使用的Rumtime Library为/MT 。然后还有一个Application工程,工程里的Sandbox.cpp会使用Hazel.dll相关的内容,相关代码如下:

// Sandbox.cpp中
void OnEvent(Hazel::Event& e) override
{
    
    
	std::string s = e.ToString();
}
// Hazel.dll中的ToString函数如下
class HAZEL_API KeyPressedEvent : public KeyEvent
{
    
    
	...
	std::string ToString()const override
	{
    
    
		std::stringstream ss;
		ss << "KeyPressedEvent:\n KeyCode : " << m_Keycode << " KeyRepeated: " << m_KeyRepeated;
		return ss.str();//这里会new一个String并返回
	}
};

也就是说,下面的代码是这样的,然后执行这段代码会在}后面报错:Sandbox.exe

void OnEvent(Hazel::Event& e) override
{
    
    
	std::string s = e.ToString();// 由dll创建了一个string
}// 由Sandbox.exe负责销毁这个string,走到这里会报错 

报错信息如下图所示:

创建string s

获取传递过来的s的地址

在这里插入图片描述
可以看到是在debug_heap.cpp报的错,说明与Heap有关,出现这个的原因是因为dll和exe分别拥有自己的Heap,导致的同一块内存在堆A上创建,又在堆B上释放

至于为什么是这样,可以分析一下上面问题的内存模型,如下图所示,由于Hazel.dll使用的/MT的模式,所以Hazel.dll和exe关于string的操作都是自己在自己特有的Heap上操作的,这个时候Sandbox.exe只能获取Hazel堆上的string s,但是不能在Sanbox.exe这边进行释放,因为这个任务必须交给Hazel.dll来执行:
在这里插入图片描述

所以解决办法有两个:

  • 把Hazel.dll改为Hazel.lib,然后通过Static Linking提供给Sandbox.exe使用(不过这个我没有测试过)
  • 让Hazel工程生成的Hazel.dll使用的运行时库改为MSVCRT.dll,也就是/MT改为/MD,/MTd改为/MDd


顺便提一下premake5.lua里如何设置相关的内容

premake5.lua里如何设置/MD或/MT选项

参考链接:https://github.com/premake/premake-core/wiki/staticruntime

staticruntime "value"

所以要使用/MD,就写staticruntime "off",要使用/MT,就用staticruntime "on"

也可以下面这样写,不过没有在工程前面就使用staticruntime好:

filter "configurations:Release"
	defines "HZ_RELEASE"
	buildoptions "/MD"
	optimize "On"

猜你喜欢

转载自blog.csdn.net/alexhu2010q/article/details/112465821