Valgrindは良いが、十分ではない

少し前までは、PVS-Studio静的アナライザーの1つを使用することの利点を実証しようとしました。 それから何も来ませんでした。 しかし、通信の過程で、静的および動的分析の方法論に関する詳細な回答を準備しました。 今、私はこの答えを小さな記事の形で書くことにしました。 このテキストは読者にとって興味深く思えるかもしれませんが、新しい潜在顧客とコミュニケーションをとるときにこの記事を使用することは可能です。



そのため、通信の過程で、およそ次の内容について質問が行われました。



私たちはすでに静的アナライザーを実験し、その精度は通常のバルグラインドよりもはるかに悪いという結論に達しました。 したがって、静的解析が必要な理由は明確ではありません。 多くの誤検知があり、valgrindの実行中に検出できないエラーは通常検出されません。



私は次の答えを用意しました。



2つの小さなプロジェクトで静的解析の利点を示すことは非常に困難です。 まず、高品質です。 第二に、静的解析は主に新しいコードのエラーを見つけて除去することに焦点を合わせています。 第三に、小さなプロジェクトのエラー密度は、大きなプロジェクトのエラー密度よりも低くなっています( 説明 )。



長く安定したコードで何かを見つけようとするのは、ありがたいことではありません。 静的分析のポイントは、初期段階で多くのエラーを防ぐことです。 はい、これらのエラーのほとんどは他の方法で見つけることができます。 それらは、プログラマ自身、または大規模なテストまたはテスターのいずれかによって認識されます。 最悪の場合、ユーザーはエラーを報告します。 しかし、いずれにしても、これは時間の無駄です。 静的解析を使用すると、多くのタイプミス、コピーペーストエラー、およびその他の間違いを早期に排除できます。 コードを書いた直後に多くのエラーを見つけることがその主な価値です。 次の段階でエラーを見つけることは、何倍も高価です。



何らかの理由で、その後、プログラマーはタイプミスやコピーペーストをしないとすぐに誰もが言います。 これは真実ではありません。 彼らはします。 誰もが: http : //www.viva64.com/en/b/0260/



さて、静的解析で何らかのエラーが見つかる可能性があると確信したとしましょう。 質問は真実ですが、valgrindのようなツールがあるため、必要です。 結局、彼らは本当に少ない誤検知を与えます。



残念ながら、はい、必要です。 すべてのタイプのエラーを検出できるテクノロジーはありません。 悲しいことですが、品質を向上させるには、さまざまな種類のツールを使用して相互に補完する必要があります。



静的解析が他の技術に役立つ場所についてはすでに書いています。 たとえば、このノートでは静的コード分析と動的コード分析の違いについて説明しました: http : //www.viva64.com/en/b/024​​8/



静的分析が単体テストを使用したテストをどのように補完するかに関する別の注記: http : //www.viva64.com/en/a/0080/



ただし、完全に抽象化しないために、例を使用して静的分析と動的分析の違いを説明してみましょう。 たとえば、SlowScannerクラスのコンストラクターでこのような興味深いフラグメントを強調してみましょう。

class SlowScanner { .... explicit SlowScanner(Fsm& fsm) { .... Fill(m_letters, m_letters + sizeof(m_letters)/sizeof(*m_letters), 0); .... } .... size_t* m_letters; .... }
      
      





PVS-Studioアナライザーは警告を生成します:V514ポインターのサイズ 'sizeof(m_letters)'を別の値で除算します。 論理エラーが存在する可能性があります。 slow.h 238



最も可能性が高いのは、クラス「m_letters」のメンバーが固定サイズの配列であったことです。 もちろん、これは単なる仮定にすぎませんが、可能性は非常に高いです。 たとえば、最初は次のように書かれていました:size_t m_letters [MAX_COUNT];。 当時、配列のサイズの決定は正しかった:

 sizeof(m_letters)/sizeof(*m_letters)
      
      





その後、配列が動的になり、変数「m_letters」が単純なポインターに変わりました。 式「sizeof(m_letters)/ sizeof(* m_letters)」は常に1に等しくなります。 32ビットシステムでは、ポインターのサイズとサイズsize_tは4です。64ビットシステムでは、これらのタイプのサイズは8になります。ただし、4を4で割るか8を8で割っても、常に1になります。



したがって、Fill()関数は配列の1バイトのみをゼロにします。 メモリが誤ってすでにゼロにリセットされている場合、または初期化されていない要素が使用されていない場合、エラーはまったく発生しません。 これは彼女の欺isです。 ただし、配列の初期化されていない要素の読み取りが発生する場合があります。



動的アナライザーはこのエラーを見つけることができますか? 知りません おそらく、初期化されていないメモリからの読み取りを検出できます。 では、なぜ彼は沈黙しているのでしょうか? ここで、静的解析と動的解析の重要な違いの1つに取り組みます。



ほとんどの場合、このコードブランチはごくまれにしか使用されないか、少なくともテストでカバーされません。 その結果、動的アナライザーは単にこのコードをチェックせず、エラーも表示しません。 動的分析の欠点は、考えられるすべてのコード分岐をカバーすることが難しいことです。 その結果、めったに使用されないコードはテストされずに残り、特にさまざまなエラーおよび非標準のエラーハンドラーはテストされません。



静的分析は、理論的に制御できるすべてのブランチをチェックします。 したがって、1つまたは別のコードが実行される頻度に関係なく、エラーを見つけることができます。



