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


111

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

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のアプローチを知っています。具体的には、上記のトリックのバージョンに忠実に答えてほしいと思います。


33
これは非常に明確で合理的で、何よりも答えられる質問のようです。近い投票の理由を見ていません。
cHao

2
struct hackをサポートしない「ansi c」コンパイラーを導入した場合、私が知っているほとんどのcプログラマーは、ご使用のコンパイラーが「正しく機能した」ことを受け入れません。彼らが規格を厳格に読むことを受け入れるだろうとは思わない。委員会は単にそれを逃した。
dmckee ---元モデレーターの子猫

4
@jamesハックは、最小限の配列を宣言しているにもかかわらず、意図した配列に十分な大きさのオブジェクトをmallocすることで機能します。したがって、構造体の厳密な定義の外で割り当てられたメモリにアクセスしています。割り当てを超えて書き込むことは間違いのない間違いではありませんが、それは割り当てを書き込むのとは異なりますが、「構造体」の外側です。
dmckee ---元モデレーターの子猫2010

2
@ジェームズ:特大mallocはここで重要です。それはメモリがあることを保証します-正当なアドレスを持ち、構造体によって '所有されている'(つまり、他のエンティティがそれを使用することは違法です)---構造体の名目上の終わりを超えています。これは、自動変数でstruct hackを使用できないことを意味します。それらは動的に割り当てられる必要があります。
dmckee ---元モデレーターの子猫2010

5
@detly:特に、後者には対処する必要がある2つの失敗方法があるため、2つのものを割り当てる/割り当てるよりも、1つのものを割り当てる/割り当てを解除する方が簡単です。これは、わずかなコスト/速度の節約よりも重要です。
jamesdlin

回答:


52

同様にC FAQは言います:

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

そして:

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

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

  • a[1][7]宣言された左辺値式のように)指定された添え字でオブジェクトにアクセスできるように見えても、配列添え字は範囲外ですint a[4][5](6.5.6)。

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

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


1
OPのコードでp->sは、配列として使用されることはありません。それはに渡されますstrcpy。その場合、プレーンに減衰します。プレーンchar *char [100];、割り当てられたオブジェクトの内部として法的に解釈できるオブジェクトを指します。
R .. GitHub ICE HELPING ICEの停止

3
おそらくこれを別の見方で見ると、J.2で説明されているように、実際の配列変数へのアクセス方法を言語が制限している可能性がありmallocますが、返された値を単に変換しただけでは、void *配列[を含む構造体]へのポインタへ。へのポインタchar(またはできればunsigned char)を使用して、割り当てられたオブジェクトの任意の部分にアクセスすることは引き続き有効です。
R .. GitHub ICE HELPING ICEの停止

@R。-J2がこれをカバーしていない可能性があることがわかりますが、6.5.6でもカバーされていませんか?
10

1
もちろんできます!タイプとサイズの情報がすべてのポインターに埋め込まれ、誤ったポインター演算がトラップされる可能性があります(CCuredなどを参照)。より哲学的なレベルでは、可能な実装があなたを捕まえることができたかどうかは関係ありません、それは未定義の動作です(iirc、ホールティング問題を解明するためのオラクルを必要とする未定義の動作のケースがあります-これがまさに理由ですそれらは未定義です)。
zwol

4
オブジェクトは配列オブジェクトではないため、6.5.6は無関係です。オブジェクトは、によって割り当てられたメモリのブロックですmalloc。bsを噴出する前に、標準で「オブジェクト」を検索してください。
R .. GitHub ICE HELPING ICEの停止

34

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

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

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

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


2
また、配列のサイズarr[x] = y;が偶然1の場合は、次のように書き直される可能性があると判断するコンパイラも想像できarr[0] = y;ます。サイズ2の配列の場合、次のarr[i] = 4;ように書き直される可能性があります。i ? arr[1] = 4 : arr[0] = 4; コンパイラーがそのような最適化を実行するのを見たことはありませんが、一部の組み込みシステムでは、非常に生産的です。PIC18xでは、8ビットのデータ型を使用して、最初のステートメントのコードは16バイト、2番目、2、4、3番目、8、12になります。合法であれば、最適化は悪くない。
スーパーキャット2013年

規格が配列境界外の配列アクセスを未定義の動作として定義している場合、構造体ハックもそうです。ただし、標準で配列アクセスをポインター演算(a[2] == a + 2)の構文糖として定義している場合は、そうではありません。私が正しければ、すべてのC標準は配列アクセスを算術ポインタとして定義しています。
yyny

13

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

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

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

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

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

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


2
これを見つけるための+1ですが、それでも矛盾すると主張します。同じオブジェクトへの2つのポインター(この場合、指定されたバイト)は等しく、1つへのポインター(によって取得されたオブジェクト全体の表現配列へのポインターmalloc)は追加で有効です。別のルートを介して取得した、追加で無効ですか?彼らがそれがUBであると主張したとしても、実装が明確に定義された使用法と想定されていない使用法を区別する計算方法がないため、それはかなり意味がありません。
R .. GitHub ICE HELPING ICEを停止する'13

