Ridefinire NULL


118

Sto scrivendo codice C per un sistema in cui l'indirizzo 0x0000 è valido e contiene la porta I / O. Pertanto, eventuali bug che accedono a un puntatore NULL non verranno rilevati e allo stesso tempo causeranno comportamenti pericolosi.

Per questo motivo desidero ridefinire NULL come un altro indirizzo, ad esempio un indirizzo non valido. Se accedo accidentalmente a tale indirizzo, riceverò un interrupt hardware in cui posso gestire l'errore. Mi capita di avere accesso a stddef.h per questo compilatore, quindi posso effettivamente modificare l'intestazione standard e ridefinire NULL.

La mia domanda è: questo sarà in conflitto con lo standard C? Per quanto posso dire dalla 7.17 dello standard, la macro è definita dall'implementazione. C'è qualcos'altro nello standard che afferma che NULL deve essere 0?

Un altro problema è che molti compilatori eseguono l'inizializzazione statica impostando tutto su zero, indipendentemente dal tipo di dati. Anche se lo standard dice che il compilatore dovrebbe impostare gli interi a zero e i puntatori a NULL. Se dovessi ridefinire NULL per il mio compilatore, allora so che tale inizializzazione statica fallirà. Potrei considerarlo un comportamento errato del compilatore anche se ho alterato in modo audace manualmente le intestazioni del compilatore? Perché so per certo che questo particolare compilatore non accede alla macro NULL durante l'inizializzazione statica.


3
Questa è davvero una buona domanda. Non ho una risposta per te, ma devo chiederti: sei sicuro che non sia possibile spostare la tua roba valida a 0x00 e lasciare che NULL sia un indirizzo non valido come nei sistemi "normali"? Se non è possibile, gli unici indirizzi non validi in modo sicuro da utilizzare sarebbero quelli che puoi essere sicuro di poter allocare e quindi mprotectproteggere. Oppure, se la piattaforma non ha ASLR o simili, indirizzi oltre la memoria fisica della piattaforma. In bocca al lupo.
Borealid

8
Come funzionerà se il tuo codice sta usando if(ptr) { /* do something on ptr*/ }? Funzionerà se NULL è definito diverso da 0x0?
Xavier T.

3
Il puntatore C non ha alcuna relazione forzata con gli indirizzi di memoria. Finché vengono rispettate le regole dell'aritmetica dei puntatori, il valore di un puntatore può essere qualsiasi cosa. La maggior parte delle implementazioni sceglie di utilizzare gli indirizzi di memoria come valori di puntatore, ma potrebbero utilizzare qualsiasi cosa purché sia ​​un isomorfismo.
datenwolf

2
@bdonlan Ciò violerebbe anche le regole (consultive) in MISRA-C.
Lundin

2
@Andreas Sì, anche questo è il mio pensiero. Le persone che si occupano di hardware non dovrebbero essere autorizzate a progettare hardware su cui il software dovrebbe funzionare! :)
Lundin

Risposte:


84

Lo standard C non richiede che i puntatori nulli siano all'indirizzo zero della macchina. TUTTAVIA, 0eseguire il cast di una costante su un valore di puntatore deve risultare in un NULLpuntatore (§6.3.2.3 / 3) e la valutazione del puntatore nullo come booleano deve essere falsa. Questo può essere un po 'scomodo se davvero non vuole un indirizzo pari a zero, e NULLnon è l'indirizzo zero.

Tuttavia, con (pesanti) modifiche al compilatore e alla libreria standard, non è impossibile NULLessere rappresentati con uno schema di bit alternativo pur rimanendo rigorosamente conformi alla libreria standard. Tuttavia, non è sufficiente cambiare semplicemente la definizione di NULLse stesso, poiché in tal NULLcaso si restituirebbe vero.

