Qual è il comportamento della stampa NULL con l’identificatore% s di printf?

È arrivata un’interessante domanda di intervista:

test 1: printf("test %s\n", NULL); printf("test %s\n", NULL); prints: test (null) test (null) test 2: printf("%s\n", NULL); printf("%s\n", NULL); prints Segmentation fault (core dumped) 

Anche se questo potrebbe funzionare bene su alcuni sistemi, almeno il mio sta lanciando un errore di segmentazione. Quale sarebbe la migliore spiegazione di questo comportamento? Sopra il codice è in C.

Di seguito sono riportate le mie informazioni su gcc:

 deep@deep:~$ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 

Per prima cosa: printf si aspetta un puntatore valido (cioè non NULL) per il suo argomento% s, quindi passarlo a un NULL è ufficialmente indefinito. Può stampare “(null)” o può cancellare tutti i file sul disco rigido – o il comportamento è corretto per quanto riguarda ANSI (almeno, questo è quello che mi dice Harbison e Steele).

Detto questo, sì, questo è un comportamento davvero strano. Si scopre che quello che sta succedendo è che quando si fa un semplice printf come questo:

 printf("%s\n", NULL); 

gcc è ( ahem ) abbastanza intelligente da debuild questo in una call to puts . Il primo printf , questo:

 printf("test %s\n", NULL); 

è abbastanza complicato che gcc emetterà invece una chiamata a real printf .

(Si noti che gcc emette avvisi relativi all’argomento printf non valido durante la compilazione. Ciò perché molto tempo fa ha sviluppato la possibilità di analizzare stringhe di formato *printf .)

Puoi vederlo da solo compilando con l’opzione -save-temps e poi guardando attraverso il file .s risultante.

Quando ho compilato il primo esempio, ho ottenuto:

 movl $.LC0, %eax movl $0, %esi movq %rax, %rdi movl $0, %eax call printf ; < -- Actually calls printf! 

(I commenti sono stati aggiunti da me.)

Ma il secondo ha prodotto questo codice:

 movl $0, %edi ; Stores NULL in the puts argument list call puts ; Calls puts 

La cosa strana è che non stampa la nuova riga seguente. È come se capisse che questo causerà un segfault, quindi non disturba. (Che cosa ha - mi ha avvisato quando l'ho compilato.)

Per quanto riguarda il linguaggio C, la ragione è che stai invocando un comportamento indefinito e qualsiasi cosa può accadere.

Per quanto riguarda la meccanica del perché questo sta accadendo, il moderno gcc ottimizza printf("%s\n", x) a puts(x) , e puts non ha il codice sciocco da stampare (null) quando vede un puntatore nullo, considerando che le implementazioni comuni di printf hanno questo caso speciale. Dato che gcc non è in grado di ottimizzare (in generale) stringhe di formato non banali come questo, in realtà printf viene chiamato quando la stringa di formato contiene altro testo.

La Sezione 7.1.4 (di C99 o C11) dice:

§7.1.4 Uso delle funzioni di libreria

¶1 Ognuna delle seguenti istruzioni si applica a meno che non venga esplicitamente indicato diversamente nelle descrizioni dettagliate che seguono: Se un argomento di una funzione ha un valore non valido (ad esempio un valore esterno al dominio della funzione o un puntatore al di fuori dello spazio indirizzo del programma, o un puntatore nullo, o un puntatore alla memoria non modificabile quando il parametro corrispondente non è const-qualificato) o un tipo (dopo la promozione) non previsto da una funzione con numero variabile di argomenti, il comportamento non è definito.

Poiché la specifica di printf() non dice nulla su ciò che accade quando si passa un puntatore nullo ad esso per lo specificatore %s , il comportamento è esplicitamente indefinito. (Si noti che passare un puntatore nullo che deve essere stampato dallo specificatore %p non è un comportamento non definito).

Ecco il “capitolo e versetto” per il comportamento della famiglia fprintf() (C2011 – è un numero di sezione diverso in C1999):

§7.21.6.1 La funzione fprintf

s Se non è presente alcun modificatore di lunghezza l , l’argomento deve essere un puntatore all’elemento iniziale di un array di tipo di carattere. […]

Se è presente un modificatore di lunghezza l , l’argomento deve essere un puntatore all’elemento iniziale di una matrice di tipo wchar_t.

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

Le specifiche per lo specifier di conversione s precludono la possibilità che un puntatore nullo sia valido poiché il puntatore nullo non punta all’elemento iniziale di una matrice del tipo appropriato. Le specifiche per lo specificatore di conversione p non richiedono che il puntatore del vuoto indichi qualcosa in particolare e NULL sia quindi valido.

Il fatto che molte implementazioni stampino una stringa come (null) quando viene passato un puntatore nullo è una gentilezza su cui è pericoloso fare affidamento. La bellezza del comportamento indefinito è che tale risposta è consentita, ma non è richiesta. Allo stesso modo, un incidente è permesso, ma non è richiesto (più è il peccato – le persone vengono morse se lavorano su un sistema indulgente e poi portano ad altri sistemi meno tolleranti).

Il puntatore NULL non punta a nessun indirizzo e il tentativo di stamparlo causa un comportamento non definito. Significato non definito spetta al compilatore o alla libreria C decidere cosa fare quando tenta di stampare NULL.