3

x86 実行可能ファイルでヒットした基本ブロックをログに記録する小さなコード カバレッジ ユーティリティを作成しました。ターゲットのソース コードやデバッグ シンボルなしで実行され、監視対象の基本ブロックが失われるだけです。

しかし、単一の実行可能イメージのカバレッジ スナップショットが繰り返される私のアプリケーションでは、これがボトルネックになりつつあります。

スピードアップを試みたので、いくつかの段階を経ました。各基本ブロックの先頭に INT3 を配置し、デバッガーとしてアタッチし、ヒットをログに記録することから始めました。次に、5 バイト (JMP REL32 のサイズ) より大きい任意のブロックにカウンターをパッチすることで、パフォーマンスを改善しようとしました。プロセス メモリ空間に小さなスタブ ('mov [blah], 1 / jmp backToTheBasicBlockWeCameFrom') を書き、それに JMP をパッチしました。例外もデバッガーの中断もないため、これにより大幅に高速化されますが、さらに高速化したいと考えています。

以下のいずれかを考えています。

1) パッチを適用したカウンターを使用してターゲット バイナリを事前にインストルメント化します (現時点では実行時にこれを行います)。PE に新しいセクションを作成し、そこにカウンターを投入し、必要なすべてのフックにパッチを適用してから、各実行後にデバッガーを使用して同じセクションからデータを読み取るだけです。これにより、ある程度の速度が向上します (私の推定では約 16%) が、小さなブロックに必要な厄介な INT3 がまだ残っており、実際にはパフォーマンスが低下します。

2) 独自の UnhandledExceptionFilter を含めるようにバイナリを計測し、上記と組み合わせて独自の int3 を処理します。これは、すべての int3 でデバッグ対象からカバレッジ ツールへのプロセス切り替えがないことを意味しますが、それでもブレークポイント例外が発生し、その後のカーネル移行が発生します。

3) Intel のハードウェア ブランチ プロファイリング命令を使用して、巧妙なことを試みます。これはかなり素晴らしいように聞こえますが、どうすればよいかわかりません.Windowsユーザーモードアプリケーションでも可能ですか? カーネル モード ドライバーがかなり簡単であれば、そこまで書くこともあるかもしれませんが、私はカーネル コーダーではなく (少し手を出します)、おそらく多くの頭痛の種になるでしょう。このアプローチを使用する他のプロジェクトはありますか? Linux カーネルにはカーネル自体を監視する機能があるようですが、特定のユーザーモード アプリケーションを監視するのは難しいと思います。

4) 市販のアプリケーションを使用します。ソースやデバッグ シンボルなしで動作し、スクリプト可能 (バッチで実行できるようにするため) である必要があり、できれば無料 (私はかなりけちです) である必要があります。ただし、有償のツールを検討する必要はありません (ツールへの支出を減らし、新しいハードウェアの購入を避けるのに十分なだけパフォーマンスを向上させることができれば、それは正当な理由になります)。

5) その他。かなり古いハードウェア (Pentium 4 っぽい) で Windows XP 上の VMWare を実行しています。JMP REL32 を 5 バイト未満にすることはできますか (そして、int3 を必要とせずに小さなブロックをキャッチできますか)?

ありがとう。

4

1 に答える 1

1

バイナリのインストルメント化を主張する場合、ほとんどの場合、最も速いカバレッジは 5 バイトのジャンプアウト ジャンプバック トリックです。(バイナリ インストルメンテーション ツールの標準的な基盤をカバーしています。)

INT 3 の解決策には、常にトラップが含まれます。はい、デバッガー空間の代わりに自分の空間でトラップを処理でき、それによって速度が向上しますが、ジャンプアウト/バック パッチに匹敵するほどにはなりません。インストルメントしている関数がたまたま 5 バイトよりも短い場合 (例: "inc eax/ret")、パッチを適用できる 5 バイトがないため、とにかくバックアップとして必要になる場合があります。

少し最適化するためにできることは、パッチが適用されたコードを調べることです。そのような検査なしで、元のコードで:

         instrn 1
         instrn 2
         instrn N
  next:

パッチを適用すると、一般的に次のようになります。

         jmp patch
         xxx 
  next:

通常、パッチが必要です。

   patch: pushf
          inc   count
          popf
          instrn1
          instrn2
          instrnN
          jmp   back

必要なのがカバレッジだけである場合は、インクリメントする必要はなく、フラグを保存する必要がないという意味です。

   patch: mov    byte ptr covered,1
          instrn1
          instrn2
          instrnN
          jmp   back

パッチのサイズを抑えるには、ワードではなくバイトを使用する必要があります。プロセッサがパッチを実行するために 2 つのキャッシュ ラインをフェッチしないように、パッチをキャッシュ ラインに配置する必要があります。

カウントすることに固執する場合は、instrn1/2/N を分析して、「inc」がだますフラグを気にしているかどうかを確認し、必要に応じて pushf/popf のみを使用するか、パッチの 2 つの命令の間にインクリメントを挿入することができます。それは気にしません。instn がとにかくretされるなどの複雑さを処理するために、これらをある程度分析する必要があります。より良いパッチを生成できます (たとえば、"jmp back" はしないでください)。

add count,1を使用すると、部分的な条件コードの更新とそれに伴うパイプラインのインターロックが回避されるため、 inc countよりも高速であることがわかる場合があります。incはキャリー ビットを設定せず、addは設定するため、これは cc-impact-analysis に少し影響します。

もう 1 つの可能性は、PC サンプリングです。コードをまったくインストルメント化しないでください。スレッドを定期的に中断して、サンプルの PC 値を取得するだけです。基本ブロックの場所がわかっている場合、基本ブロックのどこかにある PC サンプルは、ブロック全体が実行された証拠です。これは必ずしも正確なカバレッジ データを提供するとは限りません (重要な PC 値を見逃す可能性があります) が、オーバーヘッドはかなり低くなります。

ソースコードにパッチを適用する意思がある場合は、「covered[i]=true;」を挿入するだけでよいでしょう。最初に i 番目の基本ブロックを作成し、さまざまな最適化をすべてコンパイラーに任せます。パッチは必要ありません。これの本当に優れた点は、ネストされたループに基本ブロックがあり、このようにソース プローブを挿入した場合、コンパイラはプローブの割り当てがループに対してべき等であることを認識し、ループからプローブを持ち上げることです。Viola、ループ内のゼロ プローブ オーバーヘッド。これ以上何が欲しいですか?

于 2013-02-08T20:40:23.767 に答える