[Visual C++] Funktionsobjekt -> Crash

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

[Visual C++] Funktionsobjekt -> Crash

Beitrag von Krishty »

Legacy-Anwendung (x86-32, Visual C++ 2017) crasht.

Nach vielen vielen Tests habe ich die Stellen eingegrenzt: Quasi immer, wenn eine STL-Funktion ein Lambda erwartet (z.B. remove_if()). Die Codebase übergibt dann kein Lambda (mit []-Syntax) sondern legt, weil sie eben sehr alt ist, ein struct mit operator() an und übergibt davon eine Instanz.

Das hat mit Visual C++ 2015 wunderbar funktioniert. Augenscheinlich kommt dort auch nichts Exotisches vor. Jetzt crasht es oft – entweder mit value of ESP was not correctly saved across a function call oder mit Zugriffsverletzung.

Ich habe die Stellen zu Lambdas geändert, und nun funktioniert scheinbar alles. (Bin nicht 100 % sicher; Kompilieren & Starten dauert ewig.)

Während ich hier Abhängigkeitsprobleme aufzwirbele (Projekte hängen gegenseitig von einander ab): Klingelt das bei irgendwem die Glocken? Haben die an der STL was verändert, dass man nun nur noch Lambdas übergeben darf? Hat sich die Calling Convention geändert oder so?

Nachtrag: Ich habe alle drei Aufrufe an std::remove_if() von struct auf Lambda umgestellt, und tatsächlich sind nun alle Crashes verschwunden. Uh-oh …
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
DerAlbi
Establishment
Beiträge: 269
Registriert: 20.05.2011, 05:37

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von DerAlbi »

Die STL erwartet aber keine Lamdas, sondern nur komplett generische Objekte, die dann mit Objekt(Argument) aufgerufen werden. Das Lamda wird nichtmal zum Funktionszeiger degeneriert oder so. Ob das ein Lambda ist, oder nicht, spielt keine rolle. Das ist auch keine Eigenschaft der STL... wenn es da drin crasht, crasht es auch wenn man händisch struct::operator() aufruft. Ich würde da mal anfangen zu gucken, ob man das Problem außerhalb der STL erzeugen kann.

Zur not, wrappe die Aufrufe mal in

Code: Alles auswählen

template< typename Callable, typename... Args> auto Call(Callable Object, Args... args)
{
     return Object(args...);
}
Das sollte die STL-Mechanik nutzen. Aber das Inlined eigentlich brutal alles. Hmmmh. Tut die STL aber auch :-/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von Schrompf »

Könnte aber auch nur teilweise nicht neukompilierter Code gewesen sein. Wenn's so ein großes Projekt mit vielen Abhängigkeiten ist, waren evtl. auch nur Teile der Libs noch 2015er-Kompilat. Ist nur ne Vermutung anhand des Klangs Deiner Fehlerbeschreibung kombiniert mit wüstem Assoziieren.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von dot »

Klingt sehr merkwürdig und bei mir läuten da definitiv keine Glocken. Und in unseren Codebasen gibt es einige Stellen wo Funktionsobjekte an STL Algorithmen übergeben werden und noch viel mehr Stellen mit Lambdas und das alles hat immer klaglos funktioniert und tut dies auch mit VS 2017 weiterhin. Wobei ich dazusagen muss dass ich wohl schon seit Jahren nix Ernsthaftes mehr für x86 kompiliert hab...

Rein prinzipiell ist es aber so, dass Lambdas per Definition nix anderes sind als Syntaxzucker für compilergenerierte Funktionsobjekte. Dass in der STL in irgendeiner Form an irgendeiner Stelle spezieller Support für Lambdas nötig wäre, hätte ich noch nie gehört. Und basierend auf den Fehlermeldungen die ich im Zusammenhang mit Lambdas bisher so beobachten durfte, wäre ich wirklich sehr überrascht wenn irgendein mir bekannter Compiler Lambdas tatsächlich als etwas anderes als einfach nur compilergenerierte Funktionsobjekte umsetzt. Clang macht es (zumindest nach meinem Verständnis des Sourcecode) auf jeden Fall wirklich genau so (CXXRecordDecl ist in Clang die interne Repräsentation von class-types).

Ich würde auf jeden Fall nochmal sichergehen ob sich in deinem originalen Code nicht vielleicht doch irgendeine Form von UB eingeschlichen hatte die einfach nur erst jetzt mit dem neuen Optimizer in VS 2017 explodiert...
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von Krishty »

