Czy wykonywanie memcpy (0,0,0) jest bezpieczne?

80

Nie jestem dobrze zorientowany w standardzie C, więc proszę o wyrozumiałość.

Chciałbym wiedzieć, czy standard gwarantuje, że memcpy(0,0,0)jest to bezpieczne.

Jedynym ograniczeniem, jakie mogłem znaleźć, jest to, że jeśli regiony pamięci nakładają się, to zachowanie jest niezdefiniowane ...

Ale czy możemy wziąć pod uwagę, że obszary pamięci nakładają się tutaj?

Matthieu M.
źródło
7
Matematycznie przecięcie dwóch pustych zbiorów jest puste.
Benoit
Chciałem sprawdzić, czy chcesz, aby (x) libC robiło dla ciebie, ale ponieważ jest to asm (tutaj elibc / glibc), jest to trochę zbyt skomplikowane na wczesny poranek :)
Kevin
16
+1 Uwielbiam to pytanie zarówno dlatego, że jest to tak dziwny przypadek krawędzi, jak i dlatego, że uważam, że memcpy(0,0,0)jest to jeden z najdziwniejszych fragmentów kodu C, jakie widziałem.
templatetypedef
2
@eq Czy naprawdę chcesz wiedzieć, czy sugerujesz, że nie ma sytuacji, w których byś tego chciał? Czy zastanawiałeś się, czy rzeczywiste wezwanie może być, powiedzmy memcpy(outp, inp, len),? I że to może się zdarzyć w kodzie, gdzie outpi inpsą dynamicznie przydzielane i są początkowo 0? Działa to np. Z p = realloc(p, len+n)kiedy pi len0. Sam użyłem takiego memcpywywołania - chociaż technicznie jest to UB, nigdy nie spotkałem implementacji, w której nie jest to operacja typu no-op i nigdy się tego nie spodziewam.
Jim Balter,
7
@templatetypedef memcpy(0, 0, 0)najprawdopodobniej ma reprezentować dynamiczne, a nie statyczne wywołanie ... tj. te wartości parametrów nie muszą być literałami.
Jim Balter,

Odpowiedzi:

71

Mam roboczą wersję standardu C (ISO / IEC 9899: 1999) i mam kilka fajnych rzeczy do powiedzenia na temat tego połączenia. Na początek, to wspomina (§7.21.1 / 2) w odniesieniu do memcpy, że

Gdzie argument zadeklarowany jako size_tn określa długość tablicy dla funkcji, n może mieć wartość zero w wywołaniu tej funkcji. O ile wyraźnie nie określono inaczej w opisie konkretnej funkcji w tym podrozdziale, argumenty wskaźników w takim wywołaniu nadal będą miały prawidłowe wartości, jak opisano w 7.1.4 . W takim wywołaniu funkcja, która lokalizuje znak, nie znajduje żadnego wystąpienia, funkcja porównująca dwie sekwencje znaków zwraca zero, a funkcja kopiująca znaki kopiuje zero znaków.

Wskazane tutaj odniesienie wskazuje na to:

Jeśli argument funkcji ma nieprawidłową wartość (na przykład wartość spoza domeny funkcji lub wskaźnik poza przestrzenią adresową programu, wskaźnik zerowy lub wskaźnik do niemodyfikowalnej pamięci, gdy odpowiedni parametr nie jest kwalifikowana jako stała) lub typ (po promocji), którego nie oczekuje funkcja ze zmienną liczbą argumentów, zachowanie jest niezdefiniowane .

Więc wygląda na to, że zgodnie ze specyfikacją C, dzwonienie

powoduje niezdefiniowane zachowanie, ponieważ puste wskaźniki są traktowane jako „nieprawidłowe wartości”.

To powiedziawszy, byłbym całkowicie zdziwiony, gdyby jakakolwiek rzeczywista implementacja memcpyzłamała się, gdybyś to zrobił, ponieważ większość intuicyjnych implementacji, o których przychodzi mi do głowy, nie zrobiłaby nic, gdybyś powiedział, że kopiujesz zero bajtów.