In particolare, dovresti:

  • Fai in modo che gli zeri letterali nelle assegnazioni ai puntatori (o cast ai puntatori) vengano convertiti in qualche altro valore magico come -1.
  • Predisporre test di uguaglianza tra i puntatori e un numero intero costante 0per verificare invece il valore magico (§6.5.9 / 6)
  • Disporre per tutti i contesti in cui un tipo di puntatore viene valutato come booleano per verificare l'uguaglianza del valore magico invece di verificare lo zero. Ciò deriva dalla semantica del test di uguaglianza, ma il compilatore può implementarlo internamente in modo diverso. Vedere §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Come sottolineato da caf, aggiorna la semantica per l'inizializzazione di oggetti statici (§6.7.8 / 10) e inizializzatori composti parziali (§6.7.8 / 21) per riflettere la nuova rappresentazione del puntatore nullo.
  • Creare un modo alternativo per accedere al vero indirizzo zero.

Ci sono alcune cose che non devi gestire. Per esempio:

int x = 0;
void *p = (void*)x;

Dopo questo, pNON è garantito che sia un puntatore nullo. È necessario gestire solo assegnazioni costanti (questo è un buon approccio per accedere al vero indirizzo zero). Allo stesso modo:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Anche:

void *p = NULL;
int x = (int)p;

xnon è garantito che lo sia 0.

In breve, questa stessa condizione è stata apparentemente considerata dal comitato del linguaggio C, e sono state fatte considerazioni per coloro che avrebbero scelto una rappresentazione alternativa per NULL. Tutto quello che devi fare ora è apportare modifiche importanti al tuo compilatore, e presto il gioco è fatto :)

Come nota a margine, potrebbe essere possibile implementare queste modifiche con una fase di trasformazione del codice sorgente prima del compilatore vero e proprio. Cioè, invece del normale flusso di preprocessore -> compilatore -> assemblatore -> linker, dovresti aggiungere un preprocessore -> trasformazione NULL -> compilatore -> assemblatore -> linker. Quindi potresti fare trasformazioni come:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Ciò richiederebbe un parser C completo, nonché un parser di tipo e un'analisi di typedef e dichiarazioni di variabili per determinare quali identificatori corrispondono ai puntatori. Tuttavia, in questo modo si potrebbe evitare di dover apportare modifiche alle parti di generazione del codice del compilatore corretto. clang può essere utile per implementarlo: capisco che sia stato progettato pensando a trasformazioni come questa. È probabile che tu debba comunque apportare modifiche anche alla libreria standard, ovviamente.


2
Ok, non avevo trovato il testo in §6.3.2.3, ma sospettavo che da qualche parte ci sarebbe stata una simile dichiarazione :). Immagino che questo risponda alla mia domanda, per lo standard non mi è permesso ridefinire NULL a meno che non voglia scrivere un nuovo compilatore C per eseguire il backup :)
Lundin

2
Un buon trucco è quello di hackerare il compilatore in modo che il puntatore <-> conversioni di numero intero XOR un valore specifico cheèun puntatore non valido e ancora abbastanza banale che l'architettura di destinazione possa farlo a buon mercato (di solito, sarebbe un valore con un singolo bit impostato , ad esempio 0x20000000).
Simon Richter,

2
Un'altra cosa che dovresti cambiare nel compilatore è l'inizializzazione di oggetti con tipo composto - se un oggetto è parzialmente inizializzato, allora tutti i puntatori per i quali non è presente un inizializzatore esplicito devono essere inizializzati NULL.
caf

20

Lo standard afferma che un'espressione costante intera con valore 0, o un'espressione simile convertita nel void *tipo, è una costante puntatore nullo. Ciò significa che (void *)0è sempre un puntatore nullo, ma dato int i = 0;, (void *)inon è necessario che lo sia.

L'implementazione C è costituita dal compilatore insieme ai suoi header. Se modifichi le intestazioni per ridefinire NULL, ma non modifichi il compilatore per correggere le inizializzazioni statiche, hai creato un'implementazione non conforme. È l'intera implementazione presa insieme che ha un comportamento errato, e se l'hai rotto, non hai davvero nessun altro da incolpare;)

È necessario correggere più delle semplici inizializzazioni statiche, ovviamente - dato un puntatore p, if (p)è equivalente a if (p != NULL), a causa della regola di cui sopra.


8

Se usi la libreria C std, incontrerai problemi con le funzioni che possono restituire NULL. Ad esempio, la documentazione di malloc afferma:

