About Mixed Mode DLL with /NOENTRY crash since Visual Studio 2022 17.7.0 and the global static initializer problem (2024-01-15)
The problem had been originally reported https://developercommunity.visualstudio.com/t/Visual-Studio-2022-1770-CRT-crash-when/10441303.but also in other posts e.g. here https://developercommunity.visualstudio.com/t/CRT-crash-when-loading-mixed-dll-from-N/10559475#T-ND10570501
After further investigation we found out, that the reason is that we used /NOENTRY
in our Managed C++ DLL which worked fine with all compilers before we used VS17.7.
Now with VS17.7 there seems to be a change on how the atexit table/functionality in the CRT’s utility.cpp
works. The functions __scrt_initialize_onexit_tables
and __scrt_dllmain_before_initialize
must now be called to work correctly whereas in previous VS versions it seems that this was not the case.
The internal analysis was much more complex and took a few days and advanced debugging tools.
The whole problem is related to "global static initializers" and if and their order when they are initialized.
There seems to be no guarantee in Microsoft C++ that they are actually initialized before usage i.e. the function ucrtbased.dll!_register_onexit_function
crashed because the global static tables for the onexit code that is used by global static C++ class destructors and by `atexit`` functions were never initialized and the CRT wrote unitialized space.
The problem for other Visual Studio Customers seems to be, that /NOENTRY
was there for a reason, because without /NOENTRY
the mixed mode DLL used to crash for unknown reasons.
A recent suggested workaround has been posted here: https://developercommunity.visualstudio.com/t/CRT-crash-when-loading-mixed-dll-from-N/10559475#T-N10570501 i.e. a fix from Hans Passant, see: https://stackoverflow.com/questions/41485935/entry-point-for-c-cli-x64-windowsforms-app-vs-2015/41489950#41489950 where he suggested to change the managed entry point to the (compiler mangled): ?mainCRTStartupStrArray@@FYMHP$01EAPE$AAVString@System@@@Z (64-bit only).
Our own solution to avoid those crashes was to get rid of ALL global static initializers in our code but also in any third-party code.
To help the development community that work with VS 2022, here are some notes from me (@mgexo):
- This is very hard to debug, even with enabled Microsoft Source Server and source codes of CRT (which by the way can be found e.g. in
C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Tools\MSVC\14.38.32919\crt\src\vcruntime
) - To find possible culprits, generate a "Map file" in the Linker process of the
.dll
(or native.exe
) by specifying/MAP:...
filename to the linker e.g. add this parameter to the linker command line in Visual Studio/MAP:yourpathandfilename.map
- In that
.map
file then search for the tag "$initializer$
" (dollar initializer dollar) and typically you can find all static initializers that are run by the CRT when the.dll
or.exe
is loaded. - You may be surprised in some cases about some of them accidentally happening i.e. when some programmer wrote static initializers into header files that were only included.
- Some of the static initalizers will also creep in through third party linked libraries e.g. from a staticed linked
.lib
or from the Microsoft .NET framework itself. - Tip: You can add your own variable for testing to see if/how it appears in the map file but make sure that it is a non-trivial class e.g. create a static
std::vector<std::vector<int>> g_myteststructure;
at the top of a.cpp
file and then search for it in the.map
file.
Recommendation: Get rid of all such non-trivial global static initializers in DLLs that you can. Replace them with on-demand static initializers i.e. function calls that once calls they have a local static variable (also known as "Meyer's Singleton"). This avoids crashes and problems when a .dll
(or .exe
) is loaded/started and makes the startup faster in general.
Recommended Visual Studio Debugger Settings
Summary: It may happen, that DLLs crash on startup with random errors. This can happen if the Microsoft CRT is not initializing correctly inside the dll when it is started, which is related to the Entry Point see /ENTRY
(Entry-Point Symbol). In fact, especially for managed DLLs the entry point normally needs to be empty, to allow correct initialization of the .dll
. It then uses for example the _DllMainCRTStartup
and _CRT_INIT
function that is automatically generated by the compiler see dll_dllmain.obj
in the .map file. In some cases however, these
are suddenly missing, which for example caused the issue in Visual Studio 2022 17.7.0 CRT crash when loading mixed mode DLL from .NET (regression since 17.6. and any earlier version) -- Developer Community link and the call stack trace above. If it is missing, then important initialization functions like __scrt_initialize_onexit_tables
in utility.cpp
of the CRT are not getting called and then standard functions like atexit
that register a destructor will crash directly or randomly. This may happen if /NOENTRY
is defined in the linker settings of the .dll
.
Update 2024-01-23: The stories continues, see https://developercommunity.visualstudio.com/t/CRT-crash-when-loading-mixed-dll-from-N/10559475?space=21&sort=newest .