Okay; mehr Kontext:
  • Der Absturz betrifft drei Stellen in S.T.A.L.K.E.R. - Shadow of Chernobyl.
  • Die Codebase ist 15 Jahre alt und von grausamer Qualität; UB und Bugs sind definitiv drin – aber nicht in der Nähe des Codes, der crasht.
  • Die Crashes äußern sich wahlweise als Heap Corruption oder Return Address Corruption (EAX). Entweder während remove_if() oder beim Freigeben des std::vector, der mit remove_if() bearbeitet wurde.
  • Ich versuche, die Crashes mit dem Application Verifier zu isolieren. Das geht aber nur sehr bedingt, weil das 32-Bit-Spiel über ein GiB Adressraum verbraucht und der Page Heap des Application Verifier das mindestens vervierfacht – out of memory. Ich nutze aktuell mit 50-50-Wahrscheinlichkeit den Page Heap; dann kommt das mit dem Adressraum gerade hin. Dafür crasht es leider nicht jedes Mal sofort.
  • Weiterhin kann ich nicht allzu viele Vergleiche ziehen, weil das Neukompilieren und Neustarten jedes Mal um die 15 Minuten dauert.
  • Der crashende Code sieht so aus (gerade vier Mal gestartet; 2× Heap Corruption 1×Access Violation 1×EAX kaputt):

    Code: Alles auswählen

    struct SRP
    {
    	bool operator() (CParticlesPlayer::SParticlesInfo& pi)
    	{
    		return ! pi.ps;
    	}
    };
    
    […]
    
    	for(BoneInfoVecIt b_it=m_Bones.begin(); b_it!=m_Bones.end(); b_it++) {
    		SBoneInfo& b_info = *b_it;
    
    			[…]
    
    			ParticlesInfoListIt RI=std::remove_if(b_info.particles.begin(),b_info.particles.end(),SRP());
    			b_info.particles.erase(RI,b_info.particles.end());
    Er erzeugt die folgenden Maschinenbefehle:

    Code: Alles auswählen

       280: 		ParticlesInfoListIt RI=std::remove_if(b_info.particles.begin(),b_info.particles.end(),SRP());
    FAC69B96 FF 74 24 24          push        dword ptr [esp+24h]  
    FAC69B9A E8 A1 B9 EC FF       call        std::_Pass_fn<SRP,0> (0FAB35540h)  
    FAC69B9F 83 C4 04             add         esp,4  
    FAC69BA2 3B F9                cmp         edi,ecx  
    FAC69BA4 74 39                je          CParticlesPlayer::UpdateParticles+44Fh (0FAC69BDFh)  
    FAC69BA6 83 3F 00             cmp         dword ptr [edi],0  
    FAC69BA9 74 07                je          CParticlesPlayer::UpdateParticles+422h (0FAC69BB2h)  
    FAC69BAB 83 C7 18             add         edi,18h  
    FAC69BAE 3B F9                cmp         edi,ecx  
    FAC69BB0 75 F4                jne         CParticlesPlayer::UpdateParticles+416h (0FAC69BA6h)  
    FAC69BB2 3B F9                cmp         edi,ecx  
    FAC69BB4 74 29                je          CParticlesPlayer::UpdateParticles+44Fh (0FAC69BDFh)  
    FAC69BB6 8D 47 18             lea         eax,[edi+18h]  
    FAC69BB9 3B C1                cmp         eax,ecx  
    FAC69BBB 74 22                je          CParticlesPlayer::UpdateParticles+44Fh (0FAC69BDFh)  
    FAC69BBD 0F 1F 00             nop         dword ptr [eax]  
    FAC69BC0 83 38 00             cmp         dword ptr [eax],0  
    FAC69BC3 74 13                je          CParticlesPlayer::UpdateParticles+448h (0FAC69BD8h)  
    FAC69BC5 0F 10 00             movups      xmm0,xmmword ptr [eax]  
    FAC69BC8 0F 11 07             movups      xmmword ptr [edi],xmm0  
    FAC69BCB F3 0F 7E 40 10       movq        xmm0,mmword ptr [eax+10h]  
    FAC69BD0 66 0F D6 47 10       movq        mmword ptr [edi+10h],xmm0  
    FAC69BD5 83 C7 18             add         edi,18h  
    FAC69BD8 83 C0 18             add         eax,18h  
    FAC69BDB 3B C1                cmp         eax,ecx  
    FAC69BDD 75 E1                jne         CParticlesPlayer::UpdateParticles+430h (0FAC69BC0h)  
       281: 		b_info.particles.erase(RI,b_info.particles.end());
    FAC69BDF 8B 46 14             mov         eax,dword ptr [esi+14h]  
    FAC69BE2 3B F8                cmp         edi,eax  
    FAC69BE4 74 10                je          CParticlesPlayer::UpdateParticles+466h (0FAC69BF6h)  
    FAC69BE6 6A 00                push        0  
    FAC69BE8 50                   push        eax  
    FAC69BE9 57                   push        edi  
    FAC69BEA FF 15 10 A3 EB FA    call        dword ptr [__imp__memmove (0FAEBA310h)]  
    FAC69BF0 83 C4 0C             add         esp,0Ch  
    FAC69BF3 89 7E 14             mov         dword ptr [esi+14h],edi  
       248: 	VERIFY(object);
       249: 
       250: 	for(BoneInfoVecIt b_it=m_Bones.begin(); b_it!=m_Bones.end(); b_it++) {
    FAC69BF6 8B 44 24 10          mov         eax,dword ptr [esp+10h]  
    FAC69BFA 83 C6 1C             add         esi,1Ch  
    FAC69BFD 3B 70 14             cmp         esi,dword ptr [eax+14h]  
    FAC69C00 0F 85 BC FB FF FF    jne         CParticlesPlayer::UpdateParticles+32h (0FAC697C2h)  
       282: 	}
       283: }
    FAC69C06 5F                   pop         edi  
    FAC69C07 5E                   pop         esi  
    FAC69C08 8B E5                mov         esp,ebp  
    FAC69C0A 5D                   pop         ebp  
    FAC69C0B C3                   ret  
  • Die Crashes verschwinden, wenn ich die remove_if()-Zeile zu dieser hier ändere (gerade drei Mal gestartet, keine Probleme):

    Code: Alles auswählen

    			ParticlesInfoListIt RI=std::remove_if(b_info.particles.begin(), b_info.particles.end(), [](CParticlesPlayer::SParticlesInfo & pi) { return !pi.ps; });
    Dann ergibt das folgende Maschinenbefehle:

    Code: Alles auswählen

       280: 		ParticlesInfoListIt RI=std::remove_if(b_info.particles.begin(), b_info.particles.end(), [](CParticlesPlayer::SParticlesInfo & pi) { return !pi.ps; });
    FAC69B96 3B F9                cmp         edi,ecx  
    FAC69B98 74 3C                je          CParticlesPlayer::UpdateParticles+446h (0FAC69BD6h)  
    FAC69B9A 66 0F 1F 44 00 00    nop         word ptr [eax+eax]  
    FAC69BA0 83 3F 00             cmp         dword ptr [edi],0  
    FAC69BA3 74 07                je          CParticlesPlayer::UpdateParticles+41Ch (0FAC69BACh)  
    FAC69BA5 83 C7 18             add         edi,18h  
    FAC69BA8 3B F9                cmp         edi,ecx  
    FAC69BAA 75 F4                jne         CParticlesPlayer::UpdateParticles+410h (0FAC69BA0h)  
    FAC69BAC 3B F9                cmp         edi,ecx  
    FAC69BAE 74 26                je          CParticlesPlayer::UpdateParticles+446h (0FAC69BD6h)  
    FAC69BB0 8D 47 18             lea         eax,[edi+18h]  
    FAC69BB3 3B C1                cmp         eax,ecx  
    FAC69BB5 74 1F                je          CParticlesPlayer::UpdateParticles+446h (0FAC69BD6h)  
    FAC69BB7 83 38 00             cmp         dword ptr [eax],0  
    FAC69BBA 74 13                je          CParticlesPlayer::UpdateParticles+43Fh (0FAC69BCFh)  
    FAC69BBC 0F 10 00             movups      xmm0,xmmword ptr [eax]  
    FAC69BBF 0F 11 07             movups      xmmword ptr [edi],xmm0  
    FAC69BC2 F3 0F 7E 40 10       movq        xmm0,mmword ptr [eax+10h]  
    FAC69BC7 66 0F D6 47 10       movq        mmword ptr [edi+10h],xmm0  
    FAC69BCC 83 C7 18             add         edi,18h  
    FAC69BCF 83 C0 18             add         eax,18h  
    FAC69BD2 3B C1                cmp         eax,ecx  
    FAC69BD4 75 E1                jne         CParticlesPlayer::UpdateParticles+427h (0FAC69BB7h)  
       281: 		b_info.particles.erase(RI,b_info.particles.end());
    FAC69BD6 8B 46 14             mov         eax,dword ptr [esi+14h]  
    FAC69BD9 3B F8                cmp         edi,eax  
    FAC69BDB 74 10                je          CParticlesPlayer::UpdateParticles+45Dh (0FAC69BEDh)  
    FAC69BDD 6A 00                push        0  
    FAC69BDF 50                   push        eax  
    FAC69BE0 57                   push        edi  
    FAC69BE1 FF 15 10 A3 EB FA    call        dword ptr [__imp__memmove (0FAEBA310h)]  
    FAC69BE7 83 C4 0C             add         esp,0Ch  
    FAC69BEA 89 7E 14             mov         dword ptr [esi+14h],edi  
       248: 	VERIFY(object);
       249: 
       250: 	for(BoneInfoVecIt b_it=m_Bones.begin(); b_it!=m_Bones.end(); b_it++) {
    FAC69BED 8B 44 24 10          mov         eax,dword ptr [esp+10h]  
       248: 	VERIFY(object);
       249: 
       250: 	for(BoneInfoVecIt b_it=m_Bones.begin(); b_it!=m_Bones.end(); b_it++) {
    FAC69BF1 83 C6 1C             add         esi,1Ch  
    FAC69BF4 3B 70 14             cmp         esi,dword ptr [eax+14h]  
    FAC69BF7 0F 85 C5 FB FF FF    jne         CParticlesPlayer::UpdateParticles+32h (0FAC697C2h)  
       282: 	}
       283: }
    FAC69BFD 5F                   pop         edi  
    FAC69BFE 5E                   pop         esi  
    FAC69BFF 8B E5                mov         esp,ebp  
    FAC69C01 5D                   pop         ebp  
    FAC69C02 C3                   ret  
  • Betroffen war nicht nur diese Stelle, sondern auch zwei andere – einmal erneut std::remove_if(), ein anderes Mal war es std::sort(), glaube ich (hab’s vergessen). An den insgesamt drei Stellen haben die Crashes aufgehört, als ich auf ein Lambda umgestiegen bin.
  • Insgesamt ruft S.T.A.L.K.E.R. 57 Mal remove_if() auf, zwei Mal davon ist es nach dem Umstieg auf VS 2017 bös gecrasht.
  • Der deutlichste Unterschied ist wohl, dass bei der Lambda-Version nicht std::_Pass_fn aufgerufen wird; bei der Funktionsobjekt-Version schon.
  • Ich habe jahrelang nicht mit der STL gearbeitet und nie Lambdas benutzt – bitte erleuchtet mich, was falsch ist und warum andere Befehle rauskommen! Ich teilte nämlich auch dots Einstellung
    Rein prinzipiell ist es aber so, dass Lambdas per Definition nix anderes sind als Syntaxzucker für compilergenerierte Funktionsobjekte. Dass in der STL in irgendeiner Form an irgendeiner Stelle spezieller Support für Lambdas nötig wäre, hätte ich noch nie gehört. Und basierend auf den Fehlermeldungen die ich im Zusammenhang mit Lambdas bisher so beobachten durfte, wäre ich wirklich sehr überrascht wenn irgendein mir bekannter Compiler Lambdas tatsächlich als etwas anderes als einfach nur compilergenerierte Funktionsobjekte umsetzt.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von dot »

