Ist es garantiert sicher, memcpy (0,0,0) auszuführen?

80

Ich bin nicht so gut mit dem C-Standard vertraut, bitte nehmen Sie Kontakt mit mir auf.

Ich würde gerne wissen, ob es standardmäßig garantiert ist, dass memcpy(0,0,0)es sicher ist.

Die einzige Einschränkung, die ich finden konnte, ist, dass das Verhalten undefiniert ist, wenn sich die Speicherbereiche überlappen ...

Aber können wir berücksichtigen, dass sich die Speicherbereiche hier überlappen?

Matthieu M.
quelle
7
Mathematisch ist der Schnittpunkt zweier leerer Mengen leer.
Benoit
Ich wollte nach dir suchen, ob (x) libC für dich ist, aber da es asm ist (elibc / glibc hier), ist es für einen frühen Morgen etwas zu kompliziert :)
Kevin
16
+1 Ich liebe diese Frage sowohl, weil es so ein seltsamer memcpy(0,0,0)Randfall ist, als auch weil ich denke, dass es eines der seltsamsten Teile des C-Codes ist, die ich gesehen habe.
Templatetypedef
2
@eq Möchtest du es wirklich wissen oder implizierst du, dass es keine Situationen gibt, in denen du es willst? Haben Sie darüber nachgedacht, dass der eigentliche Anruf beispielsweise sein könnte memcpy(outp, inp, len)? Und dass dies in Code auftreten könnte, wo outpund inpdynamisch zugeordnet sind und anfänglich sind 0? Dies funktioniert zB mit p = realloc(p, len+n)wann pund lensind 0. Ich selbst habe einen solchen memcpyAufruf verwendet - obwohl es sich technisch gesehen um UB handelt, bin ich noch nie auf eine Implementierung gestoßen, bei der es sich nicht um ein No-Op handelt und die ich nie erwartet habe.
Jim Balter
7
@templatetypedef memcpy(0, 0, 0)soll höchstwahrscheinlich einen dynamischen, nicht statischen Aufruf darstellen ... dh diese Parameterwerte müssen keine Literale sein.
Jim Balter

Antworten:

71

Ich habe eine Entwurfsversion der C-Norm (ISO / IEC 9899: 1999), und es gibt einige lustige Dinge zu diesem Aufruf zu sagen. Für den Anfang erwähnt sie (§7.21.1 / 2) in Bezug auf memcpydas

Wenn ein als size_tn deklariertes Argument die Länge des Arrays für eine Funktion angibt, kann n bei einem Aufruf dieser Funktion den Wert Null haben. Sofern in der Beschreibung einer bestimmten Funktion in diesem Unterabschnitt nicht ausdrücklich anders angegeben, müssen Zeigerargumente für einen solchen Aufruf weiterhin gültige Werte haben, wie in 7.1.4 beschrieben . Bei einem solchen Aufruf findet eine Funktion, die ein Zeichen findet, kein Vorkommen, eine Funktion, die zwei Zeichenfolgen vergleicht, gibt Null zurück, und eine Funktion, die Zeichen kopiert, kopiert Null Zeichen.

Die hier angegebene Referenz weist darauf hin:

Wenn ein Argument für eine Funktion einen ungültigen Wert hat (z. B. einen Wert außerhalb des Funktionsbereichs oder einen Zeiger außerhalb des Adressraums des Programms oder einen Nullzeiger oder einen Zeiger auf einen nicht veränderbaren Speicher, wenn der entsprechende Parameter vorhanden ist ist nicht const-qualifiziert) oder ein Typ (nach der Heraufstufung), der von einer Funktion mit variabler Anzahl von Argumenten nicht erwartet wird, ist das Verhalten undefiniert .

Es sieht also so aus, als würde man gemäß der C-Spezifikation anrufen

führt zu undefiniertem Verhalten, da Nullzeiger als "ungültige Werte" betrachtet werden.

Trotzdem wäre ich äußerst erstaunt, wenn eine tatsächliche Implementierung von memcpypleite wäre, wenn Sie dies tun würden, da die meisten intuitiven Implementierungen, die mir einfallen, überhaupt nichts bewirken würden, wenn Sie sagen würden, dass sie null Bytes kopieren sollen.

