web-dev-qa-db-ja.com

Cでのストリクトエイリアスの違反(キャストなしでも)

iが_*i_として定義されている場合でも、_u.i_と_int *i = &u.i;_はこのコードで異なる数値を出力できますか?ここでUBをトリガーしていると仮定することしかできませんが、どの程度正確かはわかりません。

ideone demo 言語として 'C'を選択すると複製されます。しかし、 'C99 strict'が言語である場合ではなく、@ 2501が指摘したように、その後再び_gcc-5.3.0 -std=c99_!)

_// gcc       -fstrict-aliasing -std=c99   -O2
union
{   
    int i;
    short s;
} u;

int     * i = &u.i;
short   * s = &u.s;

int main()
{   
    *i  = 2;
    *s  = 100;

    printf(" *i = %d\n",  *i); // prints 2
    printf("u.i = %d\n", u.i); // prints 100

    return 0;
}
_

(gcc 5.3.0、_-fstrict-aliasing -std=c99 -O2_、および_-std=c11_も使用)

私の理論では、short- lvalue _100_を介したユニオンメンバーへの書き込みは、このように定義されているため、_*s_は「正しい」答えです。 )。しかし、オプティマイザは_*s_への書き込みが_u.i_をエイリアスできることを認識していないため、_*i=2;_が_*i_に影響を与える唯一の行であると考えています。これは合理的な理論ですか?

_*s_が_u.i_を別名設定でき、_u.i_が_*i_を別名設定できる場合、コンパイラーは_*s_が_*i_を別名設定できると確実に考えるはずです。エイリアシングは「推移的」であってはなりませんか?

最後に、ストリクトエイリアスの問題はキャスティングの不良が原因であるという前提が常にありました。しかし、これにはキャストはありません!

(私の経歴はC++です。ここでCについて妥当な質問をしたいと思っています。私の(限定的な)理解は、C99では、ある組合員を介して書き込み、次に別の組合員を介して読むことですタイプ。)

58
Aaron McDaid

不一致は、-fstrict-aliasing最適化オプションによって発行されます。その動作と可能なトラップは GCCドキュメント で説明されています:

このようなコードには特に注意してください。

      union a_union {
        int i;
        double d;
      };

      int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

直近に書かれたものとは別の組合員から読む(「タイプ・パニング」と呼ばれる)慣行は一般的です。 -fstrict-aliasingであっても、型のパニングは許可されます。ただし、メモリは共用体型を介してアクセスされます。したがって、上記のコードは期待どおりに機能します。 Structures unions enumerations and bit-fields implementation を参照してください。 しかし、このコードはそうではないかもしれません

      int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

2番目のコード例が ndefined behaviour を示しているため、適合実装はこの最適化を完全に活用できることに注意してください。 Olaf's および他の回答を参照してください。

56

C標準(つまり、C11、n1570)、6.5p7

オブジェクトは、次のいずれかのタイプを持つlvalue式によってのみアクセス値を保存します:

  • ...
  • そのメンバーの中に前述のタイプのいずれかを含む集約または共用体タイプ(サブ集約または包含された共用体のメンバーを再帰的に含む)、または文字タイプ。

ポインターの左辺値式はnotunion型であるため、この例外は適用されません。コンパイラーは、この未定義の振る舞いを正しく利用しています。

ポインターの型をunion型へのポインターにし、それぞれのメンバーで逆参照します。それはうまくいくはずです:

union {
    ...
} u, *i, *p;

C規格では厳密なエイリアシングは指定不足ですが、通常の解釈では、ユニオンエイリアシング(厳密なエイリアシングに優先します)は、ユニオンメンバーが名前で直接アクセスされた場合にのみ許可されます。

この背後にある理論的根拠については、以下を考慮してください。

