C ++: List und Liebe oder was könnte schief gehen?





„C macht es einfach, sich in den Fuß zu schießen. In C ++ ist das schwieriger, aber es wird ein ganzes Bein abnehmen. “- Björn Stroustrup, C ++ Creator.


In diesem Artikel zeigen wir Ihnen, wie Sie stabilen, sicheren und zuverlässigen Code schreiben und wie einfach es ist, ihn tatsächlich unbeabsichtigt zu beschädigen. Dafür haben wir versucht, das nützlichste und faszinierendste Material zu sammeln.







Bei SimbirSoft arbeiten wir eng mit dem Secure Code Warrior- Projekt zusammen, um andere Entwickler für die Erstellung sicherer Lösungen zu schulen. Speziell für Habr haben wir einen Artikel unseres Autors für das CodeProject.com-Portal übersetzt.



Also zum Code!



Hier ist ein kleiner Teil des abstrakten C ++ - Codes. Dieser Code wurde speziell geschrieben, um alle Arten von Problemen und Schwachstellen aufzuzeigen, die möglicherweise bei sehr realen Projekten auftreten können. Wie Sie sehen können, ist dies ein Windows-DLL- Code (dies ist ein wichtiger Punkt). Angenommen, jemand wird diesen Code in einer (natürlich sicheren) Lösung verwenden.



Schauen Sie sich den Code genauer an. Was könnte Ihrer Meinung nach daran schief gehen?



Der Code
class Finalizer
{
    struct Data
    {
        int i = 0;
        char* c = nullptr;
        
        union U
        {
            long double d;
            
            int i[sizeof(d) / sizeof(int)];
            
            char c [sizeof(i)];
        } u = {};
        
        time_t time;
    };
    
    struct DataNew;
    DataNew* data2 = nullptr;
    
    typedef DataNew* (*SpawnDataNewFunc)();
    SpawnDataNewFunc spawnDataNewFunc = nullptr;
    
    typedef Data* (*Func)();
    Func func = nullptr;
    
    Finalizer()
    {
        func = GetProcAddress(OTHER_LIB, "func")
        
        auto data = func();
        
        auto str = data->c;
        
        memset(str, 0, sizeof(str));
        
        data->u.d = 123456.789;
        
        const int i0 = data->u.i[sizeof(long double) - 1U];
        
        spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")
        data2 = spawnDataNewFunc();
    }
    
    ~Finalizer()
    {
        auto data = func();
        
        delete[] data2;
    }
};

Finalizer FINALIZER;

HMODULE OTHER_LIB;
std::vector<int>* INTEGERS;

DWORD WINAPI Init(LPVOID lpParam)
{
    OleInitialize(nullptr);
    
    ExitThread(0U);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    static std::vector<std::thread::id> THREADS;
    
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            CoInitializeEx(nullptr, COINIT_MULTITHREADED);
            
            srand(time(nullptr));
            
            OTHER_LIB = LoadLibrary("B.dll");
            
            if (OTHER_LIB = nullptr)
                return FALSE;
            
            CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
        break;
        
        case DLL_PROCESS_DETACH:
            CoUninitialize();
            
            OleUninitialize();
            {
                free(INTEGERS);
                
                const BOOL result = FreeLibrary(OTHER_LIB);
                
                if (!result)
                    throw new std::runtime_error("Required module was not loaded");
                
                return result;
            }
        break;
        
        case DLL_THREAD_ATTACH:
            THREADS.push_back(std::this_thread::get_id());
        break;
        
        case DLL_THREAD_DETACH:
            THREADS.pop_back();
        break;
    }
    return TRUE;
}

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
{
    for (int i : integers)
        i *= c;
    
    INTEGERS = new std::vector<int>(integers);
}

int Random()
{
    return rand() + rand();
}

__declspec(dllexport) long long int __cdecl _GetInt(int a)
{
    return 100 / a <= 0 ? a : a + 1 + Random();
}




Vielleicht fanden Sie diesen Code einfach, offensichtlich und sicher genug? Oder haben Sie vielleicht ein paar Probleme darin gefunden? Oder vielleicht sogar ein Dutzend oder zwei?



Nun, in diesem Snippet gibt es tatsächlich über 43 potenzielle Bedrohungen von unterschiedlicher Bedeutung!







Worauf Sie achten sollten



1) sizeof (d) (wobei d ein langes Doppel ist) ist nicht notwendigerweise ein Vielfaches von sizeof (int)



int i[sizeof(d) / sizeof(int)];


