Lo "struct hack" è un comportamento tecnicamente indefinito?


111

Quello che sto chiedendo è il noto trucco "l'ultimo membro di una struttura ha lunghezza variabile". Funziona in questo modo:

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

A causa del modo in cui la struttura è disposta in memoria, siamo in grado di sovrapporre la struttura a un blocco più grande del necessario e trattare l'ultimo membro come se fosse più grande di quello 1 charspecificato.

Quindi la domanda è: questa tecnica è un comportamento tecnicamente indefinito? . Mi sarei aspettato che lo fosse, ma ero curioso di sapere cosa dice lo standard su questo.

PS: sono consapevole dell'approccio C99 a questo, vorrei che le risposte si attengano specificamente alla versione del trucco come elencato sopra.


33
Questa sembra una domanda abbastanza chiara, ragionevole e soprattutto a cui è possibile rispondere . Non vedendo il motivo della votazione ravvicinata.
cHao

2
Se hai introdotto un compilatore "ansi c" che non supporta l'hack dello struct, la maggior parte dei programmatori c che conosco non accetterebbe che il tuo compilatore "funzioni bene". Nonostante ciò accetterebbero una lettura rigorosa dello standard. Il comitato ha semplicemente perso uno su questo.
dmckee --- gattino ex moderatore

4
@james L'hacking funziona mallocando un oggetto abbastanza grande per l'array che intendi, nonostante abbia dichiarato un array minimo. Quindi stai accedendo alla memoria allocata al di fuori della rigida definizione della struttura. Scrivere oltre la propria allocazione è indiscutibile un errore, ma è diverso dallo scrivere nella propria allocazione ma al di fuori della "struttura".
dmckee --- gattino ex moderatore

2
@ James: il malloc sovradimensionato è fondamentale qui. Assicura che ci sia memoria --- memoria con indirizzo legale ed e 'posseduta' dalla struttura (cioè è illegale per qualsiasi altra entità usarla) --- oltre l'estremità nominale della struttura. Nota che questo significa che non puoi usare lo struct hack sulle variabili automatiche: devono essere allocate dinamicamente.
dmckee --- gattino ex moderatore,

5
@detly: è più semplice allocare / deallocare una cosa piuttosto che allocare / deallocare due cose, soprattutto perché quest'ultima ha due modi di fallire che devi affrontare. Questo è più importante per me dei risparmi marginali in termini di costi / velocità.
jamesdlin

Risposte:


52

Come dice la FAQ C :

Non è chiaro se sia legale o portatile, ma è piuttosto popolare.

e:

... un'interpretazione ufficiale ha ritenuto che non sia strettamente conforme allo standard C, sebbene sembri funzionare con tutte le implementazioni note. (I compilatori che controllano attentamente i limiti degli array potrebbero emettere avvisi.)

La logica alla base del bit "strettamente conforme" è nelle specifiche, sezione J.2 Comportamento non definito , che include nell'elenco dei comportamenti indefiniti:

  • Un indice dell'array è fuori intervallo, anche se un oggetto è apparentemente accessibile con il pedice dato (come nell'espressione lvalue a[1][7]data la dichiarazione int a[4][5]) (6.5.6).

Il paragrafo 8 della sezione 6.5.6 Operatori additivi ha un'altra menzione che l'accesso oltre i limiti di matrice definiti non è definito:

Se sia l'operando puntatore che il risultato puntano a elementi dello stesso oggetto matrice, o uno dopo l'ultimo elemento dell'oggetto matrice, la valutazione non deve produrre un overflow; in caso contrario, il comportamento è indefinito.


1
Nel codice dell'OP, p->snon viene mai utilizzato come array. Viene passato a strcpy, nel qual caso decade in un piano char *, che capita di indicare un oggetto che può essere legalmente interpretato come char [100];all'interno dell'oggetto allocato.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

3
Forse un altro modo di vedere questo è che il linguaggio potrebbe plausibilmente limitare il modo in cui si accede alle variabili array effettive come descritto in J.2, ma non è possibile applicare tali restrizioni per un oggetto allocato da malloc, quando si è semplicemente convertito il restituito void *a un puntatore a [una struttura contenente] un array. È ancora valido accedere a qualsiasi parte dell'oggetto allocato utilizzando un puntatore a char(o preferibilmente unsigned char).
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

