web-dev-qa-db-ja.com

「揮発性」の定義は揮発性ですか、それともGCCには標準的なコンプライアンスの問題がありますか?

(WinAPIのSecureZeroMemoryのように)常にメモリをゼロにし、その後メモリが再びアクセスされることはないとコンパイラが判断しても、最適化されない関数が必要です。 volatileの完璧な候補のようです。しかし、実際にこれをGCCで機能させるにはいくつかの問題があります。以下に関数の例を示します。

void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

簡単です。しかし、GCCを呼び出すとGCCが実際に生成するコードは、コンパイラのバージョンと実際にゼロにしようとしているバイト数によって大きく異なります。 https://godbolt.org/g/cMaQm2

  • GCC 4.4.7および4.5.3はvolatileを無視しません。
  • GCC 4.6.4および4.7.3は、配列サイズ1、2、および4のvolatileを無視します。
  • GCC 4.8.1から4.9.2までは、配列サイズ1および2のvolatileを無視します。
  • GCC 5.1から5.3までは、配列サイズ1、2、4、8のvolatileを無視します。
  • GCC 6.1は、配列サイズについては無視します(一貫性のためのボーナスポイント)。

私がテストした他のコンパイラー(clang、icc、vc)は、コンパイラーのバージョンと配列サイズを問わず、予想されるストアを生成します。この時点で、これは(かなり古くて深刻な)GCCコンパイラのバグなのか、これが実際に動作に準拠していることを不正確にする標準のvolatileの定義であり、本質的に移植性のある「 SecureZeroMemory」機能

編集:いくつかの興味深い観察。

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>

void callMeMaybe(char* buf);

void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
    for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
    {
        *bytePtr++ = 0;
    }

    //std::atomic_thread_fence(std::memory_order_release);
}

std::size_t foo()
{
    char arr[8];
    callMeMaybe(arr);
    volatileZeroMemory(arr, sizeof arr);
    return sizeof arr;
}

callMeMaybe()からの可能な書き込みにより、6.1を除くすべてのGCCバージョンが期待されるストアを生成します。 callMeMaybeからの可能な書き込みと組み合わせた場合のみ、メモリフェンス内のコメントによってGCC 6.1もストアを生成します()。

誰かがキャッシュをフラッシュすることも提案しています。 Microsoftはnot "SecureZeroMemory"でキャッシュをフラッシュしようとします。 キャッシュはかなり高速に無効化される可能性が高いとにかく、これはおそらく大したことではないでしょう。また、別のプログラムがデータをプローブしようとした場合、またはページファイルに書き込まれる場合は、常にゼロバージョンになります。

スタンドアロン関数でmemset()を使用するGCC 6.1についてもいくつかの懸念があります。 GCC 6.1は、一部の人々のスタンドアロン機能に対して通常のループ(godboltの5.3のように)を生成するようであるため、godbolt上のGCC 6.1コンパイラは壊れたビルドになる可能性があります。 (zwolの答えのコメントを読んでください。)

87
cooky451

(WinAPIのSecureZeroMemoryのような)常にメモリをゼロにし、最適化されない関数が必要です。

これが標準関数memset_sは.


Volatileを使用したこの動作が適合しているかどうかについては、言うのは少し難しく、volatileは said 長い間バグに悩まされてきました。

1つの問題は、仕様が「揮発性オブジェクトへのアクセスは、抽象マシンの規則に従って厳密に評価される」と言っていることです。しかし、それは「揮発性オブジェクト」のみを指し、揮発性が追加されたポインターを介して不揮発性オブジェクトにアクセスすることはありません。したがって、明らかに、揮発性オブジェクトに実際にアクセスしていないとコンパイラが判断できる場合、オブジェクトを揮発性として扱う必要はありません。

15
bames53

このバージョンをポータブルC++として提供しています(ただし、セマンティクスは微妙に異なります)。

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size];

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

これで、オブジェクトの揮発性ビューを介して行われた不揮発性オブジェクトへのアクセスだけでなく、揮発性オブジェクトへの書き込みアクセスができました。

意味的な違いは、メモリが再利用されているため、メモリ領域を占有しているオブジェクトのライフタイムが正式に終了することです。そのため、コンテンツをゼロにした後のオブジェクトへのアクセスは、未定義の動作になりました(以前はほとんどの場合、未定義の動作でしたが、いくつかの例外が確実に存在していました)。

オブジェクトの有効期間中に最後ではなくこのゼロ化を使用するには、呼び出し元は配置newを使用して元の型の新しいインスタンスを再び配置する必要があります。

値の初期化を使用することで、コードを短くすることができます(あまり明確ではありません)。

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    new (ptr) volatile unsigned char[size] ();
}

そしてこの時点で、それはワンライナーであり、ヘルパー機能をほとんど保証しません。

2
Ben Voigt

右側の揮発性オブジェクトを使用し、コンパイラーにストアを配列に保存させることにより、移植可能なバージョンの関数を作成できるはずです。

void volatileZeroMemory(void* ptr, unsigned long long size)
{
    volatile unsigned char zero = 0;
    unsigned char* bytePtr = static_cast<unsigned char*>(ptr);

    while (size--)
    {
        *bytePtr++ = zero;
    }

    zero = static_cast<unsigned char*>(ptr)[zero];
}

zeroオブジェクトはvolatileとして宣言されます。これにより、コンパイラは常にゼロとして評価されますが、その値について仮定を行わないことが保証されます。

最後の割り当て式は、配列内の揮発性インデックスから読み取り、値を揮発性オブジェクトに保存します。この読み取りは最適化できないため、ループで指定されたストアをコンパイラが生成する必要があります。

0
D Krueger