Drukowanie zerowych wskaźników z% p jest niezdefiniowanym zachowaniem?

93

Czy drukowanie wskaźników o wartości null ze specyfikatorem %pkonwersji jest niezdefiniowane ?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Pytanie dotyczy standardu C, a nie implementacji C.

Dror K.
źródło
A właściwie nie myśl, że komukolwiek (w tym komitetowi C) zbytnio to obchodzi. Jest to dość sztuczny problem, który nie ma (lub prawie żadnego) znaczenia praktycznego.
P__J wspiera kobiety w Polsce
jest tak, jak printf wyświetla tylko wartość i nie dotyka (w sensie czytania lub pisania wskazanego obiektu) - nie może być UB i wskaźnik ma poprawną wartość dla swojego typu (NULL to poprawna wartość)
P__J wspiera kobiety w Polska
3
@PeterJ powiedzmy, że to, co mówisz, jest prawdą (chociaż norma wyraźnie mówi inaczej), sam fakt, że debatujemy nad tym, sprawia, że ​​pytanie jest ważne i poprawne, ponieważ wygląda na to, że zacytowana poniżej część normy sprawia, że dla zwykłego dewelopera bardzo trudno jest zrozumieć, co się dzieje .. Znaczenie: pytanie nie zasługuje na głosowanie negatywne, ponieważ ten problem wymaga wyjaśnienia!
Peter Varo,
2
@PeterJ to inna historia, dzięki za wyjaśnienie :)
Peter Varo,

Odpowiedzi:

93

Jest to jeden z tych dziwnych przypadków narożnych, w których podlegamy ograniczeniom języka angielskiego i niespójnej strukturze w standardzie. W najlepszym razie mogę przedstawić przekonujący kontrargument, którego nie da się udowodnić :) 1


Kod w pytaniu wykazuje dobrze zdefiniowane zachowanie.

Ponieważ podstawą pytania jest [7.1.4] , zacznijmy od tego:

Każda z poniższych instrukcji ma zastosowanie, chyba że wyraźnie określono inaczej w poniższych szczegółowych opisach: Jeśli argument funkcji ma nieprawidłową wartość ( np. Wartość spoza domeny funkcji lub wskaźnik poza przestrzenią adresową programu, lub pusty wskaźnik , [... inne przykłady ...] ) [...] zachowanie jest niezdefiniowane. [... inne oświadczenia ...]

To jest niezdarny język. Jedną z interpretacji jest to, że pozycje na liście są UB dla wszystkich funkcji bibliotecznych, chyba że zostaną zastąpione przez indywidualne opisy. Ale lista zaczyna się od „takich jak”, co oznacza, że ​​ma charakter poglądowy, a nie wyczerpujący. Na przykład nie wspomina o poprawnym zakończeniu zerowania ciągów (krytycznych dla zachowania np strcpy.).

Dlatego jasne jest, że celem / zakresem 7.1.4 jest po prostu to, że „nieprawidłowa wartość” prowadzi do UB ( chyba że zaznaczono inaczej ). Musimy spojrzeć na opis każdej funkcji, aby określić, co liczy się jako „nieprawidłowa wartość”.

Przykład 1 - strcpy

[7.21.2.3] mówi tylko tak:

W strcpykopiuje łańcuch znaków wskazywany przez s2(łącznie z kończącym znakiem null) do tablicy wskazywanej przez s1. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.

Nie wspomina wyraźnie o zerowych wskaźnikach, ale nie wspomina też o zerowych terminatorach. Zamiast tego można wywnioskować z „łańcucha wskazywanego przez s2”, że jedynymi poprawnymi wartościami są łańcuchy (tj. Wskaźniki do tablic znaków zakończonych znakiem null).

Rzeczywiście, ten wzór można zobaczyć w poszczególnych opisach. Kilka innych przykładów:

  • [7.6.4.1 (fenv)] przechowuje bieżące środowisko zmiennoprzecinkowe w obiekcie wskazywanym przezenvp

  • [7.12.6.4 (frexp)] przechowuje liczbę całkowitą w obiekcie int wskazywanym przezexp

  • [7.19.5.1 (fclose)] strumienia wskazywanego przezstream

