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


89

(WinAPIのSecureZeroMemoryのように)常にメモリをゼロにし、その後メモリが二度とアクセスされないとコンパイラが考えたとしても、最適化されない関数が必要です。揮発性の完璧な候補のようです。しかし、実際にこれをGCCで動作させるのにいくつか問題があります。関数の例を次に示します。

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

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

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

  • GCC4.4.7および4.5.3は揮発性を決して無視しません。
  • GCC 4.6.4および4.7.3は、配列サイズ1、2、および4の揮発性を無視します。
  • GCC 4.8.1から4.9.2までは、配列サイズ1および2の揮発性を無視します。
  • GCC 5.1から5.3までは、配列サイズ1、2、4、8の揮発性を無視します。
  • 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バージョンで期待されるストアが生成されます。メモリフェンスでコメントすると、GCC 6.1でもストアが生成されますが、callMeMaybe()からの可能な書き込みとの組み合わせのみです。

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

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


4
volatile特に証明されない限り、IMHOの使用はバグです。しかし、おそらくバグです。volatile危険であるほど指定が不十分です-使用しないでください。
Jesper Juhl 2016

19
@JesperJuhl:いいえ、volatileこの場合は適切です。
ディートリッヒエップ2016

9
@NathanOliver:コンパイラーは、を使用していてもデッドストアを最適化できるため、これは機能しませんmemset。問題は、コンパイラが何をするかを正確に知っていることmemsetです。
ディートリッヒエップ2016

8
@PaulStelian:それはvolatileポインタを作るでしょう、私たちはポインタが欲しいですvolatile(私たち++は厳密であるかどうかは関係ありませんが、厳密であるかどうかは関係ありません*p = 0)。
ディートリッヒエップ2016

7
@JesperJuhl:volatileについては、過小評価されているものは何もありません。
GManNickG 2016

回答:


82

GCCの動作準拠している可能性があり、準拠していない場合でも、volatileこのような場合に必要なことを実行することに依存するべきではありません。C委員会はvolatile、メモリマップドハードウェアレジスタと、異常な制御フロー中に変更された変数(たとえば、シグナルハンドラとsetjmp)のために設計されました。 それらはそれが信頼できる唯一のものです。 一般的な「これを最適化しない」注釈として使用することは安全ではありません。

特に、基準は重要な点で不明確です。(コードをCに変換しました。ここでは、CとC ++の間に相違はないはずです。また、疑わしい最適化の前に行われるインライン化を手動で行って、コンパイラがその時点で「見る」ものを示しています。 。)

extern void use_arr(void *, size_t);
void foo(void)
{
    char arr[8];
    use_arr(arr, sizeof arr);

    for (volatile char *p = (volatile char *)arr;
         p < (volatile char *)(arr + 8);
         p++)
      *p = 0;
}

メモリクリアループはarr揮発性修飾左辺値を介してアクセスしますが、arrそれ自体は宣言されていませんvolatile。したがって、少なくともほぼ間違いなく、Cコンパイラは、ループによって作成されたストアが「デッド」であると推測し、ループを完全に削除することができます。C根拠には、委員会がそれらの店舗の保存を要求すること意図していることを示唆するテキストがありますが、私が読んだように、標準自体は実際にはその要求をしていません。

標準が必要とするものと必要としないものの詳細については、揮発性ローカル変数が揮発性引数とは異なる方法で最適化される理由と、オプティマイザーが後者からno-opループを生成する理由を参照してください。揮発性の参照/ポインタを介して宣言された不揮発性オブジェクトにアクセスすると、そのアクセスに揮発性のルールが付与されますか?、およびGCCバグ71793

委員会の考えの 詳細についてvolatileは、C99の理論的根拠で「揮発性」という単語を検索してください。JohnRegehrの論文「VolatilesareMiscompiled」は、プログラマーの期待volatileが本番コンパイラーによってどのように満たされないかを詳細に示しています。エッセイのLLVMチームのシリーズは、「どのようなすべてのCプログラマすることがわかっているが未定義の動作について」には、特に触れていないvolatileが、どのように、なぜ現代のCコンパイラであるあなたが理解するのに役立ちますない「ポータブルアセンブラ」。


やりたいことを実行する関数をどのように実装するかという実際的な質問に対してvolatileZeroMemory:標準が何を要求するか、または要求することを意図していたかに関係なく、これには使用できないと想定するのが最も賢明ですvolatile。そこそれが機能しなかった場合、それはあまりにも多くの他のものを壊すので、仕事にに信頼できる代替手段は、:

extern void memory_optimization_fence(void *ptr, size_t size);
inline void
explicit_bzero(void *ptr, size_t size)
{
   memset(ptr, 0, size);
   memory_optimization_fence(ptr, size);
}

/* in a separate source file */
void memory_optimization_fence(void *unused1, size_t unused2) {}

ただし、memory_optimization_fenceどのような状況でもインライン化されていないことを絶対に確認する必要があります。独自のソースファイル内にある必要があり、リンク時の最適化を行ってはなりません。

コンパイラ拡張に依存する他のオプションがあり、状況によっては使用可能であり、よりタイトなコードを生成できます(そのうちの1つはこの回答の以前の版に登場しました)が、普遍的なものはありません。

(この関数explicit_bzeroは複数のCライブラリでその名前で使用できるため、呼び出すことをお勧めします。この名前には少なくとも4つの候補がありますが、それぞれが1つのCライブラリでのみ採用されています。)

また、これを機能させることができたとしても、それだけでは不十分な場合があることも知っておく必要があります。特に、考慮してください

struct aes_expanded_key { __uint128_t rndk[16]; };

