Warum funktioniert diese Implementierung von offsetof()?

Lesezeit: 7 Minuten

Chappars Benutzeravatar
Chappar

In ANSI C ist offsetof wie folgt definiert.

#define offsetof(st, m) \
    ((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))

Warum löst dies keinen Segmentierungsfehler aus, da wir einen NULL-Zeiger dereferenzieren? Oder ist dies eine Art Compiler-Hack, bei dem nur die Adresse des Offsets herausgenommen wird, sodass die Adresse statisch berechnet wird, ohne sie tatsächlich zu dereferenzieren? Ist dieser Code auch portabel?

  • Ist das die erste Frage, die ich auf SO gesehen habe, die sich über Code beschwert, der funktioniert? 🙂

    – paxdiablo

    3. April 2009 um 14:33 Uhr

  • Da war dieser Typ mit if(0){asm(nop)}, bei dem das Weglassen etwas fehlschlug …

    – RBerteig

    3. April 2009 um 20:45 Uhr

  • ANSI C (eigentlich ISO C) spezifiziert diese Definition nicht für offsetof. Es gibt lediglich an, wie es sich verhalten muss. Die tatsächliche Definition hängt von jeder Implementierung ab und kann von einer Implementierung zur anderen variieren.

    – Keith Thompson

    13. Juni 2014 um 18:40 Uhr

  • Es ist wichtig zu beachten, dass MISRA-C:2004-konformer Code dies erfordert offsetof wird nicht verwendet, da es leicht zu undefiniertem Verhalten führen kann.

    – DimP

    15. Dezember 2020 um 11:17 Uhr

Benutzeravatar von JaredPar
JaredPar

An keiner Stelle im obigen Code wird etwas dereferenziert. Eine Dereferenzierung erfolgt, wenn die * oder -> wird für einen Adresswert verwendet, um den referenzierten Wert zu finden. Die einzige Verwendung von * oben befindet sich in einer Typdeklaration zum Zweck des Gießens.

Das -> Operator wird oben verwendet, aber nicht für den Zugriff auf den Wert. Stattdessen wird es verwendet, um die Adresse des Werts abzurufen. Hier ist ein Nicht-Makro-Codebeispiel, das es etwas klarer machen sollte

SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);

Die zweite Zeile verursacht eigentlich keine Dereferenzierung (implementierungsabhängig). Es gibt einfach die Adresse von zurück SomeIntMember innerhalb der pSomeType Wert.

Was Sie sehen, ist eine Menge Umwandlungen zwischen beliebigen Typen und Zeichenzeigern. Der Grund für char ist, dass es einer der wenigen (vielleicht einzigen) Typen im C89-Standard ist, der eine explizite Größe hat. Die Größe ist 1. Indem sichergestellt wird, dass die Größe eins ist, kann der obige Code die böse Magie der Berechnung des wahren Offsets des Werts ausführen.

  • Ich habe keinen C-Standard zur Verfügung, aber ich dachte, ich hätte mich an etwas in C90 erinnert, dass nicht unbedingt beliebige Adressen verwendet (nicht nur dereferenziert) werden können. Die Begründung waren Maschinen wie der 8086 und der IBM 370, die Segmentregister verwendeten und nicht auf ihren gesamten Adressraum verweisen konnten.

    – David Thornley

    3. April 2009 um 13:56 Uhr

  • Im C-Standard ist die -> in &(pSomeType->SomeIntMember) bewirkt eine Dereferenzierung. Vielleicht könnten Sie klarstellen, was Sie meinten, wenn Sie behaupten, dass dies nicht der Fall ist.

    – MM

    30. Juli 2017 um 8:26 Uhr

  • Diese Antwort ist fraktal falsch: Sie ist nicht nur insgesamt falsch, sondern ich sehe in fast jedem einzelnen Satz mindestens einen Fehler.

    – zol

    3. August 2019 um 22:27 Uhr

Benutzeravatar von Jonathan Leffler
Jonathan Leffler

Obwohl dies eine typische Implementierung von ist offsetofes ist nicht durch den Standard vorgeschrieben, der nur sagt:

Die folgenden Typen und Makros sind im Standardheader definiert <stddef.h> […]

offsetof(type,member-designator)

die zu einem ganzzahligen konstanten Ausdruck mit Typ erweitert wird size_tdessen Wert der Offset in Bytes ist, zum Strukturmitglied (bezeichnet durch member-designator), vom Anfang seiner Struktur (bezeichnet mit type). Die Typ- und Elementbezeichnung muss wie angegeben sein

statictypet;

dann der Ausdruck &(t.member-designator) wird zu einer Adresskonstanten ausgewertet. (Wenn das angegebene Mitglied ein Bitfeld ist, ist das Verhalten undefiniert.)

Lesen Sie PJ Plaugers “The Standard C Library” für eine Diskussion darüber und die anderen darin enthaltenen Elemente <stddef.h> das sind alles Grenzfunktionen, die in der eigentlichen Sprache enthalten sein könnten (sollten?) und die möglicherweise eine spezielle Compiler-Unterstützung erfordern.

Es ist nur von historischem Interesse, aber ich habe einen frühen ANSI-C-Compiler auf 386/IX verwendet (siehe, ich habe Ihnen von historischem Interesse erzählt, circa 1990), der auf dieser Version von abgestürzt ist offsetof funktionierte aber, als ich es überarbeitete:

#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))