Diese Situation wird hier nicht getestet oder behandelt. Beispielsweise kann ein langes Double auf einigen Plattformen 10 Byte betragen (was nicht für den MS VS-Compiler gilt , sondern für RAD Studio , das früher als C ++ Builder bekannt war ).



int kann je nach Plattform auch unterschiedlich groß sein (der obige Code ist für Windows , daher ist das Problem in Bezug auf diese spezielle Situation etwas erfunden, aber für portablen Code ist dieses Problem sehr relevant).



All dies kann zu einem Problem werden, wenn wir das sogenannte Schreibwortspiel verwenden möchten . Übrigens verursacht es undefiniertes Verhaltengemäß dem C ++ - Sprachstandard. Es ist jedoch üblich, das Wortspiel zu verwenden , da moderne Compiler normalerweise das richtige, erwartete Verhalten für einen bestimmten Fall definieren (wie dies beispielsweise bei GCC der Fall ist ).







Quelle: Medium.com



Im Gegensatz zu C ++ ist das Wortspiel im modernen C übrigens durchaus akzeptabel (Sie wissen, dass C ++ und C verschiedene Sprachen sind , und Sie sollten nicht erwarten, C zu kennen, wenn Sie C ++ kennen, und umgekehrt, richtig?)



Lösung: Verwenden Sie static_assertalle diese Annahmen zur Kompilierungszeit zu kontrollieren. Es wird Sie warnen, wenn bei den Schriftgrößen etwas schief geht:



static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);


2) time_t ist ein Makro. In Visual Studio kann es auf einen 32-Bit-Integer-Typ (alt) oder einen 64-Bit-Integer-Typ (neu) verweisen



time_t time;


Der Zugriff auf eine Variable dieses Typs aus verschiedenen ausführbaren Modulen (z. B. der ausführbaren Datei und der geladenen DLL) kann zu Lese- / Schreibvorgängen außerhalb der Objektgrenzen führen, wenn die beiden Binärdateien mit einer unterschiedlichen physischen Darstellung dieses Typs kompiliert werden. Dies führt wiederum zu Speicherbeschädigungen oder Mülllesungen.







Lösung: Stellen Sie sicher, dass für den Datenaustausch zwischen allen Modulen dieselben Typen einer genau definierten Größe verwendet werden:



int64_t time;


3) B.dll (dessen Handle von der Variablen OTHER_LIB gespeichert wird ) wurde zum Zeitpunkt des Zugriffs auf die obige Variable noch nicht geladen, sodass wir die Adressen der Funktionen dieser Bibliothek nicht abrufen können.



4) Das Problem mit der Initialisierungsreihenfolge statischer Objekte ( SIOF ): ( OTHER_LIB- Objekt im Code verwendet, bevor er initialisiert wurde)



func = GetProcAddress(OTHER_LIB, "func");


FINALIZER ist ein statisches Objekt, das vor dem Aufruf der DllMain- Funktion erstellt wird . In seinem Konstruktor versuchen wir, eine Bibliothek zu verwenden, die noch nicht geladen wurde. Das Problem wird durch die Tatsache verschärft , dass die statische OTHER_LIB , die vom statischen FINALIZER verwendet wird, in der nachgeschalteten Übersetzungseinheit platziert wird. Dies bedeutet, dass es später auch initialisiert (auf Null gesetzt) ​​wird. Das heißt, in dem Moment, in dem darauf zugegriffen wird, enthält es pseudozufälligen Müll. WinAPIIm Allgemeinen sollte es normal darauf reagieren, da es mit hoher Wahrscheinlichkeit einfach überhaupt kein geladenes Modul mit einem solchen Deskriptor gibt. Und selbst wenn ein absolut unglaublicher Zufall passiert und es immer noch passiert, ist es unwahrscheinlich, dass er eine Funktion namens "Func" enthält .



Lösung: Allgemeiner Rat ist, die Verwendung globaler Objekte, insbesondere komplexer Objekte, zu vermeiden, insbesondere wenn diese voneinander abhängig sind, insbesondere in DLLs . Wenn Sie sie jedoch aus irgendeinem Grund immer noch benötigen, gehen Sie äußerst vorsichtig mit der Reihenfolge um, in der sie initialisiert werden. Um diese Reihenfolge zu steuern , fügen Sie alle Instanzen (Definitionen) globaler Objekte in eine Übersetzungseinheit einin der richtigen Reihenfolge, um sicherzustellen, dass sie korrekt initialisiert sind.



5) Das zuvor zurückgegebene Ergebnis wird vor der Verwendung nicht überprüft



