Wann und warum initialisiert ein Compiler den Speicher auf malloc / free / new / delete auf 0xCD, 0xDD usw.?

128

Ich weiß, dass der Compiler manchmal Speicher mit bestimmten Mustern wie 0xCDund initialisiert 0xDD. Ich möchte wissen, wann und warum dies geschieht.

Wann

Ist dies spezifisch für den verwendeten Compiler?

Hat malloc/newund free/deleteArbeit in der gleichen Art und Weise in Bezug auf das?

Ist es plattformspezifisch?

Wird es auf anderen Betriebssystemen wie Linuxoder auftreten VxWorks?

Warum

Nach meinem Verständnis tritt dies nur in der Win32Debug-Konfiguration auf und wird verwendet, um Speicherüberschreitungen zu erkennen und dem Compiler dabei zu helfen, Ausnahmen abzufangen.

Können Sie praktische Beispiele geben, wie diese Initialisierung nützlich ist?

Ich erinnere mich, dass ich etwas gelesen habe (möglicherweise in Code Complete 2), das besagt, dass es gut ist, den Speicher bei der Zuweisung auf ein bekanntes Muster zu initialisieren, und dass bestimmte Muster Interrupts auslösen, Win32die dazu führen, dass Ausnahmen im Debugger angezeigt werden.

Wie tragbar ist das?

LeopardSkinPillBoxHat
quelle

Antworten:

191

Eine kurze Zusammenfassung dessen, was Microsoft-Compiler für verschiedene Teile des nicht besessenen / nicht initialisierten Speichers verwenden, wenn sie für den Debug-Modus kompiliert werden (die Unterstützung kann je nach Compiler-Version variieren):

Value     Name           Description 
------   --------        -------------------------
0xCD     Clean Memory    Allocated memory via malloc or new but never 
                         written by the application. 

0xDD     Dead Memory     Memory that has been released with delete or free. 
                         It is used to detect writing through dangling pointers. 

0xED or  Aligned Fence   'No man's land' for aligned allocations. Using a 
0xBD                     different value here than 0xFD allows the runtime
                         to detect not only writing outside the allocation,
                         but to also identify mixing alignment-specific
                         allocation/deallocation routines with the regular
                         ones.

0xFD     Fence Memory    Also known as "no mans land." This is used to wrap 
                         the allocated memory (surrounding it with a fence) 
                         and is used to detect indexing arrays out of 
                         bounds or other accesses (especially writes) past
                         the end (or start) of an allocated block.

0xFD or  Buffer slack    Used to fill slack space in some memory buffers 
0xFE                     (unused parts of `std::string` or the user buffer 
                         passed to `fread()`). 0xFD is used in VS 2005 (maybe 
                         some prior versions, too), 0xFE is used in VS 2008 
                         and later.

0xCC                     When the code is compiled with the /GZ option,
                         uninitialized variables are automatically assigned 
                         to this value (at byte level). 


// the following magic values are done by the OS, not the C runtime:

0xAB  (Allocated Block?) Memory allocated by LocalAlloc(). 

0xBAADF00D Bad Food      Memory allocated by LocalAlloc() with LMEM_FIXED,but 
                         not yet written to. 

0xFEEEFEEE               OS fill heap memory, which was marked for usage, 
                         but wasn't allocated by HeapAlloc() or LocalAlloc(). 
                         Or that memory just has been freed by HeapFree(). 

Haftungsausschluss: Die Tabelle stammt aus einigen Notizen, die ich herumliegen habe - sie sind möglicherweise nicht 100% korrekt (oder kohärent).

Viele dieser Werte sind in vc / crt / src / dbgheap.c definiert:

