Сбой C++/CLI в смешанном режиме: повреждение кучи в atexit (регистрация статического деструктора)

Я работаю над развертыванием программы, а кодовая база представляет собой смесь C++/CLI и C#. C++/CLI бывает всех видов: собственный, смешанный (/clr) и безопасный (/clr:safe). В моей среде разработки я создаю DLL всего кода C++/CLI и ссылаюсь на него из кода C# (EXE). Этот метод работает безупречно.

Для моих выпусков я хочу выпустить один исполняемый файл (просто заявив, что «почему бы просто не разделить DLL и EXE?», Недопустимо).

До сих пор мне удалось скомпилировать EXE со всеми различными источниками. Однако, когда я запускаю его, я получаю диалоговое окно «XXXX перестал работать» с параметрами «Проверить онлайн», «Закрыть» и «Отладить». Детали проблемы следующие:

Problem Event Name:       APPCRASH
Fault Module Name:        StackHash_8d25
Fault Module Version:     6.1.7600.16559
Fault Module Timestamp:   4ba9b29c
Exception Code:           c0000374
Exception Offset:         000cdc9b
OS Version:               6.1.7600.2.0.0.256.48
Locale ID:                1033
Additional Information 1: 8d25
Additional Information 2: 8d25552d834e8c143c43cf1d7f83abb8
Additional Information 3: 7450
Additional Information 4: 74509ce510cd821216ce477edd86119c

Если я отлаживаю и отправляю его в Visual Studio, он сообщает:

Unhandled exception at 0x77d2dc9b in XXX.exe: A heap has been corrupted

Выбор break приводит к остановке на ntdll.dll!77d2dc9b() без дополнительной информации. Если я скажу Visual Studio продолжить, программа запустится нормально и, похоже, будет работать без происшествий, вероятно, поскольку теперь подключен отладчик.

Что вы об этом думаете? Как избежать этого повреждения кучи? Программа, кажется, работает нормально, за исключением этого.

Мой сокращенный сценарий компиляции выглядит следующим образом (для краткости я опустил проверку ошибок):

@set TARGET=x86
@set TARGETX=x86
@set OUT=%TARGETX%
@call "%VS90COMNTOOLS%\..\..\VC\vcvarsall.bat" %TARGET%

@set WIMGAPI=C:\Program Files\Windows AIK\SDKs\WIMGAPI\%TARGET%

set CL=/Zi /nologo /W4 /O2 /GS /EHa /MD /MP /D NDEBUG /D _UNICODE /D UNICODE /D INTEGRATED /Fd%OUT%\ /Fo%OUT%\
set INCLUDE=%WIMGAPI%;%INCLUDE%
set LINK=/nologo /LTCG /CLRIMAGETYPE:IJW /MANIFEST:NO /MACHINE:%TARGETX% /SUBSYSTEM:WINDOWS,6.0 /OPT:REF /OPT:ICF /DEFAULTLIB:msvcmrt.lib
set LIB=%WIMGAPI%;%LIB%
set CSC=/nologo /w:4 /d:INTEGRATED /o+ /target:module

:: Compiling resources omitted

@set CL_NATIVE=/c /FI"stdafx-native.h"
@set CL_MIXED=/c /clr /LN /FI"stdafx-mixed.h"
@set CL_PURE=/c /clr:safe /LN /GL /FI"stdafx-pure.h"

@set NATIVE=...
@set MIXED=...
@set PURE=...

cl %CL_NATIVE% %NATIVE%
cl %CL_MIXED% %MIXED%
cl %CL_PURE% %PURE%
link /LTCG /NOASSEMBLY /DLL /OUT:%OUT%\core.netmodule %OUT%\*.obj

csc %CSC% /addmodule:%OUT%\core.netmodule /out:%OUT%\GUI.netmodule /recurse:*.cs

link /FIXED /ENTRY:GUI.Program.Main /OUT:%OUT%\XXX.exe ^
/ASSEMBLYRESOURCE:%OUT%\core.resources,XXX.resources,PRIVATE /ASSEMBLYRESOURCE:%OUT%\GUI.resources,GUI.resources,PRIVATE ^
/ASSEMBLYMODULE:%OUT%\core.netmodule %OUT%\gui.res %OUT%\*.obj %OUT%\GUI.netmodule

Обновление 1

После компиляции с отладочными символами и повторной попытки я действительно получаю больше информации. Стек вызовов:

