###導入###
| 変数は卑劣です。喜んでレジに座っていても、振り向くとすぐにレジに置かれてしまうこともあります。最適化の目的で、コンパイラーはそれらをウィンドウの外に完全にスローする場合があります。変数がメモリ内をどのように移動するかに関係なく、デバッガーで変数を追跡して操作する何らかの方法が必要です。この記事では、デバッガーで変数を処理する方法を説明し、libelfin を使用した簡単な実装を示します。
|
シリーズ記事インデックス
- 環境の準備
- ブレークポイント
- レジスタとメモリ
- エルフとドワーフ
- ソースコードとシグナル
- ソースコードレベルのステップバイステップ実行
- ソースレベルのブレークポイント
- スタックの拡張
- 変数の処理
- 高度なトピック
始める前に、私のブランチで libelfin fbreg のバージョンを使用していることを確認してください。これには、現在のスタック フレームのベース アドレスの取得と場所のリストの評価をサポートするためのハックがいくつか含まれていますが、どちらもネイティブ libelfin によって提供されるものではありません。互換性のある DWARF メッセージを生成するには、-gdwarf-2 パラメーターを GCC に渡す必要がある場合があります。ただし、それを実装する前に、最新の DWARF 5 仕様で位置エンコーディングがどのように機能するかについて詳しく説明します。さらに詳しく知りたい場合は、ここで標準を入手できます。
ドワーフの場所
特定の時点でのメモリ内の変数の位置は、DW_AT_location 属性を使用して DWARF メッセージ内にエンコードされます。場所の説明は、単一の場所の説明、複合場所の説明、または場所のリストにすることができます。
- 簡単な位置の説明: オブジェクトの連続した部分 (通常はすべての部分) の位置を説明します。単純な位置記述では、アドレス指定可能なメモリまたはレジスタ内の位置、またはそれらの欠如 (既知の値の有無にかかわらず) を記述することができます。たとえば、DW_OP_fbreg -32: 格納された変数全体 - スタック フレーム ベースから始まる 32 バイト。
- 複合ロケーションの説明: フラグメントの観点からオブジェクトを記述します。各オブジェクトは、レジスターの一部に含めることも、他のフラグメントから独立したメモリー位置に保管することもできます。たとえば、 DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2: 最初の 4 バイトはレジスター 3 にあり、最後の 2 バイトはレジスター 10 の変数にあります。
- 場所リスト: 存続期間が制限されているオブジェクト、または存続期間中に場所が変更されるオブジェクトについて説明します。例えば:
- <3 つのエントリを含む loclist が続きます>
- [ 0]DW_OP_reg0
- [ 1]DW_OP_reg3
- [ 2]DW_OP_reg2
- プログラム カウンタの現在値に基づいてレジスタ間を移動する変数。
DW_AT_location は、場所の説明の種類に応じて 3 つの異なる方法でエンコードされます。 exprloc は、単純な位置記述と複合位置記述をエンコードします。これらは、バイト長とそれに続く DWARF 式または場所の説明で構成されます。 loclist および loclistptr のエンコードされたロケーション リスト。実際のロケーション リストを記述する .debug_loclists セクションのインデックスまたはオフセットを提供します。
DWARF 式
DWARF 式を使用して、変数の実際の位置を計算します。これには、スタック値を操作する一連の操作が含まれます。利用可能な DWARF 操作は多数あるため、詳しくは説明しません。代わりに、それぞれの表現の例をいくつか挙げて、作業に役立つものを提供します。また、これを恐れる必要はありません。libelfin がこの複雑さをすべて処理します。
- リテラルエンコーディング
- DW_OP_lit0、DW_OP_lit1……DW_OP_lit31
- DW_OP_addr
- DW_OP_constu <署名なし>
- レジスタ値
- DW_OP_fbreg <オフセット>
- スタック フレームのベースで見つかった値を、指定された値だけオフセットしてプッシュします。
- DW_OP_breg0、DW_OP_breg1... DW_OP_breg31
- 指定されたレジスタの内容と指定されたオフセットをスタックにプッシュします
- スタック操作
- DW_OP_dup
- DW_OP_deref
- スタックの先頭をメモリ アドレスとして扱い、そのアドレスの内容で置き換えます
- 算術演算と論理演算
- DW_OP_および
- スタックの先頭にある 2 つの値をポップし、それらの論理積をプッシュバックします
- DW_OP_プラス
- DW_OP_and と同じですが、付加価値があります
- 制御フロー操作
- DW_OP_le、DW_OP_eq、DW_OP_gt など。
- 最初の 2 つの値をポップして比較し、条件が true の場合は 1 をプッシュし、そうでない場合は 0 をプッシュします。
- DW_OP_bra
- 条件分岐: スタックの先頭が 0 でない場合、オフセットを介して式内を前方または後方にスキップします
- 入力変換
- DW_OP_convert
- スタックの最上位の値を、指定されたオフセットの DWARF 情報エントリで記述される別の型に変換します。
特別な操作
-
ドワーフ型
DWARF 型表現は、デバッガ ユーザーに有用な変数表現を提供できるほど強力である必要があります。ユーザーは多くの場合、マシン レベルではなくアプリケーション レベルでデバッグできるようにしたいと考えており、変数が何をしているのかを理解する必要があります。 DWARF タイプは、他のほとんどのデバッグ情報とともに DIE でエンコードされます。名前、エンコーディング、サイズ、バイトなどを示すプロパティを持つことができます。ポインター、配列、構造体、typedef、および C または C プログラムで使用されるその他のものを表すために、無数の type タグが使用できます。
この単純な構造を例として挙げます:
リーリー
この構造の親 DIE は次のようになります:
リーリー
上記は、test.cpp の 1 行目で宣言された、サイズ 0xb8 の test という構造体があることを示しています。次に、メンバーを説明する多数のサブ DIE があります。
リーリー
各メンバーには、名前、型 (DIE オフセット)、宣言ファイルと行、およびそのメンバーが存在する構造体を指すバイト オフセットがあります。その型のポイントは以下の通りです。
リーリー
ご覧のとおり、私のラップトップ上の int は 4 バイトの符号付き整数型で、float は 4 バイトの浮動小数点数です。整数配列型には、要素型として int 型、インデックス型として sizetype (size_t と考えてください) を指定することにより、2a 要素があります。テスト * タイプは DW_TAG_pointer_type で、テスト DIE を参照します。
単純な変数リーダーの実装
上で述べたように、libelfin は複雑さのほとんどを処理します。ただし、変数の位置を表すすべてのメソッドが実装されているわけではないため、コードでこれらを処理するのは非常に複雑になります。したがって、現在は exprloc のみをサポートすることを選択しています。必要に応じて、さらに多くの種類の式のサポートを追加してください。本当に勇気のある方は、必要なサポートを完了するために libelfin にパッチを送信してください。
変数の処理は主にメモリまたはレジスタ内のさまざまな部分を配置することを含み、読み取りまたは書き込みは以前と同じです。話を簡単にするために、読み取りを実装する方法だけを説明します。
まず、プロセスからレジスタを読み取る方法を libelfin に伝える必要があります。 expr_context から継承するクラスを作成し、ptrace を使用してすべてを処理します。
リーリー
読み取りは、デバッガー クラスの read_variables 関数によって処理されます。
リーリー
上記で最初に行ったことは、現在いる関数を見つけることです。次に、その関数内のエントリを反復して変数を見つける必要があります。
リーリー
DIE の DW_AT_location エントリを検索して位置情報を取得します:
リーリー
次に、それが exprloc であることを確認し、libelfin に式を評価するよう依頼します。
リーリー
式を評価したので、変数の内容を読み取る必要があります。メモリ内またはレジスタ内にある可能性があるため、両方のケースを処理します:
リーリー
変数の型に基づいて、説明なしで値を出力したことがわかります。このコードで、変数の書き込み、または指定された名前の変数の検索がどのようにサポートされているかを理解していただければ幸いです。
最後に、これをコマンド パーサーに追加できます:
リーリー
テストを受けてください
いくつかの変数を含む小さな関数を作成し、最適化なしでデバッグ情報を使用してコンパイルし、変数の値を読み取れるかどうかを確認します。変数が保存されているメモリ アドレスに書き込んで、プログラムの動作がどのように変化するかを確認してください。
すでに記事が 9 つあり、残りは最後の 1 つです。次回は、皆さんにとって興味深いかもしれない、より高度な概念について説明します。この投稿のコードはここで見つけることができます。