auto data = func();


func ist ein Funktionszeiger . Und es sollte auf eine Funktion von B.dll verweisen . Da wir jedoch im vorherigen Schritt alles völlig versagt haben, ist dies nullptr . Wenn wir versuchen, es zu dereferenzieren, erhalten wir anstelle des erwarteten Funktionsaufrufs eine Zugriffsverletzung oder einen allgemeinen Schutzfehler oder ähnliches.



Lösung: Wenn Sie mit externem Code arbeiten (in unserem Fall mit WinAPI ), überprüfen Sie immer das Rückgabeergebnis der aufgerufenen Funktionen. Für zuverlässige und fehlertolerante Systeme gilt diese Regel auch für Funktionen, für die ein strikter Vertrag besteht [darüber, was und wann sie zurückgegeben werden sollen].



6) Lesen / Schreiben von Müll beim Datenaustausch zwischen Modulen, die mit unterschiedlichen Ausrichtungs- / Auffülleinstellungen kompiliert wurden



auto str = data->c;


Wenn Datenstruktur (die für den Austausch von Informationen zwischen den Kommunikationsmodulen verwendet wird) hat die gleiche Module in unterschiedlicher physikalischen Präsentation wird die zuvor erwähnt in allen führt Zugriffsverletzung , einen Fehlerspeicherschutz , Fehlersegmentierung , Heapbeschädigung usw. Oder wir lesen einfach den Müll. Das genaue Ergebnis hängt vom tatsächlichen Nutzungsszenario für diesen Speicher ab. All dies kann passieren, weil es keine expliziten Ausrichtungs- / Auffüllungseinstellungen für die Struktur selbst gibt . Wenn diese globalen Einstellungen zum Zeitpunkt der Kompilierung für interagierende Module unterschiedlich waren, treten Probleme auf.







Entscheidung:Stellen Sie sicher, dass alle gemeinsam genutzten Datenstrukturen eine starke, explizit definierte und offensichtliche physische Darstellung haben (unter Verwendung von Typen mit fester Größe, explizit spezifizierter Ausrichtung usw.) und / oder interoperable Binärdateien mit denselben globalen Ausrichtungseinstellungen kompiliert wurden / Füllung.



siehe auch
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7) Verwenden der Größe eines Zeigers auf ein Array anstelle der Größe des Arrays selbst



memset(str, 0, sizeof(str));


Dies ist normalerweise das Ergebnis eines häufigen Druckfehlers. Dieses Problem kann jedoch möglicherweise auch auftreten, wenn Sie sich mit statischem Polymorphismus befassen oder wenn das Schlüsselwort auto gedankenlos verwendet wird ( insbesondere, wenn es eindeutig überbeansprucht wird ). Man möchte jedoch hoffen, dass moderne Compiler intelligent genug sind, um solche Probleme beim Kompilieren mithilfe der Funktionen eines internen statischen Analysators zu erkennen .



Entscheidung:



  • Verwechseln Sie niemals sizeof ( <vollständiger Objekttyp> ) und sizeof ( <Objektzeigertyp> ).
  • Ignorieren Sie keine Compiler-Warnungen .

  • Sie können auch ein bisschen von C ++ vorformulierten Magie durch die Kombination von typeid, constexpr, und verwenden Sie static_assert , um sicherzustellen , dass die Typen bei der Kompilierung korrekt sind ( Art Züge können hier nützlich sein , insbesondere std :: is_pointer ).


8) undefiniertes Verhalten beim Versuch, ein anderes Vereinigungsfeld als das zuvor zum Festlegen des Werts verwendete



zu lesen 9) Ein Versuch, außerhalb der Grenzen zu lesen, ist möglich, wenn die Länge eines langen Doppels zwischen Binärmodulen unterschiedlich ist



const int i0 = data->u.i[sizeof(long double) - 1U];


Dies wurde bereits früher erwähnt, daher haben wir hier gerade einen weiteren Punkt des Vorhandenseins des zuvor erwähnten Problems erhalten.



Lösung: Verweisen Sie nicht auf ein anderes Feld als das zuvor festgelegte, es sei denn, Sie sind sicher, dass Ihr Compiler es korrekt verarbeitet. Stellen Sie sicher, dass die Größen der gemeinsam genutzten Objekttypen für alle interagierenden Module gleich sind.



siehe auch
Type-punning and strict-aliasing

What is the Strict Aliasing Rule and Why do we care?