templatetypedef
quelle
3
Ich kann bestätigen, dass die zitierten Teile des Standardentwurfs im endgültigen Dokument identisch sind. Es sollte keine Probleme mit einem solchen Anruf geben, aber es wäre immer noch ein undefiniertes Verhalten, auf das Sie sich verlassen. Die Antwort auf "Ist es garantiert" lautet "Nein".
DevSolar
8
Keine Implementierung, die Sie jemals in der Produktion verwenden werden, erzeugt etwas anderes als ein No-Op für einen solchen Aufruf, aber Implementierungen, die anderweitig zulässig und sinnvoll sind ... z. B. ein C-Interpreter oder ein erweiterter Compiler mit Fehlerprüfung, der das ablehnt Anruf, weil es nicht konform ist. Das wäre natürlich nicht vernünftig, wenn der Standard den Anruf zulassen würde, wie es der Fall ist realloc(0, 0). Die Anwendungsfälle sind ähnlich und ich habe beide verwendet (siehe meinen Kommentar unter der Frage). Es ist sinnlos und bedauerlich, dass der Standard diese UB macht.
Jim Balter
5
"Ich wäre äußerst erstaunt, wenn eine tatsächliche Implementierung von memcpy fehlschlagen würde, wenn Sie dies tun würden" - ich habe eine verwendet, die dies tun würde; Wenn Sie die Länge 0 mit gültigen Zeigern übergeben haben, wurden tatsächlich 65536 Byte kopiert. (Seine Schleife hat die Länge verringert und dann getestet).
MM
14
@MattMcNabb Diese Implementierung ist fehlerhaft.
Jim Balter
3
@MattMcNabb: Fügen Sie "korrekt" zu "aktuell" hinzu, vielleicht. Ich denke, wir alle haben nicht so gute Erinnerungen an alte Ghetto-C-Bibliotheken, und ich bin mir nicht sicher, wie viele von uns es schätzen, dass diese Erinnerungen zurückgerufen werden. :)
tmyklebu
24

Nur zum Spaß zeigen die Versionshinweise für gcc-4.9 an, dass sein Optimierer diese Regeln verwendet und beispielsweise die Bedingung in entfernen kann

Dies führt dann zu unerwarteten Ergebnissen, wenn copy(0,0,0)es aufgerufen wird (siehe https://gcc.gnu.org/gcc-4.9/porting_to.html ).

Ich bin etwas ambivalent in Bezug auf das Verhalten von gcc-4.9; Das Verhalten ist möglicherweise standardkonform, aber das Aufrufen von memmove (0,0,0) ist manchmal eine nützliche Erweiterung dieser Standards.

user1998586
quelle
2
Interessant. Ich verstehe Ihre Ambivalenz, aber dies ist das Herzstück der Optimierungen in C: Der Compiler geht davon aus, dass Entwickler bestimmte Regeln befolgen, und leitet daraus ab, dass einige Optimierungen gültig sind (was sie sind, wenn die Regeln befolgt werden).
Matthieu M.
2
@tmyklebu: Gegeben char *p = 0; int i=something;, die Auswertung des Ausdrucks (p+i)ergibt ein undefiniertes Verhalten, selbst wenn ies Null ist.
Supercat
1
@tmyklebu: Alle Zeigerarithmetik (außer Vergleiche) auf einer Nullzeigerfalle zu haben, wäre meiner Meinung nach eine gute Sache; ob memcpy()es erlaubt sein sollte, eine Zeigerarithmetik für seine Argumente durchzuführen, bevor sichergestellt wird, dass eine Zählung ungleich Null vorliegt, ist eine andere Frage [wenn ich die Standards entwerfen würde, würde ich wahrscheinlich angeben, dass wenn pnull ist, abgefangen werden p+0könnte, aber memcpy(p,p,0)nichts tun würde]. Ein viel größeres Problem, IMHO, ist die Offenheit der meisten undefinierten Verhaltensweisen. Zwar gibt es einige Dinge , die wirklich sollten nicht definiertes Verhalten (zB Berufung darstellen free(p)...
supercat
1
... und anschließend ausführen p[0]=1;) Es gibt viele Dinge, die als unbestimmtes Ergebnis angegeben werden sollten (z. B. sollte ein relationaler Vergleich zwischen nicht verwandten Zeigern nicht als konsistent mit einem anderen Vergleich angegeben werden, sondern als Ergebnis einer 0 oder a 1) oder sollte so angegeben werden, dass sich ein Verhalten ergibt, das etwas lockerer ist als das von der Implementierung definierte (Compiler sollten alle möglichen Konsequenzen eines z. B. ganzzahligen Überlaufs dokumentieren müssen, aber nicht angeben, welche Konsequenz in einem bestimmten Fall auftreten würde).
Supercat
8
jemand bitte sag mir, warum bekomme ich nicht ein Stackoverflow-Abzeichen "hat einen Flammenkrieg gestartet" :-)
user1998586
0