msvcr90d.dll!_msize_dbg(void * pUserData, int nBlockUse)  Line 1511 + 0x30 bytes
msvcr90d.dll!_dllonexit_nolock(int (void)* func, void (void)* * * pbegin, void (void)* * * pend)  Line 295 + 0xd bytes
msvcr90d.dll!__dllonexit(int (void)* func, void (void)* * * pbegin, void (void)* * * pend)  Line 273 + 0x11 bytes
XXX.exe!_onexit(int (void)* func)  Line 110 + 0x1b bytes
XXX.exe!atexit(void (void)* func)  Line 127 + 0x9 bytes
XXX.exe!`dynamic initializer for 'Bytes::Null''()  Line 7 + 0xa bytes
mscorwks.dll!6cbd1b5c()
[Frames below may be incorrect and/or missing, no symbols loaded for mscorwks.dll]
...

Строка моего кода, которая «вызывает» это (динамический инициализатор для Bytes::Null):

Bytes Bytes::Null;

В заголовке, который объявлен как:

class Bytes { public: static Bytes Null; }

Я также попытался сделать глобальный внешний вид в заголовке следующим образом:

extern Bytes Null; // header
Bytes Null; // cpp file

Который так же провалился.

Похоже, что виновата функция CRT atexit, которая непреднамеренно потребовалась из-за статического инициализатора.


Исправить

Как указал Бен Фойгт, использование любых функций CRT (включая собственные статические инициализаторы) требует правильной инициализации CRT (что происходит в mainCRTStartup, WinMainCRTStartup или _DllMainCRTStartup). Я добавил смешанный файл C++/CLI с C++ main или WinMain:

using namespace System;
[STAThread] // required if using an STA COM objects (such as drag-n-drop or file dialogs)
int main() { // or "int __stdcall WinMain(void*, void*, wchar_t**, int)" for GUI applications
    array<String^> ^args_orig = Environment::GetCommandLineArgs();
    int l = args_orig->Length - 1; // required to remove first argument (program name)
    array<String^> ^args = gcnew array<String^>(l);
    if (l > 0) Array::Copy(args_orig, 1, args, 0, l);
    return XXX::CUI::Program::Main(args); // return XXX::GUI::Program::Main(args);
}

После этого программа продвигается немного дальше, но все еще имеет проблемы (которые будут рассмотрены в другом месте):

  • Когда программа написана исключительно на C#, она работает нормально, а также всякий раз, когда она просто вызывает методы C++/CLI, получает свойства C++/CLI и создает управляемые объекты C++/CLI.
  • События, добавленные C# в код C++/CLI, никогда не срабатывают (хотя должны)
  • Еще одна странная ошибка заключается в том, что возникает исключение: InvalidCastException говорит, что нельзя привести от X к X (где X совпадает с X...)

Однако, поскольку повреждение кучи исправлено (путем инициализации CRT), вопрос решен.


person coderforlife    schedule 08.02.2011    source источник
comment
@Foole Спасибо за исправление заголовка.   -  person coderforlife    schedule 08.02.2011


Ответы (1)


РЕДАКТИРОВАТЬ: Обнаружил проблему, оставив ниже предложенные шаги отладки на случай, если они помогут кому-то в будущем.

Проблема в том, что вы изменили точку входа. Вы должны использовать точку входа, предоставляемую стандартной библиотекой C++/CLI, которая настраивает внутренние ресурсы, такие как список onexit.

Удалите переключатель /ENTRY и напишите простую функцию main, которая вызывает желаемую процедуру запуска.


Хотя использование отдельных EXE и DLL может быть неприемлемо для конечного продукта, было бы неплохо протестировать эту более простую конфигурацию и посмотреть, возникнет ли у вас та же проблема.

Если вы можете воспроизвести повреждение кучи с помощью отдельного .DLL, вы знаете, что оно находится где-то в вашем родном коде C++, и его будет намного проще отлаживать без C#, смешанного с одним и тем же файлом.

Если вы не можете воспроизвести проблему с отдельными DLL и EXE, то она может быть связана с процессом интеграции (или она может быть просто менее очевидной, поскольку макет меняется в зависимости от того, что связывается).

После того, как вы найдете и устраните ошибку повреждения кучи, вы можете вернуться к одному .EXE.

Другим подходом было бы создание базы данных отладки, чтобы вы могли получить более качественные трассировки стека в случае сбоя. Даже релизные сборки (или, может быть, особенно релизные сборки) должны быть собраны с отладочной информацией.

person Ben Voigt    schedule 08.02.2011
comment
Я тестировал все с помощью системы DLL/EXE (в Visual Studio). Я компилировал с помощью PDB для кода C, однако я не включил его на этапе компоновки. Я разместил результаты, используя полную информацию об отладке выше. Что ты думаешь? - person coderforlife; 08.02.2011
comment
@thaimin: кажется, я вижу проблему. Замена функции точки входа CRT невозможна, если вы намерены также использовать любые стандартные библиотечные функции, включая atexit. - person Ben Voigt; 09.02.2011
comment
Я не использовал atexit намеренно! (хотя есть и другие функции ЭЛТ, используемые позже). Это исправление вызывает появление новых проблем! Смотрите отредактированный вопрос. - person coderforlife; 09.02.2011
comment
@thaimin: Поскольку мы исправили повреждение вашей кучи, не могли бы вы принять ответ на этот вопрос и начать новый (или, вполне возможно, для каждой из проблем, которые вы сейчас видите: я не думаю, что ваши оставшиеся проблемы от интеграции C# и C++ как таковых, но из-за того, что код инициализации .NET инициализирует части COM, которые он использует, и ваш код DragDrop не учитывает это). Я знаю, что вы хотели бы просто продолжить поиск ошибок, но любой, кто будет читать это в будущем, оценит, что исправление atexit будет аккуратно отделено и объяснено отдельно. - person Ben Voigt; 09.02.2011
comment
Спасибо за вклад и за то, что держите меня в курсе. Ваш комментарий о COM заставил меня задуматься, и исправление заключалось в добавлении [STAThread] к новым основным функциям. Я изучу проблему события. - person coderforlife; 10.02.2011