templatetypedef
źródło
3
Mogę potwierdzić, że cytowane części z projektu normy są identyczne w ostatecznym dokumencie. Z takim połączeniem nie powinno być żadnych problemów, ale nadal byłoby to niezdefiniowane zachowanie, na którym polegasz. Zatem odpowiedź na pytanie „czy to jest gwarantowane” brzmi „nie”.
DevSolar,
8
Żadna implementacja, której kiedykolwiek użyjesz w środowisku produkcyjnym, nie wyprodukuje niczego innego niż brak opcji dla takiego wywołania, ale implementacje, które robią inaczej, są dozwolone i rozsądne ... np. Interpreter C lub rozszerzony kompilator ze sprawdzaniem błędów, który odrzuca zadzwoń, ponieważ jest niezgodne. Oczywiście nie byłoby to rozsądne, gdyby Standard zezwalał na połączenie, tak jak ma to miejsce w przypadku realloc(0, 0). Przypadki użycia są podobne i użyłem ich obu (zobacz mój komentarz pod pytaniem). To bezcelowe i niefortunne, że Standard tworzy ten UB.
Jim Balter,
5
„Byłbym całkowicie zdziwiony, gdyby jakaś implementacja memcpy zepsuła się, gdybyś to zrobił” - użyłem takiej, która by to zrobiła; w rzeczywistości, jeśli przekazałeś długość 0 z poprawnymi wskaźnikami, w rzeczywistości skopiował on 65536 bajtów. (Jego pętla zmniejszyła długość, a następnie została przetestowana).
MM
14
@MattMcNabb Ta implementacja jest zepsuta.
Jim Balter,
3
@MattMcNabb: Być może dodaj „poprawne” do „rzeczywistych”. Myślę, że wszyscy mamy niezbyt miłe wspomnienia ze starych, gettowych bibliotek C i nie jestem pewien, ilu z nas ceni sobie te wspomnienia. :)
tmyklebu
24

Dla zabawy, uwagi do wydania dla gcc-4.9 wskazują, że jego optymalizator korzysta z tych reguł i na przykład może usunąć warunek w