/*
 * The following values are non-zero, constant, odd, large, and atypical
 *      Non-zero values help find bugs assuming zero filled data.
 *      Constant values are good, so that memory filling is deterministic
 *          (to help make bugs reproducible).  Of course, it is bad if
 *          the constant filling of weird values masks a bug.
 *      Mathematically odd numbers are good for finding bugs assuming a cleared
 *          lower bit.
 *      Large numbers (byte values at least) are less typical and are good
 *          at finding bad addresses.
 *      Atypical values (i.e. not too often) are good since they typically
 *          cause early detection in code.
 *      For the case of no man's land and free blocks, if you store to any
 *          of these locations, the memory integrity checker will detect it.
 *
 *      _bAlignLandFill has been changed from 0xBD to 0xED, to ensure that
 *      4 bytes of that (0xEDEDEDED) would give an inaccessible address under 3gb.
 */

static unsigned char _bNoMansLandFill = 0xFD;   /* fill no-man's land with this */
static unsigned char _bAlignLandFill  = 0xED;   /* fill no-man's land for aligned routines */
static unsigned char _bDeadLandFill   = 0xDD;   /* fill free objects with this */
static unsigned char _bCleanLandFill  = 0xCD;   /* fill new objects with this */

Es gibt auch einige Male, in denen die Debug-Laufzeit Puffer (oder Teile von Puffern) mit einem bekannten Wert füllt, z. B. dem "Slack" -Platz in std::stringder Zuweisung oder dem Puffer, an den übergeben wird fread(). In diesen Fällen wird ein Wert mit dem Namen _SECURECRT_FILL_BUFFER_PATTERN(definiert in crtdefs.h) verwendet. Ich bin mir nicht sicher, wann es genau eingeführt wurde, aber es war in der Debug-Laufzeit von mindestens VS 2005 (VC ++ 8).

Anfangs war der Wert, der zum Füllen dieser Puffer verwendet wurde, 0xFDder gleiche Wert, der für Niemandsland verwendet wurde. In VS 2008 (VC ++ 9) wurde der Wert jedoch in geändert 0xFE. Ich nehme an, das liegt daran, dass es Situationen geben kann, in denen die Fülloperation über das Ende des Puffers hinaus ausgeführt wird, z. B. wenn der Aufrufer eine Puffergröße übergeben hat, die zu groß ist, um fread(). In diesem Fall 0xFDlöst der Wert möglicherweise keine Erkennung dieses Überlaufs aus, da bei einer zu großen Puffergröße von nur eins der Füllwert dem Landwert des Niemands entspricht, der zum Initialisieren dieses Kanarienvogels verwendet wird. Keine Veränderung im Niemandsland bedeutet, dass der Überlauf nicht bemerkt wird.

Daher wurde der Füllwert in VS 2008 geändert, sodass in einem solchen Fall der Niemandslandkanarienvogel geändert wird, wodurch das Problem zur Laufzeit erkannt wird.

Wie andere angemerkt haben, besteht eine der Schlüsseleigenschaften dieser Werte darin, dass eine Zeigervariable mit einem dieser Werte, wenn die Referenzierung aufgehoben wird, zu einer Zugriffsverletzung führt, da in einer 32-Bit-Standardkonfiguration Benutzermodusadressen verwendet werden wird nicht höher als 0x7fffffff gehen.

Michael Burr
quelle
1
Ich weiß nicht, ob es auf MSDN ist - ich habe es von hier und da zusammengesetzt oder vielleicht habe ich es von einer anderen Website bekommen.
Michael Burr
2
Oh ja - ein Teil davon stammt aus der CRT-Quelle in DbgHeap.c.
Michael Burr
Ein Teil davon befindet sich auf MSDN ( msdn.microsoft.com/en-us/library/bebs9zyz.aspx ), aber nicht alle. Gute Liste.
Sean E
3
@seane - FYI Ihr Link scheint tot zu sein. Der neue (Text wurde erweitert) ist hier verfügbar: msdn.microsoft.com/en-us/library/974tc9t1.aspx
Simon Mourier
Wie heißen diese Blöcke? Handelt es sich um eine Speicherbarriere, eine Membar, einen Speicherzaun oder eine Zaunanweisung ( en.wikipedia.org/wiki/Memory_barrier )?
kr85
36