10) Auch wenn B.dll korrekt geladen und die Funktion "func" korrekt exportiert und importiert wurde, wird B.dll zu diesem Zeitpunkt immer noch aus dem Speicher entladen (da die FreeLibrary-Systemfunktion zuvor im Abschnitt DLL_PROCESS_DETACH der DllMain-Rückruffunktion aufgerufen wurde )



auto data = func();


Das Aufrufen einer virtuellen Funktion für ein zuvor zerstörtes Objekt vom polymorphen Typ sowie das Aufrufen einer Funktion in einer bereits entladenen dynamischen Bibliothek führt wahrscheinlich zu einem reinen virtuellen Aufruffehler .



Lösung: Implementieren Sie das richtige Finalisierungsverfahren in der Anwendung, um sicherzustellen, dass alle dynamischen Bibliotheken in der richtigen Reihenfolge abgeschlossen / entladen werden. Vermeiden Sie die Verwendung statischer Objekte mit komplexer Logik in DL L. Vermeiden Sie Operationen innerhalb der Bibliothek, nachdem Sie DllMain / DLL_PROCESS_DETACH aufgerufen haben (wenn die Bibliothek in ihre letzte Phase ihres Lebenszyklus eintritt - die Phase der Zerstörung ihrer statischen Objekte).



Sie müssen verstehen, wie der Lebenszyklus einer DLL aussieht:



) LoadLibrary



  • ( , )
  • DllMain -> DLL_PROCESS_ATTACH ( , )
  • [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
  • , , (, ),
  • ( / , , )
  • , ()
  • ( / , , )
  • - : ,


) FreeLibrary



  • DllMain -> DLL_PROCESS_DETACH ( , )
  • ( , )






11) Löschen eines undurchsichtigen Zeigers (der Compiler muss den vollständigen Typ kennen , um den Destruktor aufzurufen, daher kann das Löschen eines Objekts mit einem undurchsichtigen Zeiger zu Speicherverlusten und anderen Problemen führen)



12) wenn der DataNew-Destruktor virtuell ist, selbst wenn die Klasse korrekt exportiert und importiert wird und vollständig ist Informationen darüber, jedenfalls das Aufrufen des Destruktors in dieser Phase, sind ein Problem - dies führt wahrscheinlich zu einem rein virtuellen Funktionsaufruf (da der DataNew-Typ aus der bereits entladenen B.dll-Datei importiert wird ). Dieses Problem ist auch dann möglich, wenn der Destruktor nicht virtuell ist.



13) wenn die DataNew-Klasse vom abstrakten polymorphen Typ istund seine Basisklasse hat einen reinen virtuellen Destruktor ohne Körper, in jedem Fall wird ein reiner virtueller Funktionsaufruf stattfinden .



14) undefiniertes Verhalten, wenn Speicher mit new zugewiesen und mit delete [] gelöscht wird



delete[] data2;


Im Allgemeinen sollten Sie beim Freigeben von Objekten, die von externen Modulen empfangen wurden , immer vorsichtig sein .



Es wird auch empfohlen, Zeiger auf zerstörte Objekte auf Null zu setzen.



Entscheidung:



  • Beim Löschen eines Objekts muss dessen vollständiger Typ bekannt sein
  • Alle Destruktoren müssen einen Körper haben
  • Die Bibliothek, aus der der Code exportiert wird, sollte nicht zu früh entladen werden
  • Verwenden Sie immer die verschiedenen Formulare neu und löschen Sie sie korrekt. Verwechseln Sie sie nicht
  • Der Zeiger auf das entfernte Objekt muss auf Null gesetzt werden.






Beachten Sie auch Folgendes:

- Das Aufrufen von delete für einen Zeiger auf void führt zu einem undefinierten Verhalten.

Rein virtuelle Funktionen sollten nicht vom Konstruktor aufgerufen werden .

- Das Aufrufen einer virtuellen Funktion im Konstruktor ist nicht virtuell.

- Vermeiden Sie die manuelle Speicherverwaltung. Verwenden Sie Container , verschieben Sie die Semantik und intelligente Zeiger



siehe auch
Heap corruption: What could the cause be?



15) ExitThread ist die bevorzugte Methode zum Beenden eines Threads in C. In C ++ wird durch Aufrufen dieser Funktion der Thread beendet, bevor die Destruktoren lokaler Objekte aufgerufen werden (und jede andere automatische Bereinigung). Daher sollte das Beenden eines Threads in C ++ einfach durch Zurückkehren von der Thread-Funktion erfolgen



ExitThread(0U);


