Quali sono le semantiche degli oggetti sovrapposti in C?


25

Considera la seguente struttura:

struct s {
  int a, b;
};

Tipicamente 1 , questa struttura avrà dimensione 8 e allineamento 4.

Cosa succede se creiamo due struct soggetti (più precisamente, scriviamo nella memoria allocata due di questi oggetti), con il secondo oggetto che si sovrappone al primo?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

C'è qualcosa nel comportamento indefinito di questo programma? In tal caso, dove diventa indefinito? Se non è UB, è garantito stampare sempre quanto segue:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

In particolare, voglio sapere cosa succede all'oggetto indicato da o1quando o2, che si sovrappone, viene scritto. È ancora consentito accedere alla parte senza lobo ( o1->a)? L'accesso alla parte bloccata è o1->bsemplicemente uguale all'accesso o2->a?

Come si applica il tipo efficace qui? Le regole sono abbastanza chiare quando si parla di oggetti e puntatori non sovrapposti che puntano alla stessa posizione dell'ultimo negozio, ma quando si inizia a parlare del tipo effettivo di porzioni di oggetti o di oggetti sovrapposti, è meno chiaro.

Qualcosa cambierebbe se la seconda scrittura fosse di un tipo diverso? Se i membri fossero stati detti inte shortnon due ints?

Ecco un godbolt se vuoi giocarci lì.


1 Questa risposta si applica alle piattaforme in cui non è così: ad esempio, alcuni potrebbero avere dimensioni 4 e allineamento 2. Su una piattaforma in cui le dimensioni e l'allineamento erano uguali, questa domanda non si applica poiché gli oggetti allineati e sovrapposti verrebbero essere impossibile, ma non sono sicuro che esista una piattaforma del genere.


2
Sono abbastanza sicuro che sia UB, ma lascerò che un avvocato di lingua fornisca capitolo e versetto.
Barmar il

Penso che il compilatore C sui vecchi sistemi vettoriali Cray abbia forzato l'allineamento e le dimensioni allo stesso modo, con un modello ILP64 e l'allineamento forzato a 64 bit (gli indirizzi sono parole a 64 bit - nessun indirizzamento byte). Naturalmente questo ha generato molti altri problemi ...
John D McCalpin,

Risposte:


15

Fondamentalmente questa è tutta l'area grigia nello standard; la regola di aliasing rigoroso specifica i casi di base e lascia al lettore (e ai fornitori del compilatore) di compilare i dettagli.

Ci sono stati sforzi per scrivere una regola migliore, ma finora non hanno prodotto alcun testo normativo e non sono sicuro di quale sia lo stato di questo per C2x.

Come menzionato nella mia risposta alla tua domanda precedente, l'interpretazione più comune è che p->qsignifica (*p).qche il tipo efficace si applica a tutti *p, anche se poi continueremo ad applicare .q.

In base a questa interpretazione, printf("o1.a=%d\n", o1->a);causerebbe un comportamento indefinito in quanto il tipo effettivo del luogo *o1non lo è s(poiché parte di esso è stato sovrascritto).

La logica di questa interpretazione può essere vista in una funzione come:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

Con questa interpretazione, l'ultima riga potrebbe essere ottimizzata puts("5");, ma senza di essa, il compilatore dovrebbe considerare che la chiamata alla funzione potrebbe essere stata f(o1, o2);e quindi perdere tutti i vantaggi che sono presumibilmente forniti dalla rigorosa regola di aliasing.

Un argomento simile si applica a due tipi di strutture non correlati che hanno entrambi un intmembro con offset diverso.


1
Con f(s* s1, s* s2), senza restrict, il compilatore non può assumere s1e s2sono diversi puntatori. Io penso che , di nuovo senza restrict, non si può nemmeno pensare che non lo fanno in parte si sovrappongono. IAC, non vedo che la preoccupazione di OP sia ben dimostrata f()dall'analogia. In bocca al lupo per nulla. UV per la prima metà.
chux - Ripristina Monica il

@ chux-ReinstateMonica senza restrizioni, s1 == s2sarebbe consentito, ma non una sovrapposizione parziale. (L'ottimizzazione nel mio esempio di codice potrebbe ancora essere eseguita se s1 == s2)
MM

@ chux-ReinstateMonica potresti anche considerare lo stesso problema con just intinvece di struct (e un sistema con _Alignof(int) < sizeof(int)).
MM

3
Lo stato di questo tipo di domanda riguardante il tipo efficace per C2x è praticamente aperto e ancora oggetto di dibattito nel gruppo di studio. Fai attenzione però a rivendicare l'equivalenza di p->qe (*p).q. Questo potrebbe essere vero per l'interpretazione di tipo come si afferma, ma non è vero da un punto di vista operativo. È importante per gli accessi simultanei alla stessa struttura che l'accesso di un membro non implica l'accesso di nessun altro membro.
Jens Gustedt

La regola di aliasing rigorosa riguarda l' accesso . L'espressione del lato sinistro nell'espressione E1.E2non esegue l'accesso (intendo l'intera E1espressione. Alcune delle sue sottoespressioni possono eseguire l'accesso. Cioè se lo E1è (*p), quindi leggere il valore del puntatore quando si valuta l' paccesso, ma la valutazione di *po (*p)non esegue alcun accesso). La regola di aliasing rigorosa non si applica nel caso in cui non vi sia accesso.
Avvocato linguistico il
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.