Sie können diese Verwendung memmoveauch in Git 2.14.x (Q3 2017) berücksichtigen.

Siehe Commit 168e635 (16. Juli 2017) und Commit 1773664 , Commit f331ab9 , Commit 5783980 (15. Juli 2017) von René Scharfe ( rscharfe) .
(Zusammengeführt von Junio ​​C Hamano - gitster- in Commit 32f9025 , 11. August 2017)

Es verwendet ein Hilfsmakro,MOVE_ARRAY das die Größe basierend auf der für uns angegebenen Anzahl von Elementen berechnet und NULL Zeiger unterstützt, wenn diese Anzahl Null ist.
Rohe memmove(3)Aufrufe mit NULLkönnen dazu führen, dass der Compiler (zu eifrig) spätere NULLÜberprüfungen optimiert .

MOVE_ARRAYFügt einen sicheren und praktischen Helfer zum Verschieben potenziell überlappender Bereiche von Array-Einträgen hinzu.
Es leitet die Elementgröße ab, multipliziert automatisch und sicher, um die Größe in Bytes zu erhalten, führt eine grundlegende Typensicherheitsprüfung durch Vergleichen der Elementgrößen durch und memmove(3)unterstützt im Gegensatz dazu NULLZeiger, wenn 0 Elemente verschoben werden sollen.

Beispiele :

Es verwendet das Makro,BUILD_ASSERT_OR_ZERO das eine Abhängigkeit von @condder Erstellungszeit behauptet, als Ausdruck (wobei dies die Bedingung für die Kompilierungszeit ist, die wahr sein muss).
Die Kompilierung schlägt fehl, wenn die Bedingung nicht erfüllt ist oder vom Compiler nicht ausgewertet werden kann.

Beispiel:

VonC
quelle
2
Die Existenz von Optimierern, die "clever" und "dumm" für Antonyme halten, macht den Test für n notwendig, aber effizienterer Code wäre im Allgemeinen bei einer Implementierung möglich, die garantiert, dass memmove (any, any, 0) ein No-Op ist . Wenn ein Compiler einen Aufruf von memmove () nicht durch einen Aufruf von memmoveAtLeastOneByte () ersetzen kann, führt die Problemumgehung zum Schutz vor der "Optimierung" von cleveren / dummen Compilern im Allgemeinen zu einem zusätzlichen Vergleich, den ein Compiler nicht beseitigen kann.
Supercat
-3

Nein, memcpy(0,0,0)ist nicht sicher. Die Standardbibliothek wird bei diesem Aufruf wahrscheinlich nicht fehlschlagen. In einer Testumgebung ist jedoch möglicherweise zusätzlicher Code in memcpy () vorhanden, um Pufferüberläufe und andere Probleme zu erkennen. Und wie diese spezielle Version von memcpy () auf NULL-Zeiger reagiert, ist nicht definiert.

Kai Petzke
quelle
Ich kann nichts sehen, was Ihre Antwort zu den vorhandenen Antworten hinzufügt, die bereits darauf hingewiesen haben, dass dies mit Auszügen aus dem Standard nicht sicher war.
Matthieu M.
Ich habe einen Grund geschrieben, warum bestimmte Implementierungen eines memcpy ("in einer Testumgebung") zu Problemen mit memcpy (0, 0, 0) führen können. Ich denke, das ist neu.
Kai Petzke