Was ich frage, ist der bekannte Trick "Das letzte Mitglied einer Struktur hat eine variable Länge". Es geht ungefähr so:
struct T {
int len;
char s[1];
};
struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");
Aufgrund der Art und Weise, wie die Struktur im Speicher angeordnet ist, können wir die Struktur über einen größeren als den erforderlichen Block legen und das letzte Element so behandeln, als wäre es größer als der 1 char
angegebene.
Die Frage ist also: Ist diese Technik technisch undefiniertes Verhalten? . Ich würde das erwarten, war aber neugierig, was der Standard dazu sagt.
PS: Ich bin mir des C99-Ansatzes bewusst. Ich möchte, dass sich die Antworten speziell an die oben aufgeführte Version des Tricks halten.
c
undefined-behavior
c89
Evan Teran
quelle
quelle
Antworten:
Wie die C FAQ sagt:
und:
Die Begründung für das "streng konforme" Bit befindet sich in der Spezifikation, Abschnitt J.2 Undefiniertes Verhalten , die in der Liste des undefinierten Verhaltens Folgendes enthält:
In Abschnitt 8 von Abschnitt 6.5.6 Additive Operatoren wird erneut erwähnt, dass der Zugriff über definierte Array-Grenzen hinaus nicht definiert ist:
quelle
p->s
niemals als Array verwendet. Es wird an übergebenstrcpy
, in diesem Fall zerfällt es in eine Ebenechar *
, die zufällig auf ein Objekt verweist, das legal alschar [100];
innerhalb des zugewiesenen Objekts interpretiert werden kann .malloc
, wenn Sie lediglich die zurückgegebenen konvertiert habenvoid *
auf einen Zeiger auf [eine Struktur, die] ein Array enthält. Es ist weiterhin gültig, mit einem Zeiger aufchar
(oder vorzugsweiseunsigned char
) auf einen Teil des zugewiesenen Objekts zuzugreifen .malloc
. Suchen Sie im Standard nach "Objekt", bevor Sie bs ausspucken.Ich glaube, dass es technisch gesehen undefiniertes Verhalten ist. Der Standard spricht ihn (wohl) nicht direkt an, so dass er unter das "oder durch das Weglassen einer expliziten Definition des Verhaltens" fällt. Klausel (§4 / 2 von C99, §3.16 / 2 von C89), die besagt, dass es sich um undefiniertes Verhalten handelt.
Das obige "wohl" hängt von der Definition des Array-Subskriptionsoperators ab. Insbesondere heißt es: "Ein Postfix-Ausdruck, gefolgt von einem Ausdruck in eckigen Klammern [], ist eine tiefgestellte Bezeichnung eines Array-Objekts." (C89, §6.3.2.1 / 2).
Sie können argumentieren, dass das "eines Array-Objekts" hier verletzt wird (da Sie außerhalb des definierten Bereichs des Array-Objekts abonnieren). In diesem Fall ist das Verhalten (ein kleines bisschen mehr) explizit undefiniert und nicht nur undefiniert mit freundlicher Genehmigung von nichts, was es ganz definiert.
Theoretisch kann ich mir einen Compiler vorstellen, der Array-Grenzen überprüft und (zum Beispiel) das Programm abbricht, wenn Sie versuchen, einen Index außerhalb des Bereichs zu verwenden. Tatsächlich weiß ich nicht, dass so etwas existiert, und angesichts der Popularität dieses Codestils ist es schwer vorstellbar, dass sich irgendjemand damit abfinden würde, selbst wenn ein Compiler unter bestimmten Umständen versucht hätte, Indizes durchzusetzen diese Situation.
quelle
arr[x] = y;
wie folgt umgeschrieben wirdarr[0] = y;
. Für ein Array der Größe 2 wirdarr[i] = 4;
möglicherweise Folgendes umgeschrieben:i ? arr[1] = 4 : arr[0] = 4;
Während ich noch nie einen Compiler gesehen habe, der solche Optimierungen durchführt, können sie auf einigen eingebetteten Systemen sehr produktiv sein. Auf einem PIC18x, der 8-Bit-Datentypen verwendet, wäre der Code für die erste Anweisung sechzehn Bytes, die zweite, zwei oder vier und die dritte, acht oder zwölf. Keine schlechte Optimierung, wenn legal.a[2] == a + 2
) definiert, ist dies nicht der Fall. Wenn ich richtig bin, definieren alle C-Standards den Array-Zugriff als Zeigerarithmatik.Ja, es ist undefiniertes Verhalten.
Der C-Sprachfehlerbericht Nr. 051 gibt eine endgültige Antwort auf diese Frage:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
Im C99-Begründungsdokument fügt der C-Ausschuss Folgendes hinzu:
quelle
malloc
) ist in der Addition gültig. Wie kann also der identische Zeiger? über eine andere Route erhalten, in der Hinzufügung ungültig sein? Selbst wenn sie behaupten wollen, es sei UB, ist das ziemlich bedeutungslos, da eine Implementierung rechnerisch nicht zwischen der genau definierten und der angeblich undefinierten Verwendung unterscheiden kann.*foo
enthalten ist) Einzelelement-Arrayboz
, der Ausdruckfoo->boz[biz()*391]=9;
könnte vereinfacht werden alsbiz(),foo->boz[0]=9;
). Leider bedeutet die Ablehnung von Nullelement-Arrays durch Compiler, dass viel Code stattdessen Einzelelement-Arrays verwendet und durch diese Optimierung beschädigt würde.Diese spezielle Vorgehensweise ist in keinem C-Standard explizit definiert, aber C99 enthält den "Struktur-Hack" als Teil der Sprache. In C99 kann das letzte Mitglied einer Struktur ein "flexibles Array-Mitglied" sein, das als
char foo[]
(mit dem gewünschten Typ anstelle vonchar
) deklariert ist .quelle
Es ist kein undefiniertes Verhalten , unabhängig davon, was jemand, offiziell oder anderweitig , sagt, da es durch den Standard definiert ist.
p->s
, außer wenn es als l-Wert verwendet wird, wird zu einem Zeiger ausgewertet, der mit identisch ist(char *)p + offsetof(struct T, s)
. Dies ist insbesondere ein gültigerchar
Zeiger innerhalb des malloc'd-Objekts, und unmittelbar darauf folgen 100 (oder mehr, abhängig von Ausrichtungsüberlegungen) aufeinanderfolgende Adressen, die auch alschar
Objekte innerhalb des zugewiesenen Objekts gültig sind . Die Tatsache, dass der Zeiger durch Verwenden->
von abgeleitet wurde, anstatt den Versatz explizit zu dem Zeiger hinzuzufügen, der vonmalloc
, in den umgewandelt wurdechar *
, zurückgegeben wird, ist irrelevant.Technisch gesehen
p->s[0]
ist das einzelne Element deschar
Arrays innerhalb der Struktur, die nächsten paar Elemente (z. B.p->s[1]
bisp->s[3]
) sind wahrscheinlich Auffüllbytes innerhalb der Struktur, die beschädigt werden können, wenn Sie die Zuordnung zur Struktur als Ganzes durchführen, aber nicht, wenn Sie nur auf einzelne zugreifen Mitglieder und der Rest der Elemente sind zusätzlicher Speicherplatz im zugewiesenen Objekt, den Sie nach Belieben verwenden können, solange Sie die Ausrichtungsanforderungen erfüllen (undchar
keine Ausrichtungsanforderungen haben).Wenn Sie befürchten, dass die Möglichkeit einer Überlappung mit Auffüllbytes in der Struktur Nasen-Dämonen hervorrufen könnte, können Sie dies vermeiden, indem Sie das
1
In[1]
durch einen Wert ersetzen, der sicherstellt, dass am Ende der Struktur kein Auffüllen erfolgt. Eine einfache, aber verschwenderische Möglichkeit, dies zu tun, besteht darin, eine Struktur mit identischen Elementen außer keinem Array am Ende zu erstellen unds[sizeof struct that_other_struct];
für das Array zu verwenden. Dannp->s[i]
ist klar definiert als ein Element des Arrays in der Struktur füri<sizeof struct that_other_struct
und als ein char-Objekt an einer Adresse nach dem Ende der Struktur füri>=sizeof struct that_other_struct
.Bearbeiten: Bei dem obigen Trick, um die richtige Größe zu erhalten, müssen Sie möglicherweise auch eine Vereinigung mit jedem einfachen Typ vor das Array setzen, um sicherzustellen, dass das Array selbst mit maximaler Ausrichtung beginnt und nicht in der Mitte des Auffüllens eines anderen Elements . Auch hier glaube ich nicht, dass dies notwendig ist, aber ich biete es den paranoidesten Sprachanwälten da draußen an.
Bearbeiten 2: Die Überlappung mit Füllbytes ist aufgrund eines anderen Teils des Standards definitiv kein Problem. C erfordert, dass, wenn zwei Strukturen in einer anfänglichen Teilsequenz ihrer Elemente übereinstimmen, auf die gemeinsamen Anfangselemente über einen Zeiger auf einen der beiden Typen zugegriffen werden kann. Wenn daher eine Struktur
struct T
deklariert würde, die mit einem größeren endgültigen Array identisch ist, jedoch mit einem größeren endgültigen Array,s[0]
müsste das Element mit dem Elements[0]
in übereinstimmenstruct T
, und das Vorhandensein dieser zusätzlichen Elemente könnte den Zugriff auf gemeinsame Elemente der größeren Struktur nicht beeinflussen oder durch diesen beeinflusst werden mit einem Zeiger aufstruct T
.quelle
malloc
handelt, das als Array zugewiesen wird, oder wenn es sich um eine größere Struktur handelt, auf die über einen Zeiger auf eine kleinere Struktur zugegriffen wird, deren Elemente unter anderem eine anfängliche Teilmenge der Elemente der größeren Struktur sind Fälle.malloc
kein Speicherbereich zugewiesen wird, auf den mit Zeigerarithmetik zugegriffen werden kann, welchen Nutzen hätte dies? Und wennp->s[1]
wird definiert durch den Standard als syntaktischer Zucker für Zeigerarithmetik, dann dieser Antwort lediglich bekräftigt , diemalloc
nützlich ist. Was gibt es noch zu besprechen? :)1
. So einfach ist das.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
die Annahmeif
Zweig eingegeben wird . Dies ist UB (und es wird nicht garantiert, dass es initialisiert wirdn
) gemäß 6.5.6 p8 (letzter Satz), wie ich es gelesen habe. Siehe auch: 6.5.9 S. 6 mit Fußnote 109. (Verweise auf C11 n1570.) [...]Ja, es ist technisch undefiniertes Verhalten.
Beachten Sie, dass es mindestens drei Möglichkeiten gibt, den "Struktur-Hack" zu implementieren:
(1) Deklarieren des nachfolgenden Arrays mit der Größe 0 (die "beliebteste" Methode im Legacy-Code). Dies ist offensichtlich UB, da die Array-Deklarationen der Größe Null in C immer unzulässig sind. Selbst wenn sie kompiliert werden, übernimmt die Sprache keine Garantie für das Verhalten von Code, der gegen Einschränkungen verstößt.
(2) Deklarieren des Arrays mit minimaler zulässiger Größe - 1 (Ihr Fall). In diesem Fall ist jeder Versuch, einen Zeiger auf eine Zeigerarithmetik zu
p->s[0]
verwenden, die darüber hinausgeht,p->s[1]
undefiniertes Verhalten. Beispielsweise kann eine Debugging-Implementierung einen speziellen Zeiger mit eingebetteten Bereichsinformationen erzeugen, der jedes Mal abgefangen wird, wenn Sie versuchen, einen Zeiger darüber hinaus zu erstellenp->s[1]
.(3) Deklarieren des Arrays mit einer "sehr großen" Größe wie beispielsweise 10000. Die Idee ist, dass die deklarierte Größe größer sein soll als alles, was Sie in der Praxis benötigen könnten. Diese Methode ist hinsichtlich des Array-Zugriffsbereichs frei von UB. In der Praxis werden wir jedoch natürlich immer weniger Speicher zuweisen (nur so viel, wie wirklich benötigt wird). Ich bin mir nicht sicher, ob dies legal ist, dh ich frage mich, wie legal es ist, weniger Speicher für das Objekt zuzuweisen als die deklarierte Größe des Objekts (vorausgesetzt, wir greifen niemals auf die "nicht zugewiesenen" Mitglieder zu).
quelle
s[1]
ist nicht undefiniertes Verhalten. Es ist dasselbe wie*(s+1)
, was dasselbe ist wie*((char *)p + offsetof(struct T, s) + 1)
, was ein gültiger Zeiger auf achar
im zugewiesenen Objekt ist.foo[]
syntaktische Zucker für*foo
), ist jeder Zugriff über die kleinere deklarierte Größe und die zugewiesene Größe hinaus UB, unabhängig davon, wie die Zeigerarithmetik ausgeführt wurde.foo[]
in einer Struktur ist kein syntaktischer Zucker für*foo
; Es ist ein flexibles C99-Array-Mitglied. Im Übrigen siehe meine Antwort und Kommentare zu anderen Antworten.unsigned char [sizeof object]
Array zugegriffen werden kann . Ich stehe zu meiner Behauptung, dass das flexible Array-Mitglied "Hack" für Pre-C99 ein genau definiertes Verhalten aufweist.Der Standard ist ziemlich klar, dass Sie nicht auf Dinge neben dem Ende eines Arrays zugreifen können. (und das Übergehen von Zeigern hilft nicht, da Sie nach dem Ende des Arrays nicht einmal Zeiger nach einem inkrementieren dürfen).
Und für "Arbeiten in der Praxis". Ich habe gesehen, dass der gcc / g ++ - Optimierer diesen Teil des Standards verwendet und somit falschen Code generiert, wenn dieses ungültige C erfüllt wird.
quelle
Wenn ein Compiler so etwas akzeptiert
Ich denke, es ist ziemlich klar, dass es bereit sein muss, einen Index für 'dat' über seine Länge hinaus zu akzeptieren. Auf der anderen Seite, wenn jemand etwas codiert wie:
und greift dann später auf somestruct-> dat [x] zu; Ich würde nicht glauben, dass der Compiler verpflichtet ist, Adressberechnungscode zu verwenden, der mit großen Werten von x funktioniert. Ich denke, wenn man wirklich sicher sein wollte, wäre das richtige Paradigma eher wie folgt:
und dann ein Malloc von (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + gewünschte_array_length) Bytes ausführen (wobei zu berücksichtigen ist, dass die Ergebnisse undefiniert sein können, wenn die gewünschte_Array_Länge größer als LARGEST_DAT_SIZE ist).
Übrigens denke ich, dass die Entscheidung, Arrays mit der Länge Null zu verbieten, unglücklich war (einige ältere Dialekte wie Turbo C unterstützen dies), da ein Array mit der Länge Null als Zeichen dafür angesehen werden kann, dass der Compiler Code generieren muss, der mit größeren Indizes funktioniert .
quelle