void encrypt(const char *key, const char *iv,
             const char *in, char *out, size_t size)
{
    aes_expanded_key ek;
    expand_key(key, ek);
    encrypt_with_ek(ek, iv, in, out, size);
    explicit_bzero(&ek, sizeof ek);
}

AESアクセラレーション命令を備えたハードウェアを想定すると、expand_keyおよびencrypt_with_ekがインラインの場合、コンパイラはek、を呼び出すまで、完全にベクトルレジスタファイルに保持できる可能性があります。explicit_bzeroこれにより、機密データをスタックコピーして消去するだけで、さらに悪いことに、まだベクトルレジスタにあるキーについては気にしないでください!


6
それは興味深いです...私は委員会のコメントへの参照を見たいと思います。
ディートリッヒエップ2016

10
この正方形は6.7.3(7)の定義でどのvolatileように[...]したがって、そのようなオブジェクトを参照する式は、5.1.2.3で説明されているように、抽象マシンの規則に従って厳密に評価されるものとします。さらに、すべてのシーケンスポイントで、オブジェクトに最後に格納された値は、前述の未知の要因によって変更された場合を除いて、抽象マシンによって規定された値と一致する必要があります。揮発性修飾型を持つオブジェクトへのアクセスを構成するものは、実装によって定義されます。
Iwillnotexist Idonotexist 2016

15
@IwillnotexistIdonotexistそのパッセージのキーワードはオブジェクトです。 volatile sig_atomic_t flag;揮発性オブジェクトです。 *(volatile char *)fooは単に揮発性修飾左辺値介したアクセスであり、標準では特別な効果を持たせる必要はありません。
zwol 2016

3
この規格は、「準拠」実装であるために何かが満たさなければならない基準を示しています。特定のプラットフォームでの実装が「適切な」実装または「使用可能な」実装であるために満たす必要のある基準を説明する努力はありません。GCCの扱いはvolatile、それを「準拠」実装にするのに十分かもしれませんが、それは「良い」または「有用」であるのに十分であるという意味ではありません。多くの種類のシステムプログラミングにとって、それはそれらの点でひどく不十分であると見なされるべきです。
スーパーキャット2016

3
C仕様もむしろ直接言う実際の実装の必要はありません、それはその値が使用されていないことを推測することができる場合、式の一部を評価し、何の必要な副作用は(生成されていないことを」関数を呼び出すか、揮発性のオブジェクトにアクセスすることにより生じたいかなる含みます) 。」(私のことを強調してください)。
Johannes Schaub-litb 2016

15

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

これが標準機能のmemset_s目的です。


volatileでのこの動作が適合しているかどうかについては、それを言うのは少し難しいです。volatileは長い間バグに悩まされてきたと言われています。

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


4
注:これはC11標準の一部であり、まだすべてのツールチェーンで使用できるわけではありません。
ディートリッヒエップ2016

5
興味深いことに、この関数はC11に対して標準化されていますが、C ++ 11、C ++ 14、またはC ++ 17に対しては標準化されていないことに注意してください。したがって、技術的にはC ++のソリューションではありませんが、実用的な観点からは、これが最良のオプションのように思われることに同意します。この時点で、GCCの動作が適合しているかどうか疑問に思います。編集:実際、VS 2015にはmemset_sがないため、まだそれほどポータブルではありません。
cooky451 2016

2
@ cooky451 C ++ 17は参照によりC11標準ライブラリを取り込むと思いました(2番目のその他を参照)。
nwp 2016

14
また、memset_sC11標準として説明することは誇張です。これはAnnexKの一部であり、C11ではオプションです(したがって、C ++でもオプションです)。基本的に、Microsoftを含むすべての実装者は、そもそもそれを考えていた(!)が、それを採用することを拒否しました。最後に、彼らがC-nextでそれを廃棄することについて話していると聞きました。
zwol 2016

8
@ cooky451特定のサークルでは、Microsoftは、基本的に他のすべての人の反対意見を超えてC標準に何かを強制し、それを自分で実装することを気にしないことで有名です。(これの最もひどい例は、基になるタイプsize_tが許可されるものに関するルールのC99の緩和です。Win64ABIはC90に準拠していません。それは...大丈夫ではありませんが、ひどいことではありません... MSVCは、実際のようなC99のものを拾っていたuintmax_tし、%zuタイムリーに、彼らはしませんでした)。
zwol

2

私はこのバージョンをポータブル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
関数の実行後にオブジェクトにアクセスするとUBが呼び出される場合、そのようなアクセスによって、オブジェクトが「クリア」される前に保持されていた値が生成される可能性があります。それはセキュリティの反対ではないのですか?
スーパーキャット2018

0

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

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コンパイラは、それは常にゼロとして評価されていても、その値についての仮定を行うことはできないことを保証します。

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


1
これはまったく機能しません...生成されているコードを見てください。
cooky451 2016

1
生成されたASMmo 'をよく読んだら、関数呼び出しをインライン化してループを保持しているように見え*ptrますが、そのループ中には何も保存せず、実際には何もしません...ループしているだけです。wtf、私の脳があります。
underscore_d

3
@underscore_dこれは、揮発性の読み取りを維持しながらストアを最適化するためです。
Dクルーガー

1
ええ、それは結果を不変にダンプしedxます:私はこれを取得します:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
underscore_d

1
任意のvolatile unsigned char constフィルバイトを渡すことができるように関数を変更すると...それは読み取られません。生成されたインライン呼び出しvolatileFill()は、です[load RAX with sizeof] .L9: subq $1, %rax; jne .L9。オプティマイザーが(A)フィルバイトを再読み取りせず、(B)何もしないループをわざわざ保持するのはなぜですか?
underscore_d
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.