Eine nette Eigenschaft des Füllwerts 0xCCCCCCCC ist, dass in der x86-Assembly der Opcode 0xCC der int3- Opcode ist, der der Software-Haltepunkt-Interrupt ist. Wenn Sie also jemals versuchen, Code in einem nicht initialisierten Speicher auszuführen, der mit diesem Füllwert gefüllt ist, erreichen Sie sofort einen Haltepunkt, und das Betriebssystem ermöglicht es Ihnen, einen Debugger anzuhängen (oder den Prozess abzubrechen).

Adam Rosenfield
quelle
6
Und 0xCD ist die intAnweisung. Wenn Sie also 0xCD 0xCD ausführen, wird eine generiert int CD, die ebenfalls abfängt .
Tad Marshall
2
In der heutigen Welt erlaubt Data Execution Prevention der CPU nicht einmal, eine Anweisung vom Heap abzurufen. Diese Antwort ist seit XP SP2 veraltet.
MSalters
2
@MSalters: Ja, es stimmt, dass neu zugewiesener Speicher standardmäßig nicht ausführbar ist, aber jemand könnte den Speicher problemlos verwenden VirtualProtect()oder mprotect()ausführbar machen.
Adam Rosenfield
Sie können keinen Code aus einem Datenblock ausführen. JE. Rate nochmal.
Dan
9

Visual Studio ist compiler- und betriebssystemspezifisch und setzt verschiedene Arten von Speicher auf unterschiedliche Werte, sodass Sie im Debugger leicht erkennen können, ob Sie in malloced Speicher, ein festes Array oder ein nicht initialisiertes Objekt übergegangen sind. Jemand wird die Details posten, während ich sie google ...

http://msdn.microsoft.com/en-us/library/974tc9t1.aspx

Martin Beckett
quelle
Ich vermute, dass damit überprüft wird, ob Sie vergessen haben, Ihre Zeichenfolgen auch ordnungsgemäß zu beenden (da diese 0xCDs oder 0xDDs gedruckt werden).
Strager
0xCC = nicht initialisierte lokale (Stapel-) Variable 0xCD = nicht initialisierte Klassenvariable (Heap?) 0xDD = gelöschte Variable
FryGuy
@FryGuy Es gibt einen praktischen Grund, der (einige) dieser Werte diktiert, wie ich hier erkläre .
Glenn Slayden
4

Es ist nicht das Betriebssystem - es ist der Compiler. Sie können das Verhalten auch ändern - siehe unten in diesem Beitrag.

Microsoft Visual Studio generiert (im Debug-Modus) eine Binärdatei, die den Stapelspeicher mit 0xCC vorfüllt. Außerdem wird zwischen jedem Stapelrahmen ein Leerzeichen eingefügt, um Pufferüberläufe zu erkennen. Ein sehr einfaches Beispiel dafür, wo dies nützlich ist, finden Sie hier (in der Praxis würde Visual Studio dieses Problem erkennen und eine Warnung ausgeben):

...
   bool error; // uninitialised value
   if(something)
   {
      error = true;
   }
   return error;

Wenn Visual Studio Variablen nicht auf einen bekannten Wert vorinitialisiert hat, ist dieser Fehler möglicherweise schwer zu finden. Mit vorinitialisierten Variablen (oder besser gesagt vorinitialisiertem Stapelspeicher) ist das Problem bei jedem Lauf reproduzierbar.

Es gibt jedoch ein kleines Problem. Der von Visual Studio verwendete Wert ist TRUE - alles außer 0 wäre. Es ist tatsächlich sehr wahrscheinlich, dass, wenn Sie Ihren Code im Release-Modus ausführen, unitialisierte Variablen einem Stapelspeicher zugewiesen werden, der zufällig 0 enthält. Dies bedeutet, dass Sie einen Fehler in unitialisierten Variablen haben können, der sich nur im Release-Modus manifestiert.

Das hat mich geärgert, also habe ich ein Skript geschrieben , um den Vorfüllwert durch direktes Bearbeiten der Binärdatei zu ändern, sodass ich nicht initialisierte Variablenprobleme finden kann, die nur angezeigt werden, wenn der Stapel eine Null enthält. Dieses Skript ändert nur die Stapelvorfüllung. Ich habe nie mit dem Heap-Pre-Fill experimentiert, obwohl es möglich sein sollte. Möglicherweise müssen Sie die Laufzeit-DLL bearbeiten, möglicherweise nicht.

