记 QT 应用开发中的一个二进制兼容性问题
筆者在參與開發一個集成了 QT 的跨平臺桌面應用程序,目標平臺是 Windows 和 Mac。一段時間以來,運行 Windows 平臺的應用程序時,不斷地被類似于如下這樣的崩潰問題所折磨。
這里提示說,在 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);} }這個 崩潰發生的具體位置如下:
即在 lambda 表達式返回的第 200 行發生了崩潰。代碼本身,實在是看不出來有任何問題。
筆者想知道,是不是這里的 lambda 真有什么問題。于是筆者做了一個實驗,移除上面這個函數中的幾乎所有代碼,只保留如下這一行:
void MainWindow::on_cameraComboBox_currentTextChanged(const QString& arg1) {auto str = arg1.toStdString(); }再次運行 QT 應用程序,執行到這個函數時,崩發潰依然生。這種崩潰發生之莫名其妙,簡直是讓人懷疑人生。這次崩潰的具體位置如下圖中 Visual Studio 的箭頭所指的位置:
如圖所示,崩潰發生在函數執行結束時的第 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
總結
以上是生活随笔為你收集整理的记 QT 应用开发中的一个二进制兼容性问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OpenCV_008-OpenCV 中的
- 下一篇: OpenCV 中的图像处理 004_平滑