@R. - Posso vedere come J2 potrebbe non coprire questo, ma non è anche coperto da 6.5.6?
detly

1
Certo che potrebbe! Le informazioni sul tipo e la dimensione potrebbero essere incorporate in ogni puntatore e qualsiasi aritmetica errata del puntatore potrebbe quindi essere intrappolata - vedere ad esempio CCured . A un livello più filosofico, non importa se nessuna possibile implementazione possa catturarti, è ancora un comportamento indefinito (ci sono, iirc, casi di comportamento indefinito che richiederebbero un oracolo per inchiodare il Problema di Arresto - che è precisamente il motivo sono indefiniti).
zwol

4
L'oggetto non è un oggetto array, quindi 6.5.6 è irrilevante. L'oggetto è il blocco di memoria allocato da malloc. Cerca "oggetto" nello standard prima di lanciare bs.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

34

Credo che tecnicamente sia un comportamento indefinito. Lo standard (probabilmente) non lo affronta direttamente, quindi rientra nella "o dall'omissione di qualsiasi definizione esplicita di comportamento". clausola (§4 / 2 di C99, §3.16 / 2 di C89) che dice che è un comportamento indefinito.

Il "discutibilmente" sopra dipende dalla definizione dell'operatore di indice di matrice. Nello specifico, si dice: "Un'espressione postfissa seguita da un'espressione tra parentesi quadre [] è una designazione con indice di un oggetto array". (C89, §6.3.2.1 / 2).

