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

Lesezeit: 8 Minuten

Benutzeravatar von Matthieu M
Matthias M.

Ich kenne mich mit dem C-Standard nicht so gut aus, also bitte haben Sie Geduld mit mir.

Ich würde gerne wissen, ob dies durch den Standard garantiert ist memcpy(0,0,0) ist sicher.

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

Aber können wir davon ausgehen, dass sich die Speicherregionen hier überschneiden?

  • Mathematisch ist die Schnittmenge zweier leerer Mengen leer.

    – Benoît

    9. März 2011 um 8:22 Uhr

  • Ich wollte für Sie nachsehen, ob (x)libC das für Sie erledigt, aber wie es ist asm (hier elibc/glibc), ist für einen frühen Morgen etwas zu kompliziert 🙂

    – Kevin

    9. März 2011 um 8:25 Uhr

  • +1 Ich liebe diese Frage, weil es so ein seltsamer Grenzfall ist und weil ich denke memcpy(0,0,0) ist eines der seltsamsten Teile von C-Code, die ich je gesehen habe.

    – Vorlagentypdef

    9. März 2011 um 8:29 Uhr

  • @eq Willst du es wirklich wissen oder implizieren Sie, dass es keine Situationen gibt, in denen Sie es wollen würden? Haben Sie darüber nachgedacht, dass der eigentliche Anruf z. memcpy(outp, inp, len)? Und dass dies im Code auftreten könnte, wo outp und inp werden dynamisch zugewiesen und sind initial 0? Das funktioniert zB mit p = realloc(p, len+n) Wenn p und len sind 0. Ich selbst habe so einen benutzt memcpy call — obwohl es technisch gesehen UB ist, bin ich noch nie auf eine Implementierung gestoßen, bei der es kein No-Op ist und erwarte es auch nie.

    – Jim Balter

    9. März 2011 um 8:50 Uhr


  • @templatetypedef memcpy(0, 0, 0) soll höchstwahrscheinlich einen dynamischen, nicht statischen Aufruf darstellen … dh diese Parameterwerte müssen keine Literale sein.

    – Jim Balter

    9. März 2011 um 8:53 Uhr

Ich habe eine Entwurfsversion des C-Standards (ISO/IEC 9899:1999), und es gibt einige lustige Dinge über diesen Aufruf zu sagen. Für den Anfang erwähnt es (§7.21.1/2) in Bezug auf memcpy das

Wo ein Argument als deklariert wird size_t n gibt die Länge des Arrays für eine Funktion an, n kann bei einem Aufruf dieser Funktion den Wert Null annehmen. Sofern in der Beschreibung einer bestimmten Funktion in diesem Unterabschnitt nicht ausdrücklich anders angegeben, Zeigerargumente bei einem solchen Aufruf müssen 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.

Darauf weist die hier angegebene Referenz hin:

Wenn ein Argument für eine Funktion einen ungültigen Wert hat (z. B. einen Wert außerhalb der Domäne der Funktion oder einen Zeiger außerhalb des Adressraums des Programms,
oder ein Nullzeigeroder ein Zeiger auf nicht modifizierbaren Speicher, wenn der entsprechende Parameter nicht konstant qualifiziert ist) oder ein Typ (nach der Heraufstufung), der von einer Funktion mit variabler Anzahl von Argumenten nicht erwartet wird, Das Verhalten ist undefiniert.

So sieht es laut C-Spec aus, Calling

memcpy(0, 0, 0)

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

Das heißt, ich wäre absolut erstaunt, wenn eine tatsächliche Umsetzung von memcpy brach, wenn Sie dies taten, da die meisten intuitiven Implementierungen, die mir einfallen, überhaupt nichts bewirken würden, wenn Sie sagten, null Bytes zu kopieren.

  • Ich kann versichern, dass die zitierten Teile aus dem Norm-Entwurf im endgültigen Dokument identisch sind. Bei einem solchen Aufruf sollte es keine Probleme geben, aber es wäre immer noch ein undefiniertes Verhalten, auf das Sie sich verlassen. Die Antwort auf “ist es garantiert” ist also “nein”.

    – DevSolar

    9. März 2011 um 8:33 Uhr


  • Keine Implementierung, die Sie jemals in der Produktion verwenden werden, wird etwas anderes als eine No-Op für einen solchen Aufruf erzeugen, aber Implementierungen, die etwas anderes tun, sind zulässig und sinnvoll … z. B. ein C-Interpreter oder erweiterter Compiler mit Fehlerprüfung, der die ablehnt Rufen Sie an, weil es nicht konform ist. Das wäre natürlich nicht sinnvoll, wenn der Standard den Aufruf zulassen würde, wie es für realloc(0, 0). Die Anwendungsfälle sind ähnlich, und ich habe sie beide verwendet (siehe meinen Kommentar unter der Frage). Es ist sinnlos und schade, dass der Standard dieses UB macht.

    – Jim Balter

    9. März 2011 um 9:01 Uhr


  • “Ich wäre absolut erstaunt, wenn eine tatsächliche Implementierung von Memcpy kaputt gehen 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, wurden tatsächlich 65536 Bytes kopiert. (Seine Schleife dekrementierte die Länge und testete dann).

    – MM

    12. Juli 2014 um 6:01 Uhr

  • @MattMcNabb Diese Implementierung ist kaputt.

    – Jim Balter

    13. Juli 2014 um 22:16 Uhr

  • @MattMcNabb: Vielleicht “richtig” zu “tatsächlich” hinzufügen. 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

    9. September 2014 um 11:20 Uhr

Benutzeravatar von user1998586
Benutzer1998586