Lösung: Verwenden Sie diese Funktion niemals manuell in C ++ - Code.



16) Im Hauptteil von DllMain kann das Aufrufen von Standardfunktionen, für die andere System- DLLs als Kernel32.dll erforderlich sind, zu verschiedenen schwer zu diagnostizierenden Problemen führen



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


Lösung in DllMain:



  • Vermeiden Sie komplizierte (De-) Initialisierungen
  • Vermeiden Sie es, Funktionen aus anderen Bibliotheken aufzurufen (oder seien Sie zumindest äußerst vorsichtig damit).






17) falsche Initialisierung des Pseudozufallszahlengenerators in einer Multithread-Umgebung



18) da die von der Zeitfunktion zurückgegebene Zeit eine Auflösung von 1 Sek. Hat, erhält jeder Thread im Programm, der diese Funktion während dieses Zeitraums aufruft, am Ausgang den gleichen Wert. Die Verwendung dieser Nummer zum Initialisieren von PRNG kann zu Kollisionen führen (z. B. Generieren derselben Pseudozufallsnamen für temporäre Dateien, derselben Portnummern usw.). Eine mögliche Lösung besteht darin, das resultierende Ergebnis mit einem pseudozufälligen Wert zu mischen ( xor ) , wie z. B. der Adresse eines Stapels oder Objekts im Heap, einer genaueren Zeit usw.



srand(time(nullptr));


Lösung: MS VS erfordert eine PRNG-Initialisierung für jeden Thread . Darüber hinaus bietet die Verwendung der Unix- Zeit als Initialisierer eine unzureichende Entropie . Eine erweiterte Initialisierungswertgenerierung wird bevorzugt .



siehe auch
Is there an alternative to using time to seed a random number generation?

C++ seeding surprises

Getting random numbers in a thread-safe way [C#]


19) kann einen Deadlock oder Absturz verursachen (oder Abhängigkeitsschleifen in der DLL- Ladereihenfolge erstellen )



OTHER_LIB = LoadLibrary("B.dll");


Lösung: Verwenden Sie LoadLibrary nicht am DllMain-Einstiegspunkt . Jede komplexe ( De- ) Initialisierung muss in bestimmten vom DLL-Entwickler exportierten Funktionen wie "Init" und "Deint" durchgeführt werden . Die Bibliothek stellt dem Benutzer diese Funktionen zur Verfügung, und der Benutzer muss sie zum richtigen Zeitpunkt korrekt aufrufen. Beide Parteien müssen diesen Vertrag strikt einhalten.







20) Tippfehler (Bedingung ist immer falsch), falsche Programmlogik und möglicher Ressourcenverlust (da OTHER_LIB bei erfolgreichem Download niemals entladen wird)



if (OTHER_LIB = nullptr)
    return FALSE;


Der Zuweisungsoperator gibt durch Kopieren einen Link des linken Typs zurück, d.h. if prüft auf den Wert OTHER_LIB (der nullptr ist) und nullptr wird als false interpretiert.



Lösung: Verwenden Sie immer die umgekehrte Form, um Tippfehler wie diese zu vermeiden:



if/while (<constant> == <variable/expression>)


21) Es wird empfohlen, die Systemfunktion _beginthread zu verwenden, um einen neuen Thread in der Anwendung zu erstellen (insbesondere wenn die Anwendung mit einer statischen Version der C-Laufzeitbibliothek verknüpft war). Andernfalls können beim Aufrufen von ExitThread, DisableThreadLibraryCalls



22 Speicherverluste auftreten. 22) Alle externen Aufrufe von DllMain werden serialisiert, also im Hauptteil Diese Funktion sollte nicht versuchen, Threads / Prozesse zu erstellen oder mit ihnen zu interagieren, da sonst Deadlocks auftreten können



CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);


23) Das Aufrufen von COM-Funktionen während der DLL-Beendigung kann zu einem falschen Speicherzugriff führen, da die entsprechende Komponente möglicherweise bereits entladen ist



CoUninitialize();


24) Es gibt keine Möglichkeit, die Lade- und Entladereihenfolge von in Bearbeitung befindlichen COM / OLE-Diensten zu steuern. Rufen Sie daher OleInitialize oder OleUninitialize nicht über die DllMain-Funktion auf



OleUninitialize();


siehe auch
COM Clients and Servers

In-process, Out-of-process, and Remote Servers



25) Frei aufrufen eines mit new zugewiesenen Speicherblocks



