5

厳密なエイリアシングによるバグがいくつかあったので、それらすべてを修正しようと思いました。それが何であるかを詳細に調べたところ、GCCが警告を発行しない場合があり、実装できないものもあるようです。少なくとも私の理解では、以下のすべてが壊れています。私の理解は間違っていますか、これらすべてのことを行う正しい方法はありますか、それとも技術的にルールを破ってシステムテストで十分にカバーする必要があるコードがありますか?

バグは、以下のように、char バッファーと unsigned char バッファーが混在しているコードに起因していました。

size_t Process(char *buf, char *end)
{
    char *p = buf;
    ProcessSome((unsigned char**)&p, (unsigned char*)end);
    //GCC decided p could not be changed by ProcessSome and so always returned 0
    return (size_t)(p - buf);
}

これを以下に変更すると問題が解決したようですが、まだキャストが含まれているため、なぜこれが機能し、警告が表示されないのかわかりません。

size_t Process(char *buf, char *end)
{
    unsigned char *buf2 = (unsigned char *)buf;
    unsigned char *p = buf2;
    unsigned char *end2 = (unsigned char*)end;
    ProcessSome(&p, end2);
    return (size_t)(p - buf2);
}

また、警告なしで動作するように見える他の場所がたくさんあります

//contains a unsigned char* of data. Possibly from the network, disk, etc.
//the buffer contents itself is 8 byte aligned.
const Buffer *buffer = foo();
const uint16_t *utf16Text = (const uint16_t*)buffer->GetData();//const unsigned char*
//... read utf16Text. Does not even seem to ever be a warning


//also seems to work fine
size_t len = CalculateWorstCaseLength(...);
Buffer *buffer = new Buffer(len * 2);
uint16_t *utf16 = (uint16_t*)buffer->GetData();//unsigned char*
len = DoSomeProcessing(utf16, len, ...);
buffer->Truncate(len * 2);
send(buffer);

そして、いくつかの...

struct Hash128
{
    unsigned char data[16];
};
...
size_t operator ()(const Hash128 &hash)
{
    return *(size_t*)hash.data;//warning
}

非チャーケース。これには警告がありません。たとえそれが悪いとしても、どうすれば回避できますか (どちらの方法でもうまくいくようです)。

int *x = fromsomewhere();//aligned to 16 bytes, array of 4
__m128i xmm = _mm_load_si128((__m128*i)x);
__m128i xmm2 = *(__m128i*)x;

他の API を見ると、私の理解では、ルールに違反しているさまざまなケースがあるようです (Linux/GCC 固有のものに遭遇したことはありませんが、どこかに必ずあるはずです)。

  1. CoCreateInstance 明示的なポインター キャストを必要とする void** 出力パラメーターがあります。Direct3D にもこのようなものがあります。

  2. LARGE_INTEGER は、さまざまなメンバーに対して読み取り/書き込みを行う可能性が高い共用体です (たとえば、一部のコードは高/低を使用し、他のコードは int64 を読み取る可能性があります)。

  3. CPython の実装は、PyObject* を、たまたま最初に同じメモリ レイアウトを持つ他の多くのものに非常に喜んでキャストしたことを思い出します。

  4. 私が見た多くのハッシュ実装は、入力バッファーを uint32_t* にキャストし、おそらく uint8_t を使用して最後に 1 ~ 3 バイトを処理します。

  5. 私が見たほとんどすべてのメモリ アロケータの実装では、char* または unsigned char* を使用しています。これらは、目的の型にキャストする必要があります (おそらく、返された void* を介してですが、内部的には少なくとも char でした)。

4

2 に答える 2

4

まず、へのポインターcharとへのポインターunsigned charは、文字列のエイリアシングに関する規則からほとんど除外されています。char*任意の型のポインタをまたはに変換することができunsigned char*、ポイントされたオブジェクトをchar またはの配列として見ることができunsigned charます。さて、あなたのコードに関して:

size_t Process(char *buf, char *end)
{
    char *p = buf;
    ProcessSome((unsigned char**)&p, (unsigned char*)end);
    //GCC decided p could not be changed by ProcessSome and so always returned 0
    return (size_t)(p - buf);
}

char*ここでの問題は、 a を であるかのように見ようとしていることですunsigned char*。それは保証されていません。キャストが明確に見えることを考えると、厳密なエイリアシング分析を自動的にオフにしないことについて g++ は少し鈍感ですが、技術的には標準でカバーされています。

size_t Process(char *buf, char *end)
{
    unsigned char *buf2 = (unsigned char *)buf;
    unsigned char *p = buf2;
    unsigned char *end2 = (unsigned char*)end;
    ProcessSome(&p, end2);
    return (size_t)(p - buf2);
}

一方、すべての変換には と が含まchar*unsigned char*、どちらも何かに別名を付ける可能性があるため、これを機能させるにはコンパイラが必要です。

残りに関しては、戻り値の型が何であるかを言っていない buffer->GetData()ので、言うのは難しいです。しかし、それが char*unsigned char*またはvoid*である場合、コードは完全に合法です ( の 2 回目の使用でキャストが欠落している場合を除く buffer->GetData())。すべてのキャストに a char*、 anunsigned char*または a void*(修飾子を無視const する) が含まれている限り、コンパイラはエイリアシングの可能性があると想定する必要があります。元のポインターがこれらの型のいずれかを持つ場合、それはポインターからターゲット型へのキャスト。言語は、任意のポインターをこれらの型のいずれかに変換し、元の型に戻し、同じ値を回復できることを保証します。(もちろん、char*元々がuint16_t、アラインメントの問題が発生する可能性がありますが、コンパイラは通常これを認識できません。)

最後の例に関しては、 の型を示していない hash.dataので、言いにくいです。char*void*または の場合unsigned char*、言語はコードを保証します (技術的には、 char ポインターが を変換することによって作成されたsize_t*場合、実際には、ポインターが十分にアラインされており、ポイント先のバイトが のトラップ値を形成しない場合size_t)。

一般に、「型パニング」の本当に保証された唯一の方法は、 によるものmemcpyです。それ以外の場合、少なくともエイリアシングに関する限り、あなたが行っているようなポインターキャストはvoid*char*またはからである限り保証されます。unsigned char*(これらのいずれかから、アラインメントの問題が発生したり、逆参照した場合にトラッピング値にアクセスしたりする可能性があります。)

他の標準から追加の保証が得られる場合があることに注意してください。Posix には次のようなものが必要です。

void (*pf)();
*((void**)&pf) = ...

たとえば、働くこと。(通常、エイリアシングが関連する可能性のある関数で他に何もしなければ、g++ を使用しても、キャストと逆参照はすぐに機能します。)

unionそして、私が知っているすべてのコンパイラは、場合によってはfor 型のしゃれを使用できます。(そして、g++ を含む少なくとも一部は、他の場合に を正当に使用すると失敗します。 が表示されていない場合、コンパイラ ライターにとって a をunion正しく処理することは困難です。)unionunion

于 2013-07-18T12:12:40.147 に答える
0

char/unsigned charポインタは厳密なエイリアシング ルールから除外されます。

ユニオン トリックは技術的にはエイリアシング エラーですが、主流のコンパイラでは明示的に許可されています。

したがって、いくつかの例は有効です(言語によってはUBですが、コンパイラによって明確に定義されているものもあります)。

しかし、はい、エイリアシングの規則に違反するコードがたくさんあります。また、MSVC は厳密なエイリアシングに基づく最適化を行わないため、特に Windows 用に記述されたコードは、厳密なエイリアシング ルールに違反する傾向があることに注意してください。

于 2013-07-18T11:40:35.157 に答える