Airsource Ltd.
quelle
1
Gibt VS keine Warnung aus, wenn ein Wert wie GCC verwendet wird, bevor er initialisiert wird?
Strager
3
Ja, aber nicht immer, da dies von der statischen Analyse abhängt. Folglich ist es ziemlich leicht, es mit Zeigerarithmetik zu verwechseln.
Airsource Ltd
3
"Es ist nicht das Betriebssystem - es ist der Compiler." Eigentlich ist es nicht der Compiler - es ist die Laufzeitbibliothek.
Adrian McCarthy
Beim Debuggen zeigt der Visual Studio-Debugger den Wert eines Bools an, wenn nicht 0 oder 1 mit so etwas wie true (204) . Es ist also relativ einfach, diese Art von Fehler zu erkennen, wenn Sie Code verfolgen.
Phil1970
4

Ist dies spezifisch für den verwendeten Compiler?

Tatsächlich ist es fast immer eine Funktion der Laufzeitbibliothek (wie die C-Laufzeitbibliothek). Die Laufzeit ist normalerweise stark mit dem Compiler korreliert, es gibt jedoch einige Kombinationen, die Sie austauschen können.

Ich glaube, unter Windows verwendet der Debug-Heap (HeapAlloc usw.) auch spezielle Füllmuster, die sich von denen unterscheiden, die aus dem Malloc stammen, und kostenlose Implementierungen in der Debug-C-Laufzeitbibliothek. Es kann also auch eine Betriebssystemfunktion sein, aber meistens ist es nur die Sprachlaufzeitbibliothek.

Funktionieren malloc / new und free / delete in dieser Hinsicht genauso?

Der Speicherverwaltungsteil von new und delete wird normalerweise mit malloc und free implementiert, sodass der mit new und delete zugewiesene Speicher normalerweise dieselben Funktionen aufweist.

Ist es plattformspezifisch?

Die Details sind laufzeitspezifisch. Die tatsächlich verwendeten Werte werden häufig so gewählt, dass sie nicht nur beim Betrachten eines Hex-Dumps ungewöhnlich und offensichtlich aussehen, sondern auch bestimmte Eigenschaften aufweisen, die die Funktionen des Prozessors nutzen können. Beispielsweise werden häufig ungerade Werte verwendet, da sie einen Ausrichtungsfehler verursachen können. Es werden große Werte verwendet (im Gegensatz zu 0), da sie überraschende Verzögerungen verursachen, wenn Sie eine Schleife zu einem nicht initialisierten Zähler durchführen. Unter x86 ist 0xCC eine int 3Anweisung. Wenn Sie also einen nicht initialisierten Speicher ausführen , wird er abgefangen .

Wird es unter anderen Betriebssystemen wie Linux oder VxWorks auftreten?

Dies hängt hauptsächlich von der verwendeten Laufzeitbibliothek ab.

Können Sie praktische Beispiele geben, wie nützlich diese Initialisierung ist?

Ich habe oben einige aufgelistet. Die Werte werden im Allgemeinen ausgewählt, um die Wahrscheinlichkeit zu erhöhen, dass etwas Ungewöhnliches passiert, wenn Sie etwas mit ungültigen Speicherbereichen tun: lange Verzögerungen, Traps, Ausrichtungsfehler usw. Heap-Manager verwenden manchmal auch spezielle Füllwerte für die Lücken zwischen Zuweisungen. Wenn sich diese Muster jemals ändern, weiß es, dass irgendwo ein schlechter Schreibvorgang (wie ein Pufferüberlauf) aufgetreten ist.

Ich erinnere mich, dass ich etwas gelesen habe (möglicherweise in Code Complete 2), dass es gut ist, den Speicher bei der Zuweisung auf ein bekanntes Muster zu initialisieren, und bestimmte Muster in Win32 Interrupts auslösen, die dazu führen, dass im Debugger Ausnahmen angezeigt werden.