26) Wenn der Anwendungsprozess gerade seine Arbeit beendet (wie durch einen Wert ungleich Null des Parameters lpvReserved angezeigt), wurden alle Threads im Prozess mit Ausnahme des aktuellen entweder bereits beendet oder wurden zwangsweise gestoppt, wenn Aufrufen der ExitProcess-Funktion, die einige der Prozessressourcen, z. B. den Heap, in einem inkonsistenten Zustand belassen kann. Daher ist es nicht DLL- sicher, Ressourcen zu bereinigen . Stattdessen sollte die DLL es dem Betriebssystem ermöglichen, Speicher zurückzugewinnen.



free(INTEGERS);


Lösung: Stellen Sie sicher, dass der alte C-Stil der manuellen Speicherzuweisung nicht mit dem „neuen“ C ++ - Stil gemischt wird. Seien Sie äußerst vorsichtig, wenn Sie Ressourcen in der DllMain- Funktion verwalten .



27) kann dazu führen, dass die DLL auch dann verwendet wird, wenn das System seinen Exit-Code ausgeführt hat



const BOOL result = FreeLibrary(OTHER_LIB);


Lösung: Do nicht nennen Free am DllMain Einstiegspunkt.



28) Der aktuelle (möglicherweise Haupt-) Thread stürzt ab



throw new std::runtime_error("    ");


Lösung: Vermeiden Sie es, Ausnahmen in der DllMain-Funktion auszulösen. Wenn die DLL aus irgendeinem Grund nicht korrekt geladen werden kann, sollte die Funktion einfach FALSE zurückgeben. Sie sollten auch keine Ausnahmen aus dem Abschnitt DLL_PROCESS_DETACH auslösen.



Seien Sie immer vorsichtig, wenn Sie Ausnahmen außerhalb der DLL auslösen. Komplexe Objekte (z. B. Klassen der Standardbibliothek ) können in verschiedenen ausführbaren Modulen unterschiedliche physische Darstellungen (und sogar Arbeitslogiken) aufweisen, wenn sie mit unterschiedlichen (inkompatiblen) Versionen der Laufzeitbibliotheken kompiliert werden .







Versuchen Sie, nur einfache Datentypen zwischen Modulen auszutauschen(mit fester Größe und gut definierter binärer Darstellung).



Denken Sie daran, dass durch das Beenden des Hauptthreads automatisch alle anderen Threads beendet werden (die nicht korrekt beendet werden und daher den Speicher beschädigen können, wodurch Synchronisationsprimitive und andere Objekte in einem unvorhersehbaren und falschen Zustand verbleiben. Außerdem existieren diese Threads bereits zu dem Zeitpunkt nicht mehr, zu dem sie existieren statische Objekte starten ihre eigene Dekonstruktion. Warten Sie also nicht, bis Threads in den Destruktoren statischer Objekte fertig sind.



siehe auch
Top 20 C++ multithreading mistakes and how to avoid them



29) Sie können eine Ausnahme auslösen (z. B. std :: bad_alloc), die hier nicht abgefangen wird



THREADS.push_back(std::this_thread::get_id());


Da der Abschnitt DLL_THREAD_ATTACH von einem unbekannten externen Code aufgerufen wird, sollten Sie hier kein korrektes Verhalten erwarten.



Lösung: Verwenden Sie try / catch, um Anweisungen einzuschließen, die Ausnahmen auslösen können, die höchstwahrscheinlich nicht korrekt behandelt werden können (insbesondere, wenn sie die DLL verlassen ).



siehe auch
How can I handle a destructor that fails?



30) UB, wenn Streams vor dem Laden dieser DLL angezeigt wurden



THREADS.pop_back();


Themen , die bereits zu dem Zeitpunkt existieren die DLL (einschließlich der , dass direkt lädt die geladen wird , DLL ) rufen Sie nicht die geladene DLL Eintrittspunktfunktion (weshalb sie mit dem THREADS Vektor während der DLL_THREAD_ATTACH Ereignis nicht registriert sind), während sie es noch nennen mit dem DLL_THREAD_DETACH Ereignis nach der Vollendung.

Dies bedeutet, dass die Anzahl der Aufrufe der Abschnitte DLL_THREAD_ATTACH und DLL_THREAD_DETACH der DllMain-Funktion unterschiedlich ist.



31) Es ist besser, Ganzzahltypen mit fester Größe zu verwenden.



32) Das Übergeben eines komplexen Objekts zwischen Modulen kann abstürzen, wenn es mit unterschiedlichen Link- und Kompilierungseinstellungen und Flags (unterschiedliche Versionen der Laufzeitbibliothek usw.) kompiliert wird.



