That time I had to patch the Universal CRT
Colin Finck
Colin Finck
|
23.11.2021
23.11.2021
|
Tech
Tech
|
4
4
Minutes read
Minutes read
Ich habe gerade einen Blog-Beitrag fertiggestellt, in dem ich fast die gesamte Microsoft-Build-Toolchain für unsere Windows-Software durch Open-Source-Alternativen ersetzt habe, die besser zu unseren Bedürfnissen passen. Außer der Visual Studio C-Laufzeitbibliothek, die heutzutage Universal CRT genannt wird (abgekürzt auf UCRT oder einfach CRT).
Die CRT hatte ohne Probleme funktioniert, und ich erwartete nicht, dass sich dies ändert, zumal unsere Software hauptsächlich moderne C++-Konstrukte verwendete. Die Dinge nahmen jedoch eine unerwartete Wendung, als ich eine riesige Speicherleak in einer unserer Anwendungen beobachtete, die mehrfach nacheinander std::threads erstellte und beendete.
Die Ursache für diesen Leak zu finden, kostete eine Weile, teils, weil er nur unter Windows XP und früher auftrat, nicht auf meinem Entwicklungsrechner mit Windows 10. Ich hatte bereits einige meiner Codes und den Kompatibilitätskleber aus meinem vorherigen Beitrag in Verdacht.
Aber es stellte sich schließlich heraus, dass bereits so einfacher Code wie
#include <thread> static void _ThreadProc() { return; } int main() { for (;;) { std::thread(_ThreadProc).detach(); } return 0; }
bereits unter Windows XP Chaos verursachte. Nicht nur mit meiner Toolchain, sondern auch, wenn sie mit Microsofts offizieller v141_xp Toolchain kompiliert wurde. Der verwendete Speicher des Prozesses wuchs im Task-Manager schnell, und bald genug begrüßte mich Windows mit einer Warnung vor Erschöpfung des virtuellen Speichers.
Interessanterweise passierte all dies nur, wenn die CRT statisch verknüpft wurde. Eine dynamisch verknüpfte CRT verursachte keine Probleme, was mir einige weitere Hinweise gab, um das Problem nachzuvollziehen.
Das Problem
Jeder Thread, der von std::thread erzeugt wird, landet in der CRT-Funktion _beginthreadex, egal ob die ursprüngliche Microsoft v141_xp Toolchain oder meine libc++/winpthreads verwendet wird. Glücklicherweise hat unsere Visual Studio-Installation den CRT-Code in C:\Program Files (x86)\Windows Kits\10\Source\10.0.20348.0\ucrt, sodass ich leicht nachverfolgen kann, was als Nächstes passiert.
Der Aufruf landet in der thread_start-Templatefunktion in startup\thread.cpp. Diese Funktion ruft __acrt_getptd() auf, um eine thread-spezifische Datenstruktur zu erhalten und implizit zu erstellen. Die Datenstruktur muss gereinigt werden, wenn der Thread beendet wird.
Zuvor wurde __acrt_initialize() aus internal\initialization.cpp bereits von der CRT beim Starten meiner Anwendung aufgerufen. Es ruft eine Reihe von Initialisierungsroutinen auf, von denen eine __acrt_initialize_ptd() aus _internal\per_threaddata.cpp ist. __acrt_initialize_ptd ruft __acrt_FlsAlloc(destroy_fls) auf, um einen Index für die speichergebundene Ablage zuzuweisen, der es allen nachfolgenden Threads ermöglicht, eine thread-spezifische Datenstruktur zu speichern. Der gegebene destroy_fls-Callback ist eine Funktion in derselben Datei, die automatisch aufgerufen werden soll, wenn ein Thread beendet wird, um seine thread-spezifische Datenstruktur zu bereinigen.
__acrt_FlsAlloc wird in _internal\winapithunks.cpp so implementiert:
if (auto const fls_alloc = try_get_FlsAlloc()) { return fls_alloc(callback); } return TlsAlloc()
Wie Sie sehen, wird der Callback nur für Betriebssysteme übergeben, die FlsAlloc unterstützen. Die Fallback-Funktion TlsAlloc bietet keinen solchen Callback-Parameter. Daher wird die destroy_fls-Aufräumfunktion in diesem Fall nie aufgerufen.
FlsAlloc wurde mit Windows Server 2003 (NT 5.2) eingeführt, was erklärt, warum das Problem auf Windows XP (NT 5.1) und früheren Versionen auftritt, nicht aber auf meinem aktuellen Windows 10. Wenn das alles wäre, würde meine Anwendung unter Windows XP immer die thread-spezifische Datenstruktur leaken, egal ob sie eine statisch verlinkte CRT oder eine DLL verwendet. Ich bin sicher, dass Microsoft dies während der Tests erkannt hätte. Das Problem wird jedoch gemildert, wenn die CRT als DLL verknüpft ist:
Die Datei _dll\appcrtdllmain.cpp implementiert __acrt_DllMain, die für jeden neuen Thread aufgerufen wird, wenn die CRT als DLL verknüpft ist. __acrt_DllMain ruft seinerseits DllMainDispatch auf, die einen DLL_THREAD_DETACH-Handler implementiert, der __acrt_thread_detach() aus internal\initialization.cpp aufruft. Nun ruft __acrt_thread_detach() __acrt_freeptd() auf, um die thread-spezifische Datenstruktur explizit zu bereinigen. Dies geschieht jedes Mal, wenn ein Thread endet und die CRT-DLL vom Thread getrennt wird. Deshalb leckt der Speicher nicht, wenn die CRT als DLL verlinkt ist. Wenn die CRT statisch verlinkt wird, wird niemals ein __acrt_DllMain-Handler aufgerufen, und folglich wird __acrt_freeptd() auch nicht aufgerufen. Die Funktion __acrt_freeptd wird sogar aus einer Release-Berechnung optimiert, weil es keinen einzigen Aufruf dorthin gibt.
Die Lösung
Ich habe diesen Fehler bei Microsoft gemeldet zusammen mit einem vorgeschlagenen Fix. Meine Lösung ist so einfach wie das Aufrufen von __acrt_freeptd() auf allen Ausstiegswegen der Funktion common_end_thread in startup\thread.cpp. Dadurch wird die thread-spezifische Datenstruktur explizit freigegeben, ohne auf Callback-Magie zu vertrauen. Ein Blick in startup\thread.cpp bestätigt auch, dass common_end_thread in allen Situationen aufgerufen wird, wenn ein CRT-Thread endet.
Aber einen Code-Fix zu haben, ist nur die halbe Lösung. Ich kann mir den CRT-Quellcode vielleicht ansehen, aber Microsoft hat längst alle offiziellen Wege eliminiert, um eine modifizierte CRT neu zu bauen. Selbst das Neuaufbauen der betroffenen thread.cpp-Datei mit allen Headern einer Visual Studio-Installation schlägt fehl, weil corecrt_internal_state_isolation.h fehlt.
Die praktische Lösung
Ich wollte nicht weitere 6 Monate warten, bis ein Fix in Visual Studio erscheint (war schon einmal da, habe das schon gemacht). Ich benötigte sofort eine praktische Lösung.
Was ich letztendlich tat, war, nur die betroffene thread.cpp zu nehmen, sie zu reparieren, aber ansonsten von vorne zu beginnen. Ich habe sie nicht innerhalb des CRT-Quellbaum gebaut, zusammen mit allen anderen Dateien. Verdammt, ich habe sogar den gesamten Standard-Include-Pfad verworfen, um keine internen CRT-Header einzufügen, die von weiteren nicht existierenden Dateien abhängen.
thread.cpp enthält zwei interne Header corecrt_internal.h und process.h und verwendet einige Strukturen daraus. Glücklicherweise konnten all diese Strukturen im Visual Studio CRT-Quellcode gefunden werden, und sie wurden in eine glue.h-Datei portiert. Lokale Dateien mit den Namen corecrt_internal.h und process.h wurden als Drop-in-Ersatz für ihre Originals erstellt. Sie inkludieren jetzt einfach glue.h.
Durch das Aufrufen von Microsofts cl-Compiler mit dem /c-Parameter kann thread.cpp in eine Objektdatei kompiliert werden, wobei die Linkschritte übersprungen werden. Nach ein paar Runden des sorgfältigen Anpasens der Include-Verzeichnisse (/I-Parameter des Compilers) habe ich schließlich die gewünschte thread.obj-Datei erhalten. Keine zusätzlichen Präprozessor-Definitionen (über den /D-Parameter) waren dafür notwendig.
Schließlich brauchte ich eine Möglichkeit, die festgelegte thread.obj-Datei zu verwenden. Dafür wurde die kompilierte UCRT aus C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x86\libucrt.lib in einer x86 Native Tools Command Prompt untersucht über
lib /list libucrt.lib
Dies offenbarte den internen Namen von thread.obj in der kompilierte UCRT-Bibliothek: d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt....\startup\mt\objfre\i386\thread.obj
Durch den Aufruf von
lib /out:libucrt-removed.lib /remove:d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt\..\..\startup\mt\objfre\i386\thread.obj libucrt.lib
schuf ich eine neue libucrt-removed.lib ohne diese thread.obj-Datei. Schließlich führte ein Aufruf von
lib /out:libucrt-patched.lib libucrt-removed.lib thread.obj
zu einer patchten libucrt.lib mit meiner festgelegten thread.obj-Datei.
Ich habe dann die Projekteigenschaften meiner Anwendung in Visual Studio so angepasst, dass die standardmäßige statische UCRT ausgeschlossen wird (/NODEFAULTLIB:libucrt.lib) und libucrt-patched.lib als zusätzliche Abhängigkeit hinzugefügt. Das war's! Meine neu kompilierte Anwendung, die gegen die gepatchte UCRT aufgebaut wurde, wurde sofort auf Windows XP getestet und leckte beim Thread-Abbau keinen Speicher mehr.
Beachten Sie, dass all dies nur für den Release-Build gilt! Der Debug-Build verwendet eine Debug-Version der CRT (libucrtd.lib), die ich hier nicht gepatcht habe.
Fazit
Mit diesem positiven Ergebnis habe ich beschlossen, meinen Fix in einem GitHub-Repo zu veröffentlichen und all diese Anweisungen zu automatisieren. Sie finden es unter https://github.com/enlyze/ucrt-patch
Unser Drone CI-Runner baut es automatisch bei jedem Commit und pusht die gepatchte UCRT zu https://github.com/enlyze/ucrt-patched
Mit dieser Infrastruktur kann ich die feste UCRT in alle unsere Anwendungen leicht einfügen. Ich bin auch vorbereitet, wenn ich jemals einen weiteren Patch auf die UCRT anwenden muss. Wer weiß, ob ich bald auf einen weiteren Bug stoße?
Ich gebe zu, dass dies eine sehr spezifische Lösung für ein sehr spezifisches Problem ist. Aber hoffentlich können diese Anweisungen mehr Menschen helfen als nur uns.
Ich habe gerade einen Blog-Beitrag fertiggestellt, in dem ich fast die gesamte Microsoft-Build-Toolchain für unsere Windows-Software durch Open-Source-Alternativen ersetzt habe, die besser zu unseren Bedürfnissen passen. Außer der Visual Studio C-Laufzeitbibliothek, die heutzutage Universal CRT genannt wird (abgekürzt auf UCRT oder einfach CRT).
Die CRT hatte ohne Probleme funktioniert, und ich erwartete nicht, dass sich dies ändert, zumal unsere Software hauptsächlich moderne C++-Konstrukte verwendete. Die Dinge nahmen jedoch eine unerwartete Wendung, als ich eine riesige Speicherleak in einer unserer Anwendungen beobachtete, die mehrfach nacheinander std::threads erstellte und beendete.
Die Ursache für diesen Leak zu finden, kostete eine Weile, teils, weil er nur unter Windows XP und früher auftrat, nicht auf meinem Entwicklungsrechner mit Windows 10. Ich hatte bereits einige meiner Codes und den Kompatibilitätskleber aus meinem vorherigen Beitrag in Verdacht.
Aber es stellte sich schließlich heraus, dass bereits so einfacher Code wie
#include <thread> static void _ThreadProc() { return; } int main() { for (;;) { std::thread(_ThreadProc).detach(); } return 0; }
bereits unter Windows XP Chaos verursachte. Nicht nur mit meiner Toolchain, sondern auch, wenn sie mit Microsofts offizieller v141_xp Toolchain kompiliert wurde. Der verwendete Speicher des Prozesses wuchs im Task-Manager schnell, und bald genug begrüßte mich Windows mit einer Warnung vor Erschöpfung des virtuellen Speichers.
Interessanterweise passierte all dies nur, wenn die CRT statisch verknüpft wurde. Eine dynamisch verknüpfte CRT verursachte keine Probleme, was mir einige weitere Hinweise gab, um das Problem nachzuvollziehen.
Das Problem
Jeder Thread, der von std::thread erzeugt wird, landet in der CRT-Funktion _beginthreadex, egal ob die ursprüngliche Microsoft v141_xp Toolchain oder meine libc++/winpthreads verwendet wird. Glücklicherweise hat unsere Visual Studio-Installation den CRT-Code in C:\Program Files (x86)\Windows Kits\10\Source\10.0.20348.0\ucrt, sodass ich leicht nachverfolgen kann, was als Nächstes passiert.
Der Aufruf landet in der thread_start-Templatefunktion in startup\thread.cpp. Diese Funktion ruft __acrt_getptd() auf, um eine thread-spezifische Datenstruktur zu erhalten und implizit zu erstellen. Die Datenstruktur muss gereinigt werden, wenn der Thread beendet wird.
Zuvor wurde __acrt_initialize() aus internal\initialization.cpp bereits von der CRT beim Starten meiner Anwendung aufgerufen. Es ruft eine Reihe von Initialisierungsroutinen auf, von denen eine __acrt_initialize_ptd() aus _internal\per_threaddata.cpp ist. __acrt_initialize_ptd ruft __acrt_FlsAlloc(destroy_fls) auf, um einen Index für die speichergebundene Ablage zuzuweisen, der es allen nachfolgenden Threads ermöglicht, eine thread-spezifische Datenstruktur zu speichern. Der gegebene destroy_fls-Callback ist eine Funktion in derselben Datei, die automatisch aufgerufen werden soll, wenn ein Thread beendet wird, um seine thread-spezifische Datenstruktur zu bereinigen.
__acrt_FlsAlloc wird in _internal\winapithunks.cpp so implementiert:
if (auto const fls_alloc = try_get_FlsAlloc()) { return fls_alloc(callback); } return TlsAlloc()
Wie Sie sehen, wird der Callback nur für Betriebssysteme übergeben, die FlsAlloc unterstützen. Die Fallback-Funktion TlsAlloc bietet keinen solchen Callback-Parameter. Daher wird die destroy_fls-Aufräumfunktion in diesem Fall nie aufgerufen.
FlsAlloc wurde mit Windows Server 2003 (NT 5.2) eingeführt, was erklärt, warum das Problem auf Windows XP (NT 5.1) und früheren Versionen auftritt, nicht aber auf meinem aktuellen Windows 10. Wenn das alles wäre, würde meine Anwendung unter Windows XP immer die thread-spezifische Datenstruktur leaken, egal ob sie eine statisch verlinkte CRT oder eine DLL verwendet. Ich bin sicher, dass Microsoft dies während der Tests erkannt hätte. Das Problem wird jedoch gemildert, wenn die CRT als DLL verknüpft ist:
Die Datei _dll\appcrtdllmain.cpp implementiert __acrt_DllMain, die für jeden neuen Thread aufgerufen wird, wenn die CRT als DLL verknüpft ist. __acrt_DllMain ruft seinerseits DllMainDispatch auf, die einen DLL_THREAD_DETACH-Handler implementiert, der __acrt_thread_detach() aus internal\initialization.cpp aufruft. Nun ruft __acrt_thread_detach() __acrt_freeptd() auf, um die thread-spezifische Datenstruktur explizit zu bereinigen. Dies geschieht jedes Mal, wenn ein Thread endet und die CRT-DLL vom Thread getrennt wird. Deshalb leckt der Speicher nicht, wenn die CRT als DLL verlinkt ist. Wenn die CRT statisch verlinkt wird, wird niemals ein __acrt_DllMain-Handler aufgerufen, und folglich wird __acrt_freeptd() auch nicht aufgerufen. Die Funktion __acrt_freeptd wird sogar aus einer Release-Berechnung optimiert, weil es keinen einzigen Aufruf dorthin gibt.
Die Lösung
Ich habe diesen Fehler bei Microsoft gemeldet zusammen mit einem vorgeschlagenen Fix. Meine Lösung ist so einfach wie das Aufrufen von __acrt_freeptd() auf allen Ausstiegswegen der Funktion common_end_thread in startup\thread.cpp. Dadurch wird die thread-spezifische Datenstruktur explizit freigegeben, ohne auf Callback-Magie zu vertrauen. Ein Blick in startup\thread.cpp bestätigt auch, dass common_end_thread in allen Situationen aufgerufen wird, wenn ein CRT-Thread endet.
Aber einen Code-Fix zu haben, ist nur die halbe Lösung. Ich kann mir den CRT-Quellcode vielleicht ansehen, aber Microsoft hat längst alle offiziellen Wege eliminiert, um eine modifizierte CRT neu zu bauen. Selbst das Neuaufbauen der betroffenen thread.cpp-Datei mit allen Headern einer Visual Studio-Installation schlägt fehl, weil corecrt_internal_state_isolation.h fehlt.
Die praktische Lösung
Ich wollte nicht weitere 6 Monate warten, bis ein Fix in Visual Studio erscheint (war schon einmal da, habe das schon gemacht). Ich benötigte sofort eine praktische Lösung.
Was ich letztendlich tat, war, nur die betroffene thread.cpp zu nehmen, sie zu reparieren, aber ansonsten von vorne zu beginnen. Ich habe sie nicht innerhalb des CRT-Quellbaum gebaut, zusammen mit allen anderen Dateien. Verdammt, ich habe sogar den gesamten Standard-Include-Pfad verworfen, um keine internen CRT-Header einzufügen, die von weiteren nicht existierenden Dateien abhängen.
thread.cpp enthält zwei interne Header corecrt_internal.h und process.h und verwendet einige Strukturen daraus. Glücklicherweise konnten all diese Strukturen im Visual Studio CRT-Quellcode gefunden werden, und sie wurden in eine glue.h-Datei portiert. Lokale Dateien mit den Namen corecrt_internal.h und process.h wurden als Drop-in-Ersatz für ihre Originals erstellt. Sie inkludieren jetzt einfach glue.h.
Durch das Aufrufen von Microsofts cl-Compiler mit dem /c-Parameter kann thread.cpp in eine Objektdatei kompiliert werden, wobei die Linkschritte übersprungen werden. Nach ein paar Runden des sorgfältigen Anpasens der Include-Verzeichnisse (/I-Parameter des Compilers) habe ich schließlich die gewünschte thread.obj-Datei erhalten. Keine zusätzlichen Präprozessor-Definitionen (über den /D-Parameter) waren dafür notwendig.
Schließlich brauchte ich eine Möglichkeit, die festgelegte thread.obj-Datei zu verwenden. Dafür wurde die kompilierte UCRT aus C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x86\libucrt.lib in einer x86 Native Tools Command Prompt untersucht über
lib /list libucrt.lib
Dies offenbarte den internen Namen von thread.obj in der kompilierte UCRT-Bibliothek: d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt....\startup\mt\objfre\i386\thread.obj
Durch den Aufruf von
lib /out:libucrt-removed.lib /remove:d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt\..\..\startup\mt\objfre\i386\thread.obj libucrt.lib
schuf ich eine neue libucrt-removed.lib ohne diese thread.obj-Datei. Schließlich führte ein Aufruf von
lib /out:libucrt-patched.lib libucrt-removed.lib thread.obj
zu einer patchten libucrt.lib mit meiner festgelegten thread.obj-Datei.
Ich habe dann die Projekteigenschaften meiner Anwendung in Visual Studio so angepasst, dass die standardmäßige statische UCRT ausgeschlossen wird (/NODEFAULTLIB:libucrt.lib) und libucrt-patched.lib als zusätzliche Abhängigkeit hinzugefügt. Das war's! Meine neu kompilierte Anwendung, die gegen die gepatchte UCRT aufgebaut wurde, wurde sofort auf Windows XP getestet und leckte beim Thread-Abbau keinen Speicher mehr.
Beachten Sie, dass all dies nur für den Release-Build gilt! Der Debug-Build verwendet eine Debug-Version der CRT (libucrtd.lib), die ich hier nicht gepatcht habe.
Fazit
Mit diesem positiven Ergebnis habe ich beschlossen, meinen Fix in einem GitHub-Repo zu veröffentlichen und all diese Anweisungen zu automatisieren. Sie finden es unter https://github.com/enlyze/ucrt-patch
Unser Drone CI-Runner baut es automatisch bei jedem Commit und pusht die gepatchte UCRT zu https://github.com/enlyze/ucrt-patched
Mit dieser Infrastruktur kann ich die feste UCRT in alle unsere Anwendungen leicht einfügen. Ich bin auch vorbereitet, wenn ich jemals einen weiteren Patch auf die UCRT anwenden muss. Wer weiß, ob ich bald auf einen weiteren Bug stoße?
Ich gebe zu, dass dies eine sehr spezifische Lösung für ein sehr spezifisches Problem ist. Aber hoffentlich können diese Anweisungen mehr Menschen helfen als nur uns.
Ich habe gerade einen Blog-Beitrag fertiggestellt, in dem ich fast die gesamte Microsoft-Build-Toolchain für unsere Windows-Software durch Open-Source-Alternativen ersetzt habe, die besser zu unseren Bedürfnissen passen. Außer der Visual Studio C-Laufzeitbibliothek, die heutzutage Universal CRT genannt wird (abgekürzt auf UCRT oder einfach CRT).
Die CRT hatte ohne Probleme funktioniert, und ich erwartete nicht, dass sich dies ändert, zumal unsere Software hauptsächlich moderne C++-Konstrukte verwendete. Die Dinge nahmen jedoch eine unerwartete Wendung, als ich eine riesige Speicherleak in einer unserer Anwendungen beobachtete, die mehrfach nacheinander std::threads erstellte und beendete.
Die Ursache für diesen Leak zu finden, kostete eine Weile, teils, weil er nur unter Windows XP und früher auftrat, nicht auf meinem Entwicklungsrechner mit Windows 10. Ich hatte bereits einige meiner Codes und den Kompatibilitätskleber aus meinem vorherigen Beitrag in Verdacht.
Aber es stellte sich schließlich heraus, dass bereits so einfacher Code wie
#include <thread> static void _ThreadProc() { return; } int main() { for (;;) { std::thread(_ThreadProc).detach(); } return 0; }
bereits unter Windows XP Chaos verursachte. Nicht nur mit meiner Toolchain, sondern auch, wenn sie mit Microsofts offizieller v141_xp Toolchain kompiliert wurde. Der verwendete Speicher des Prozesses wuchs im Task-Manager schnell, und bald genug begrüßte mich Windows mit einer Warnung vor Erschöpfung des virtuellen Speichers.
Interessanterweise passierte all dies nur, wenn die CRT statisch verknüpft wurde. Eine dynamisch verknüpfte CRT verursachte keine Probleme, was mir einige weitere Hinweise gab, um das Problem nachzuvollziehen.
Das Problem
Jeder Thread, der von std::thread erzeugt wird, landet in der CRT-Funktion _beginthreadex, egal ob die ursprüngliche Microsoft v141_xp Toolchain oder meine libc++/winpthreads verwendet wird. Glücklicherweise hat unsere Visual Studio-Installation den CRT-Code in C:\Program Files (x86)\Windows Kits\10\Source\10.0.20348.0\ucrt, sodass ich leicht nachverfolgen kann, was als Nächstes passiert.
Der Aufruf landet in der thread_start-Templatefunktion in startup\thread.cpp. Diese Funktion ruft __acrt_getptd() auf, um eine thread-spezifische Datenstruktur zu erhalten und implizit zu erstellen. Die Datenstruktur muss gereinigt werden, wenn der Thread beendet wird.
Zuvor wurde __acrt_initialize() aus internal\initialization.cpp bereits von der CRT beim Starten meiner Anwendung aufgerufen. Es ruft eine Reihe von Initialisierungsroutinen auf, von denen eine __acrt_initialize_ptd() aus _internal\per_threaddata.cpp ist. __acrt_initialize_ptd ruft __acrt_FlsAlloc(destroy_fls) auf, um einen Index für die speichergebundene Ablage zuzuweisen, der es allen nachfolgenden Threads ermöglicht, eine thread-spezifische Datenstruktur zu speichern. Der gegebene destroy_fls-Callback ist eine Funktion in derselben Datei, die automatisch aufgerufen werden soll, wenn ein Thread beendet wird, um seine thread-spezifische Datenstruktur zu bereinigen.
__acrt_FlsAlloc wird in _internal\winapithunks.cpp so implementiert:
if (auto const fls_alloc = try_get_FlsAlloc()) { return fls_alloc(callback); } return TlsAlloc()
Wie Sie sehen, wird der Callback nur für Betriebssysteme übergeben, die FlsAlloc unterstützen. Die Fallback-Funktion TlsAlloc bietet keinen solchen Callback-Parameter. Daher wird die destroy_fls-Aufräumfunktion in diesem Fall nie aufgerufen.
FlsAlloc wurde mit Windows Server 2003 (NT 5.2) eingeführt, was erklärt, warum das Problem auf Windows XP (NT 5.1) und früheren Versionen auftritt, nicht aber auf meinem aktuellen Windows 10. Wenn das alles wäre, würde meine Anwendung unter Windows XP immer die thread-spezifische Datenstruktur leaken, egal ob sie eine statisch verlinkte CRT oder eine DLL verwendet. Ich bin sicher, dass Microsoft dies während der Tests erkannt hätte. Das Problem wird jedoch gemildert, wenn die CRT als DLL verknüpft ist:
Die Datei _dll\appcrtdllmain.cpp implementiert __acrt_DllMain, die für jeden neuen Thread aufgerufen wird, wenn die CRT als DLL verknüpft ist. __acrt_DllMain ruft seinerseits DllMainDispatch auf, die einen DLL_THREAD_DETACH-Handler implementiert, der __acrt_thread_detach() aus internal\initialization.cpp aufruft. Nun ruft __acrt_thread_detach() __acrt_freeptd() auf, um die thread-spezifische Datenstruktur explizit zu bereinigen. Dies geschieht jedes Mal, wenn ein Thread endet und die CRT-DLL vom Thread getrennt wird. Deshalb leckt der Speicher nicht, wenn die CRT als DLL verlinkt ist. Wenn die CRT statisch verlinkt wird, wird niemals ein __acrt_DllMain-Handler aufgerufen, und folglich wird __acrt_freeptd() auch nicht aufgerufen. Die Funktion __acrt_freeptd wird sogar aus einer Release-Berechnung optimiert, weil es keinen einzigen Aufruf dorthin gibt.
Die Lösung
Ich habe diesen Fehler bei Microsoft gemeldet zusammen mit einem vorgeschlagenen Fix. Meine Lösung ist so einfach wie das Aufrufen von __acrt_freeptd() auf allen Ausstiegswegen der Funktion common_end_thread in startup\thread.cpp. Dadurch wird die thread-spezifische Datenstruktur explizit freigegeben, ohne auf Callback-Magie zu vertrauen. Ein Blick in startup\thread.cpp bestätigt auch, dass common_end_thread in allen Situationen aufgerufen wird, wenn ein CRT-Thread endet.
Aber einen Code-Fix zu haben, ist nur die halbe Lösung. Ich kann mir den CRT-Quellcode vielleicht ansehen, aber Microsoft hat längst alle offiziellen Wege eliminiert, um eine modifizierte CRT neu zu bauen. Selbst das Neuaufbauen der betroffenen thread.cpp-Datei mit allen Headern einer Visual Studio-Installation schlägt fehl, weil corecrt_internal_state_isolation.h fehlt.
Die praktische Lösung
Ich wollte nicht weitere 6 Monate warten, bis ein Fix in Visual Studio erscheint (war schon einmal da, habe das schon gemacht). Ich benötigte sofort eine praktische Lösung.
Was ich letztendlich tat, war, nur die betroffene thread.cpp zu nehmen, sie zu reparieren, aber ansonsten von vorne zu beginnen. Ich habe sie nicht innerhalb des CRT-Quellbaum gebaut, zusammen mit allen anderen Dateien. Verdammt, ich habe sogar den gesamten Standard-Include-Pfad verworfen, um keine internen CRT-Header einzufügen, die von weiteren nicht existierenden Dateien abhängen.
thread.cpp enthält zwei interne Header corecrt_internal.h und process.h und verwendet einige Strukturen daraus. Glücklicherweise konnten all diese Strukturen im Visual Studio CRT-Quellcode gefunden werden, und sie wurden in eine glue.h-Datei portiert. Lokale Dateien mit den Namen corecrt_internal.h und process.h wurden als Drop-in-Ersatz für ihre Originals erstellt. Sie inkludieren jetzt einfach glue.h.
Durch das Aufrufen von Microsofts cl-Compiler mit dem /c-Parameter kann thread.cpp in eine Objektdatei kompiliert werden, wobei die Linkschritte übersprungen werden. Nach ein paar Runden des sorgfältigen Anpasens der Include-Verzeichnisse (/I-Parameter des Compilers) habe ich schließlich die gewünschte thread.obj-Datei erhalten. Keine zusätzlichen Präprozessor-Definitionen (über den /D-Parameter) waren dafür notwendig.
Schließlich brauchte ich eine Möglichkeit, die festgelegte thread.obj-Datei zu verwenden. Dafür wurde die kompilierte UCRT aus C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x86\libucrt.lib in einer x86 Native Tools Command Prompt untersucht über
lib /list libucrt.lib
Dies offenbarte den internen Namen von thread.obj in der kompilierte UCRT-Bibliothek: d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt....\startup\mt\objfre\i386\thread.obj
Durch den Aufruf von
lib /out:libucrt-removed.lib /remove:d:\os\obj\x86fre\minkernel\crts\ucrt\src\appcrt\dll\mt\..\..\startup\mt\objfre\i386\thread.obj libucrt.lib
schuf ich eine neue libucrt-removed.lib ohne diese thread.obj-Datei. Schließlich führte ein Aufruf von
lib /out:libucrt-patched.lib libucrt-removed.lib thread.obj
zu einer patchten libucrt.lib mit meiner festgelegten thread.obj-Datei.
Ich habe dann die Projekteigenschaften meiner Anwendung in Visual Studio so angepasst, dass die standardmäßige statische UCRT ausgeschlossen wird (/NODEFAULTLIB:libucrt.lib) und libucrt-patched.lib als zusätzliche Abhängigkeit hinzugefügt. Das war's! Meine neu kompilierte Anwendung, die gegen die gepatchte UCRT aufgebaut wurde, wurde sofort auf Windows XP getestet und leckte beim Thread-Abbau keinen Speicher mehr.
Beachten Sie, dass all dies nur für den Release-Build gilt! Der Debug-Build verwendet eine Debug-Version der CRT (libucrtd.lib), die ich hier nicht gepatcht habe.
Fazit
Mit diesem positiven Ergebnis habe ich beschlossen, meinen Fix in einem GitHub-Repo zu veröffentlichen und all diese Anweisungen zu automatisieren. Sie finden es unter https://github.com/enlyze/ucrt-patch
Unser Drone CI-Runner baut es automatisch bei jedem Commit und pusht die gepatchte UCRT zu https://github.com/enlyze/ucrt-patched
Mit dieser Infrastruktur kann ich die feste UCRT in alle unsere Anwendungen leicht einfügen. Ich bin auch vorbereitet, wenn ich jemals einen weiteren Patch auf die UCRT anwenden muss. Wer weiß, ob ich bald auf einen weiteren Bug stoße?
Ich gebe zu, dass dies eine sehr spezifische Lösung für ein sehr spezifisches Problem ist. Aber hoffentlich können diese Anweisungen mehr Menschen helfen als nur uns.
Read more
01.04.2025
ENLYZE customer reviews - Insights from our customer satisfaction survey
Kacey Ende
3
Min
28.02.2025
AI or Knockout: The modular AI tech stack as the key to successful AI in manufacturing
Clemens Hensen
6
Min
05.02.2025
Combine and visualize your production data with the new Power BI integration from ENLYZE
Julius Scheuber
5
Min
ENLYZE kennenlernen
Talk to an expert and find out how ENLYZE can help you with your production.
ENLYZE kennenlernen