web-dev-qa-db-ja.com

「struct hack」は技術的に未定義の動作ですか?

私が尋ねているのは、よく知られている「構造体の最後のメンバーは可変長です」というトリックです。それはこのようなものになります:

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

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

構造体がメモリに配置される方法により、必要なブロックよりも大きい構造体に構造体をオーバーレイし、最後のメンバーを指定された1 charよりも大きいものとして扱うことができます。

だから問題は:この手法は技術的に未定義の動作ですか?。私はそれがそうであると期待しますが、標準がこれについて何を言っているのか興味がありました。

PS:私はこれに対するC99のアプローチを知っています。上記のトリックのバージョンに特に忠実に答えてほしいと思います。

110
Evan Teran

C FAQ が言うように:

合法かポータブルかははっきりしないが、かなり人気がある。

そして:

...公式の解釈では、C規格に厳密に準拠していないと見なされていますが、既知のすべての実装で機能するようです。 (配列の境界を注意深くチェックするコンパイラーは警告を発行する場合があります。)

「厳密に準拠する」ビットの根拠は、仕様のセクションにあります。J.2未定義の動作は、未定義の動作のリストに含まれています。

  • 与えられた添え字でオブジェクトにアクセスできるように見える場合でも、配列の添え字は範囲外です(宣言a[1][7]が与えられたlvalue式int a[4][5]のように)(6.5.6)。

セクションのパラグラフ86.5.6加算演算子には、定義された配列の境界を超えるアクセスは未定義であるという別の記述があります。

ポインターオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素の1つ前を指している場合、評価によってオーバーフローは発生しません。それ以外の場合、動作は未定義です。

52
Carl Norum

技術的には未定義の動作だと思います。標準は(おそらく)それを直接扱っていないため、「または動作の明示的な定義の省略」に該当します。未定義の動作であることを示す節(C99の§4/ 2、C89の§3.16/ 2)。

上記の「間違いなく」は、配列の添字演算子の定義に依存します。具体的には、「角かっこ[]内の式が後に続くPostfix式は、配列オブジェクトの添え字付きの指定です。」 (C89、§6.3.2.1/ 2)。

「配列オブジェクトの」がここで違反されていると主張することができます(配列オブジェクトの定義された範囲外で添え字を付けているため)。この場合、動作は(ほんの少し)単に未定義ではなく、明示的に未定義です。それを完全に定義しているものはありません。

理論的には、配列の境界チェックを行い、範囲外の添え字を使用しようとしたときに(たとえば)プログラムを中止するコンパイラを想像できます。実際、私はそのようなことが存在することを知りません、そしてこのスタイルのコードの人気を考えると、コンパイラーがある状況下で添え字を強制しようとしたとしても、誰かがそうすることに我慢するだろうとは想像しがたいですこの状況。

34
Jerry Coffin

はい、それは未定義の動作です。

C言語欠陥レポート#051は、この質問に対する決定的な回答を提供します。

イディオムは一般的ですが、厳密に準拠していません

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

C99理論的根拠文書で、C委員会は以下を追加します。

この構造の妥当性は常に疑問視されてきました。委員会は、1つの欠陥報告への対応として、スペースの存在の有無に関係なく、配列p-> itemsには1つの項目しか含まれていないため、未定義の動作であると判断しました。

13
ouah

その特定の方法は、どのC標準でも明示的に定義されていませんが、C99には言語の一部として「構造体ハック」が含まれています。 C99では、構造体の最後のメンバーはchar foo[]charの代わりに任意の型を使用)として宣言された「柔軟な配列メンバー」である場合があります。

11
Chuck

これは未定義の動作ではありません、それは誰にでも関係ありません公式またはその他は、それが標準によって定義されているためです。 p->sは、左辺値として使用される場合を除いて、(char *)p + offsetof(struct T, s)と同一のポインターに評価されます。特に、これはmallocされたオブジェクト内の有効なcharポインターであり、その直後に100(またはそれ以上、アライメントの考慮事項に依存)の連続アドレスがあり、charとしても有効です。割り当てられたオブジェクト内のオブジェクト。ポインタがmallocによって返されたポインタに明示的にオフセットを追加する代わりに->を使用して導出され、char *にキャストされたという事実は関係ありません。

技術的には、p->s[0]は、構造体内のchar配列の単一の要素であり、次のいくつかの要素(p->s[1]からp->s[3]など)は、構造体内のバイトにパディングされている可能性があります。これは、構造体全体への割り当てを実行すると破損する可能性がありますが、個々のメンバーにアクセスするだけでは破損せず、残りの要素は、割り当てられたオブジェクト内の追加スペースであり、自由に使用できます。アラインメント要件に従います(およびcharにはアラインメント要件はありません)。

構造体のパディングバイトとオーバーラップする可能性が何らかの理由で鼻の悪魔を呼び出すのではないかと心配している場合は、1[1]を、パディングがないことを保証する値に置き換えることで回避できます。構造体の終わり。これを行う単純で無駄な方法は、最後に配列がないことを除いて同一のメンバーで構造体を作成し、配列にs[sizeof struct that_other_struct];を使用することです。次に、p->s[i]は、i<sizeof struct that_other_structの構造体の配列の要素として、およびi>=sizeof struct that_other_structの構造体の末尾に続くアドレスのcharオブジェクトとして明確に定義されています。

編集:実際には、正しいサイズを取得するための上記のトリックでは、すべての単純型を含む共用体を配列の前に配置して、配列自体が他の要素のパディングの中央。繰り返しになりますが、これが必要だとは思いませんが、言語の弁護士の中で最も偏執的な人のために提供しています。

編集2:標準の別の部分により、埋め込みバイトとの重複は問題にはなりません。 Cは、2つの構造体がそれらの要素の最初のサブシーケンスで一致する場合、いずれかの型へのポインターを介して共通の初期要素にアクセスできることを要求します。結果として、struct Tと同一の構造体がより大きな最終配列で宣言されている場合、要素s[0]s[0]の要素struct Tと一致する必要があります。これらの追加要素の存在は、struct Tへのポインターを使用して、より大きな構造体の共通要素にアクセスすることで影響を受けないか、影響を受ける可能性があります。

はい、技術的に未定義の動作です。

「struct hack」を実装するには少なくとも3つの方法があることに注意してください。

(1)サイズ0の後続配列を​​宣言(レガシーコードで最も「人気のある」方法)。ゼロサイズの配列宣言はCでは常に不正であるため、これは明らかにUBです。コンパイルしても、言語は制約違反コードの動作について保証しません。

(2)有効な最小サイズの配列を宣言-1(あなたのケース)。この場合、p->s[0]へのポインターを取得し、それをp->s[1]を超えるポインター演算に使用しようとすると、未定義の動作になります。たとえば、デバッグ実装では、範囲情報が埋め込まれた特別なポインターを生成できます。これにより、p->s[1]を超えるポインターを作成しようとするたびにトラップされます。

(3)「非常に大きい」サイズで配列を宣言たとえば、10000のようにします。つまり、宣言されたサイズは、実際に必要なものよりも大きくなるはずです。この方法では、アレイのアクセス範囲に関してUBはありません。ただし、実際には、もちろん、実際には(実際に必要な分だけ)少量のメモリを割り当てます。これの合法性についてはわかりません。つまり、オブジェクトの宣言されたサイズよりも少ないメモリをオブジェクトに割り当てることはどの程度合法なのでしょうか(「割り当てられていない」メンバーにアクセスしないと仮定します)。

7
AnT

標準では、配列の末尾の横にあるものにはアクセスできないことが明確です。 (そして、配列の終わりの後でポインタを1を超えてインクリメントすることもできないので、ポインタを経由することは役に立ちません)。

そして「実際に働く」ために。 gcc/g ++オプティマイザが標準のこの部分を使用しているため、この無効なCに遭遇すると間違ったコードが生成されるのを見てきました。

3

コンパイラが次のようなものを受け入れる場合

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

「dat」の添え字をその長さを超えて受け入れる準備ができている必要があることはかなり明白だと思います。一方、誰かが次のようなコードを書いた場合:

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

その後、後でsomestruct-> dat [x]にアクセスします。コンパイラーは、xの大きな値で機能するアドレス計算コードを使用する義務を負っていないと思います。本当に安全になりたいのであれば、適切なパラダイムは次のようになるでしょう。

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

次に、(sizeof(MYSTRUCT)-LARGEST_DAT_SIZE + desired_array_length)バイトのmallocを実行します(desired_array_lengthがLARGEST_DAT_SIZEより大きい場合、結果が未定義になる可能性があることに注意してください)。

ちなみに、長さ0の配列はコンパイラがより大きなインデックスで動作するコードを生成する必要があるという印と見なすことができるため、長さ0の配列を禁止する決定は不幸なものであったと思います(Turbo Cなどの一部の古い方言はそれをサポートします)。 。

1
supercat