C / C ++アプリケーションのソースコードのエラーを見つけるための新しいツールをプログラマの注意を喚起します。 PVS-Studioアナライザーの一部として、新しい一般ルールのセットが実装されました。 この機能は現在無料です。 PVS-Studioは
http://www.viva64.com/en/pvs-studio-download/からダウンロードできます。
この記事では、PVS-Studioの新機能について簡単に説明します。 TortoiseSVNプロジェクトのソースコードの静的分析を使用して、新しい診断機能の使用方法を示します。
PVS-Studioは、Microsoft Visual Studio 2005/2008/2010と統合する最新のソースコードアナライザーです。 このアナライザーを使用すると、警告リストを簡単に操作し、作業に複数のプロセッサーコアを使用できます。 PVS-Studioは、C / C ++ / C ++ 0xの最新のWindowsアプリケーションの開発者を対象としています。
これまで、PVS-Studioには2つのルールセットが含まれていました。
1つ目は 、64ビットプログラムの欠陥を検出することです。
2つ目は 、OpenMPテクノロジーに基づいて構築された並列プログラムの欠陥を検出することです。
アナライザーには、コード内のさまざまなエラーを検出する
3番目の汎用チェックセットがあります。 この一連のルールは無料で、制限なしで使用できます。 このキットが時間とともに支払われるかどうかは、今言うのは難しいです。 汎用分析の分野では、私たちは道を歩み始めたところです。
PVS-Studio 4.00 BETAを
ダウンロードすることで、新しいルールセットに慣れることができます。 バグや改善提案についてコメントをお送りします。 すぐに注意したいのは、最初に50の汎用ルールのみを実装したことです。 これは少しです。 したがって、PVS-Studioをすぐにダウンロードして試してみても、コードに興味深いものが見つからない場合は、急いで結論に至らないでください。 今後、チェックセットが大幅に増加するPVS-Studioを再試行することをお勧めします。 近い将来、診断ルールのベースの拡大に積極的に取り組む予定です(健康が十分で幸運な場合)。
TortoiseSVNの例を使用して、新しいPVS-Studioルールセットの使用方法を示しましょう。 TortoiseSVNは、Windowsシェルの拡張機能として実装されているSubversionバージョン管理システムのクライアントです。 TortoiseSVNは多くの開発者によく知られているので、このアプリケーションをより詳細に説明することは意味がないと思います。 2007年にSourceForge.netでTortoiseSVNが「開発者向けのツールとユーティリティ」のカテゴリで最高のプロジェクトとして認められたことにのみ注目します。
ステップ1
会社「Systems of software validation」LLC(これが私たちです)のWebサイトからPVS-Studioをダウンロードします。 アンケートに記入したり、キャプチャを解決したりする必要がないことを喜んでいただければ幸いです。
ダウンロードするだけです。
ステップ2
PVS-Studioをインストールします。 [次へ]ボタンをクリックしても安全です。 何も設定する必要はありません。 PVS-Studioディストリビューションはデジタル署名されています。 ただし、一部のウイルス対策ソフトウェアは、Visual Studioでの統合に関連するアクションに依然として警戒しています。 すべて許可されています。
ステップ3
TortoiseSVNプロジェクトのソースパッケージを
ダウンロードします。 ソースコードバージョン1.6.11を使用しました。
ステップ4
TortoiseSVNプロジェクトを開き、分析を開始します。 これを行うには、PVS-Studioで[ソリューションの確認]コマンドを選択します。
ステップ5
アナライザーはしばらく考えます(TortoiseSVNプロジェクトはそれほど単純ではなく、多数のファイルが含まれています)。 したがって、私たちはこの瞬間に何かをして少し待つのを急いでいません。 しばらくすると、進行状況ダイアログが表示され、分析が開始されます。 分析の速度は、プロセッサのコアの数に依存します。 PVS-Studioが多くのリソースを消費する場合は、設定でその欲求を制限できます。
アナライザーは独自のウィンドウでメッセージを発行します。このウィンドウには、さまざまなタイプのメッセージをオン/オフするためのコントロールがあります。 そして、これらの機会を利用します。 64ビットに関連する多くのエラーを見るのはまったく面白くないです。 さらに、64ビット分析が支払われるため、制限があります(詳細については、トライアルモードを参照して
ください )。
このウィンドウには3つのボタンのセットが表示され、これらのボタンは3つのルールセットからのメッセージを表示します。
1)64-64ビットの欠陥に関する診断メッセージ(Viva64)。
2)MP-並行欠陥に関する診断メッセージ(VivaMP)。
3)GA-一般的なタイプの診断メッセージ。
一般的なルールセット(GA)のみに関心があります。 他のボタンを押すと、リスト内の余分なメッセージがすぐに非表示になります。
分析の完了を待っています。
ステップ6
分析が完了すると、アナライザーがコードレビューを推奨しているプログラム内のすべての場所のリストが表示されます。
すべての警告は重要度の3つのレベルに分かれています(これはPVS-Studio 4.00の新機能です)。 通常、第1レベルと第2レベルのみを見るのが理にかなっています。 PVS-Studio 4.00 BETAは、33個の第1レベルのアラート、14個の第2レベルのアラート、8個の第3レベルのアラートを発行しました。
最初のレベルのアラートで導入を開始する価値があります。 したがって、第2レベルのメッセージの出力を示すボタンを無効にすることができます。 第3レベルはデフォルトですでに無効になっています。
ステップ7
アナライザーによって検出されたコード内の興味深い場所を考慮してください。
状況1
最初は、2つのメッセージが同じ機能に属します。 この機能はどこでも特に使用されないことが望まれます。
V530関数 'empty'の戻り値を使用する必要があります。 contextmenu.cpp 434
V530関数 'remove'の戻り値を使用する必要があります。 contextmenu.cpp 442
STDMETHODIMP CShellExt ::初期化(....)
{
...
ignoreprops.empty();
for(int p = 0; p <props.GetCount(); ++ p)
{
if(props.GetItemName(p)。
比較(SVN_PROP_IGNORE)== 0)
{
std :: string st = props.GetItemValue(p);
ignoreprops = UTF8ToWide(st.c_str());
//すべてのエスケープ文字( '\\')を削除します
std :: remove(ignoredprops.begin()、
ignoreprops.end()、 '\\');
休憩;
}
}
...
}
以下では、コードの断片に関する簡単なコメントのみを示します。 PVS-Studioのオンラインドキュメントでコードが疑わしいと思われる理由について詳しく知ることができます(
ロシア語と
英語で利用可能です)。 PVS-Studioには、PDFファイル形式のドキュメント(同じもの)もバンドルされています。 診断メッセージの対応する説明へのリンクがさらに提供されます。
警告
V530は、「ignoredprops.empty()」は行をまったくクリアせず、「std :: remove()」は実際に文字を削除しないことを示しています。
状況2
これにより、タイプ 'char'の変数が値0x80以上であることを確認します。
V547式 'c> = 0x80'は常にfalseです。 符号付きchar型の値の範囲:[-128、127]。 pathutils.cpp 559
CString CPathUtils :: PathUnescape(const char *パス)
{
//クイックパスを試す
size_t i = 0;
for(; char c = path [i]; ++ i)
if((c> = 0x80)||(c == '%'))
{
//クイックパスは機能しません
//非ラテン文字またはエスケープ文字
std :: string utf8Path(path);
CPathUtils :: Unescape(&utf8Path [0]);
return CUnicodeUtils :: UTF8ToUTF16(utf8Path);
}
...
}
メッセージ
V547は、そのようなチェックが無意味であることを通知します。 タイプ 'char'の値は常に0x80より小さいため、条件は常にfalseです。 そして、おそらくこのエラーのために、「ラテン文字またはエスケープされた文字に対してクイックパスが機能しない」というコメントがコードに記述されています。 もちろん機能しませんが、文字列を変換できないためではありません。 非ラテン文字に出くわすと、単に「if」演算子の本体には入りません。
状況3
多くのスレッドは、CreateThread / ExitThread関数を使用して作成および強制終了されます。 したがって、スレッドのスタックがすぐに使い果たされたり、スレッドの終了時にリソースの一部が解放されなかったりするリスクがあります。 詳細については、警告
V513の説明をお読み
ください 。 _beginthreadex()および_endthreadex()関数を使用する方がはるかに安全です。
サンプルコードを指定しても意味がありませんが、すべてのメッセージのテキストは同じです。
V513 CreateThread / ExitThread関数の代わりに_beginthreadex / _endthreadex関数を使用します。 crashhandler.cpp 379
状況4
これは、関連するTortoiseSVNユーティリティに適用されます。 CrashLogを使用すると、別のクラッシュで終了する可能性が非常に高くなります。
V510 'printf_s'関数は、4番目の実引数としてクラス型変数を受け取ることを期待されていません。 excprpt.cpp 199
文字列CExceptionReport :: getCrashLog()
{
...
_tprintf_s(buf、_T( "%s \\%s.xml")、
getenv( "TEMP")、CUtility :: getAppName());
...
}
メッセージ
V510は、std :: string型のパラメーターをprintf_s関数に渡すことは非常に悪いことを警告しています。 そして、CUtility :: getAppName()関数は、正確にstd :: stringを返します。 エラーは、「。c_str()」の記述を忘れたことです。 結果は、単に誤ったデータ出力またはプログラムのクラッシュのいずれかです。
状況5
彼らは配列をクリアしたかったが、できなかった。
V530関数 'empty'の戻り値を使用する必要があります。 mailmsg.cpp 40
CMailMsgおよびCMailMsg :: SetFrom(string sAddress、
文字列sName)
{
if(initIfNeeded())
{
// 1人の送信者のみが許可されます
if(m_from.size())
m_from.empty();
m_from.push_back(TStrStrPair(sAddress、sName));
}
return * this;
}
繰り返しますが、
V530メッセージは、「clear()」の代わりに「empty()」が誤って書き込まれたことを示しています。
状況6
また、SetTo()関数では、配列をクリアできませんでした。
V530関数 'empty'の戻り値を使用する必要があります。 mailmsg.cpp 54
CMailMsg&CMailMsg :: SetTo(string sAddress、
文字列sName)
{
if(initIfNeeded())
{
//許可される受信者は1人のみ
if(m_to.size())
m_to.empty();
m_to.push_back(TStrStrPair(sAddress、sName));
}
return * this;
}
状況7
もちろん、誤検知があります。 たとえば、TortoiseSVNプロジェクトに含まれているzlibライブラリのコードスニペットです。 ここではエラーはありませんが、
V501でそのような場所を
マークすること
は非常に便利です。
V501 '-'演算子の左右に同じ部分式があります:size-size zutil.c 213
voidpf zcalloc(不透明、アイテム、サイズ)
voidpf不透明;
署名されていないアイテム。
符号なしサイズ。
{
/ *コンパイラを幸せにします* /
if(不透明)アイテム+ =サイズ-サイズ;
return(voidpf)calloc(アイテム、サイズ);
}
コンパイラは確かに満足していますが、減算は疑わしいようです。
状況8
エンコードには他にも暗い場所があります。 常に偽である別の条件を次に示します。
V547式 '* utf8CheckBuf> = 0xF5'は常にfalseです。 符号付きchar型の値の範囲:[-128、127]。 tortoiseblame.cpp 312
BOOL TortoiseBlame :: OpenFile(定数TCHAR * fileName)
{
...
//不正なutf8シーケンスの各行をチェックします。
//見つかった場合、処理します
//ファイルはASCIIとして、それ以外は想定
// UTF8ファイル。
char * utf8CheckBuf = lineptr;
while((bUTF8)&&(* utf8CheckBuf))
{
if((* utf8CheckBuf == 0xC0)||
(* utf8CheckBuf == 0xC1)||
(* utf8CheckBuf> = 0xF5))
{
bUTF8 = false;
休憩;
}
...
}
ところで、条件「* utf8CheckBuf == 0xC0」、「* utf8CheckBuf == 0xC1」も常にfalseです。 したがって、コード「bUTF8 = false;」は制御を獲得しません。 PVS-Studioアナライザーが式「* utf8CheckBuf == 0xC0」をsc責しなかったという事実は欠陥です。 改善点のリストに記録されています。 次のバージョンでは、これを誓います。
状況9
次の投稿はそれほど単純ではありません。 理論的には、コードにエラーがあります。 実際には-このコードは機能します。
V507ローカル配列 'stringbuf'へのポインターは、この配列のスコープ外に格納されます。 そのようなポインターは無効になります。 mainwindow.cpp 277
LRESULT CALLBACK CMainWindow :: WinMsgHandler(....)
{
...
if(pNMHDR-> code == TTN_GETDISPINFO)
{
LPTOOLTIPTEXT lpttt;
lpttt =(LPTOOLTIPTEXT)lParam;
lpttt-> hinst = hResource;
//のリソース識別子を指定します
//指定されたボタンの説明テキスト。
TCHAR stringbuf [MAX_PATH] = {0};
...
lpttt-> lpszText = stringbuf;
}
...
}
診断メッセージ
V507は、オブジェクトが存在の終了後に使用されることを警告します。 バッファー 'stringbuf'は、 'if'ステートメントの本体を終了した後に使用されます。
「stringbuf」がクラスオブジェクト、たとえばstd :: stringの場合、このコードは正しく動作しません。 すでに破壊されたオブジェクトを使用します。 しかし、ここでは、「stringbuf」はスタック上に作成された配列です。 Visual C ++コンパイラはスタックのこのセクションを再利用せず、バッファは関数「CMainWindow :: WinMsgHandler」の最後まで存在します。 したがって、コードは潜在的に危険ですが、エラーは発生しません。
状況10
そして、上記の例と同じ場所です。 繰り返しますが、これは機能するコードですが、脆弱です。
V507ローカル配列 'stringbuf'へのポインターは、この配列のスコープ外に格納されます。 そのようなポインターは無効になります。 picwindow.cpp 443
if((HWND)wParam == m_AlphaSlider.GetWindow())
{
LPTOOLTIPTEXT lpttt;
lpttt =(LPTOOLTIPTEXT)lParam;
lpttt-> hinst = hResource;
TCHAR stringbuf [MAX_PATH] = {0};
_stprintf_s(stringbuf、.....);
lpttt-> lpszText = stringbuf;
}
状況11
デストラクタで例外をスローし、スローしないのは悪い考えです。
V509デストラクタ内の「スロー」演算子は、try..catchブロック内に配置する必要があります。 デストラクタ内で例外を発生させることは違法です。 cachefileoutbuffer.cpp 52
CCacheFileOutBuffer ::〜CCacheFileOutBuffer()
{
if(IsOpen())
{
streamOffsets.push_back(GetFileSize());
size_t lastOffset = streamOffsets [0];
(size_t i = 1
count = streamOffsets.size();
i <カウント; ++ i)
{
size_t offset = streamOffsets [i];
size_t size = offset-lastOffset;
if(size> =(DWORD)(-1))
throw CStreamException( "stream too large");
追加((DWORD)サイズ);
lastOffset = offset;
}
追加((DWORD)(streamOffsets.size()-1));
}
}
メッセージ
V509は、例外処理中にCCacheFileOutBufferオブジェクトが破壊されると、新しい例外によりプログラムが即座にクラッシュすることを警告しています。
状況12
意味をなさない別の比較。
V547式 'endRevision <0'は常にfalseです。 符号なしの型の値が<0になることはありません。cachelogquery.cpp 999
typedef index_t revision_t;
...
void CCacheLogQuery :: InternalLog(
revision_t startRevision
、revision_t endRevision
、const CDictionaryBasedTempPathおよびstartPath
、intの制限
、const CLogOptions&options)
{
...
// rev <0のログを受信できません
if(endRevision <0)
endRevision = 0;
...
}
ここに負の値を指定することはできません。 変数endRevisionは符号なしの型であるため、常にendRevision> = 0です。潜在的な問題は、負の値が符号なしの型に変わった場合、非常に大きな数で作業を開始することです。 このため、検証はありません。
状況13
最初のレベルの有用なメッセージはこれ以上ありません。 まだです。 結局のところ、これはPVS-Studioの新機能を記述する最初の試みにすぎません。 開発計画には、確認する価値がある少なくとも150の例を記録しています。 前回の記事に回答し、コードを書く段階で静的分析によってエラーを特定できる例を送信した読者に再び感謝します。
2番目のレベルを見てみましょう。 Copy-Pasteテクニックの使用に関連する1つの興味深いエラーが見つかります。 ちなみに、3番目のレベルでは何もおもしろいものがなかったので、デフォルトでオフにすることは無駄ではありません。
V524「GetDbgHelpVersion」関数が「GetImageHlpVersion」関数(SymbolEngine.h、98行目)と完全に同等であることは奇妙です。 symbolengine.h 105
BOOL GetImageHlpVersion(DWORDおよびdwMS、DWORDおよびdwLS)
{
return(GetInMemoryFileVersion(( "DBGHELP.DLL")、
dwMS、
dwLS));
}
BOOL GetDbgHelpVersion(DWORDおよびdwMS、DWORDおよびdwLS)
{
return(GetInMemoryFileVersion(( "DBGHELP.DLL")、
dwMS、
dwLS));
}
2つの疑わしい同一の機能が検出されると、メッセージ
V524が発行されます。 ほとんどの場合、最初の関数は「dbghelp.dll」ではなく「imagehlp.dll」ファイルのバージョンを受け取る必要があります。
ステップ8
これで見つかったエラーを修正する必要があります。 この段階は理解可能であり、スキップします。
見つかったエラーについては、TortoiseSVNの開発者に報告します。
ステップ9
それでは、誤検知について少し話しましょう。 以下は、誤検知とは何か、それらに対処する方法を明確にするための例です。
最初の誤検知。 PVS-Studioはメモリをコピーするゲームを理解していませんでした。
V512「memcpy」関数を呼び出すと、バッファーのオーバーフローまたはアンダーフローが発生します。 resmodule.cpp 838
const WORD *
CResModule :: CountMemReplaceMenuExResource(....)
{
...
if(newMenu!= NULL){
CopyMemory(&newMenu [* wordcount]、p0、7 * sizeof(WORD));
}
...
}
警告
V512は、バッファーが完全に処理されていないこと、または逆にバッファーを超えたことを示します。 アナライザーは、WORDタイプの1つのオブジェクトのみを操作すると判断し、7つのオブジェクトをコピーすることになります。
2番目の誤検知。 ここで、アナライザーは配列の一部のみが処理されたと信じています。
V512「memcmp」関数を呼び出すと、バッファーのオーバーフローまたはアンダーフローが発生します。 sshsha.c 317
static int sha1_96_verify(....)
{
unsigned char correct [20];
sha1_do_hmac(handle、blk、len、seq、correct);
return!memcmp(正しい、blk + len、12);
}
実際、「正しい」配列の一部のみが比較されますが、それは意図されたものです。
3番目の例は誤検知です。
V517「if(A){...} else if(A){...}」パターンの使用が検出されました。 論理エラーが存在する可能性があります。 tree234.c 195
static void * add234_internal(....)
{
...
if((c = t-> cmp(e、n-> elems [0]))<0)
childnum = 0;
else if(c == 0)
return n-> elems [0]; / *すでに存在します* /
else if(n-> elems [1] == NULL
|| (c = t-> cmp(e、n-> elems [1]))<0)
childnum = 1;
else if(c == 0)
return n-> elems [1]; / *すでに存在します* /
else if(n-> elems [2] == NULL
|| (c = t-> cmp(e、n-> elems [2]))<0)
childnum = 2;
else if(c == 0)
return n-> elems [2]; / *すでに存在します* /
他に
childnum = 3;
...
アナライザーは、チェック「c == 0」が複数回存在するという事実を好まない。 変数 'c'は他の条件「c = t-> cmp(e、n-> elems [2])」内で変化するため、コードは正しいです。 ただし、これはかなりまれな状況です。 多くの場合、
V517メッセージはコードの本当の欠陥を示します。
他の誤検知は、興味深いものではないため考慮されません。 プログラマーが偽りであることを理解するのは十分簡単であり、それらに注意を払うべきではありません。
誤検知に対処する方法はいくつかあります。
1)コードを書き換えることができます。 これは時々非常に合理的です。 リファクタリングは、誤検知で最後の例を傷つけません(add234_internal関数と警告V517について話している)。
2)設定で一部のチェックを無効にできます。これにより、プロジェクトで常に誤検知が発生します。 切断後、これらのメッセージはすべてテーブルからすぐに消えます。 詳細は、「
設定:検出可能なエラー 」をご覧ください。
3)誤検知がチェックする価値のないコードに関連する場合、個々のファイルまたはフォルダーを除外できます。 マスクを使用できます。 詳細は、「
設定:ファイルをチェックしない 」をご覧
ください 。 これは、サードパーティのライブラリを分析から除外するのに便利です。
4)このメカニズムを使用して、特定のテキストを含むメッセージを抑制することができます。 詳細については、「
設定:メッセージの抑制 」。
5)特定の警告を抑制する必要がある状況があります。 この場合、「誤警報としてマーク」機能を使用できます。 この場合、「//-V error code」という形式の小さなコメントがコードに挿入されます。 この方法でマークアップされたコードセクションは、エラーリストで非表示にできます。 これを行うには、マークされたメッセージの表示をオン/オフするFAボタンを使用します。 詳細はこちら:
誤った警告を抑制します。
ご清聴ありがとうございました。
PVS-Studioをお試しください。 フィードバックをお送りください。 質問してください。 何か面白いものを見つけたら例を示してください。 実装のための新しい診断ルールを提案します。
心から、PVS-Studioの開発者の一人であるAndrey Karpov氏。
フィードバックページを使用してお問い合わせください。
または、電子メール:support [@] viva64.com、karpov [@] viva64.com。
メールでお問い合わせください。 私は個人的に、この記事の著者はコミュニケーションに参加しますが、ほとんどの企業で見られるような抽象的な人間ロボットではありません。