Das war eine Art Compiler-Fehler, nicht zuletzt, weil der Header mit dem Compiler verteilt wurde und nicht funktionierte.

  • “… <stddef.h> das sind alles Grenzmerkmale, die in der eigentlichen Sprache enthalten sein könnten (sollten?) – Ich würde sagen, sie sind Teil der eigentlichen Sprache, da selbst eine eigenständige Implementierung erforderlich ist, um sie immer zu unterstützen …

    – Antti Haapala – Слава Україні

    17. Oktober 2018 um 14:47 Uhr


  • Wieso nur static type t und nicht einfach type t?

    – explogx

    20. Mai 2020 um 17:52 Uhr

  • @eigenslacker — hauptsächlich habe ich den Standard kopiert und das steht da. Es hat wahrscheinlich eine tiefgreifende Bedeutung, vielleicht im Zusammenhang mit VLA (Array mit variabler Länge – und variabel modifizierte Typen), mit denen nicht verwendet werden kann static. Es kann mit unvollständigen Typen zu tun haben – dito.

    – Jonathan Leffler

    20. Mai 2020 um 17:56 Uhr

In ANSI-C, offsetof ist NICHT so definiert. Einer der Gründe, warum es nicht so definiert ist, ist, dass einige Umgebungen tatsächlich Nullzeiger-Ausnahmen auslösen oder auf andere Weise abstürzen. Daher verlässt ANSI C die Implementierung von offsetof( ) offen für Compiler-Ersteller.

Der oben gezeigte Code ist typisch für Compiler/Umgebungen, die nicht aktiv nach NULL-Zeigern suchen, sondern nur fehlschlagen, wenn Bytes von einem NULL-Zeiger gelesen werden.

  • Nur um klar zu sein, die offsetof() Makro wurde sehr häufig und weit verbreitet implementiert, wie in der Frage gezeigt, oder noch einfacher ohne die Subtraktion, auf der überwiegenden Mehrheit der Plattformen, auf denen Zeiger effektiv ganze Zahlen sind. Die meisten C-Compiler suchen nicht aktiv nach NULL-Zeigern. Der verwendete Ausdruck tut es NICHT Dereferenzierung irgendetwas — es berechnet einfach den Offset, indem es eine Adresse (die zufällig Null ist) mit einer einfachen arithmetischen Addition des intern bekannten Offsets des Mitglieds verwendet. Im optimierten Zustand wird nicht einmal eine Laufzeitaddition durchgeführt.

    – Greg A. Woods

    11. August 2017 um 3:48 Uhr

Um den letzten Teil der Frage zu beantworten, der Code ist nicht portierbar.

Das Ergebnis der Subtraktion zweier Zeiger ist nur dann definiert und übertragbar, wenn die beiden Zeiger auf Objekte im selben Array zeigen oder auf eines nach dem letzten Objekt des Arrays zeigen (7.6.2 Additive Operators, H&S Fifth Edition).

Es tritt kein Segfault auf, weil Sie es nicht dereferenzieren. Die Zeigeradresse wird als Zahl verwendet, die von einer anderen Zahl subtrahiert wird und nicht zum Adressieren von Speicheroperationen verwendet wird.

Benutzeravatar von Sean Bright
Sean Hell

Es berechnet den Versatz des Stabes m relativ zur Startadresse der Darstellung eines Objekts vom Typ st.

((st *)(0)) bezieht sich auf a NULL Zeiger des Typs st *.
&((st *)(0))->m bezieht sich auf die Adresse des Mitglieds m in diesem Objekt. Da die Startadresse dieses Objekts ist 0 (NULL)ist die Adresse von Member m genau der Offset.

char * Konvertierung und die Differenz berechnet den Offset in Bytes. Gemäß Zeigeroperationen, wenn Sie einen Unterschied zwischen zwei Zeigern des Typs machen T *ist das Ergebnis die Anzahl der Objekte des Typs T zwischen den beiden in den Operanden enthaltenen Adressen dargestellt.

Benutzeravatar von Jonathan Leffler
Jonathan Leffler

Listing 1: Ein repräsentatives Set von offsetof() Makrodefinitionen

// Keil 8051 compiler
#define offsetof(s,m) (size_t)&(((s *)0)->m)

// Microsoft x86 compiler (version 7)
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

// Diab Coldfire compiler
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

typedef struct 
{
    int     i;
    float   f;
    char    c;
} SFOO;

int main(void)
{
  printf("Offset of 'f' is %zu\n", offsetof(SFOO, f));
}

Die verschiedenen Operatoren innerhalb des Makros werden in einer solchen Reihenfolge ausgewertet, dass die folgenden Schritte ausgeführt werden:

  1. ((s *)0) nimmt die Ganzzahl Null und wandelt sie als Zeiger auf um s.
  2. ((s *)0)->m dereferenziert diesen Zeiger, um auf das Strukturmitglied zu zeigen m.
  3. &(((s *)0)->m) berechnet die Adresse von m.
  4. (size_t)&(((s *)0)->m) wandelt das Ergebnis in einen geeigneten Datentyp um.

Per Definition befindet sich die Struktur selbst an Adresse 0. Daraus folgt, dass die Adresse des Felds, auf das gezeigt wird (Schritt 3 oben), der Offset in Bytes vom Beginn der Struktur sein muss.

1393270cookie-checkWarum funktioniert diese Implementierung von offsetof()?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy