2009-04-03 19 views
30

ANSI Cでは、offsetofは以下のように定義されています。我々はNULLポインタを参照解除されているのでoffsetof()のこの実装はなぜ機能しますか?

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

なぜこれがセグメンテーションフォールトをスローしないのだろうか?または、オフセットのアドレスだけが取り出されるのを見て、実際に逆参照せずに静的にアドレスを計算するコンパイラのハッキングの何らかの種類ですか?また、このコードは移植可能ですか?

+2

これは、私がSO上で見た最初の質問ですか? : – paxdiablo

+2

if(0){asm(nop)}を出した人が何か失敗した場合... – RBerteig

+3

ANSI C(実際にはISO C)は、この定義を 'offsetof'に指定していません。どのように動作しなければならないかを指定するだけです。実際の定義は各実装までであり、ある実装を別の実装に変えることができます。 –

答えて

32

上記のコードでは、逆参照されたものはありません。逆参照は、*または->が参照値を検索するためにアドレス値に使用されたときに発生します。上記の*の唯一の使用は、キャストのための型宣言にあります。

上記の->演算子が使用されていますが、値にアクセスするためには使用されていません。代わりに、値のアドレスを取得するために使用されます。ここで

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

二行目は、実際にデリファレンス(実装依存)が発生しません、それは少し明確にすべきである非マクロコードサンプルです。これは、単にのアドレスをpSomeTypeの値で返します。

あなたが見るものは、任意の型とcharポインタの間の多くのキャストです。 charの理由は、明示的なサイズを持つC89標準の唯一の型(たぶん唯一の型)の1つです。サイズは1です。上記のコードはサイズが1であることを保証することで、値の真のオフセットを計算する悪魔の魔法を実行できます。

+0

私はC標準を利用することはできませんが、C90では、任意のアドレスを(間接参照するだけでなく)使うことができないということを思い出していました。その理由は、セグメント・レジスタを使用していた8086やIBM 370のようなマシンであり、アドレス空間全体を参照することができませんでした。 –

+0

C標準では、 '&(pSomeType-> SomeIntMember)'の ' - >'は逆参照を引き起こします。おそらく、そうではないと主張したときの意味を明確にすることができます。 –

2

逆参照していないため、セグメンテーションが発生しません。ポインタアドレスは、メモリ操作のアドレス指定に使用されていない別の番号から減算された数値として使用されています。

2

タイプstのオブジェクトの表示の開始アドレスに対する、mのオフセットを計算します。

((st *)(0))st *NULLポインタを指します。 &((st *)(0))->mは、このオブジェクトのメンバmのアドレスを参照します。このオブジェクトの開始アドレスは0 (NULL)なので、メンバーmのアドレスはちょうどオフセットです。

char *変換し、その差はオフセットをバイト単位で計算します。ポインタ操作によれば、タイプT *の2つのポインタの間に違いを生じさせた場合、結果は、オペランドに含まれる2つのアドレスの間に表されるタイプTのオブジェクトの数になります。

+0

ショーン、なぜその減算が必要でしたか?私たちはちょうど(char *)&((st *)(0)) - > m? – chappar

+0

私は減算が本当に必要ではないと思っていますが、私は100%確信していません... –

+0

nullポインタが値0で内部的に表されないC実装があります。このような実装では、コンパイラはポインタの算術演算でnullポインタを処理する方法を知らないので、このCコードが完全に失敗するか、またはNULLポインタの表現にキャンセルされます)。 – vinc17

8

ANSI Cでは、offsetofはそのように定義されていません。そのように定義されていない理由の1つは、一部の環境で実際にnullポインタ例外がスローされたり、別の方法でクラッシュすることです。したがって、ANSI Cは、offsetof()の実装をコンパイラビルダーに公開します。

上記のコードは、NULLポインタを積極的にチェックするのではなく、NULLポインタからバイトを読み取った場合にのみ失敗するコンパイラ/環境で一般的です。

+0

'offsetof()'マクロは、ポインタが効果的に整数である大部分のプラットフォームでは、問題に示されているように、あるいは単に減算なしで広く実装されています。ほとんどのCコンパイラは、積極的にNULLポインタをチェックしません。使用される表現は** NOT ** dereference * anything *です。単にメンバーの内部的に既知のオフセットの単純な算術加算でアドレス(ゼロになる)を使用してオフセットを計算します。最適化されると、実行時の加算も行われません。 –

6

質問の最後の部分に答えるために、コードは移植性がありません。

2つのポインタを減算した結果は、2つのポインタが同じ配列内のオブジェクトを指し示すか、または配列の最後のオブジェクトを指すポインタ(7.6。2つの加法演算子、H & S第5版)

7

ことがoffsetofの典型的な実装であるが、それは単に言う標準で義務付けされていません。

次のタイプとマクロがで定義されています標準ヘッダ<stddef.h> [...]

offsetof(type,member-designator)

size_t

型を持つ整数定数式、(によって指定その構造の先頭から 、(member-designatorによって指定される)構造部材に、バイト単位でオフセットさ の値に展開されtype)。型部材指示 は

statictypet;

所与次いで発現&(t.member-designator)アドレス定数に評価するようなものでなければなりません。 (指定したメンバーがビットフィールドである場合、動作は未定義である。)

読むPJ Plaugerの「標準Cライブラリ」、それについての説明は、すべてのボーダーライン機能です<stddef.h>内の他のアイテムそれは適切な言語であり、特別なコンパイラのサポートが必要な場合があります。

私は386/IX上で初期のANSI Cコンパイラを使っていましたが(私は、1990年頃の歴史的な関心事をお伝えしました)、offsetofのバージョンでクラッシュしましたが、

ヘッダがコンパイラと一緒に配布され、機能しなかったではない、少なくともので、種類のコンパイラのバグだった
#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024)) 

1

リスト1:offsetof()マクロ定義

// 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)); 
} 

の代表的なセットマクロ内の種々の演算子は、以下のステップが実行されるように順番に評価される:

  1. ((s *)0)整数を取り0を返し、sへのポインタとしてキャストします。
  2. ((s *)0)->mは、構造体メンバを指すポインタmを参照しています。
  3. &(((s *)0)->m)は、mのアドレスを計算します。
  4. (size_t)&(((s *)0)->m)は、結果を適切なデータ型にキャストします。

定義により、構造体自体はアドレス0に存在します。その結果、(上記のステップ3)で指し示されたフィールドのアドレスは、構造体の先頭からのバイト単位のオフセットでなければなりません。

関連する問題