web-dev-qa-db-de.com

Ist die Sicherheit von memcpy (0,0,0) garantiert?

Ich bin im C-Standard nicht so gut versiert, also bitte um Verstand.

Ich würde gerne wissen, ob durch die Norm garantiert ist, dass memcpy(0,0,0) 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 uns vorstellen, dass sich die Speicherbereiche hier überschneiden?

68
Matthieu M.

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

Ein als size_t n deklariertes Argument gibt die Länge des Arrays für eine .__ an. Funktion kann n den Wert Null haben, wenn diese Funktion aufgerufen wird. Wenn nicht ausdrücklich angegeben. ansonsten in der Beschreibung einer bestimmten Funktion in dieser Unterklausel pointer-Argumente Bei einem solchen Aufruf müssen noch gültige Werte vorhanden sein, wie in 7.1.4 beschrieben. Bei einem solchen Anruf wird ein Eine Funktion, die ein Zeichen findet, findet kein Vorkommen, eine Funktion, die zwei .__ vergleicht. Zeichenfolgen 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 aufweist (beispielsweise einen Wert Außerhalb der Domäne der Funktion oder einen Zeiger außerhalb des Adressraums des Programms, Oder einen Nullzeiger oder Zeiger auf nicht änderbaren Speicher, wenn der entsprechende -Parameter nicht für const qualifiziert ist) oder einen Typ (nach Aufstieg), der von einer Funktion nicht erwartet wird Bei variabler Anzahl von Argumenten ist das Verhalten undefiniert.

So sieht es nach der C-Spezifikation aus, rufend

memcpy(0, 0, 0)

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

Das heißt, ich wäre völlig erstaunt, wenn eine tatsächliche Implementierung von memcpy in diesem Fall kaputt gehen würde, da die meisten intuitiven Implementierungen, die ich mir vorstellen kann, nichts tun würden, wenn Sie sagten, null Byte zu kopieren.

66
templatetypedef

Nur zum Spaß zeigen die Versionshinweise für gcc-4.9 an, dass der Optimierer von diesen Regeln Gebrauch macht und beispielsweise 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 zu unerwarteten Ergebnissen führt, wenn copy(0,0,0) aufgerufen wird (siehe https://gcc.gnu.org/gcc-4.9/porting_to.html ).

Ich bin etwas ambivalent bezüglich des Verhaltens von gcc-4.9. Das Verhalten kann normkonform sein, aber memmove (0,0,0) aufrufen zu können, ist manchmal eine nützliche Erweiterung dieser Standards.

21
user1998586

Sie können auch die Verwendung von memmove in Git 2.14.x (Q3 2017) berücksichtigen.

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

Es verwendet ein helper-Makro MOVE_ARRAY , das die Größe __. Basierend auf der angegebenen Anzahl von Elementen für uns berechnet und NULL Zeiger unterstützt, wenn diese Anzahl Null ist.
Raw memmove(3) Aufrufe mit NULL können Bewirken, dass der Compiler spätere NULL-Prüfungen (übermäßig) optimiert.

MOVE_ARRAY fügt einen sicheren und bequemen Helfer hinzu, um möglicherweise überlappende Bereiche von Feldeinträgen zu verschieben.
Sie leitet die Elementgröße an, multipliziert sich automatisch und sicher, um die Größe in Bytes zu ermitteln, führt eine grundlegende Sicherheitsüberprüfung durch, indem sie die Elementgrößen vergleicht. Anders als memmove(3) werden NULL-Zeiger unterstützt, 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 das macro BUILD_ASSERT_OR_ZERO , das eine Build-Time-Abhängigkeit als Ausdruck feststellt (wobei @cond die Kompilierzeit-Bedingung ist, die wahr sein muss).
Die Kompilierung schlägt fehl, wenn die Bedingung nicht erfüllt 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))
0
VonC