Puoi sostenere che "di un oggetto array" è stato violato qui (dato che stai scrivendo al di fuori dell'intervallo definito dell'oggetto array), nel qual caso il comportamento è (un po 'più) esplicitamente indefinito, invece che semplicemente indefinito per gentile concessione di nulla che lo definisca.

In teoria, posso immaginare un compilatore che esegue il controllo dei limiti di array e (ad esempio) interrompe il programma quando / se si tenta di utilizzare un indice fuori intervallo. In effetti, non so che esista una cosa del genere, e data la popolarità di questo stile di codice, anche se un compilatore cercasse di imporre gli indici in alcune circostanze, è difficile immaginare che qualcuno lo sopporterebbe in questo modo questa situazione.


2
Posso anche immaginare un compilatore che potrebbe decidere che se un array fosse di dimensione 1, arr[x] = y;potrebbe essere riscritto come arr[0] = y;; per un array di dimensione 2, arr[i] = 4;potrebbe essere riscritto come i ? arr[1] = 4 : arr[0] = 4; Anche se non ho mai visto un compilatore eseguire tali ottimizzazioni, su alcuni sistemi embedded potrebbero essere molto produttivi. Su un PIC18x, utilizzando i tipi di dati a 8 bit, il codice per la prima istruzione sarebbe di sedici byte, il secondo, due o quattro e il terzo, otto o dodici. Non è una cattiva ottimizzazione se legale.
supercat

Se lo standard definisce l'accesso all'array al di fuori dei limiti dell'array come comportamento indefinito, allora lo è anche l'hack dello struct. Se, tuttavia, lo standard definisce l'accesso agli array come zucchero sintattico per pointer arithmetic ( a[2] == a + 2), non lo fa. Se ho ragione, tutti gli standard C definiscono l'accesso agli array come aritmatico del puntatore.
yyny

13

Sì, è un comportamento indefinito.

Il rapporto sui difetti del linguaggio C # 051 fornisce una risposta definitiva a questa domanda:

L'idioma, sebbene comune, non è strettamente conforme

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

Nel documento C99 Rationale il Comitato C aggiunge:

La validità di questo costrutto è sempre stata discutibile. Nella risposta a un rapporto sui difetti, il Comitato ha deciso che si trattava di un comportamento indefinito perché la matrice p-> elementi contiene solo un elemento, indipendentemente dal fatto che lo spazio esista.


2
+1 per aver trovato questo, ma continuo a sostenere che è contraddittorio. Due puntatori allo stesso oggetto (in questo caso, il byte dato) sono uguali, e un puntatore ad esso (il puntatore nella matrice di rappresentazione dell'intero oggetto ottenuto da malloc) è valido nell'addizione, quindi come può il puntatore identico, ottenuto tramite un altro percorso, non è valido nell'aggiunta? Anche se vogliono affermare che si tratta di UB, ciò è piuttosto privo di significato, perché dal punto di vista computazionale non c'è modo per un'implementazione di distinguere tra l'uso ben definito e l'uso presumibilmente indefinito.
R .. GitHub SMETTA DI AIUTARE ICE

È un peccato che i compilatori C abbiano iniziato a vietare la dichiarazione di array di lunghezza zero; se non fosse stato per quel divieto, molti compilatori non avrebbero dovuto fare alcun trattamento speciale per farli funzionare come "dovrebbero", ma sarebbero comunque stati in grado di scrivere codice in casi speciali per array a elemento singolo (ad esempio se *foocontiene un array a elemento singolo boz, l'espressione foo->boz[biz()*391]=9;potrebbe essere semplificata come biz(),foo->boz[0]=9;). Sfortunatamente, il rifiuto degli array di zero elementi da parte dei compilatori significa che molto codice utilizza invece array a elemento singolo e sarebbe rotto da tale ottimizzazione.
supercat

11

Quel modo particolare di farlo non è esplicitamente definito in nessuno standard C, ma C99 include la "struct hack" come parte del linguaggio. In C99, l'ultimo membro di una struttura può essere un "membro di array flessibile", dichiarato come char foo[](con qualunque tipo si desideri al posto di char).


Per essere pedanti, questo non è lo struct hack. Lo struct hack usa un array con una dimensione fissa, non un membro di array flessibile. Lo struct hack è ciò di cui è stato chiesto ed è UB. I membri di array flessibili sembrano solo un tentativo di placare il tipo di gente visto in questo thread che si lamenta di questo fatto.
underscore_d

7

Non è un comportamento indefinito , a prescindere da ciò che qualcuno, ufficiale o meno , dice, perché è definito dallo standard. p->s, tranne quando usato come lvalue, restituisce un puntatore identico a (char *)p + offsetof(struct T, s). In particolare, questo è un charpuntatore valido all'interno dell'oggetto malloc'd, e ci sono 100 (o più, a seconda delle considerazioni sull'allineamento) successivi indirizzi immediatamente successivi che sono validi anche come charoggetti all'interno dell'oggetto allocato. Il fatto che il puntatore sia stato derivato usando ->invece di aggiungere esplicitamente l'offset al puntatore restituito da malloc, cast to char *, è irrilevante.

Tecnicamente, p->s[0]è il singolo elemento chardell'array all'interno della struttura, i pochi elementi successivi (ad esempio p->s[1]attraverso p->s[3]) sono probabilmente byte di riempimento all'interno della struttura, che potrebbero essere corrotti se esegui l'assegnazione alla struttura nel suo insieme ma non se accedi semplicemente a singoli membri, e il resto degli elementi è spazio aggiuntivo nell'oggetto assegnato che sei libero di usare come preferisci, a condizione che tu obbedisca ai requisiti di allineamento (e charnon abbia requisiti di allineamento).

Se sei preoccupato che la possibilità di sovrapporsi con padding byte nella struttura possa in qualche modo richiamare i demoni nasali, potresti evitarlo sostituendo 1in [1]con un valore che assicuri che non ci sia riempimento alla fine della struttura. Un modo semplice ma dispendioso per farlo sarebbe creare una struttura con membri identici tranne nessun array alla fine, e usarla s[sizeof struct that_other_struct];per l'array. Quindi, p->s[i]è chiaramente definito come un elemento dell'array nella struct for i<sizeof struct that_other_structe come un oggetto char in un indirizzo che segue la fine della struct for i>=sizeof struct that_other_struct.

Modifica: in realtà, nel trucco sopra per ottenere la dimensione giusta, potresti anche dover mettere un'unione contenente ogni tipo semplice prima dell'array, per assicurarti che l'array stesso inizi con l'allineamento massimo piuttosto che nel mezzo del riempimento di qualche altro elemento . Ancora una volta, non credo che tutto ciò sia necessario, ma lo offro al più paranoico degli avvocati linguistici là fuori.

Modifica 2: la sovrapposizione con i byte di riempimento non è sicuramente un problema, a causa di un'altra parte dello standard. C richiede che se due strutture concordano in una sottosequenza iniziale dei loro elementi, è possibile accedere agli elementi iniziali comuni tramite un puntatore a entrambi i tipi. Di conseguenza, se struct Tfosse dichiarata una struttura identica a ma con un array finale più grande, l'elemento s[0]dovrebbe coincidere con l'elemento s[0]in struct T, e la presenza di questi elementi aggiuntivi non potrebbe influenzare o essere influenzata dall'accesso agli elementi comuni della struttura più grande utilizzando un puntatore a struct T.


4
Hai ragione sul fatto che la natura dell'aritmetica del puntatore è irrilevante, ma ti sbagli sull'accesso oltre la dimensione dichiarata dell'array. Vedi N1494 (ultima bozza pubblica C1x) sezione 6.5.6 paragrafo 8 - non sei nemmeno autorizzato a fare l' addizione che prende un puntatore più di un elemento oltre la dimensione dichiarata dell'array, e non puoi dereferenziarlo anche se è solo un elemento passato.
zwol

1
@ Zack: è vero se l'oggetto è un array. Non è vero se l'oggetto è un oggetto allocato da malloccui si accede come array o se si tratta di una struttura più grande a cui si accede tramite un puntatore a una struttura più piccola i cui elementi sono un sottoinsieme iniziale degli elementi della struttura più grande, tra gli altri casi.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

6
+1 Se mallocnon alloca un intervallo di memoria a cui è possibile accedere con l'aritmetica del puntatore, che utilità sarebbe? E se p->s[1]è definito dallo standard come zucchero sintattico per l'aritmetica dei puntatori, allora questa risposta si limita a riaffermare che mallocè utile. Cosa resta da discutere? :)
Daniel Earwicker

3
Puoi sostenere che è ben definito quanto vuoi, ma ciò non cambia il fatto che non lo sia. Lo standard è molto chiaro sull'accesso oltre i limiti di un array, e il limite di questo array lo è 1. È proprio così semplice.
Gare di leggerezza in orbita il

3
@R .., penso, la tua supposizione che due indicatori di confronto uguali debbano comportarsi allo stesso modo è sbagliata. Considera int m[1]; int n[1]; if(m+1 == n) m[1] = 0;l' ifidea di inserire il ramo. Questo è UB (e non è garantito l'inizializzazione n) come da 6.5.6 p8 (ultima frase), come l'ho letto. Correlati: 6.5.9 p6 con nota 109. (I riferimenti sono a C11 n1570.) [...]
mafso