Cコンパイラが長さゼロの配列の宣言を禁止し始めたことは残念です。その禁止事項がなければ、多くのコンパイラは、「本来あるべき」ように機能させるために特別な処理を行う必要はありませんでしたが、単一要素配列のコードを特別な場合に(たとえば*foo、単一要素の配列boz、式foo->boz[biz()*391]=9;は)に簡略化できますbiz(),foo->boz[0]=9;。残念ながら、コンパイラーによるゼロ要素配列の拒否は、多くのコードが代わりに単一要素配列を使用することを意味し、その最適化によって破壊されます。
スーパーキャット2012年

11

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


平凡であるために、それは構造体ハックではありません。struct hackは、柔軟な配列メンバーではなく、固定サイズの配列を使用します。struct hackは、尋ねられたものであり、UBです。柔軟な配列メンバーは、このスレッドでその事実について不平を言っている人々をなだめる試みのように見えます。
underscore_d

7

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

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

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

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

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


4
ポインタ演算の性質は無関係であるということは正しいですが、配列の宣言されたサイズを超えるアクセスについては間違っています。N1494(最新のパブリックC1xドラフト)セクション6.5.6パラグラフ8を参照してください- 宣言された配列のサイズを超えて複数の要素を指すポインターを追加することも許可されていません。それは過去の要素の1つにすぎません。
zwol

1
@Zack:オブジェクトが配列の場合、これは真です。オブジェクトがmalloc配列としてアクセスされている割り当てられたオブジェクトである場合、または特に、大きな構造体の要素の最初のサブセットである要素を持つ小さな構造体へのポインターを介してアクセスされる大きな構造体である場合は、正しくありません。ケース。
R .. GitHub ICE HELPING ICEの停止

6
+1 mallocポインタ演算でアクセスできるメモリの範囲を割り当てない場合、それはどのように使用されますか?場合とp->s[1]されて定義されたポインタ演算のためのシンタックスシュガーとして標準によって、この答えは、単にそれが再アサートmalloc便利です。議論すべきことは何ですか?:)
Daniel Earwicker

3
あなたはそれが好きなだけ明確に定義されていると主張することができますが、それはそうではないという事実を変えません。標準では、配列の境界を越えたアクセスについて非常に明確であり、この配列の境界は1です。それはまさにそれと同じくらい簡単です。
オービットのライトネスレース

3
@R ..、私は、等しいと比較する2つのポインタが同じように動作する必要があるというあなたの仮定は間違っていると思います。ブランチに入るとint m[1]; int n[1]; if(m+1 == n) m[1] = 0;想定してくださいif。これはn私が読んだ6.5.6 p8(最後の文)によるUB(初期化が保証されていない)です。関連:6.5.9 p6と脚注109。(参照はC11 n1570を参照。)[...]
mafso

7

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

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

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

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

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


1
(2)では、s[1]未定義の動作ではありません。これ*(s+1)は、と同じ*((char *)p + offsetof(struct T, s) + 1)です。これはchar、割り当てられたオブジェクト内のへの有効なポインタです。
R .. GitHub ICE HELPING ICEの停止

一方、(3)は未定義の動作であるとほぼ確信しています。そのアドレスにあるそのような構造体に依存する操作を実行するときはいつでも、コンパイラーは構造体の任意の部分から読み取るマシンコードを自由に生成できます。それは役に立たないかもしれませんし、厳密な割り当てチェックの安全機能かもしれませんが、実装がそれを行うことができなかった理由はありません。
R .. GitHub ICE HELPING ICEの停止

R:配列がサイズを持つように宣言された場合(のfoo[]構文上の砂糖ではない*foo)、ポインターの計算方法に関係なく、宣言されたサイズと割り当てられたサイズの小さい方を超えるアクセスはUBになります。
zwol

1
@ザック、あなたはいくつかの点で間違っています。foo[]構造体では、構文糖ではありません*foo。これは、C99の柔軟な配列メンバーです。残りについては、私の回答と他の回答に関するコメントを参照してください。
R .. GitHub ICE HELPING ICEの停止

6
問題は、委員会の一部のメンバーがこの「ハック」をUBにすることを必死に望んでいることです。Cの実装がポインターの境界を強制できる妖精の国を想定しているためです。しかし、良くも悪くも、そうすることは標準の他の部分と競合します-ポインターの同等性を比較する機能(境界がポインター自体にエンコードされている場合)、または架空のオーバーレイされたunsigned char [sizeof object]配列を介してオブジェクトにアクセスできるという要件。C99以前の柔軟な配列メンバー「ハック」には明確に定義された動作があるという私の主張を支持します。
R .. GitHub ICE HELPING ICEの停止

3

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

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


例を挙げることができますか?
Tal

1

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

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

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

typedef struct {
  int何でも;
  char dat [1];
MY_STRUCT;

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

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
  int何でも;
  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などの一部の古い方言はそれをサポートします)。 。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.