ところで、トピックから少し離れましょう。 アナライザーだけでなく、 コード監査サービスも提供できます。 このような監査の結果は、改善のための一連の推奨事項を含む文書になる場合があります。 コーディング標準に含めることができます。 私たちはすでにそのような作業を実行した経験があります。 たとえば、アレイのサイズの計算に関連するエラーを回避するために、特別なテクノロジー(Chromiumでスパイ)を使用することをお勧めします。

 template <typename T, size_t N> char (&ArraySizeHelper(T (&array)[N]))[N]; #define arraysize(array) (sizeof(ArraySizeHelper(array)))
      
      





マクロ 'arraysize'は、通常のポインターには適用できません。 コンパイルエラーが発生します。 したがって、偶発的なエラーから身を守ります。 配列が突然ポインターに変わった場合、サイズが計算されている場所をスキップできません。



静的および動的分析に戻りましょう。 たとえば、次の関数を見てください。

 inline RECODE_RESULT _rune2hex(wchar32 in, char* out, size_t out_size, size_t &out_writed) { static const char hex_digs[]="0123456789ABCDEF"; bool leading = true; out_writed = 0; RECODE_RESULT res = RECODE_OK; for (int i = 7; i >=0; i--){ unsigned char h = (unsigned char)(in>>(i*4) & 0x0F); if (h || !leading || i==0){ if (out_writed + 1 >= out_size){ res = RECODE_EOOUTPUT; break; } out[out_writed++] = hex_digs[h]; } } return res; }
      
      





動的分析の観点からは、ここで疑わしいものはありません。 次に、PVS-Studio静的アナライザーは、「主要な」変数に注意を払うことを提案します。V560条件式の一部は常に偽です!!主要な。 recyr_int.hh 220



間違いはないと思います。 「主要な」変数は、リファクタリング後に不要であることが判明しました。 もしそうでなければ? 突然コードが追加されませんか? これは注意を払うべき場所です。 また、変数が不要な場合は、アナライザーだけでなく、このコードをサポートする人々を混乱させないように、変数を削除します。



式の一部が常に一定であるという警告は、面白くないかもしれません。 次に、V560診断で検出されたエラーの例を見てください。コードでそれが見つからないことに驚くでしょう: http ://www.viva64.com/en/examples/V560/



このようなエラーは、動的分析では見つけることができません。 彼はここを探す必要はありません。 これらは、単に誤った論理式です。



残念ながら、提案されているプロジェクトでは、静的アナライザーの利点を包括的に示すことはできません。 プロジェクトの一部であるライブラリの1つに目を向けます。 実際、ある意味では、ライブラリーのエラーはプロジェクト自体のエラーでもあります。



sslDeriveKeys関数がプライベートデータを操作することを考えてみましょう。

 int32 sslDeriveKeys(ssl_t *ssl) { .... unsigned char buf[SSL_MD5_HASH_SIZE + SSL_SHA1_HASH_SIZE]; .... memset(buf, 0x0, SSL_MD5_HASH_SIZE + SSL_SHA1_HASH_SIZE); psFree(ssl->sec.premaster); ssl->sec.premaster = NULL; ssl->sec.premasterSize = 0; skipPremaster: if (createKeyBlock(ssl, ssl->sec.clientRandom, ssl->sec.serverRandom, ssl->sec.masterSecret, SSL_HS_MASTER_SIZE) < 0) { matrixStrDebugMsg("Unable to create key block\n", NULL); return -1; } return SSL_HS_MASTER_SIZE; }
      
      





ここでは、動的アナライザーは何も検出しません。 言語の観点からのコードは完全に正しいです。 エラーを見つけるには、静的アナライザーが実行できる上位レベルのパターンを考える必要があります。



ローカル配列「buf」に興味があります。 プライベートデータを格納するため、関数の最後にmemset()関数を使用してこの配列をリセットしようとしました。 これは間違いです。



memset()を呼び出した後、ローカル配列「buf」が使用されなくなったことを参照してください。 これは、C / C ++言語の観点からは呼び出しに影響がないため、コンパイラにはmemset()関数呼び出しを削除する権利があることを意味します。 さらに、それは法律上だけではなく、リリース版ではこの機能を実際に削除します。



その結果、プライベートデータはメモリに残り、理論的には必要のない場所に行くことができます。 したがって、サードパーティのライブラリのエラーにより、プロジェクトのセキュリティがわずかに低下します。



PVS-Studioは次の警告を発行します。V597コンパイラーは、「buf」バッファーのフラッシュに使用される「memset」関数呼び出しを削除できます。 RtlSecureZeroMemory()関数を使用して、プライベートデータを消去する必要があります。 sslv3.c 123



このエラーは潜在的な脆弱性です。 それは非常に取るに足らないように見えるかもしれません。 ただし、ネットワークを介してプライベートデータのフラグメントを送信するまで、非常に不快な結果を招く可能性があります。 このような奇跡がどのように起こるかは、ABBYY Dmitry Meshcheryakovの専門家による記事で説明されています: http : //habrahabr.ru/company/abbyy/blog/127259/



静的コードアナライザーと動的コードアナライザーの違いを示すことができたと思います。 これらの2つのアプローチは、互いに補完し合っています。 静的分析が多くの偽陽性を生み出すという事実は問題ではありません。 それらを使用して、アナライザーを削除および構成できます。 関心がある場合は、警告の数を快適なレベルに減らすために、このような作業を行うことができます。



興味のある方は、可能な協力のための次のステップを概説し、実際の大規模な実プロジェクトでアナライザーの機能を実証することをお勧めします。



All Articles