记 QT 应用开发中的一个二进制兼容性问题

2022-03-29  本文已影响0人  hanpfei

笔者在参与开发一个集成了 QT 的跨平台桌面应用程序,目标平台是 Windows 和 Mac。一段时间以来,运行 Windows 平台的应用程序时,不断地被类似于如下这样的崩溃问题所折磨。

C++ Runtime 库崩溃

这里提示说,在 C++ Runtime 库中发生了崩溃,C++ Runtime 库中的一个断言失败了,即断言 _ASSERTE(__acrt_first_block == header); 失败了,这也是崩溃直接发生的位置。断言失败的这行代码更详细的上下文如下:

崩溃直接位置上下文

从发送崩溃的这段代码中,基本上看不出任何线索。

通过 Visual Studio 查看这个崩溃发生的调用堆栈,找到这个堆栈中最靠近栈顶的我们自己编写的代码,可以看到这个崩溃发生在如下函数中:

void MainWindow::on_cameraComboBox_currentTextChanged(const QString& arg1) {
  auto camera_device =
      std::find_if(camera_info_.begin(), camera_info_.end(),
                   [&arg1](const CameraDeviceInfo& info) -> bool {
                     std::string device_name = info.name;
                     return device_name == arg1.toStdString();
                   });
  if (camera_device != std::end(camera_info_)) {
    MediaEngineImpl::GetInstance().SetCameraDevice(camera_device->id);
  }
}

这个 崩溃发生的具体位置如下:

微信图片_20220329155621.png

即在 lambda 表达式返回的第 200 行发生了崩溃。代码本身,实在是看不出来有任何问题。

笔者想知道,是不是这里的 lambda 真有什么问题。于是笔者做了一个实验,移除上面这个函数中的几乎所有代码,只保留如下这一行:

void MainWindow::on_cameraComboBox_currentTextChanged(const QString& arg1) {
  auto str = arg1.toStdString();
}

再次运行 QT 应用程序,执行到这个函数时,崩发溃依然生。这种崩溃发生之莫名其妙,简直是让人怀疑人生。这次崩溃的具体位置如下图中 Visual Studio 的箭头所指的位置:

崩溃 2

如图所示,崩溃发生在函数执行结束时的第 197 行。

这,其实是一个二进制兼容性问题。跨二进制编译目标传递对象时,有风险出现。比如一个动态链接库创建的对象,传给了另一个动态链接库来销毁。或者动态链接库创建了一个对象,但在应用程序中销毁了。

二进制兼容性问题的类型可能有很多。对象的创建和销毁在不同的二进制目标中导致的二进制兼容性问题可能也有不少。但就这个问题来说,出现二进制兼容性问题的原因在于 C++ MSVC 运行时库,即两个编译目标在编译链接时链接了不同的 C++ MSVC 运行时库。

C++ MSVC 运行时库会执行一些诸如内存分配与释放之类的操作。Windows 有动态 MSVC 运行时库和静态 MSVC 运行库时之分。Windows 平台的 C/C++ 程序,需要链接动态 MSVC 运行时库时,加 "/MDd" 或 "/MD" 编译标记(其中前者为 debug 版,后者为 release 版),需要链接静态 MSVC 运行时库时,加 "/MTd" 或 "/MT" 编译标记(其中前者为 debug 版,后者为 release 版)(webrtc\build\config\win\BUILD.gn)。

动态 MSVC 运行时库和静态 MSVC 运行时库在不同的堆中分配内存。大体可以理解为,链接静态 MSVC 运行时库时,每个编译目标都有一个堆,如每个动态链接库有自己的堆,可执行文件也有自己的堆;链接动态 MSVC 运行时库时,则是所有链接动态 MSVC 运行时库的各个编译目标共用同一个堆。

当编译应用程序和动态链接库时链接了不同的 MSVC 运行时库,而又在两者之间共同管理了对象的生命周期,则会出现我们这里遇到的崩溃。在这里,QT 链接了动态 MSVC 运行时库,我们的可执行程序链接了静态 MSVC 运行时库。这段代码中,QT 创建了一个 std::string 给到应用程序,其中引用了一块在 QT 内部,在堆上分配的内存,即通过动态 MSVC 运行时库分配的内存。std::string 是个模板类,应用程序中,函数返回时,释放由 QT 返回的 std::string 对象,释放对象时,将内存还回给堆,但这时会还回给静态 MSVC 运行时库,于是就出现了上面的问题。

如此说来,两个同时链接动态 MSVC 运行时库的编译目标可以共同管理对象的生命周期,因为它们共用同一堆。而链接了静态 MSVC 运行时库的编译目标,不能与其它编译目标共同管理对象的生命周期,无论另一个编译目标链接的是静态 MSVC 运行时库,还是动态 MSVC 运行时库。

如此,解决问题的办法也就明了了。使用了 QT 的程序,最好链接动态 MSVC 运行时库。我们为我们的应用程序添加链接动态 MSVC 运行时库的编译标记:

if(SSZRTC_SYSTEM_NAME STREQUAL "win")
  set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD")
  set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd")
endif()

如果一个动态链接库,new 了一个非模板类对象,返回给应用程序,随后在应用程序中 delete,是不是也有很大的风险出现?这种情况相对安全一点,这是因为 delete 释放内存是一个间接操作。

对于上面的这种问题,还有一种解决的思路,即严格遵守谁创建的对象谁释放的原则。一个动态链接库开出了创建对象的接口,则同时也必须开出释放对象的接口。

在 Stackoverflow 上有一个问题在讨论这个,Debug Assertion Failed! Expression: __acrt_first_block == header ,有兴趣的也可以参考一下。

参考文档

Debug Assertion Failed! Expression: __acrt_first_block == header

上一篇下一篇

猜你喜欢

热点阅读