Przykład 2 - printf

[7.19.6.1] mówi tak o %p:

p- Argument będzie wskaźnikiem do void. Wartość wskaźnika jest konwertowana na sekwencję drukowanych znaków w sposób zdefiniowany w implementacji.

Null jest prawidłową wartością wskaźnika, aw tej sekcji nie ma wyraźnej wzmianki, że null jest przypadkiem specjalnym, ani że wskaźnik musi wskazywać na obiekt. Tak więc jest to określone zachowanie.


1. Chyba że zgłosi się autor standardów lub jeśli nie znajdziemy czegoś podobnego do dokumentu uzasadniającego wyjaśnienia.

Oliver Charlesworth
źródło
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Bhargav Rao
1
„jeszcze nie wspomina o terminatorach zerowych” jest słabe w przykładzie 1 - strcpy, ponieważ specyfikacja mówi, że „kopiuje łańcuch ”. string jest jawnie zdefiniowany jako posiadający znak null .
chux - Przywróć Monikę
1
@chux - w pewnym sensie o to mi chodzi - należy wywnioskować, co jest prawidłowe / nieprawidłowe z kontekstu, zamiast zakładać, że lista w 7.1.4 jest wyczerpująca. (Jednak istnienie tej części mojej odpowiedzi wykonany nieco więcej sensu w kontekście uwag, które zostały już usunięte, twierdząc, że był strcpy kontrprzykład.)
Oliver Charlesworth
1
Sednem problemu jest to, jak czytelnik zinterpretuje takie pliki . Czy to oznacza, że istnieją przykłady możliwych nieprawidłowych wartości ? Czy to oznacza, że istnieją przykłady, które zawsze są niepoprawnymi wartościami ? Dla przypomnienia posłużę się pierwszą interpretacją.
ninjalj,
1
@ninjalj - Tak, zgadzam się. Zasadniczo to właśnie próbuję przekazać w mojej odpowiedzi, tj. „Są to przykłady typów rzeczy, które mogą być nieprawidłowymi wartościami”. :)
Oliver Charlesworth,
20

Krótka odpowiedź

Tak . Drukowanie wskaźników zerowych ze specyfikatorem %pkonwersji ma niezdefiniowane zachowanie. Powiedziawszy to, nie znam żadnej istniejącej implementacji, która mogłaby się źle zachować.

Odpowiedź dotyczy dowolnego z norm C (C89 / C99 / C11).


Długa odpowiedź

Specyfikator %pkonwersji oczekuje argumentu typu wskaźnik na void, konwersja wskaźnika do drukowalnych znaków jest zdefiniowana przez implementację. Nie stwierdza, że ​​oczekiwany jest pusty wskaźnik.

We wprowadzeniu do funkcji biblioteki standardowej stwierdza się, że wskaźniki puste jako argumenty funkcji (biblioteki standardowej) są uważane za nieprawidłowe wartości, chyba że wyraźnie określono inaczej.

C99 / C11 §7.1.4 p1