7

Sì, è un comportamento tecnicamente indefinito.

Nota che ci sono almeno tre modi per implementare lo "struct hack":

(1) Dichiarare l'array finale con dimensione 0 (il modo più "popolare" nel codice legacy). Questo è ovviamente UB, poiché le dichiarazioni di array di dimensioni zero sono sempre illegali in C. Anche se si compila, il linguaggio non fornisce alcuna garanzia sul comportamento di qualsiasi codice che viola i vincoli.

(2) Dichiarare l'array con una dimensione legale minima - 1 (il tuo caso). In questo caso qualsiasi tentativo di prendere il puntatore p->s[0]e usarlo per l'aritmetica del puntatore che va oltre p->s[1]è un comportamento indefinito. Ad esempio, un'implementazione di debug può produrre un puntatore speciale con informazioni sull'intervallo incorporate, che intercetterà ogni volta che si tenta di creare un puntatore oltre p->s[1].

(3) Dichiarare l'array con una dimensione "molto grande" come 10000, per esempio. L'idea è che la dimensione dichiarata dovrebbe essere maggiore di qualsiasi cosa tu possa aver bisogno nella pratica reale. Questo metodo è privo di UB per quanto riguarda il raggio di accesso all'array. Tuttavia, in pratica, ovviamente, allocheremo sempre una minore quantità di memoria (solo quella realmente necessaria). Non sono sicuro della legalità di questo, cioè mi chiedo quanto sia legale allocare meno memoria per l'oggetto rispetto alla dimensione dichiarata dell'oggetto (assumendo che non abbiamo mai accesso ai membri "non allocati").


