La stampa di puntatori nulli con% p è un comportamento indefinito?


93

È un comportamento indefinito stampare puntatori nulli con l'identificatore di %pconversione?

#include <stdio.h>

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

    printf("%p", p);

    return 0;
}

La domanda si applica allo standard C e non alle implementazioni C.


A non credo che a nessuno (compreso il comitato C) importi troppo. È un problema abbastanza artificioso, senza (o quasi) significato pratico.
P__J__

è come printf visualizza solo il valore e non tocca (nel significato di leggere o scrivere l'oggetto appuntito) - non può essere UB i puntatore ha un valore valido per il suo tipo valore (NULL è il valore valido )
P__J__

3
@PeterJ diciamo che quello che stai dicendo è vero (anche se chiaramente lo standard afferma il contrario), il solo fatto che stiamo discutendo su questo rende la domanda valida e corretta, poiché sembra che la parte citata di seguito dello standard fa è molto difficile capire per uno sviluppatore normale cosa diavolo sta succedendo .. Significato: la domanda non merita il voto negativo, perché questo problema richiede chiarimenti!
Peter Varo


2
@PeterJ questa è una storia diversa quindi, grazie per il chiarimento :)
Peter Varo

Risposte:


93

Questo è uno di quegli strani casi d'angolo in cui siamo soggetti alle limitazioni della lingua inglese e alla struttura incoerente nello standard. Quindi, nella migliore delle ipotesi, posso fare una controargomentazione convincente, poiché è impossibile dimostrarlo :) 1


Il codice nella domanda mostra un comportamento ben definito.

Poiché [7.1.4] è la base della domanda, iniziamo da lì:

Ognuna delle seguenti affermazioni si applica a meno che non sia diversamente specificato nelle descrizioni dettagliate che seguono: Se un argomento di una funzione ha un valore non valido ( come un valore al di fuori del dominio della funzione, o un puntatore al di fuori dello spazio degli indirizzi del programma, o un puntatore nullo , [... altri esempi ...] ) [...] il comportamento è indefinito. [... altre dichiarazioni ...]

Questo è un linguaggio goffo. Un'interpretazione è che gli elementi nell'elenco sono UB per tutte le funzioni di libreria, a meno che non vengano sovrascritti dalle singole descrizioni. Ma l'elenco inizia con "come", a indicare che è illustrativo, non esaustivo. Ad esempio, non menziona la corretta terminazione nulla delle stringhe (fondamentale per il comportamento di eg strcpy).

Quindi è chiaro che lo scopo / ambito di 7.1.4 è semplicemente che un "valore non valido" porta a UB ( se non diversamente specificato ). Dobbiamo guardare alla descrizione di ogni funzione per determinare cosa conta come un "valore non valido".

Esempio 1 - strcpy

[7.21.2.3] dice solo questo:

La strcpyfunzione copia la stringa a cui punta s2(incluso il carattere null di terminazione) nell'array a cui punta s1. Se la copia avviene tra oggetti che si sovrappongono, il comportamento è indefinito.

Non fa menzione esplicita di puntatori nulli, ma non fa nemmeno menzione di terminatori nulli. Invece, si deduce da "stringa puntata da s2" che gli unici valori validi sono le stringhe (cioè i puntatori a matrici di caratteri con terminazione null).

In effetti, questo modello può essere visto in tutte le descrizioni individuali. Alcuni altri esempi:

  • [7.6.4.1 (fenv)] memorizza l'ambiente in virgola mobile corrente nell'oggetto puntato daenvp

  • [7.12.6.4 (frexp)] memorizza il numero intero nell'oggetto int puntato daexp

  • [7.19.5.1 (fclose)] lo stream puntato dastream

Esempio 2 - printf

[7.19.6.1] dice questo su %p:

p- L'argomento deve essere un puntatore a void. Il valore del puntatore viene convertito in una sequenza di caratteri di stampa, in un modo definito dall'implementazione.

Null è un valore di puntatore valido e questa sezione non menziona esplicitamente che null è un caso speciale, né che il puntatore deve puntare a un oggetto. Quindi è definito comportamento.


1. A meno che non si faccia avanti un autore di standard, oa meno che non riusciamo a trovare qualcosa di simile a un documento razionale che chiarisca le cose.


I commenti non sono per discussioni estese; questa conversazione è stata spostata nella chat .
Bhargav Rao

1
"ma non fa menzione di terminatori nulli" è debole nell'esempio 1 - strcpy poiché la specifica dice "copia la stringa ". la stringa è definita esplicitamente come avente un carattere null .
chux - Ripristina Monica il

1
@chux - Questo è in qualche modo il mio punto - si deve dedurre ciò che è valido / non valido dal contesto, piuttosto che presumere che l'elenco in 7.1.4 sia esaustivo. (Tuttavia, l'esistenza di questa parte della mia risposta aveva più senso nel contesto dei commenti che da allora sono stati cancellati, sostenendo che strcpy era un controesempio.)
Oliver Charlesworth