Se la funzione non è riuscita ad allocare il blocco di memoria richiesto, viene restituito un puntatore nullo.

Poiché malloc e le relative funzioni sono già compilate in binari con un valore NULL specifico, se ridefinisci NULL, non sarai in grado di usare direttamente la libreria C std a meno che tu non possa ricostruire l'intera catena di strumenti, comprese le librerie C std.

Anche a causa dell'uso di NULL da parte della libreria std, se ridefinisci NULL prima di includere le intestazioni std, potresti sovrascrivere una definizione NULL elencata nelle intestazioni. Qualunque cosa inline sarebbe incoerente dagli oggetti compilati.

Definirei invece il tuo NULL, "MYPRODUCT_NULL", per i tuoi usi ed eviterei o tradurrei da / verso la libreria C std.


6

Lascia NULL da solo e tratta IO sulla porta 0x0000 come un caso speciale, magari usando una routine scritta in assembler, e quindi non soggetta alla semantica C standard. IOW, non ridefinire NULL, ridefinire la porta 0x00000.

Nota che se stai scrivendo o modificando un compilatore C, il lavoro richiesto per evitare di dereferenziare NULL (supponendo che nel tuo caso la CPU non aiuti) è lo stesso indipendentemente da come NULL è definito, quindi è più facile lasciare NULL definito come zero e assicurati che lo zero non possa mai essere dereferenziato da C.


Il problema si verificherà solo quando si accede accidentalmente a NULL, non quando si accede intenzionalmente alla porta. Perché dovrei ridefinire l'I / O della porta per allora? Funziona già come dovrebbe.
Lundin

2
@Lundin Accidentalmente o no, NULL può essere dereferenziato solo in un programma C utilizzando *p, p[]o p(), quindi il compilatore deve preoccuparsi solo di quelli per proteggere la porta IO 0x0000.
Apalala

@ Lundin La seconda parte della tua domanda: una volta limitato l'accesso all'indirizzo zero dall'interno di C, hai bisogno di un altro modo per raggiungere la porta 0x0000. Una funzione scritta in assembler può farlo. Dall'interno di C, la porta potrebbe essere mappata su 0xFFFF o qualsiasi altra cosa, ma è meglio usare una funzione e dimenticare il numero di porta.
Apalala

3

Considerando l'estrema difficoltà nel ridefinire NULL come menzionato da altri, forse è più facile ridefinire la dereferenziazione per indirizzi hardware ben noti. Quando crei un indirizzo, aggiungi 1 a ogni indirizzo noto, in modo che la tua porta di I / O ben nota sia:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Se gli indirizzi che ti interessano sono raggruppati insieme e puoi stare sicuro che l'aggiunta di 1 all'indirizzo non è in conflitto con nulla (cosa che non dovrebbe nella maggior parte dei casi), potresti essere in grado di farlo in sicurezza. E poi non devi preoccuparti di ricostruire la tua catena di strumenti / std lib e le espressioni nella forma:

  if (pointer)
  {
     ...
  }

ancora lavoro

Pazzo lo so, ma ho solo pensato di lanciare l'idea là fuori :).


Il problema si verificherà solo quando si accede accidentalmente a NULL, non quando si accede intenzionalmente alla porta. Perché dovrei ridefinire l'I / O della porta per allora? Funziona già come dovrebbe.
Lundin

@LundIn Immagino che tu debba scegliere quale è più doloroso, modificando la ricostruzione dell'intera toolchain o cambiando questa parte del tuo codice.
Doug T.

2

Il pattern di bit per il puntatore nullo potrebbe non essere lo stesso del pattern di bit per l'intero 0. Ma l'espansione della macro NULL deve essere una costante di puntatore nullo, cioè un numero intero costante di valore 0 a cui può essere eseguito il cast di (void *).

Per ottenere il risultato desiderato rimanendo conforme, dovrai modificare (o forse configurare) la tua catena di strumenti, ma è realizzabile.


1

Stai cercando guai. La ridefinizione NULLsu un valore non nullo interromperà questo codice:

   if (myPointer)
   {
      // myPointer non è nullo
      ...
   }
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.