1
In (2), s[1]non è un comportamento indefinito. È lo stesso di *(s+1), che è lo stesso di *((char *)p + offsetof(struct T, s) + 1), che è un puntatore valido a chara nell'oggetto allocato.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

D'altra parte, sono quasi sicuro che (3) sia un comportamento indefinito. Ogni volta che si esegue qualsiasi operazione che dipende da una struttura simile che risiede a quell'indirizzo, il compilatore è libero di generare codice macchina che legge da qualsiasi parte della struttura. Potrebbe essere inutile o potrebbe essere una funzione di sicurezza per un controllo rigoroso dell'allocazione, ma non c'è motivo per cui un'implementazione non possa farlo.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

R: Se un array è stato dichiarato avere una dimensione (non è solo lo foo[]zucchero sintattico per *foo), allora qualsiasi accesso oltre il più piccolo tra la sua dimensione dichiarata e la sua dimensione allocata è UB, indipendentemente da come è stata eseguita l'aritmetica del puntatore.
zwol

1
@ Zack, ti ​​sbagli su diverse cose. foo[]in una struttura non è zucchero sintattico per *foo; è un membro di array flessibile C99. Per il resto, vedere la mia risposta e commenti su altre risposte.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

6
Il problema è che alcuni membri del comitato vogliono disperatamente che questo "hack" sia UB, perché immaginano un paese fatato in cui un'implementazione C potrebbe imporre limiti di puntamento. Nel bene e nel male, tuttavia, ciò sarebbe in conflitto con altre parti dello standard: cose come la capacità di confrontare i puntatori per l'uguaglianza (se i limiti fossero codificati nel puntatore stesso) o il requisito che qualsiasi oggetto sia accessibile tramite un immaginario unsigned char [sizeof object]array sovrapposto . Sostengo la mia affermazione che il membro dell'array flessibile "hack" per pre-C99 ha un comportamento ben definito.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

3

Lo standard è abbastanza chiaro che non è possibile accedere a cose oltre alla fine di un array. (e passare attraverso i puntatori non aiuta, poiché non ti è permesso nemmeno incrementare i puntatori oltre uno dopo la fine dell'array).

E per "lavorare in pratica". Ho visto l'ottimizzatore gcc / g ++ utilizzare questa parte dello standard generando così un codice sbagliato quando si incontra questo C. non valido.


Puoi fare un esempio?
Tal

1

Se un compilatore accetta qualcosa di simile

typedef struct {
  int len;
  char dat [];
};

Penso che sia abbastanza chiaro che deve essere pronto ad accettare un pedice su "dat" oltre la sua lunghezza. D'altra parte, se qualcuno codifica qualcosa come:

typedef struct {
  int qualunque;
  char dat [1];
} MY_STRUCT;

e successivamente accede a somestruct-> dat [x]; Non penserei che il compilatore abbia alcun obbligo di utilizzare codice di calcolo dell'indirizzo che funzionerà con valori elevati di x. Penso che se si volesse essere davvero al sicuro, il paradigma corretto sarebbe più simile a:

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
  int qualunque;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

e quindi eseguire un malloc di (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desiderato_array_length) byte (tenendo presente che se desiderato_array_length è maggiore di LARGEST_DAT_SIZE, i risultati potrebbero non essere definiti).

Per inciso, penso che la decisione di vietare gli array di lunghezza zero sia stata sfortunata (alcuni dialetti più vecchi come Turbo C lo supportano) poiché un array di lunghezza zero potrebbe essere considerato come un segno che il compilatore deve generare codice che funzionerà con indici più grandi .

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.