Wie tragbar ist das?

Beim Schreiben von Solid Code (und möglicherweise von Code Complete ) werden wichtige Aspekte bei der Auswahl von Füllmustern berücksichtigt. Ich habe einige davon hier erwähnt, und der Wikipedia-Artikel über Magic Number (Programmierung) fasst sie ebenfalls zusammen. Einige der Tricks hängen von den Besonderheiten des von Ihnen verwendeten Prozessors ab (z. B. ob ausgerichtete Lese- und Schreibvorgänge erforderlich sind und welche Werte Anweisungen zugeordnet werden, die abgefangen werden). Andere Tricks, wie die Verwendung großer Werte und ungewöhnlicher Werte, die in einem Speicherauszug auffallen, sind portabler.

Adrian McCarthy
quelle
2

Dieser Artikel beschreibt ungewöhnliche Speicherbitmuster und verschiedene Techniken, die Sie verwenden können, wenn Sie auf diese Werte stoßen.

Stephen Kellett
quelle
2

Der offensichtliche Grund für das "Warum" ist, dass Sie eine Klasse wie diese haben:

class Foo
{
public:
    void SomeFunction()
    {
        cout << _obj->value << endl;
    }

private:
    SomeObject *_obj;
}

Und dann instanziieren Sie eine a Foound rufen an SomeFunction, es wird eine Zugriffsverletzung geben, die versucht zu lesen0xCDCDCDCD . Dies bedeutet, dass Sie vergessen haben, etwas zu initialisieren. Das ist das "Warum". Wenn nicht, hat sich der Zeiger möglicherweise mit einem anderen Speicher ausgerichtet, und das Debuggen ist schwieriger. Sie erfahren nur, warum Sie eine Zugriffsverletzung erhalten. Beachten Sie, dass dieser Fall ziemlich einfach war, aber in einer größeren Klasse ist es leicht, diesen Fehler zu machen.

AFAIK, dies funktioniert nur im Visual Studio-Compiler im Debug-Modus (im Gegensatz zur Veröffentlichung).

FryGuy
quelle
Ihre Erklärung folgt nicht, da Sie beim Versuch auch eine Zugriffsverletzung erhalten 0x00000000würden, die genauso nützlich wäre (oder mehr als eine schlechte Adresse). Wie ich in einem anderen Kommentar auf dieser Seite ausgeführt habe, besteht der wahre Grund für 0xCD(und 0xCC) darin, dass es sich um interpretierbare x86-Opcodes handelt, die einen Software-Interrupt auslösen. Dies ermöglicht eine ordnungsgemäße Wiederherstellung im Debugger in nur einem bestimmten und seltenen Fehlertyp nämlich wenn die CPU fälschlicherweise versucht, Bytes in einem Nicht-Code-Bereich auszuführen. Abgesehen von dieser funktionalen Verwendung sind Füllwerte nur Hinweise, wie Sie bemerken.
Glenn Slayden
2

Es ist leicht zu erkennen, dass sich der Speicher von seinem ursprünglichen Startwert geändert hat, im Allgemeinen während des Debuggens, manchmal aber auch für den Release-Code, da Sie dem Prozess während der Ausführung Debugger hinzufügen können.

Es ist nicht nur Speicher, viele Debugger setzen den Registerinhalt zu Beginn des Prozesses auf einen Sentinel-Wert (einige Versionen von AIX setzen einige Register, auf 0xdeadbeefdie leicht humorvoll ist).

paxdiablo
quelle
1

Der IBM XLC-Compiler verfügt über die Option "initauto", mit der automatischen Variablen ein von Ihnen angegebener Wert zugewiesen wird. Ich habe Folgendes für meine Debug-Builds verwendet:

-Wc,'initauto(deadbeef,word)'

Wenn ich mir die Speicherung einer nicht initialisierten Variablen ansehen würde, würde sie auf 0xdeadbeef gesetzt

Anthony Giorgio
quelle