33) Der Zugriff auf Objekt c über seine virtuelle Adresse (die von Modulen gemeinsam genutzt wird) kann Probleme verursachen, wenn Zeiger in diesen Modulen unterschiedlich behandelt werden (z. B. wenn Module unterschiedlichen LARGEADDRESSAWARE- Parametern zugeordnet sind ).



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


siehe auch
Is it possible to use more than 2 Gbytes of memory in a 32-bit program launched in the 64-bit Windows?

Application with LARGEADDRESSAWARE flag set getting less virtual memory

Drawbacks of using /LARGEADDRESSAWARE for 32 bit Windows executables?

how to check if exe is set as LARGEADDRESSAWARE [C#]

/LARGEADDRESSAWARE [Ru]

ASLR (Address Space Layout Randomization) [Ru]



Und auch...
Virtual memory

Physical Address Extension

Tagged pointer

std::ptrdiff_t

What is uintptr_t data type

Pointer arithmetic

Pointer aliasing

What is the strict aliasing rule?

reinterpret_cast conversion

restrict type qualifier



Die obige Liste ist kaum vollständig, daher können Sie den Kommentaren wahrscheinlich etwas Wichtiges hinzufügen.



Das Arbeiten mit Zeigern ist tatsächlich viel komplexer, als die Leute normalerweise an sie denken. Ohne Zweifel können sich erfahrene Entwickler an andere vorhandene Nuancen und Feinheiten erinnern (z. B. etwas über den Unterschied zwischen Zeigern auf ein Objekt und Zeigern auf eine Funktion , aufgrund dessen möglicherweise nicht alle Bits des Zeigers verwendet werden können usw. .).







34) Eine Ausnahme kann innerhalb einer Funktion ausgelöst werden :



INTEGERS = new std::vector<int>(integers);


Die throw () - Spezifikation dieser Funktion ist leer:



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


std :: unerwartet wird von der C ++ - Laufzeit aufgerufen, wenn eine Ausnahmespezifikation verletzt wird: Eine Ausnahme wird von einer Funktion ausgelöst, deren Ausnahmespezifikation Ausnahmen dieses Typs nicht zulässt.



Lösung: Verwenden Sie try / catch (insbesondere beim Zuweisen von Ressourcen, insbesondere in DLLs ) oder nothrow des neuen Operators. Gehen Sie auf keinen Fall von der naiven Annahme aus, dass alle Versuche, verschiedene Arten von Ressourcen zuzuweisen, immer erfolgreich enden .



siehe auch
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









Problem 1: Die Bildung eines solchen "zufälligeren" Wertes ist falsch. Nach dem zentralen Grenzwertsatz tendiert die Summe der unabhängigen Zufallsvariablen zu einer Normalverteilung und nicht zu einer Gleichverteilung (selbst wenn die ursprünglichen Werte selbst gleichmäßig verteilt sind).



Problem 2: Möglicher Überlauf des Integer-Typs ( undefiniertes Verhalten für vorzeichenbehaftete Integer-Typen )



return rand() + rand();


Achten Sie bei der Arbeit mit Pseudozufallszahlengeneratoren, Verschlüsselung und dergleichen immer auf die Verwendung hausgemachter "Lösungen". Wenn Sie nicht über eine spezielle Ausbildung und Erfahrung in diesen hochspezifischen Bereichen verfügen, stehen die Chancen sehr hoch, dass Sie sich einfach überlisten und die Situation verschlimmern.



35) Der Name der exportierten Funktion wird dekoriert (geändert), um diese Verwendung von externem "C" zu verhindern.



36) Namen, die mit '_' beginnen, sind für C ++ implizit verboten, da dieser Benennungsstil für die STL reserviert ist



__declspec(dllexport) long long int __cdecl _GetInt(int a)


Mehrere Probleme (und ihre möglichen Lösungen):



37) rand ist nicht threadsicher, verwenden Sie stattdessen rand_r / rand_s



38) rand ist veraltet, verwenden Sie besser modern
C++11 <random>


39) Es ist keine Tatsache, dass die Rand-Funktion speziell für den aktuellen Thread initialisiert wurde (MS VS erfordert die Initialisierung dieser Funktion für jeden Thread, in dem sie aufgerufen wird).



40) Es gibt spezielle Generatoren für Pseudozufallszahlen , und es ist besser, sie in hackresistenten Lösungen zu verwenden (sie sind geeignet) tragbare Lösungen wie Libsodium / randombytes_buf , OpenSSL / RAND_bytes usw.)