Sehr strange. Soweit ich das sehen kann ist dieses _Pass_fn() eine Optimierung in der MSVC STL die checked ob das übergebene Funktionsobjekt < sizeof(void*) ist und wenn ja by value und ansonsten by reference passed. Die Funktion tut nix andres als je nachdem entweder eine Kopie oder eine Referenz auf das übergebene Objekt zu returnen. Die Tatsache dass das nicht wegoptimiert wird und direkt nach dem call std::_Pass_fn<SRP,0> esp modifiziert wird ist imo sehr verdächtig. Ich würde mich mal aus dem Fenster lehnen und meinen, dass das eher ein Codegen Bug im Compiler ist als ein Bug in der STL...

Btw: Der operator() von deinem Comparator sollte wohl const sein ;)

Edit: Ok, das mit esp ist natürlich cleanup der Argumente des __cdecl Aufruf von _Pass_fn...auf jeden Fall ein toller Weg um ein nop zu implementieren... :mrgreen:
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von Krishty »

Tja, Code Generation Bugs habe ich ja mittlerweile fast wöchentlich :( Bei x86-32 scheiße ich drauf, der Compiler wird ja eh bald weggeschmissen. Die 64-Bit-Bugs in meinen anderen Projekten muss ich aber langsam mal melden.
dot hat geschrieben:Btw: Der operator() von deinem Comparator sollte wohl const sein ;)
Der ist ja nicht von mir, sondern von Pavel Wodkaskowski anno 2003 :P
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
DerAlbi
Establishment
Beiträge: 269
Registriert: 20.05.2011, 05:37

Re: [Visual C++] Funktionsobjekt -> Crash

Beitrag von DerAlbi »

Also wenn sich der generierte Code ändert, ist das nicht zwangsläufig ein Bug bei der Code-Generierung.
Wer kennt es nicht, dass Bugs erst auftauchen, wenn man agressiv optimiert oder wenn man irgendwas im Code ändert, der Compiler den Code umstellt oder anders inlined und plötzlich knallts. Die Ursache kann vollkommen versteckt wo anders sein.
Antworten