
コンパイラとサードパーティの静的コード分析ツールには共通のタスクがあります-危険なコードフラグメントを識別する。 ただし、実行する分析のタイプには大きな違いがあります。 インテルC ++コンパイラーとPVS-Studioアナライザーの例を使用して、アプローチの違いを示し、それらの原因を説明します。
今回は、Notepad ++バージョン5.8.2がテスト対象として機能します。
メモ帳++
まず、選択したプロジェクトについて。
Notepad ++は、多数の言語をサポートし、標準のNotepadに代わる無料のソースコードエディターです。 Microsoft Windowsで実行され、GPLの下でリリースされます。 プロジェクトが気に入ったのは、C ++で書かれており、サイズが小さい-73,000行のコードです。 そして最も重要なことは、プロジェクト設定の/ W4スイッチと/ WXスイッチで示されるように、これはかなり正確なプロジェクトであり、各警告をエラーとして処理することを強制します。
コンパイラーの静的分析
ここで、コンパイラーと別の専用ツールの観点からプロジェクトの分析を検討します。 コンパイラは常に、非常に小さなローカルコードフラグメントのみを処理することで発行できる警告に引き寄せられます。 この設定は、コンパイラに課せられる非常に厳しいパフォーマンス要件によるものです。 分散プロジェクトアセンブリ用のツールがあることは偶然ではありません。 中規模および大規模プロジェクトのコンパイル時間は、開発方法の選択に影響を与える重要な要因です。 したがって、コンパイラーから5%のパフォーマンス向上を実現できる場合、これが実装されます。
このような最適化により、コンパイラーはよりモノリシックになり、実際、前処理、
ASTの構築、コードの生成などの段階が明確に区別されなくなります。 たとえば、間接的な証拠から、Visual C ++を使用してプリプロセスされた "* .i"ファイルをコンパイルおよび生成するプリプロセッサアルゴリズムが異なると主張できます。 また、コンパイラはASTツリー全体を保存する必要がなく、有害ですらありません。 特定のノードのコードが生成され、これらのノードよりも多くのノードが不要になるとすぐに、それらはすぐに破棄されます。 コンパイル中に、ASTがまったく存在しない場合があります。 これは必要ありません。 生成されたコードの一部を解体しました。 これにより、使用されるメモリとキャッシュの量が節約されるため、速度が向上します。
このアプローチの結果は、警告の「局所性」です。 コンパイラは、高レベルのエラーの検出に役立つさまざまな構造を意図的に保存します。 実際に、Intel C ++がNotepad ++プロジェクトに対して生成するローカル警告を確認してみましょう。 メモ帳++プロジェクトは、/ W4スイッチを使用して警告なしでVisual C ++コンパイラーによってコンパイルされることを思い出してください。 当然、Intel C ++コンパイラには異なる警告セットがあり、特定のスイッチ/ W5 [Intel C ++]も設定します。 さらに、Intel C ++コンパイラが「備考」と呼ぶものを見ていきます。
インテルC ++から受信したメッセージの種類を見てみましょう。 そのため、CharUpper関数を操作しているときに、同じタイプの4つのエラーを見つけました(記事の最後の注を参照)。 このエラーを診断する「局所性」に注意してください-非常に危険な型変換を発見しました。 対応するコードフラグメントを考えます。
wchar_t * destStr = new wchar_t [len + 1];
...
for(int j = 0; j <nbChar; j ++)
{
if(ケース==大文字)
destStr [j] =
(wchar_t):: CharUpperW((LPWSTR)destStr [j]);
他に
destStr [j] =
(wchar_t):: CharLowerW((LPWSTR)destStr [j]);
}
奇妙な型変換があります。 Intel C ++コンパイラは、「#810:conversion from」LPWSTR = {WCHAR = {__ wchar_t} *}「to」__wchar_t「重要なビットを失う可能性があります」と報告します。 CharUpper関数のプロトタイプを見てみましょう。
LPTSTR WINAPI CharUpper(
__inout LPTSTR lpsz
);
この関数は、個々の文字ではなく、文字列で機能します。 ここで、シンボルはポインタに変換され、このポインタによる特定のメモリ領域が変更されます。 ホラー
しかし、真実は、インテルC ++によって発見された恐怖はおそらくそこで終わるということです。 それ以外のすべては、エラーを引き起こすコードよりもはるかに退屈で、ずさんなコードである可能性が高くなります。 しかし、まだいくつかの他の警告を考慮してください。
多数のメッセージ#1125が発行されました。
「#1125:関数」ウィンドウ:: init(HINSTANCE、HWND)「非表示」TabBarPlus :: init "-仮想関数のオーバーライドを意図?"
これらはエラーではなく、関数の命名に失敗しました。 私たちは他の人へのこのメッセージに興味を持っています。 検証にはいくつかのクラスが関係しているようですが、ここには特別なデータは保存されません。 コンパイラは基本クラスに関するさまざまな情報を保存する必要があります。 そのため、この診断が実装されています。
次の例。 メッセージ「#186:符号なし整数とゼロの無意味な比較」は、無意味な比較に対して発行されます。
static LRESULT CALLBACK hookProcMouse(
UINT nCode、WPARAM wParam、LPARAM lParam)
{
if(nCode <0)
{
...
0を返します。
}
...
}
条件「nCode <0」は常にfalseです。 良いローカル診断の良い例。 したがって、エラーを検出することはかなり可能です。
最新のIntel C ++警告を考慮すれば十分です。 「地域性」の考え方はすでに明確だと思います。
void ScintillaKeyMap :: showCurrentSettings(){
int i = :: SendDlgItemMessage(...);
...
for(size_t i = 0; i <nrKeys; i ++)
{
...
}
}
ここでも、間違いはありません。変数の命名があまりうまくいっていません。 最初は、変数「i」のタイプは「int」です。 次に、「size_t」タイプの新しい変数「i」が「for()」ステートメントで宣言され、他の目的に使用されます。 "size_t i"の宣言時のコンパイラは、同じ名前の変数が既に存在することを知っており、警告を発行します。 繰り返しますが、これはコンパイラが追加のデータを保存する必要はありませんでした。 彼はまだ、関数の本体の終わりまで、変数「int i」が利用可能であることを覚えている必要があります。
外部静的コードアナライザー
静的コード分析のための専用ツールに移りましょう。 起動の頻度はコンパイラの頻度よりも桁違いに低いため、これらの厳密な速度制限はなくなりました。 彼らの仕事の速度は、コードをコンパイルするよりも10倍遅いかもしれません。 これは重要ではありません。 たとえば、プログラマは日中にコンパイラを操作します。 夜間に静的コードアナライザーが起動し、午前中にプログラマーは疑わしい場所のレポートを受け取ります。 かなり合理的なアプローチ。
速度を落とすことで、静的コード分析ツールはコードツリー全体を保存し、それを数回調べ、多くの追加情報を保存できます。 これにより、「ぼやけた」高レベルのエラーを見つけることができます。
PVS-Studio静的アナライザーによって、Notepad ++で何が面白いかを見てみましょう。 私は実験版を使用していることに注意してください。これはまだダウンロードできません。 PVS-Studioバージョン4.00では、一般的なルールの無料セットを1〜2か月で紹介します。
当然、Intel C ++アナライザーと同様に、PVS-Studioは「ローカル」エラーに起因するエラーを検出します。 最初の例:
bool _isPointXValid;
bool _isPointYValid;
bool isPointValid(){
return _isPointXValid && _isPointXValid;
};
PVS-Studioアナライザーは、「V501:「&&」演算子の左右に同じ副次式があります:_isPointXValid && _isPointXValid。」
エラーの本質は明らかだと思いますが、これ以上詳しくは説明しません。 診断は「ローカル」です。単一の式を分析するプロセスで検証を実行できるためです。
_iContMap配列のクリアが不完全になる別のローカルエラー:
#define CONT_MAP_MAX 50
int _iContMap [CONT_MAP_MAX];
...
DockingManager :: DockingManager()
{
...
memset(_iContMap、-1、CONT_MAP_MAX);
...
}
「V512:memset関数を呼び出すと、バッファオーバーフローまたはアンダーフローが発生する」という警告が発行されます。 コードの正しいバージョン:
memset(_iContMap、-1、CONT_MAP_MAX * sizeof(int));
それでは、もっと興味深いことに移りましょう。 何かが間違っていると疑われるようにするために、2つのブランチを同時に分析する必要があるコード:
void TabBarPlus :: drawItem(
DRAWITEMSTRUCT * pDrawItemStruct)
{
...
if(!_isVertical)
フラグ| = DT_BOTTOM;
他に
フラグ| = DT_BOTTOM;
...
}
PVS-Studioのレポート:「V523:「then」ステートメントは「else」ステートメントと同等です。」 近所のコードを見ると、実際には著者が次のように書きたかったと結論付けることができます。
if(!_isVertical)
フラグ| = DT_VCENTER;
他に
フラグ| = DT_BOTTOM;
さあ、勇気を出してください。 次のコードフラグメントの形式でテストされます。
void KeyWordsStyleDialog :: updateDlg()
{
...
スタイル&w1Style =
_pUserLang-> _ styleArray.getStyler(STYLE_WORD1_INDEX);
styleUpdate(w1Style、_pFgColour [0]、_ pBgColour [0]、
IDC_KEYWORD1_FONT_COMBO、IDC_KEYWORD1_FONTSIZE_COMBO、
IDC_KEYWORD1_BOLD_CHECK、IDC_KEYWORD1_ITALIC_CHECK、
IDC_KEYWORD1_UNDERLINE_CHECK);
スタイル&w2Style =
_pUserLang-> _ styleArray.getStyler(STYLE_WORD2_INDEX);
styleUpdate(w2Style、_pFgColour [1]、_ pBgColour [1]、
IDC_KEYWORD2_FONT_COMBO、IDC_KEYWORD2_FONTSIZE_COMBO、
IDC_KEYWORD2_BOLD_CHECK、IDC_KEYWORD2_ITALIC_CHECK、
IDC_KEYWORD2_UNDERLINE_CHECK);
スタイル&w3Style =
_pUserLang-> _ styleArray.getStyler(STYLE_WORD3_INDEX);
styleUpdate(w3Style、_pFgColour [2]、_ pBgColour [2]、
IDC_KEYWORD3_FONT_COMBO、IDC_KEYWORD3_FONTSIZE_COMBO、
IDC_KEYWORD3_BOLD_CHECK、IDC_KEYWORD3_BOLD_CHECK、
IDC_KEYWORD3_UNDERLINE_CHECK);
スタイル&w4Style =
_pUserLang-> _ styleArray.getStyler(STYLE_WORD4_INDEX);
styleUpdate(w4Style、_pFgColour [3]、_ pBgColour [3]、
IDC_KEYWORD4_FONT_COMBO、IDC_KEYWORD4_FONTSIZE_COMBO、
IDC_KEYWORD4_BOLD_CHECK、IDC_KEYWORD4_ITALIC_CHECK、
IDC_KEYWORD4_UNDERLINE_CHECK);
...
}
ここでエラーを見つける可能性のあるPVS-Studioアナライザーを誇りに思っていると言えます。 私はあなたがそれに気づくことはまずないと思い、コード断片全体をスキップして、説明を待っています。 このようなコードをレビューすることは事実上不可能です(コードレビュー)。 しかし、静的アナライザーは忍耐強くてつまらないものです。「V525:同様のブロックのコレクションを含むコード。 行576、580、584、588の項目「7」、「7」、「6」、「7」を確認します。
テキストを短くして、興味深いことを強調します。
styleUpdate(...
IDC_KEYWORD1_BOLD_CHECK、IDC_KEYWORD1_ITALIC_CHECK、
...);
styleUpdate(...
IDC_KEYWORD2_BOLD_CHECK、IDC_KEYWORD2_ITALIC_CHECK、
...);
styleUpdate(...
IDC_KEYWORD3_BOLD_CHECK、!! IDC_KEYWORD3_BOLD_CHECK !!、
...);
styleUpdate(...
IDC_KEYWORD4_BOLD_CHECK、IDC_KEYWORD4_ITALIC_CHECK、
コードはほとんどの場合、Copy-Pasteによって作成されました。 その結果、IDC_KEYWORD3_ITALIC_CHECKの代わりにIDC_KEYWORD3_BOLD_CHECKが使用されました。 警告は少し奇妙に見え、数字「7」、「7」、「6」、「7」について話します。 残念ながら、より理解しやすいメッセージを伝えることは困難です。 これらの番号は、次の形式のマクロから取得されます。
#define IDC_KEYWORD1_ITALIC_CHECK(IDC_KEYWORD1 + 7)
#define IDC_KEYWORD3_BOLD_CHECK(IDC_KEYWORD3 + 6)
最後に挙げた例は、PVS-Studioアナライザーがコードの大きなセクション全体を同時に処理し、その中の繰り返し構造を検出し、ヒューリスティックに基づいて何かが間違っていると疑うことができたという事実を示しています。 これは、情報処理レベルの非常に大きな違いです。
いくつかの数字
コンパイラーの「ローカル」分析のもう1つの結果と、専用ツールでのよりグローバルな分析について説明しましょう。 「ローカル分析」では、この状況が本当に危険かどうかを明確にすることは困難です。 結果として、桁違いに多くの誤検知が発生します。 例で説明します。
Notepad ++プロジェクトを分析するとき、PVS-Studioツールは10個の警告のみを発行しました。 これらのうち、4つのメッセージは実際のエラーを示しています。 結果は控えめですが、PVS-Studioの汎用静的解析は開発が始まったばかりです。 時間が経つにつれて、彼は最高の一人になります。
Notepad ++プロジェクトを分析すると、Intel C ++コンパイラーは439の警告と3,139のコメントを生成しました。 どれだけの人が実際の間違いを本当に指摘しているかはわかりません。 私が見るのに十分な強さから、CharUpperに関連する実際のエラーは4つしか見られませんでした(上記の説明を参照)。
3578のメッセージは多すぎて、それぞれを注意深く調べることはできません。 コンパイラーは、プログラムの20行ごとに注意を払うことを提案していることがわかりました(73000/3578 = 20)。 これは深刻ではありません。 汎用アナライザーの場合は、可能であれば、不要なものをすべて遮断してください。
Viva64ルールセット(PVS-Studioに含まれる)を試してみた人は、それが同じ膨大な割合の誤
検知を生成することに気付くかもしれません。 しかし、別の状況があります。 そこで、すべての疑わしいタイプの変換を識別できる必要があります。 誤ったメッセージを生成しないよりも、エラーを見逃さないことが重要です。 さらに、設定を使用して誤ったメッセージを適切にフィルタリングします。
-更新:
ここに嘘を書いたことがわかりました。 CharUpperWの例では、エラーはありません。 残念ながら、誰も私を修正しませんでした。 サムは、PVS-Studioで同様のルールを実装することに決めたときに気付きました。
実際、
CharUpperWは文字列と個々の文字の両方で
動作することができます。 ポインターの上部がゼロの場合、これはポインターではなく、シンボルであると見なされます。 このコースでのWIN APIはその曲率によって悲しまれましたが、メモ帳++のコードは正しく作成されました。
ところで、Intel C ++はエラーをまったく見つけられなかったことがわかりました。