Nur zum Spaß weisen die Versionshinweise für gcc-4.9 darauf hin, dass sein Optimierer diese Regeln verwendet und zum Beispiel die Bedingung in entfernen kann

int copy (int* dest, int* src, size_t nbytes) {
    memmove (dest, src, nbytes);
    if (src != NULL)
        return *src;
    return 0;
}

was dann zu unerwarteten Ergebnissen führt, wenn copy(0,0,0) heißt (vgl https://gcc.gnu.org/gcc-4.9/porting_to.html).

Ich bin etwas ambivalent bezüglich des Verhaltens von gcc-4.9; Das Verhalten ist möglicherweise standardkonform, aber die Möglichkeit, memmove(0,0,0) aufzurufen, ist manchmal eine nützliche Erweiterung dieser Standards.

  • Interessant. Ich verstehe Ihre Ambivalenz, aber das ist das Herzstück der Optimierungen in C: der Compiler geht davon aus dass Entwickler bestimmten Regeln folgen und somit leitet ab dass einige Optimierungen gültig sind (was sie sind, wenn die Regeln befolgt werden).

    – Matthias M.

    19. Juli 2014 um 12:45 Uhr

  • @tmyklebu: Gegeben char *p = 0; int i=something;Auswertung des Ausdrucks (p+i) führt zu undefiniertem Verhalten, selbst wenn i ist Null.

    – Superkatze

    6. Dezember 2014 um 21:32 Uhr

  • @tmyklebu: Alle Zeigerarithmetik (außer Vergleichen) auf einer Nullzeigerfalle zu haben, wäre meiner Meinung nach eine gute Sache; ob memcpy() sollte es erlaubt sein, jede Zeigerarithmetik an seinen Argumenten durchzuführen, bevor sichergestellt wird, dass eine Zählung ungleich Null ist, ist eine andere Frage [if I were designing the standards, I would probably specify that if p is null, p+0 could trap, but memcpy(p,p,0) would do nothing]. Ein viel größeres Problem, IMHO, ist die Offenheit der meisten undefinierten Verhaltensweisen. Während es einige Dinge gibt, die wirklich sollte stellen undefiniertes Verhalten dar (z. B. Aufrufen free(p)

    – Superkatze

    10. Dezember 2014 um 16:43 Uhr


  • …und anschließend performen 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 entweder 0 oder 1), oder sollte so spezifiziert werden, dass er ein Verhalten ergibt, das etwas lockerer als durch die Implementierung definiert ist (Compiler sollten aufgefordert werden, alle möglichen Konsequenzen eines zB Integer-Überlaufs zu dokumentieren, aber nicht angeben, welche Konsequenzen in einem bestimmten Fall auftreten würden).

    – Superkatze

    10. Dezember 2014 um 16:52 Uhr


  • Bitte sagt mir jemand, warum bekomme ich kein Stackoverflow-Abzeichen “started a flame war” 🙂

    – Benutzer1998586

    20. Juni 2015 um 8:28 Uhr

Sie können auch diese Verwendung von in Betracht ziehen memmove gesehen in Git 2.14.x (Q3 2017)

Sehen Commit 168e635 (16. Juli 2017) und 1773664 festschreiben, f331ab9 übergeben, Festschreiben 5783980 (15. Juli 2017) von René Scharfe (rscharfe).
(Zusammengeführt von Junio ​​C. Hamano — gitster in Commit 32f902511.08.2017)

Es verwendet ein Hilfsmakro MOVE_ARRAY die die Größe anhand der angegebenen Anzahl von Elementen für uns berechnet und stützt NULL
Zeiger, wenn diese Zahl Null ist.
Roh memmove(3) telefoniert mit NULL kann dazu führen, dass der Compiler später (übereifrig) optimiert NULL Schecks.

MOVE_ARRAY fügt einen sicheren und bequemen 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 Typsicherheitsprüfung durch, indem es Elementgrößen vergleicht und unterscheidet memmove(3) es unterstützt NULL Zeiger wenn 0 Elemente verschoben werden sollen.

#define MOVE_ARRAY(dst, src, n) move_array((dst), (src), (n), sizeof(*(dst)) + \
    BUILD_ASSERT_OR_ZERO(sizeof(*(dst)) == sizeof(*(src))))
static inline void move_array(void *dst, const void *src, size_t n, size_t size)
{
    if (n)
        memmove(dst, src, st_mult(size, n));
}

Beispiele:

- memmove(dst, src, (n) * sizeof(*dst));
+ MOVE_ARRAY(dst, src, n);

Es verwendet die Makro BUILD_ASSERT_OR_ZERO die eine Build-Time-Abhängigkeit geltend macht, als Ausdruck (with @cond die Bedingung zur Kompilierzeit ist, die wahr sein muss).
Die Kompilierung schlägt fehl, wenn die Bedingung nicht wahr ist oder vom Compiler nicht ausgewertet werden kann.

#define BUILD_ASSERT_OR_ZERO(cond) \
(sizeof(char [1 - 2*!(cond)]) - 1)

Beispiel:

#define foo_to_char(foo)                \
     ((char *)(foo)                     \
      + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))

  • Die Existenz von Optimierern, die denken, dass „clever“ und „dumm“ Antonyme sind, 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) keine Operation wäre . Wenn ein Compiler einen Aufruf von memmove() nicht durch einen Aufruf von memmoveAtLeastOneByte() ersetzen kann, führt die Problemumgehung zum Schutz vor der “Optimierung” cleverer/dummer Compiler im Allgemeinen zu einem zusätzlichen Vergleich, den ein Compiler nicht eliminieren kann.

    – Superkatze

    14. August 2017 um 23:34 Uhr


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

1419420cookie-checkIst es garantiert sicher, memcpy(0,0,0) auszuführen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy