GDB には、リバース デバッグをサポートする新しいバージョンがあります ( http://www.gnu.org/software/gdb/news/reversible.htmlを参照)。それがどのように機能するのか疑問に思いました。
リバース デバッグを機能させるには、各ステップのメモリを含むマシンの状態全体を保存する必要があるようです。これにより、多くのメモリを使用することは言うまでもなく、パフォーマンスが非常に遅くなります。これらの問題はどのように解決されますか?
GDB には、リバース デバッグをサポートする新しいバージョンがあります ( http://www.gnu.org/software/gdb/news/reversible.htmlを参照)。それがどのように機能するのか疑問に思いました。
リバース デバッグを機能させるには、各ステップのメモリを含むマシンの状態全体を保存する必要があるようです。これにより、多くのメモリを使用することは言うまでもなく、パフォーマンスが非常に遅くなります。これらの問題はどのように解決されますか?
私は gdb のメンテナーであり、新しいリバース デバッグの作成者の 1 人です。その仕組みについてお話しできれば幸いです。何人かが推測しているように、後で復元できるように、マシンの状態を十分に保存する必要があります。いくつかのスキームがありますが、その 1 つは、各マシン命令によって変更されるレジスタまたはメモリ位置を単純に保存することです。次に、その命令を「元に戻す」には、それらのレジスタまたはメモリ位置のデータを元に戻すだけです。
はい、それは高価ですが、最新の CPU は非常に高速であるため、インタラクティブな操作 (ステッピングやブレークポイントの実行) を行っている場合は、それほど気にすることはありません。
逆実行を実装するには、シミュレーター、仮想マシン、およびハードウェアレコーダーの使用を忘れてはならないことに注意してください。
これを実装する別のソリューションは、ハードウェアベースのデバッガーでGreenHillsとLauterbachによって実行されるような、物理ハードウェアでの実行をトレースすることです。各命令のアクションのこの固定トレースに基づいて、各命令の効果を順番に削除することにより、トレース内の任意のポイントに移動できます。これは、デバッガーに表示される状態に影響を与えるすべてのものをトレースできることを前提としていることに注意してください。
もう1つの方法は、VmWareWorkstation6.5およびVirtutechSimics3.0(以降)で使用され、Visual Studio 2010に付属しているように見えるチェックポイント+再実行方法を使用することです。ここでは、仮想マシンまたはシミュレーターを使用します。システムの実行に関する間接的なレベルを取得します。状態全体を定期的にディスクまたはメモリにダンプしてから、シミュレータがまったく同じプログラムパスを決定論的に再実行できることに依存します。
簡略化すると、次のように機能します。システムの実行時に時間Tにいるとします。時間T-1に移動するには、ポイントt <Tからチェックポイントを取得し、(Tt-1)サイクルを実行して、現在の場所の1サイクル前に終了します。これは非常にうまく機能するように作成でき、ディスクIOを実行し、カーネルレベルのコードで構成され、デバイスドライバーの作業を実行するワークロードにも適用できます。重要なのは、すべてのプロセッサ、デバイス、メモリ、およびIOを含むターゲットシステム全体を含むシミュレータを用意することです。詳細については、gdbメーリングリストとそれに続くgdbメーリングリストの説明を参照してください。私はこのアプローチを定期的に使用して、特にデバイスドライバーや初期のOSブートでトリッキーなコードをデバッグしています。
もう1つの情報源は、チェックポインティングに関するVirtutechホワイトペーパーです(完全な開示で私が書いたものです)。
EclipseCon セッション中に、 Chronon Debugger for Javaでこれを行う方法についても尋ねました。これは、実際にステップを戻すことはできませんが、記録されたプログラムの実行を再生して、リバース デバッグのように感じることができます。(主な違いは、Chronon デバッガーでは実行中のプログラムを変更できないのに対し、他のほとんどの Java デバッガーでは変更できることです。)
私の理解が正しければ、プログラムの内部状態のすべての変化が記録されるように、実行中のプログラムのバイトコードを操作します。外部状態を追加で記録する必要はありません。それらが何らかの方法でプログラムに影響を与える場合、その外部状態に一致する内部変数が必要です (したがって、その内部変数で十分です)。
再生時に、基本的に、記録された状態変化から実行中のプログラムのすべての状態を再作成できます。
興味深いことに、状態の変化は、最初に見たときに予想されるよりもはるかに小さいです。したがって、条件付きの "if" ステートメントがある場合、プログラムが then ステートメントまたは else ステートメントのどちらを使用したかを記録するために、少なくとも 1 ビットは必要であると考えるでしょう。多くの場合、それらの異なるブランチに戻り値が含まれている場合のように、それを回避することもできます。次に、戻り値のみを記録し (とにかく必要になります) 、戻り値自体から実行された分岐に関する決定を再計算するだけで十分です。
ネイサンフェルマンは書いた:
しかし、逆デバッグでは、入力した次のコマンドとステップコマンドのみをロールバックできますか、それとも任意の数の命令を元に戻すことができますか?
命令はいくつでも元に戻すことができます。たとえば、前進するときに停止したポイントでのみ停止することに制限されません。新しいブレークポイントを設定して、それに逆方向に実行できます。
たとえば、命令にブレークポイントを設定してそれまで実行させた場合、スキップしても前の命令にロールバックできますか?
はい。ブレークポイントに到達する前に記録モードをオンにしている限り。
mozillarr
は、GDB リバース デバッグのより堅牢な代替手段です。
GDB の組み込みの記録と再生には重大な制限があります。たとえば、AVX 命令はサポートされていません。gdb リバース デバッグは、「プロセス レコードはアドレスで命令 0xf0d をサポートしていません」で失敗します。
rr の利点:
rr は、最初に、スレッド切り替えなどのすべての非決定論的イベントで何が起こったかを記録する方法でプログラムを実行することによってこれを実現します。
次に、2 回目のリプレイ実行中に、驚くほど小さいそのトレース ファイルを使用して、元の非決定論的な実行で発生したことを正確に再構築しますが、決定論的な方法で順方向または逆方向に再構築します。
rr はもともと Mozilla によって開発され、翌日の夜間のテストで現れたタイミングのバグを再現するのに役立ちました。しかし、リバース デバッグの側面は、実行中に数時間しか発生しないバグがある場合にも基本となります。
次の例は、その機能の一部、特に 、reverse-next
コマンドをreverse-step
示しています。reverse-continue
Ubuntu 18.04 にインストールします。
sudo apt-get install rr linux-tools-common linux-tools-generic linux-cloud-tools-generic
sudo cpupower frequency-set -g performance
# Overcome "rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is 3."
echo 'kernel.perf_event_paranoid=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
テストプログラム:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int f() {
int i;
i = 0;
i = 1;
i = 2;
return i;
}
int main(void) {
int i;
i = 0;
i = 1;
i = 2;
/* Local call. */
f();
printf("i = %d\n", i);
/* Is randomness completely removed?
* Recently fixed: https://github.com/mozilla/rr/issues/2088 */
i = time(NULL);
printf("time(NULL) = %d\n", i);
return EXIT_SUCCESS;
}
コンパイルして実行:
gcc -O0 -ggdb3 -o reverse.out -std=c89 -Wextra reverse.c
rr record ./reverse.out
rr replay
これで、GDB セッション内に残され、デバッグを適切にリバースできます。
(rr) break main
Breakpoint 1 at 0x55da250e96b0: file a.c, line 16.
(rr) continue
Continuing.
Breakpoint 1, main () at a.c:16
16 i = 0;
(rr) next
17 i = 1;
(rr) print i
$1 = 0
(rr) next
18 i = 2;
(rr) print i
$2 = 1
(rr) reverse-next
17 i = 1;
(rr) print i
$3 = 0
(rr) next
18 i = 2;
(rr) print i
$4 = 1
(rr) next
21 f();
(rr) step
f () at a.c:7
7 i = 0;
(rr) reverse-step
main () at a.c:21
21 f();
(rr) next
23 printf("i = %d\n", i);
(rr) next
i = 2
27 i = time(NULL);
(rr) reverse-next
23 printf("i = %d\n", i);
(rr) next
i = 2
27 i = time(NULL);
(rr) next
28 printf("time(NULL) = %d\n", i);
(rr) print i
$5 = 1509245372
(rr) reverse-next
27 i = time(NULL);
(rr) next
28 printf("time(NULL) = %d\n", i);
(rr) print i
$6 = 1509245372
(rr) reverse-continue
Continuing.
Breakpoint 1, main () at a.c:16
16 i = 0;
複雑なソフトウェアをデバッグする場合、クラッシュ ポイントまで実行してから、ディープ フレームに陥る可能性があります。reverse-next
その場合、より高いフレームでは、最初に次のことを行う必要があることを忘れないでください。
reverse-finish
そのフレームまでは、いつものことをするだけup
では十分ではありません。
私の意見では、rr の最も深刻な制限は次のとおりです。
UndoDB は rr の商用版です: https://undo.ioどちらもトレース / リプレイ ベースですが、機能とパフォーマンスの点でどのように比較されるかはわかりません。
ODB と呼ばれる別のリバース デバッガーのしくみを次に示します。エキス:
Omniscient Debugging は、プログラム内の各「関心のあるポイント」(値の設定、メソッド呼び出しの作成、例外のスロー/キャッチ) で「タイムスタンプ」を収集し、プログラマーがそれらのタイムスタンプを使用して、そのプログラムの実行履歴。
ODB ... はプログラムのクラスがロードされるときにコードを挿入し、プログラムが実行されるとイベントが記録されます。
gdb も同じように機能すると思います。
リバース デバッグとは、プログラムを逆方向に実行できることを意味します。これは、問題の原因を追跡するのに非常に役立ちます。
各ステップの完全なマシン状態を保存する必要はなく、変更のみを保存します。おそらくまだかなり高価です。