运行时库

前言

写了一点简单的c++程序,项目A输出可执行文件,项目B输出动态链接库,A依赖B。解决方案.sln是用生成工具生成的,本来相安无事,也没有手动配置Runtime Library的选项

某天,我加了点东西,这时编译通过,但运行报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
Debug Assertion Failed!

Program: C:\Work\assert\Win32\Debug\assert.exe
File: minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp
Line: 996

Expression: __acrt_first_block == header

For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.

(Press Retry to debug the application)

环境:

  • win11
  • vs2022 v143
  • Debug x64

持续更新…


正文

这时我看了一眼解决方案的Runtime Library选项,项目A是/MDd,项目B是/MTd

查阅资料可得:

  • /MDd/MD是使用运行时库的多线程DLL版本,编译器会将MSVCRT.lib放入.obj文件中。实际运行时的工作代码在MSVCxxx.DLL中
  • /MTd/MT是使用运行时库的多线程,静态编译的版本,编译器会将LIBCMT.lib放入.obj文件中,以便链接器使用LIBCMT.lib解析外部符号。

诶诶,那我把项目A改成/MTd是不是也行了?如行!错辣


再次查阅资料可得,

在 EXE 采取 /MT 运行时库时,依赖的静态库 (x.lib) 的编译也必须是 MT。因为静态库的原因,EXE 只需要把自己需要的代码从静态库 (x.lib) 提取出来就行,不考虑静态库里的运行库。所以这种情况下 EXE 和静态库使用的运行时库是同一套代码,都是 EXE 的运行时库;依赖的动态库 (x.dll) 的编译也必须是 /MT。因为是动态库,EXE 在加载动态库时,是将其全部代码 (包括一份运行时库代码) 加载进了 EXE 进程空间,这样 EXE 在运行时就包含了两套运行时库代码,一个是动态库的,一个是 EXE 的。

在 EXE 采取 /MD 运行时库时,所依赖的无论是静态库还是动态库,都必须采用 /MD 来编译。这种情况下编译出来的 EXE 和 DLL(或 EXE 和 LIB)都依赖 MD 运行时库(即 VCRUNTIMExx.dll、MSVCPxx.dll、ucrtbase.dll)。因为都依赖 MD 运行时库,所以 EXE 和 DLL(或 EXE 和 LIB)用的是同一套运行时库。

有以下三个变量在管理堆:

  • __acrt_heap,都指向进程默认堆
  • __acrt_first_block 指向最新被分配的堆块
  • __acrt_last_block 指向最久被分配的堆块

在释放堆块时,运行时库会检测当前释放的堆块是否与当前运行时库的__acrt_first_block相等,如果相等,则继续释放;如果不相等,则断言失败。

问题在于一个运行时库分配堆块,另一个运行时库释放堆块。那么解决的方法就是在传递参数或返回参数时,通过指针或引用的方式,直接传递,防止中间的类复制(调用复制构造函数)。


给大佬跪了

我开始review自己的代码,项目A里定义了一个类Derived,继承自dll里定义的类Base。Base有一个接口,接受指针作为参数,Derived也继承了这个接口。我在项目A里new了一个指针,作为参数传给这个接口。

这让我想起之前有处理过多进程之间的代码,它们之间是无法像单进程那样传递指针的,于是乎再次找到一些蛛丝马迹:

When you build with /MT[d], every module (EXE and DLL) contains its own copy of the C runtime, and in particular of the heap manager. One heap manager doesn’t know what to do with a pointer allocated by another - from the first manager’s point of view, it’s just some random address.

When you build with /MD[d], all modules share the same copy of C runtime DLL, and all use the same heap manager.

It’s best not to spread resource management across modules: if the DLL provides a function for allocating memory, have it also provide a function that the EXE could call to deallocate it later. Otherwise, you have to ensure that all modules are built by the same compiler version, the same settings (e.g. Debug vs Release) and all use C runtime DLL (so /MD or /MDd).

也许这暂时解答了我的疑惑,那么草草收场,未完待续…


参考资料