41) potenzielle Division durch Null: kann zum Absturz des aktuellen Threads führen



42) Operatoren mit unterschiedlicher Priorität werden in derselben Zeile verwendet , was zu Chaos in der Reihenfolge der Berechnung führt - verwenden Sie Klammern und / oder Sequenzpunkteum die offensichtliche Reihenfolge der Berechnung anzugeben



43) möglicher ganzzahliger Überlauf



return 100 / a <= 0 ? a : a + 1 + Random();




siehe auch
Do not use std::rand() for generating pseudorandom numbers





Und auch...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





Und das ist noch nicht alles!



Stellen Sie sich vor, Sie haben wichtige Inhalte im Speicher (z. B. das Kennwort eines Benutzers). Natürlich möchten Sie es nicht länger als nötig im Speicher behalten, um die Wahrscheinlichkeit zu erhöhen, dass jemand es von dort aus lesen kann .



Ein naiver Ansatz zur Lösung dieses Problems würde ungefähr so ​​aussehen:



bool login(char* const userNameBuf, const size_t userNameBufSize,
           char* const pwdBuf, const size_t pwdBufSize) throw()
{
    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)
        return false;
    
    // Here some actual implementation, which does not checks params
    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,
    //   while both of them obviously contains private information 
    const bool result = doLoginInternall(userNameBuf, pwdBuf);
    
    // We want to minimize the time this private information is stored within the memory
    memset(userNameBuf, 0, userNameBufSize);
    memset(pwdBuf, 0, pwdBufSize);
}


Und es wird sicherlich nicht so funktionieren, wie wir es gerne hätten. Was ist dann zu tun? :(



Falsche "Lösung" # 1: Wenn Memset nicht funktioniert, machen wir es manuell!



void clearMemory(char* const memBuf, const size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
}


Warum passt das auch nicht zu uns? Tatsache ist , dass es keine Einschränkungen in diesem Code sind, der würde nicht ein moderner Compiler ermöglichen , es zu optimieren (die übrigens die Memset Funktion , wenn es immer noch verwendet wird, wird höchstwahrscheinlich werden integriert in ).



siehe auch
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



Falsche "Lösung" # 2: Versuchen Sie, die vorherige "Lösung" zu "verbessern", indem Sie mit dem flüchtigen Schlüsselwort herumspielen



void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (volatile size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
    
    *(volatile char*)memBuf = *(volatile char*)memBuf;
    // There is also possibility for someone to remove this "useless" code in the future
}


Ob das funktioniert? Vielleicht. Dieser Ansatz wird beispielsweise in RtlSecureZeroMemory verwendet (was Sie anhand der tatsächlichen Implementierung dieser Funktion in den Windows SDK- Quellen selbst sehen können ).



Diese Technik funktioniert jedoch nicht bei allen Compilern wie erwartet .



siehe auch
volatile member functions



Falsche "Lösung" # 3: Verwenden Sie eine unangemessene OS-API- Funktion (z. B. RtlZeroMemory ) oder STL (z. B. std :: fill, std :: for_each).



RtlZeroMemory(memBuf, memBufSize);


Weitere Beispiele für Versuche, dieses Problem zu lösen, finden Sie hier .



Und wie ist es richtig?



  • Verwenden Sie die richtige Betriebssystem-API- Funktion , z. B. RtlSecureZeroMemory für Windows
  • benutze die Funktion C11 memset_s :


Darüber hinaus können wir verhindern, dass der Compiler den Code optimiert, indem wir den Wert der Variablen (in eine Datei, eine Konsole oder einen anderen Stream) drucken. Dies ist jedoch offensichtlich nicht sehr nützlich.



siehe auch
Safe clearing of private Data



Zusammenfassen



Dies ist natürlich keine vollständige Liste aller möglichen Probleme, Nuancen und Feinheiten, die beim Schreiben von Anwendungen in C / C ++ auftreten können .



Es gibt auch tolle Dinge wie:





Und vieles mehr.







Gibt es noch etwas hinzuzufügen? Teilen Sie Ihre interessanten Erfahrungen in den Kommentaren!



PS Möchten Sie mehr wissen?
Software security errors

Common weakness enumeration

Common types of software vulnerabilities



Vulnerability database

Vulnerability notes database

National vulnerability database



Coding standards

Application security verification standard

Guidelines for the use of the C++ language in critical systems



Secure programming HOWTO

32 OpenMP Traps For C++ Developers

A Collection of Examples of 64-bit Errors in Real Programs




All Articles