[...] Jeśli argument funkcji ma nieprawidłową wartość (np. [...] pusty wskaźnik, [...] zachowanie jest niezdefiniowane.

Przykłady funkcji (biblioteki standardowej), które oczekują zerowych wskaźników jako prawidłowych argumentów:

  • fflush() używa pustego wskaźnika do opróżniania „wszystkich strumieni” (które mają zastosowanie).
  • freopen() używa pustego wskaźnika do wskazania pliku „aktualnie skojarzonego” ze strumieniem.
  • snprintf() umożliwia przekazanie pustego wskaźnika, gdy „n” jest równe zero.
  • realloc() używa pustego wskaźnika do przydzielania nowego obiektu.
  • free() pozwala na przekazanie pustego wskaźnika.
  • strtok() używa pustego wskaźnika dla kolejnych wywołań.

Jeśli weźmiemy przypadek za snprintf(), sensowne jest zezwolenie na przekazanie wskaźnika zerowego, gdy „n” jest równe zero, ale nie ma to miejsca w przypadku innych funkcji (biblioteki standardowej), które dopuszczają podobne zero „n”. Na przykład: memcpy(), memmove(), strncpy(), memset(), memcmp().

Jest to określone nie tylko we wstępie do biblioteki standardowej, ale także raz jeszcze we wstępie do tych funkcji:

C99 §7.21.1 p2 / C11 §7.24.1 p2

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.


Czy to jest zamierzone?

Nie wiem, czy UB %pze wskaźnikiem zerowym jest w rzeczywistości celowe, ale ponieważ standard wyraźnie stwierdza, że ​​wskaźniki zerowe są uważane za nieprawidłowe wartości jako argumenty funkcji biblioteki standardowej, a następnie idzie i wyraźnie określa przypadki, w których wartość null wskaźnik jest ważny argument (snprintf, wolna, itd), a następnie idzie i po raz kolejny powtarza wymóg argumenty ważność nawet w zera „n” przypadki ( memcpy, memmove, memset), to myślę, że to rozsądne założenie, że Komisja normalizacyjna C nie przejmuje się zbytnio niezdefiniowaniem takich rzeczy.

Dror K.
źródło
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Bhargav Rao
1
@JeroenMostert: Jaki jest cel tego argumentu? Cytat z 7.1.4 jest raczej jasny, prawda? O co można się spierać, „chyba że wyraźnie zaznaczono inaczej”, kiedy nie jest powiedziane inaczej? Czego można się spierać, jeśli chodzi o fakt, że (niepowiązana) biblioteka funkcji łańcuchowych ma podobne sformułowanie, więc sformułowanie nie wydaje się być przypadkowe? Myślę, że ta odpowiedź (chociaż nie jest zbyt przydatna w praktyce ) jest tak poprawna, jak to tylko możliwe.
Damon,
3
@Damon: Twój mityczny sprzęt nie jest mityczny, istnieje wiele architektur, w których wartości, które nie reprezentują prawidłowych adresów, mogą nie zostać załadowane do rejestrów adresów. Przekazywanie zerowych wskaźników jako argumentów funkcji jest jednak nadal wymagane do działania na tych platformach jako ogólny mechanizm. Samo umieszczenie jednego na stosie nie spowoduje wybuchu.
Jeroen Mostert
1
@anatolyg: w procesorach x86 adresy mają dwie części - segment i przesunięcie. W 8086 ładowanie rejestru segmentowego jest jak ładowanie każdego innego, ale na wszystkich późniejszych maszynach pobiera deskryptor segmentu. Załadowanie nieprawidłowego deskryptora powoduje pułapkę. Wiele kodu dla procesorów 80386 i późniejszych, jednak używa tylko jednego segmentu, a więc nigdy nie ładuje rejestry segmentowe w ogóle .
supercat,
1
Myślę, że wszyscy zgodziliby się, że drukowanie pustego wskaźnika %pnie powinno być niezdefiniowanym zachowaniem
MM
-1

Autorzy standardu C nie starali się wyczerpująco wyszczególnić wszystkich wymagań behawioralnych, które implementacja musi spełnić, aby nadawała się do określonego celu. Zamiast tego spodziewali się, że ludzie piszący kompilatory wykażą się pewnym zdrowym rozsądkiem, niezależnie od tego, czy standard tego wymaga, czy nie.

Pytanie, czy coś wywołuje UB, rzadko jest samo w sobie użyteczne. Prawdziwe ważne kwestie to:

  1. Czy ktoś, kto próbuje napisać wysokiej jakości kompilator, powinien zachowywać się w przewidywalny sposób? W przypadku opisywanego scenariusza odpowiedź brzmi zdecydowanie tak.

  2. Czy programiści powinni mieć prawo oczekiwać, że wysokiej jakości kompilatory dla wszystkiego, co przypomina zwykłe platformy, będą zachowywać się w przewidywalny sposób? W opisanym scenariuszu powiedziałbym, że tak.

  3. Czy niektórzy tępi autorzy kompilatorów mogą rozciągnąć interpretację standardu, aby usprawiedliwić zrobienie czegoś dziwnego? Mam nadzieję, że nie, ale nie wykluczam tego.

  4. Czy kompilatory dezynfekujące powinny wrzeszczeć o tym zachowaniu? To zależałoby od poziomu paranoi ich użytkowników; kompilator czyszczący prawdopodobnie nie powinien domyślnie skrzeczać o takim zachowaniu, ale być może zapewniać opcję konfiguracji na wypadek, gdyby programy mogły zostać przeniesione do "sprytnych" / głupich kompilatorów, które zachowują się dziwnie.

Jeśli rozsądna interpretacja normy oznaczałaby, że zdefiniowano zachowanie, ale niektórzy autorzy kompilatorów rozciągają tę interpretację, aby uzasadnić postępowanie w inny sposób, czy to naprawdę ma znaczenie, co mówi standard?

supercat
źródło
1. Często zdarza się, że programiści uważają założenia przyjęte przez nowoczesnych / agresywnych optymalizatorów za sprzeczne z tym, co uważają za „rozsądne” lub „jakościowe”. 2. Jeśli chodzi o niejasności w specyfikacji, nierzadko zdarza się, że realizatorzy nie zgadzają się co do przysługujących im swobód. 3. Jeśli chodzi o członków komitetu normalizacyjnego C, nawet oni nie zawsze zgadzają się co do „poprawnej” interpretacji, nie mówiąc już o tym, jaka powinna być. Biorąc pod uwagę powyższe, za czyją rozsądną interpretację powinniśmy się kierować?
Dror K.,
6
Odpowiedź na pytanie „czy ten konkretny fragment kodu wywołuje UB czy nie” z rozprawą na temat tego, co myślisz o użyteczności UB lub jak kompilatory powinny się zachować, jest kiepską próbą odpowiedzi, zwłaszcza że możesz skopiować i wkleić to jako odpowiedź na prawie każde pytanie dotyczące konkretnego UB. Jako powtórzenie twojego retorycznego rozkwitu: tak, to naprawdę ważne, co mówi Standard, bez względu na to, co robią niektórzy twórcy kompilatorów lub co o nich myślisz, ponieważ to robią, ponieważ Standard jest tym, od czego zaczynają zarówno programiści, jak i autorzy kompilatorów.
Jeroen Mostert
1
@JeroenMostert: Odpowiedź na pytanie „Czy X wywołuje niezdefiniowane zachowanie” będzie często zależeć od tego, co rozumie się przez pytanie. Jeśli uważa się, że program ma niezdefiniowane zachowanie, a norma nie nakładałaby żadnych wymagań na zachowanie zgodnej implementacji, to prawie wszystkie programy wywołują UB. Autorzy Standardu wyraźnie pozwalają implementacjom zachowywać się w dowolny sposób, jeśli program zagnieździ wywołania funkcji zbyt głęboko, o ile implementacja może poprawnie przetworzyć co najmniej jeden (prawdopodobnie wymyślony) tekst źródłowy, który korzysta z ograniczeń tłumaczenia w Stadardzie.
supercat
@supercat: bardzo interesujące, ale czy printf("%p", (void*) 0)nieokreślone zachowanie jest zgodne ze standardem? Głęboko zagnieżdżone wywołania funkcji są tak samo istotne jak cena herbaty w Chinach. I tak, UB jest bardzo powszechne w programach w świecie rzeczywistym - co z tego?
Jeroen Mostert,
1
@JeroenMostert: Ponieważ Standard pozwoliłby tępej implementacji traktować prawie każdy program jako posiadający UB, to, co powinno mieć znaczenie, będzie zachowanie implementacji bez tępych. Jeśli nie zauważyłeś, nie napisałem tylko kopii / wklej o UB, ale odpowiedziałem na pytanie dotyczące %pkażdego możliwego znaczenia pytania.
supercat