void f(int *a, short *b) { 

ルールの目的は、コンパイラがabがエイリアスしないと仮定し、fで効率的なコードを生成できることです。しかし、コンパイラがabが重複する共用体メンバーである可能性があるという事実を考慮しなければならない場合、実際にはそれらの仮定を行うことができませんでした。

2つのポインターが関数パラメーターであるかどうかは重要ではありませんが、厳密なエイリアス規則はそれに基づいて区別しません。

12
M.M

厳密なエイリアスルールを尊重しないため、このコードは実際にUBを呼び出します。 6.5 Expressions§7のC99のn1256ドラフト:

オブジェクトには、次のいずれかのタイプの左辺値式によってのみアクセス値が保存されます。
—オブジェクトの有効なタイプと互換性のあるタイプ、
—オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
—オブジェクトの有効なタイプに対応する署名付きまたは署名なしのタイプであるタイプ、
—オブジェクトの有効なタイプの修飾バージョンに対応する署名付きまたは署名なしのタイプであるタイプ、
—そのメンバー(前述のサブアグリゲートまたは包含ユニオンのメンバーを再帰的に含む)の中に前述のタイプの1つを含む集約またはユニオンタイプ、または
—文字タイプ。

_*i = 2;_とprintf(" *i = %d\n", *i);の間では、短いオブジェクトのみが変更されます。厳密なエイリアスルールの助けを借りて、コンパイラはiが指すintオブジェクトが変更されていないことを自由に想定でき、メインメモリからリロードせずにキャッシュされた値を直接使用できます。

普通の人間が期待するものとはあからさまにではありませんが、厳密なエイリアスルールは、コンパイラがキャッシュされた値を使用できるように正確に記述されています。

2番目の印刷物については、6.2.6.1型の表現/全般§7の同じ標準で共用体が参照されます。

ユニオン型のオブジェクトのメンバーに値が格納されている場合、そのメンバーに対応しないが他のメンバーに対応するオブジェクト表現のバイトは、指定されていない値を取ります。

_u.s_が保存されているため、_u.i_は値を取りました標準では指定されていません

ただし、6.5.2.3構造およびユニオンメンバー§3注82で後から読むことができます。

ユニオンオブジェクトのコンテンツにアクセスするために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されたメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しい型のオブジェクト表現として再解釈されます6.2.6で説明されています(「タイプパンニング」と呼ばれることもあるプロセス)。これはトラップ表現である可能性があります。

メモは規範的ではありませんが、標準の理解を深めることができます。 _u.s_が_*s_ポインターを介して格納されている場合、shortに対応するバイトは2の値に変更されています。リトルエンディアンシステムを想定すると、100はshortの値よりも小さいため、上位バイトが0だったため、intとしての表現は2になります。

TL/DR:規範的でなくても、ノート82では、x86またはx64ファミリーのリトルエンディアンシステムで、printf("u.i = %d\n", u.i);が2を出力することを要求する必要があります。 iが指す値は変更されておらず、_100_を出力する可能性があると仮定しました

7
Serge Ballesta

C標準の議論の余地のある分野を調査しています。

これは厳密なエイリアスルールです。

オブジェクトには、次のいずれかのタイプの左辺値式によってのみアクセス値が保存されます。

  • オブジェクトの有効なタイプと互換性のあるタイプ、
  • オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型、
  • オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型、
  • メンバーの中に前述のタイプのいずれかを含む集約または共用体タイプ(再帰的に、サブ集約または包含された共用体のメンバーを含む)、
  • 文字タイプ。

(C2011、6.5/7)

左辺値式_*i_の型はintです。左辺値式_*s_の型はshortです。これらの型は相互に互換性がなく、他の特定の型とも互換性がありません。また、厳密なエイリアス規則は、ポインターがエイリアスされている場合に両方のアクセスが適合する他の代替手段を提供しません。

アクセスの少なくとも1つが不適合である場合、動作は未定義であるため、報告する結果、または実際には他の結果は完全に受け入れられます。実際には、コンパイラは、printf()呼び出しを使用して割り当てを並べ替えるコード、またはメモリから再読み込みする代わりに、レジスタから以前にロードした_*i_の値を使用するコードを生成する必要があります同様のこと。

前述の論争は、人々が時々脚注 95を指すために発生します:

ユニオンオブジェクトの内容を読み取るために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されたメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しい型のオブジェクト表現として再解釈されます6.2.6で説明されています(「タイプパンニング」と呼ばれることもあるプロセス)。これはトラップ表現である可能性があります。

脚注は情報提供であり、規範的ではないため、対立する場合にどのテキストが優先されるかは疑いの余地がありません。個人的に、私は単に実装ガイダンスとして脚注を取り、組合員のストレージが重複するという事実の意味を明確にします。

6
John Bollinger

これは、オプティマイザが魔法をかけた結果のようです。

-O0、両方の行が期待どおりに100を出力します(リトルエンディアンを想定)。 -O2、いくつかの並べ替えが行われています。

gdbの出力は次のとおりです。

(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000

Temporary breakpoint 1, main () at /tmp/x1.c:14
14      {
(gdb) step
15          *i  = 2;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
15          *i  = 2;
(gdb)
16          *s  = 100;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
 *i = 2
19          printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22      }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)

他の人が述べているように、これが起こる理由は、問題の変数が共用体の一部であっても、ある型の変数に別の型へのポインタを介してアクセスするのは未定義の動作だからです。したがって、この場合、オプティマイザーは自由に実行できます。

他のタイプの変数は、明確に定義された動作を保証する共用体を介してのみ直接読み取ることができます。

興味深いのは、-Wstrict-aliasing=2、gcc(4.8.4以降)は、このコードについて文句を言いません。

5
dbush

C89には、偶然によるものであろうと設計によるものであろうと、2つの異なる方法で解釈された言語が含まれています(その間のさまざまな解釈とともに)。問題は、あるタイプに使用されるストレージが別のタイプのポインターを介してアクセスされる可能性があることをコンパイラーが認識する必要がある場合の問題です。 C89の理論的根拠にある例では、グローバル変数これは明らかに共用体の一部ではないと異なる型へのポインターとの間でエイリアシングが考慮され、コードにはエイリアシングが発生することを示唆するものはない。

1つの解釈は言語をひどく不自由にしますが、もう1つの解釈は特定の最適化の使用を「非準拠」モードに制限します。セカンドクラスのステータスを与えられた優先最適化を行わなかった人々がC89を書いてそれらの解釈を明確に一致させた場合、標準のこれらの部分は広く非難され、壊れていないという何らかの明確な認識があったでしょうCの方言は、指定された規則の非縮図解釈を尊重します。

残念なことに、代わりに起こったことは、コンパイラライターが不自由な解釈を適用することをルールが明らかに必要としないためです。プログラマーは、彼らの観点からすれば、標準のずさんさにも関わらず、そうすべきだと誰もが自明であるように見えるので、標準がコンパイラが賢明に振る舞うことを命じなかったと文句を言う理由はありませんでした。一方、一部の人々は、標準では常にコンパイラーがRitchieのシステムプログラミング言語の意味的に弱められたサブセットを処理することを許可しているため、標準準拠のコンパイラーが他の何かを処理することを期待される理由はないと主張します。

この問題の賢明な解決策は、Cが複数のコンパイルモードが存在するように十分に多様な目的に使用されることを認識することです。1つの必須モードは、アドレスが取得されたすべてのアクセスを、基礎となるストレージを直接読み書きするかのように処理します、およびanyポインターベースの型のパンニングサポートのレベルを期待するコードと互換性があります。コードがディレクティブを明示的に使用して、あるタイプとして使用されていたストレージを別のタイプとして使用するために再解釈またはリサイクルする必要がある場合を示す場合を除き、別のモードではC11よりも制限があります。他のモードでは、いくつかの最適化が許可されますが、より厳密な方言の下で破損するコードがサポートされます。特定の方言を特別にサポートしていないコンパイラは、より定義されたエイリアシング動作を持つ方言に置き換えることができます。

1
supercat