który następnie daje nieoczekiwane wyniki, gdy copy(0,0,0)zostanie wywołany (patrz https://gcc.gnu.org/gcc-4.9/porting_to.html ).

Jestem nieco ambiwalentny co do zachowania gcc-4.9; zachowanie może być zgodne ze standardami, ale możliwość wywołania memmove (0,0,0) jest czasami przydatnym rozszerzeniem tych standardów.

user1998586
źródło
2
Ciekawy. Rozumiem twoją ambiwalencję, ale to jest sedno optymalizacji w C: kompilator zakłada, że programiści przestrzegają pewnych reguł, a tym samym wnioskuje, że niektóre optymalizacje są poprawne (a są, jeśli są przestrzegane).
Matthieu M.,
2
@tmyklebu: Biorąc pod uwagę char *p = 0; int i=something;, ocena wyrażenia (p+i)przyniesie niezdefiniowane zachowanie, nawet jeśli iwynosi zero.
supercat
1
@tmyklebu: Posiadanie całej arytmetyki wskaźników (innych niż porównania) w pułapce na wskaźnik zerowy byłoby dobrą rzeczą; czy memcpy()powinno się zezwolić na wykonywanie jakiejkolwiek arytmetyki wskaźnikowej na jego argumentach przed zapewnieniem niezerowej liczby, to inna kwestia [gdybym projektował standardy, prawdopodobnie określiłbym, że jeśli pjest zerowy, p+0mógłby pułapkować, ale memcpy(p,p,0)nic nie zrobiłbym]. O wiele większym problemem, IMHO, jest otwartość większości niezdefiniowanych zachowań. Chociaż jest kilka rzeczy, które naprawdę powinny reprezentować niezdefiniowane zachowanie (np. free(p)
Dzwonienie
1
... a następnie wykonując p[0]=1;) istnieje wiele rzeczy, które powinny być określone jako dające nieokreślony wynik (np. relacyjne porównanie między niepowiązanymi wskaźnikami nie powinno być określone jako zgodne z jakimkolwiek innym porównaniem, ale powinno być określone jako dające 0 lub a 1), lub powinno być określone jako dające zachowanie nieco luźniejsze niż zdefiniowane w implementacji (kompilatory powinny być zobowiązane do udokumentowania wszystkich możliwych konsekwencji np. przepełnienia liczb całkowitych, ale nie powinny określać, jakie konsekwencje wystąpiłyby w danym przypadku).
supercat
8
ktoś, proszę, powiedz mi, dlaczego nie dostanę odznaki
przepełnienia stosu
0

Możesz również rozważyć to użycie memmovewidoczne w Git 2.14.x (Q3 2017)

Zobacz zatwierdzenie 168e635 (16 lipca 2017) i zatwierdzenie 1773664 , zatwierdzenie f331ab9 , zatwierdzenie 5783980 (15 lipca 2017) autorstwa René Scharfe ( rscharfe) .
(Scalone przez Junio ​​C Hamano - gitster- w zatwierdzeniu 32f9025 , 11 sierpnia 2017)

Używa makra pomocniczego,MOVE_ARRAY które oblicza rozmiar na podstawie określonej dla nas liczby elementów i obsługuje NULL wskaźniki, gdy ta liczba wynosi zero. Wywołania
surowe memmove(3)with NULLmogą spowodować, że kompilator (zbyt chętnie) zoptymalizuje późniejsze NULLsprawdzenia.

MOVE_ARRAYdodaje bezpiecznego i wygodnego pomocnika do przenoszenia potencjalnie nakładających się zakresów wpisów tablicy.
Wychodzi z rozmiaru elementu, mnoży się automatycznie i bezpiecznie, aby uzyskać rozmiar w bajtach, przeprowadza podstawowe sprawdzenie bezpieczeństwa typu, porównując rozmiary elementów iw przeciwieństwie do memmove(3)obsługuje NULLwskaźniki, jeśli 0 elementów ma zostać przesuniętych.

Przykłady :

Używa makra,BUILD_ASSERT_OR_ZERO które zapewnia zależność od czasu kompilacji, jako wyrażenie (przy @condczym warunek czasu kompilacji musi być prawdziwy).
Kompilacja zakończy się niepowodzeniem, jeśli warunek nie jest prawdziwy lub nie może zostać oceniony przez kompilator.

Przykład:

VonC
źródło
2
Istnienie optymalizatorów, które uważają, że „sprytny” i „głupi” są antonimami, sprawia, że ​​test na n jest konieczny, ale bardziej wydajny kod byłby generalnie możliwy w przypadku implementacji, która gwarantowała, że ​​memmove (any, any, 0) nie będzie działaniem . O ile kompilator nie może zamienić wywołania memmove () wywołaniem memmoveAtLeastOneByte (), obejście chroniące przed „optymalizacją” sprytnych / głupich kompilatorów generalnie spowoduje dodatkowe porównanie, którego kompilator nie będzie w stanie wyeliminować.
supercat
-3

Nie, memcpy(0,0,0)nie jest bezpieczne. Biblioteka standardowa prawdopodobnie nie zawiedzie podczas tego wywołania. Jednak w środowisku testowym memcpy () może zawierać dodatkowy kod w celu wykrycia przepełnienia bufora i innych problemów. A jak ta specjalna wersja memcpy () reaguje na wskaźniki NULL jest… no cóż, nieokreślone.

Kai Petzke
źródło
Nie widzę niczego, co twoja odpowiedź dodaje do istniejących odpowiedzi, które już wskazywały, że nie jest to bezpieczne, z fragmentami normy.
Matthieu M.
Napisałem powód, dla którego niektóre implementacje memcpy („w środowisku testowym”) mogą prowadzić do problemów z memcpy (0, 0, 0). Myślę, że to nowość.
Kai Petzke