Qtアプリケーション開発に使える!さまざまなデバッグ手法
(掲載 2023年1月27日)
Qtを用いた開発では、さまざまな手法でデバッグが行われています。この記事では、C/C++アプリケーションのデバッグに活用できる、自動テスト、コードカバレッジ、静的解析、ロギングなどのさまざまな手法やツールをご紹介します。
自動テスト
最初に行う必要があるのは、コードが適切にテストされていることを確認することです。これ自体はデバッグではありませんが、1つのバグを修正するときに新しいバグを発生させないようにすることができます。そのためのアイデアの1つに、「テスト駆動開発」と呼ばれるものがあります。これは、テスト対象のコードを記述する前にテストを記述することを意味します。この方法では、テストが正しいものをテストしていることを確認できます。テストが失敗した場合は、それを修正して合格させます。リファクタリングなどを行う際リグレッションを発生させないよう、対象のクラスの完全な単体テストカバレッジを追加することは非常に良い考えです。これを行うには、既存の単体テストフレームワークの1つを使用できます。(筆者は)Qt開発者として、QTestLibには詳しいですが、Google Test、Catchを使うこともできますし、実際には他にもたくさんあります。これらは、テストを書く際にすべてのテストメソッドが呼び出されることを確認し、失敗をどのように処理するかなど、すべてを設定する必要がないので、時間の節約に役立ちます。
さらにもう 1 つのステップとして、これらのテストを「継続的インテグレーション(CI)」と統合して、毎コミット後または毎晩、ゼロからのフルビルドと、すべての単体テストのフル実行を自動で行うことができます。これらはすべて、アプリケーションの品質を向上させ、実際にバグを修正するときにリグレッションを発生させないようにするため、以降でご紹介するデバッグの際には非常に有効です。
コードカバレッジ
もうひとつ、単体テストと密接に関係しているのが、コードカバレッジです。対象コードに対して充分な単体テストがあり、かつ高い網羅性でカバー出来ていることを確認するために使用されます。 ただし、100%のカバレッジを達成しようとすることは通常無意味です。重要なのは、アプリケーションの重要な部分をテストすることです。例えば、stringクラスやvectorクラスのように、どこでも使われている単純なユーティリティクラスがそれにあたります。あるいは、実際のビジネスロジックが存在するアプリケーションの一部であり、そこにバグがあっては困る場合もあります。単体テストが重要なコードを十分にカバーしているかどうかを確認するためにできることの1つが、コードカバレッジを計測することです。
単体テストでファイルの90%がカバーされていることを確認するツールはたくさんあり、テストで欠落しているコードの部分を正確に把握することができます。最もよく知られているツールは、Linuxのgcovです。コンパイラオプションに--coverageを渡すと、gcov の情報が生成されるため、GCCやClangなどのコンパイラと連携します。Windowsでは、Visual Studioに含まれるツールもありますし、OpenCppCoverageをインストールすることもできます。また、Squish Cocoもその一つです。これらすべてのツールを使用すると、単体テストによって実際にどの程度のコードがテストされているかを測定することができます。コードカバレッジツールは、飛行機関連などいくつかの分野で非常に重要視されよく使用されます。そのため大変多くのソリューションが存在するのです。
静的コード解析
コードの品質を向上させるためにできるもう一つのことは、静的コード解析を導入することです。これもまた、さまざまなツールがある分野です。目的はアプリケーションを実行することなくバグを検出することで、コードを調べて、バグがある可能性のある構造またはパターンがどこにあるかを示すことができます。最近の最も有名なツールは、実はコンパイラそのものなのです。GCCやClang、Visual Studioで十分な量の警告を有効にすると、コードの改善点についてコンパイラからフィードバックが得られます。したがって、できるだけ多くの問題を検出するために、これらの警告をできるだけ多く有効にすることをお勧めします。また、1 つだけでなく複数のコンパイラを使用してください。2~3個使えるなら、コンパイラから異なるフィードバックが得られるため、そのほうがよいでしょう。他にも、追加で使用できるツールがいくつかあります。よく知られているのはclang-tidyで、C++コードに対する多くのチェック機能があり、中にはコードの自動修正機能を持つものもあります。同じclangベースのライブラリを使用して、KDAB開発者の Sérgio Martins氏はClazyと呼ばれるClang用のプラグインを作成しました。これは、特にQtを使っているときに、一般的なC++アプリケーションでもよくあるコーディングミスを検出することができます。
さらに、Coverity、PVS-Studioなど、より専門的なツールもたくさんあります。これらは継続的インテグレーションの一部として設定することもでき、コードの改善点に関するフィードバックを定期的に得ることができます。
ロギング
もう少し、デバッグの観点で期待されるものに近いものをご紹介します。どの関数に入るのか、この変数の値は何なのか、といったメッセージをロギングするのです。CやC++に含まれるprintfやcout、QtのqDebugのようなフレームワークに付属するもの、log4cxx, boost::log, log4cplus, easylogging++ などの追加ライブラリ...これらのほとんどに共通することは、デバッグ文のセット全体をオンにしたりオフにするインストールを行うということです。つまり、パーサをデバッグしている場合はパーサのメッセージセットだけを有効にし、印刷をデバッグしている場合は印刷に関するメッセージのすべてを有効にする、といったようにです。この方法は、1秒間に10,000通のメッセージが送られてきて、それを理解しなければならないとか、デフォルトですべてがオフになっていて、必要なものをコメントしなければならないといった場合よりも、ずっと実用的な方法です。どこかのスイッチを切り替えるだけで必要なものが得られるのであれば、とても簡単なことです。
アサーション
アサーションについてご説明します。これは、実行時に「これは絶対に起こらない」と言い切るためのものです。もし起きてしまったら、アプリケーションを停止してエラー原因のデバッグを行います。これは、ロジックエラーに用いられる方法です。自分のプログラムは絶対にこんなことは起きないようにできていると思いがちです。特に、この関数に渡す前にこのポインタを知っていてはいけないという前提条件や、この関数は決してユニットポインタを返さないという後条件、関数実行中に変化してはいけないものである不変条件などに有効です。その他にも以外にもいろいろなことに使えますが、このファイルが見つからないといったランタイムエラーには使用できません。これはロジックエラーというより、セットアップの問題です。ユーザーがファイルを置き忘れたからといって、アプリケーションを中断させたくはないでしょう。
もうひとつのアサーションは、静的アサーションです。これはコンパイル時に発生するものです。コンパイラはチェックに失敗したことを教えてくれるので、それを調べなければなりません。例えば、値の量が間違っている列挙型、一貫性のない読み取り専用データ、期待通りの継承でない場合、サポートされていないCPUアーキテクチャなどに対して使用されます。
トレース
トレースによってできることの1つは、アプリケーションで使用されている動的ライブラリを調査することです。Linuxの ldd、WindowsのDependencies.exe、macOSのotoolなどのツールを使用して行うことができます。これらツールは、アプリケーションで使用されている共有ライブラリを単にリストアップするだけです。
実行時に、アプリケーションが何をしているかをブラック ボックスとして把握することもできます。外部から、開いているすべてのファイルを表示するようにアプリケーションに要求します。これは、straceまたは他のオペレーティングシステムの同等のツールを使用して実行できることです。ファイルを開いたり、ソケットを使ったり、システムコールを経由するものであればいつでも表示するように要求できます。これは、実際にアプリケーションのソースコードを持っていない場合の一般的なデバッグ手法ですが、持っている場合にも非常に役立ちます。
デバッガー
次に、Linuxのgdb、macOSのlldb、Windowsのcdbなどのよく知られているデバッガーを使用して、アプリケーションでステップ毎にデバッグを実行できます。しかし、rrと呼ばれるデバッガーがあることは意外に知られていません。このツールは、素晴らしいことにアプリケーション内で前後に移動できます。その方法は、アプリケーションの実行を記録し、うまくいけば探しているバグを引き起こすことができます。そしてその記録を (rrによって起動されたgdbで) 再生すると、前後に移動したり、前方にスキップしたり、後方にスキップしたりできます。記録されたアプリケーションの実行を好きな方法でナビゲートすることができるのです。これは、通常gdbで、値が間違っていると思い、どうやって計算しているのだろうと疑問を感じ、時間をさかのぼって調べる必要があるような問題にぶつかったときに非常に便利です。タイムマシンを持っていないので勿論それはできませんが、rrにはすべての情報が記録されているため、遡ることができます。したがって、これは検討する価値のあるツールであると思います。
KDABのGammaRay
Qtを使った開発を行っている方のために、KDABが開発したGammaRayというツールについてお伝えします。無料で使用できるオープンソースで、githubで入手できます。このツールは、Qtアプリケーションをイントロスペクトして、存在するすべてのQObject、すべてのQWidgets、シーン内のすべてのQML要素、Qt 3Dシーン内のすべての3D要素などをグラフィカルに表示することが可能です。そのような情報をすべて得ることができ、アプリケーションについてより詳しく知ることができる多数のモジュールを備えています。GammaRay自体はデバッガーではなく、コードを追っていくようなものではありません。これは、これは、Qtアプリケーションの概要と、Qtが作成したすべてのものを取得するようなもので、ウィジェットのサイズが十分でない理由などの問題をデバッグする方法です。最小サイズが間違っていないか、サイズポリシーは間違っていないか、といったプロパティに関する情報を、GammaRayから取得できます。
組み込み開発であればリモートデバッグもサポートしていますし、実行中のアプリケーションへのアタッチもサポートしており、なかなか面白いツールです。
Valgrind
Valgrindは、Linuxで特に有名なツールです。macOSでも動作します。アプリケーションの実行速度が遅くなりますが、正しくコンパイルされたアプリケーションが何をしているかを調べてくれます。バイナリで実行され、メモリを削除した後にメモリを使用している、またはメモリを初期化せずに使用している、といったエラーを検出します。再現性がないように見える動作がある場合に非常に役立ちます。Valgrindは無効なメモリ使用の原因がどこにあるのかを正確に教えてくれるので、アプリケーションを再実行すると、別の結果が出てしまうような不具合でも力を発揮します。これ以外にもValgrindは様々なツールを備えています。レースコンディションのためのhelgrind、メモリ使用量を調べるmassif、プロファイリングを行うcallgrindがあります。この3つについては本記事で紹介されている代替ツールの方が良いかもしれません。ただし、デフォルトのツールであるmemcheckは非常に便利です。
サニタイザー
最後に、サニタイザーについて話しましょう。これは、無効なメモリの使用やデバッグ削除などの問題を検出する別の方法です。サニタイザーは、外部ツールを使う代わりに、これらすべての検証を行うコードを独自のコードに挿入するようコンパイラに実際に要求することができます。コードがメモリを割り当てるときはいつでも、それを記憶しています。コードがメモリ領域を使用するときは、最初にそれができるかどうかをチェックします。これは非常に強力です。Valgrindよりもはるかに高速です。LinuxでGCC 4.9以降または Clang 3.1以降を使用している場合、またはWindowsでClang 6以降、または Visual Studio 2019 16.4以降を使用している場合、これはコンパイラの一部となっています。
サニタイザーには次の4種類があります。
・アドレスサニタイザー:メモリ使用時の問題をすべて教えてくれます。
・リークサニタイザー(アドレスサニタイザーに含まれる) :メモリ リークについて教えてくれます。
・スレッドサニタイザー:特に競合状態の検知について非常に優れており、他の方法では検出するのが非常に困難です。
・未定義動作サニタイザー:コードに未定義の動作がある場合に通知します。
初期化されていないメモリを検出するメモリサニタイザーなど、いくつかの追加のサニタイザーが開発されていますが、これにはすべてのライブラリを再コンパイルする必要があるため、あまり便利ではありません。
以上、この記事はCおよび C++ アプリケーションで使用できるすべてのデバッグツールの概要です。これらのいずれかについてさらに詳しく知りたい場合は、KDABにてトレーニングサービスを行っています。ご興味ある方は、SRAの営業担当もしく問合せページにご相談いただくか、KDABJapanのWebページより直接お問合せください。
出典:C/C++ Debugging Tools【KDAB】
KDAB日本語トップページ