1
Il nocciolo della questione è come il lettore interpreterà come . Significa che alcuni esempi di possibili valori non validi sono ? Significa che alcuni esempi che sono sempre valori non validi lo sono ? Per la cronaca, vado con la prima interpretazione.
ninjalj

1
@ninjalj - Sì, d'accordo. Questo è essenzialmente ciò che sto cercando di trasmettere nella mia risposta qui, cioè "questi sono esempi di tipi di cose che potrebbero essere valori non validi". :)
Oliver Charlesworth

20

La risposta breve

. La stampa di puntatori nulli con l'identificatore di %pconversione ha un comportamento indefinito. Detto questo, non sono a conoscenza di alcuna implementazione conforme esistente che si comporterebbe male.

La risposta si applica a qualsiasi standard C (C89 / C99 / C11).


La lunga risposta

Lo %pspecificatore di conversione si aspetta che un argomento di tipo pointer sia void, la conversione del puntatore in caratteri stampabili è definita dall'implementazione. Non afferma che è previsto un puntatore nullo.

L'introduzione alle funzioni della libreria standard afferma che i puntatori nulli come argomenti alle funzioni (della libreria standard) sono considerati valori non validi, a meno che non sia esplicitamente dichiarato diversamente.

C99 / C11 §7.1.4 p1

