Ich benötige eine Funktion, die (wie SecureZeroMemory von WinAPI) den Speicher immer auf Null setzt und nicht optimiert wird, selbst wenn der Compiler glaubt, dass der Speicher danach nie wieder aufgerufen wird. Scheint ein perfekter Kandidat für volatile zu sein. Aber ich habe einige Probleme damit, dass dies mit GCC funktioniert. Hier ist eine Beispielfunktion:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Einfach genug. Der Code, den GCC tatsächlich generiert, wenn Sie ihn aufrufen, hängt jedoch stark von der Compilerversion und der Anzahl der Bytes ab, die Sie tatsächlich auf Null setzen möchten. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 und 4.5.3 ignorieren niemals die flüchtigen Bestandteile.
- GCC 4.6.4 und 4.7.3 ignorieren flüchtig für die Arraygrößen 1, 2 und 4.
- GCC 4.8.1 bis 4.9.2 ignorieren flüchtig für Arraygrößen 1 und 2.
- GCC 5.1 bis 5.3 ignorieren flüchtig für Arraygrößen 1, 2, 4, 8.
- GCC 6.1 ignoriert es einfach für jede Arraygröße (Bonuspunkte für Konsistenz).
Jeder andere von mir getestete Compiler (clang, icc, vc) generiert die erwarteten Speicher mit jeder Compilerversion und jeder Arraygröße. An diesem Punkt frage ich mich also, ob dies ein (ziemlich alter und schwerwiegender?) GCC-Compiler-Fehler ist oder ob die Definition von flüchtig im Standard ungenau ist, dass dies tatsächlich konform ist, was es im Wesentlichen unmöglich macht, ein tragbares Gerät zu schreiben. " SecureZeroMemory "Funktion?
Edit: Einige interessante Beobachtungen.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>
void callMeMaybe(char* buf);
void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
{
*bytePtr++ = 0;
}
//std::atomic_thread_fence(std::memory_order_release);
}
std::size_t foo()
{
char arr[8];
callMeMaybe(arr);
volatileZeroMemory(arr, sizeof arr);
return sizeof arr;
}
Durch das mögliche Schreiben von callMeMaybe () werden alle GCC-Versionen außer 6.1 die erwarteten Speicher generieren. Durch das Kommentieren im Speicherzaun generiert GCC 6.1 auch die Speicher, allerdings nur in Kombination mit dem möglichen Schreiben von callMeMaybe ().
Jemand hat auch vorgeschlagen, die Caches zu leeren. Microsoft versucht in "SecureZeroMemory" überhaupt nicht , den Cache zu leeren. Der Cache wird wahrscheinlich sowieso ziemlich schnell ungültig, daher ist dies wahrscheinlich keine große Sache. Wenn ein anderes Programm versucht hat, die Daten zu prüfen, oder wenn sie in die Auslagerungsdatei geschrieben werden sollen, ist dies immer die auf Null gesetzte Version.
Es gibt auch einige Bedenken bezüglich GCC 6.1, das memset () in der Standalone-Funktion verwendet. Der GCC 6.1-Compiler für Godbolt ist möglicherweise fehlerhaft, da GCC 6.1 für einige Benutzer eine normale Schleife (wie 5.3 für Godbolt) für die Standalone-Funktion zu generieren scheint. (Lesen Sie die Kommentare zu zwols Antwort.)
volatile
ist ein Fehler, sofern nicht anders nachgewiesen. Aber höchstwahrscheinlich ein Fehler.volatile
ist so unterbestimmt, dass es gefährlich ist - benutze es einfach nicht.volatile
ist in diesem Fall angemessen.memset
. Das Problem ist, dass Compiler genau wissen, was siememset
tun.volatile
Zeiger ergeben, auf den wir einen Zeiger wollenvolatile
(es ist uns egal, ob er++
streng ist, aber ob er*p = 0
streng ist).Antworten:
Das Verhalten von GCC kann konform sein, und selbst wenn dies nicht der
volatile
Fall ist, sollten Sie sich in solchen Fällen nicht darauf verlassen, das zu tun, was Sie wollen. Das C-Komitee wurdevolatile
für speicherabgebildete Hardwareregister und für Variablen entwickelt, die während eines abnormalen Steuerflusses geändert wurden (z. B. Signalhandler undsetjmp
). Dies sind die einzigen Dinge, für die es zuverlässig ist. Es ist nicht sicher, die allgemeine Annotation "Nicht optimieren" zu verwenden.Insbesondere ist der Standard in einem wichtigen Punkt unklar. (Ich habe Ihren Code in C konvertiert. Hier sollte es keine Abweichungen zwischen C und C ++ geben. Ich habe auch manuell das Inlining durchgeführt, das vor der fragwürdigen Optimierung erfolgen würde, um zu zeigen, was der Compiler zu diesem Zeitpunkt "sieht" .)
Die Speicherlöschschleife greift
arr
über einen flüchtig qualifizierten Wert zu, wird jedocharr
selbst nicht deklariertvolatile
. Es ist daher zumindest wohl zulässig, dass der C-Compiler daraus schließt, dass die von der Schleife erstellten Speicher "tot" sind, und die Schleife insgesamt löscht. Es gibt Text in dem C Rationale , das bedeutet , dass der Ausschuß gemeint , diese Geschäfte zu verlangen , zu erhalten, aber der Standard selbst macht nicht wirklich , dass die Anforderung, wie ich es gelesen.Weitere Informationen darüber, was der Standard erfordert oder nicht, finden Sie unter Warum wird eine flüchtige lokale Variable anders als ein flüchtiges Argument optimiert und warum generiert der Optimierer aus letzterem eine No-Op-Schleife? , Ist ein Zugriff erklärt nicht-flüchtiges Objekt durch einen flüchtigen Verweis / Zeiger confer flüchtigen Regeln auf die Zugriffe? und GCC-Fehler 71793 .
Um mehr darüber zu erfahren, wofür das Komitee gedacht hat
volatile
, suchen Sie in der C99-Begründung nach dem Wort "volatile". John Regehrs Artikel " Volatiles are Miscompiled " zeigt im Detail, wie die Erwartungen von Programmierernvolatile
von Produktions-Compilern möglicherweise nicht erfüllt werden. Die Aufsatzserie des LLVM-Teams " Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte " geht nicht speziell darauf ein,volatile
sondern hilft Ihnen zu verstehen, wie und warum moderne C-Compiler keine "tragbaren Assembler" sind.Zur praktischen Frage, wie eine Funktion implementiert werden kann, die das tut, was Sie
volatileZeroMemory
tun wollten : Unabhängig davon, was der Standard erfordert oder erfordern sollte, ist es am klügsten anzunehmen, dass Sie dies nicht verwendenvolatile
können. Es gibt eine Alternative, auf die man sich bei der Arbeit verlassen kann, weil sie viel zu viele andere Dinge kaputt machen würde, wenn sie nicht funktionieren würde:Sie müssen jedoch unbedingt sicherstellen, dass
memory_optimization_fence
dies unter keinen Umständen inline ist. Es muss sich in einer eigenen Quelldatei befinden und darf keiner Optimierung der Verbindungszeit unterzogen werden.Es gibt andere Optionen, die sich auf Compiler-Erweiterungen stützen und unter bestimmten Umständen verwendet werden können und engeren Code generieren können (eine davon wurde in einer früheren Ausgabe dieser Antwort veröffentlicht), aber keine ist universell.
(Ich empfehle, die Funktion aufzurufen
explicit_bzero
, da sie unter diesem Namen in mehr als einer C-Bibliothek verfügbar ist. Es gibt mindestens vier weitere Kandidaten für den Namen, aber jeder wurde nur von einer einzelnen C-Bibliothek übernommen.)Sie sollten auch wissen, dass dies möglicherweise nicht ausreicht, selbst wenn Sie dies zum Laufen bringen können. Insbesondere berücksichtigen
Unter der Annahme , Hardware mit AES - Beschleunigung Anweisungen, wenn
expand_key
undencrypt_with_ek
Inline ist, kann der Compiler in der Lage sein zu haltenek
völlig Datei in dem Vektorregister - bis den Anruf anexplicit_bzero
, welche Kräfte es zu den sensiblen Daten auf den Stapel zu kopieren nur um es zu löschen, und, Schlimmer noch, macht nichts gegen die Schlüssel, die noch in den Vektorregistern sitzen!quelle
volatile
als [...] ? Daher muss jeder Ausdruck, der sich auf ein solches Objekt bezieht, streng nach den Regeln der abstrakten Maschine bewertet werden, wie in 5.1.2.3 beschrieben. Darüber hinaus muss an jedem Sequenzpunkt der zuletzt im Objekt gespeicherte Wert mit dem von der abstrakten Maschine vorgeschriebenen Wert übereinstimmen , sofern er nicht durch die zuvor genannten unbekannten Faktoren geändert wurde. Was einen Zugriff auf ein Objekt mit einem flüchtig qualifizierten Typ darstellt, ist implementierungsdefiniert. ?volatile sig_atomic_t flag;
ist ein flüchtiges Objekt .*(volatile char *)foo
ist lediglich ein Zugang über einen flüchtig qualifizierten Wert, und der Standard verlangt keine Spezialeffekte.volatile
kann ausreichen, um eine "konforme" Implementierung zu erreichen, aber das bedeutet nicht, dass es ausreicht, "gut" oder "nützlich" zu sein. Für viele Arten der Systemprogrammierung sollte diesbezüglich als äußerst mangelhaft angesehen werden.Dafür ist die Standardfunktion gedacht
memset_s
.Ob dieses Verhalten mit flüchtigen Bestandteilen konform ist oder nicht, ist etwas schwer zu sagen, und flüchtige Menschen sollen seit langem von Fehlern geplagt sein.
Ein Problem ist, dass die Spezifikationen besagen, dass "Zugriffe auf flüchtige Objekte streng nach den Regeln der abstrakten Maschine bewertet werden". Dies bezieht sich jedoch nur auf "flüchtige Objekte" und nicht auf den Zugriff auf ein nichtflüchtiges Objekt über einen Zeiger, dem flüchtige Elemente hinzugefügt wurden. Wenn ein Compiler also feststellen kann, dass Sie nicht wirklich auf ein flüchtiges Objekt zugreifen, ist es anscheinend nicht erforderlich, das Objekt schließlich als flüchtig zu behandeln.
quelle
memset_s
als C11-Standard ist eine Übertreibung. Es ist Teil von Anhang K, der in C11 optional ist (und daher auch in C ++ optional ist). Grundsätzlich haben sich alle Implementierer, einschließlich Microsoft, dessen Idee es überhaupt war (!), Abgelehnt, es aufzugreifen. Zuletzt hörte ich, dass sie darüber sprachen, es in C-next zu verschrotten.size_t
. Der Win64 ABI ist nicht konform mit C90. Das wäre ... nicht in Ordnung , aber nicht schrecklich ... wenn MSVC hatte tatsächlich C99-Dinge wieuintmax_t
und%zu
rechtzeitig aufgegriffen , aber sie taten es nicht .)Ich biete diese Version als portables C ++ an (obwohl die Semantik subtil unterschiedlich ist):
Jetzt haben Sie Schreibzugriffe auf ein flüchtiges Objekt und nicht nur Zugriff auf ein nichtflüchtiges Objekt, das über eine flüchtige Ansicht des Objekts erstellt wurde.
Der semantische Unterschied besteht darin, dass nun die Lebensdauer aller Objekte, die den Speicherbereich belegen, formal beendet wird, da der Speicher wiederverwendet wurde. Der Zugriff auf das Objekt nach dem Nullstellen seines Inhalts ist jetzt sicherlich ein undefiniertes Verhalten (früher wäre es in den meisten Fällen ein undefiniertes Verhalten gewesen, aber es gab sicherlich einige Ausnahmen).
Um diese Nullung während der Lebensdauer eines Objekts anstatt am Ende zu verwenden, sollte der Aufrufer die Platzierung verwenden
new
, um eine neue Instanz des ursprünglichen Typs wieder zurückzusetzen.Der Code kann durch Verwendung der Wertinitialisierung verkürzt (wenn auch weniger klar) werden:
und zu diesem Zeitpunkt ist es ein Einzeiler und garantiert kaum eine Hilfsfunktion.
quelle
Es sollte möglich sein, eine tragbare Version der Funktion zu schreiben, indem ein flüchtiges Objekt auf der rechten Seite verwendet wird und der Compiler gezwungen wird, die Speicher im Array beizubehalten.
Das
zero
Objekt wird deklariertvolatile
, um sicherzustellen , dass der Compiler keine Annahmen über seinen Wert treffen kann, obwohl er immer als Null ausgewertet wird.Der endgültige Zuweisungsausdruck liest aus einem flüchtigen Index im Array und speichert den Wert in einem flüchtigen Objekt. Da dieser Lesevorgang nicht optimiert werden kann, wird sichergestellt, dass der Compiler die in der Schleife angegebenen Speicher generieren muss.
quelle
*ptr
während dieser Schleife keine Speicherung vorzunehmen oder überhaupt irgendetwas ... nur eine Schleife. wtf, da geht mein Gehirn.edx
: Ich.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
Füllbyte übergeben werden kann, wird es nicht einmal gelesen . Der generierte Inline-Aufruf vonvolatileFill()
ist nur[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Warum liest der Optimierer (A) das Füllbyte nicht erneut und (B) behält die Schleife bei, wo er nichts tut?