[...] Se un argomento di una funzione ha un valore non valido (come [...] un puntatore nullo, [...] il comportamento è indefinito.

Esempi di funzioni (libreria standard) che prevedono puntatori nulli come argomenti validi:

  • fflush() usa un puntatore nullo per scaricare "tutti i flussi" (che si applicano).
  • freopen() utilizza un puntatore nullo per indicare il file "attualmente associato" al flusso.
  • snprintf() permette di passare un puntatore nullo quando 'n' è zero.
  • realloc() usa un puntatore nullo per allocare un nuovo oggetto.
  • free() permette di passare un puntatore nullo.
  • strtok() utilizza un puntatore nullo per le chiamate successive.

Se prendiamo il caso per snprintf(), ha senso consentire il passaggio di un puntatore nullo quando 'n' è zero, ma questo non è il caso per altre funzioni (libreria standard) che consentono uno zero simile 'n'. Ad esempio: memcpy(), memmove(), strncpy(), memset(), memcmp().

Non è specificato solo nell'introduzione alla libreria standard, ma anche nell'introduzione a queste funzioni:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Dove un argomento dichiarato come size_tn specifica la lunghezza dell'array per una funzione, n può avere il valore zero in una chiamata a quella funzione. A meno che non sia esplicitamente dichiarato diversamente nella descrizione di una particolare funzione in questo sottoclausola, gli argomenti del puntatore su tale chiamata devono ancora avere valori validi come descritto in 7.1.4.


È intenzionale?

Non so se l'UB di %pcon un puntatore nullo sia in realtà intenzionale, ma poiché lo standard afferma esplicitamente che i puntatori nulli sono considerati valori non validi come argomenti per le funzioni di libreria standard, quindi va e specifica esplicitamente i casi in cui un nullo puntatore è un argomento valido (snprintf, libero, ecc), e poi va e ancora una volta si ripete l'obbligo per gli argomenti siano valide anche in zero 'n' casi ( memcpy, memmove, memset), quindi penso che sia ragionevole supporre che la Il comitato per gli standard C non si preoccupa troppo di avere queste cose indefinite.


I commenti non sono per discussioni estese; questa conversazione è stata spostata nella chat .
Bhargav Rao

1
@ JeroenMostert: qual è lo scopo di questo argomento? La citazione data di 7.1.4 è piuttosto chiara, non è vero? Cosa c'è da discutere "a meno che non sia esplicitamente dichiarato diversamente" quando non viene affermato diversamente? Cosa c'è da discutere sul fatto che la libreria di funzioni stringa (non correlata) abbia una dicitura simile, quindi la dicitura non sembra essere accidentale? Penso che questa risposta (sebbene non molto utile nella pratica ) sia la più corretta possibile.
Damon

3
@Damon: il tuo hardware mitico non è mitico, ci sono molte architetture in cui i valori che non rappresentano indirizzi validi potrebbero non essere caricati nei registri degli indirizzi. Tuttavia, il passaggio di puntatori null come argomenti di funzione è ancora necessario per funzionare su quelle piattaforme come meccanismo generale. Il semplice fatto di metterne uno in pila non farà saltare le cose.
Jeroen Mostert

1
@anatolyg: sui processori x86, gli indirizzi hanno due parti: un segmento e un offset. Sull'8086, caricare un registro di segmento è come caricare qualsiasi altro, ma su tutte le macchine successive recupera un descrittore di segmento. Il caricamento di un descrittore non valido causa una trap. Un sacco di codice per i processori 80386 e più tardi, tuttavia, utilizza solo un segmento, e quindi mai carichi registri di segmento a tutti .
supercat

1
Penso che tutti sarebbero d'accordo sul fatto che la stampa di un puntatore nullo con %pnon dovrebbe essere un comportamento indefinito
MM

-1

Gli autori dello standard C non hanno fatto alcuno sforzo per elencare in modo esaustivo tutti i requisiti comportamentali che un'implementazione deve soddisfare per essere adatta a uno scopo particolare. Invece, si aspettavano che le persone che scrivono compilatori esercitassero una certa dose di buon senso indipendentemente dal fatto che lo Standard lo richiedesse o meno.

La questione se qualcosa invoca UB è raramente utile di per sé. Le vere domande importanti sono:

  1. Qualcuno che sta cercando di scrivere un compilatore di qualità dovrebbe farlo comportarsi in modo prevedibile? Per lo scenario descritto la risposta è chiaramente sì.

  2. I programmatori dovrebbero avere il diritto di aspettarsi che compilatori di qualità per qualcosa di simile a piattaforme normali si comportino in modo prevedibile? Nello scenario descritto, direi che la risposta è sì.

  3. Potrebbero alcuni ottusi scrittori di compilatori estendere l'interpretazione dello Standard in modo da giustificare il fatto di fare qualcosa di strano? Spero di no, ma non lo escludo.

  4. I compilatori disinfettanti dovrebbero lamentarsi del comportamento? Ciò dipenderebbe dal livello di paranoia dei loro utenti; un compilatore disinfettante probabilmente non dovrebbe lamentarsi per impostazione predefinita di tale comportamento, ma forse fornire un'opzione di configurazione da fare nel caso in cui i programmi possano essere portati su compilatori "intelligenti" / stupidi che si comportano in modo strano.

Se un'interpretazione ragionevole dello Standard implicherebbe la definizione di un comportamento, ma alcuni autori di compilatori estendono l'interpretazione per giustificare il fatto di fare diversamente, è davvero importante quello che dice lo Standard?


1. Non è raro che i programmatori trovino le ipotesi fatte da ottimizzatori moderni / aggressivi in ​​contrasto con ciò che considerano "ragionevole" o "di qualità". 2. Quando si tratta di ambiguità nella specifica, non è raro che gli implementatori siano in disaccordo su quali libertà possono assumere. 3. Quando si tratta di membri del comitato per gli standard C, anche loro non sono sempre d'accordo su quale sia l'interpretazione "corretta", figuriamoci su quale dovrebbe essere. Considerato quanto sopra, quale ragionevole interpretazione dovremmo seguire?
Dror K.

6
Rispondere alla domanda "questo particolare pezzo di codice richiama UB o no" con una dissertazione su ciò che pensi dell'utilità di UB o su come dovrebbero comportarsi i compilatori è un misero tentativo di risposta, soprattutto perché puoi copiare e incollare questo come una risposta a quasi tutte le domande su un particolare UB. Come controreplica al tuo fiorire retorico: sì, è davvero importante quello che dice lo Standard, non importa quello che fanno alcuni autori di compilatori o cosa pensi di loro per farlo, perché lo Standard è ciò da cui partono sia i programmatori che gli autori di compilatori.
Jeroen Mostert

1
@ JeroenMostert: La risposta a "X invoca un comportamento indefinito" dipenderà spesso da cosa si intende per domanda. Se si considera che un programma abbia un comportamento indefinito se lo standard non impone requisiti sul comportamento di un'implementazione conforme, quasi tutti i programmi invocano UB. Gli autori dello Standard consentono chiaramente alle implementazioni di comportarsi in modo arbitrario se un programma annida chiamate alla funzione troppo profondamente, a condizione che un'implementazione possa elaborare correttamente almeno un testo sorgente (possibilmente inventato) che eserciti i limiti di traduzione nello Stadard.
supercat

@supercat: molto interessante, ma è un printf("%p", (void*) 0)comportamento indefinito o no, secondo lo Standard? Le chiamate di funzione profondamente annidate sono importanti quanto il prezzo del tè in Cina. E sì, UB è molto comune nei programmi del mondo reale - che ne è?
Jeroen Mostert

1
@JeroenMostert: Poiché lo Standard consentirebbe a un'implementazione ottusa di considerare quasi tutti i programmi come dotati di UB, ciò che dovrebbe importare sarà il comportamento delle implementazioni non ottuse. Nel caso non l'avessi notato, non ho solo scritto un copia / incolla su UB, ma ho risposto alla domanda %pper ogni possibile significato della domanda.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.