2016年に、ほとんどのプログラムがサンドボックスで実行されると、最も能力のない開発者でさえシステムに害を与えることはできませんが、問題に遭遇するのは奇妙です。これについては後で説明します。 正直に言うと、彼女がWin32Apiで遠い過去に行ったことを望んでいましたが、最近私はそれに出くわしました。 それ以前は、
問題
GDIオブジェクトのリークまたは使用が多すぎます。
症状
- タスクマネージャーの[詳細]タブで、GDIオブジェクトの列に10,000の脅威が表示されます(この列がない場合は、テーブルの見出しを右クリックして[列の選択]を選択することで追加できます)。
- C#または別の言語で開発する場合、実行されたCLRは特定ではない例外をスローします。
メッセージ:GDI +で一般的なエラーが発生しました。
ソース:System.Drawing
TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
タイプ: System.Runtime.InteropServices.ExternalException
また、特定の設定またはシステムバージョンでは、例外はないかもしれませんが、アプリケーションは単一のオブジェクトを描画できません。
- C / C ++で開発すると、Create%SOME_GDI_OBJECT%などのすべてのGDIメソッドがNULLを返し始めました
なんで?
Windowsファミリのシステムでは、一度に65535個までのGDIオブジェクトを作成できます。 実際、この数は非常に多く、通常のシナリオでは到達しないはずです。 プロセスには10000の制限がありますが、これは変更できます(レジストリで、HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Windows \ GDIProcessHandleQuotaの値を256から65535に変更することを強くお勧めします)。 これが行われると、1つのプロセスがエラーメッセージを表示できないような方法でシステムを配置する機会が得られます。 この場合、システムは再起動後にのみ有効になります。
修正方法
きちんとしたCLR駆動の世界に住んでいる場合、アプリケーションで通常のメモリリークが発生する確率は10分の9です。 問題は不快ですが、それは非常にありふれたものであり、それを見つけるための少なくとも12の優れたツールがあります。 これについては詳しく説明しません。 プロファイラを使用して、GDIリソース上のラッパーオブジェクトの数が増加しているかどうかを確認する必要があります。これらは、ブラシ、ビットマップ、ペン、領域、グラフィックスです。 これが当てはまる場合、ラッキーです。記事でタブを閉じることができます。
ラッパーオブジェクトのリークがなかった場合は、コード内でGDI関数とそれらが削除されないスクリプトを直接使用します。
他の人はあなたに何をアドバイスしますか?
この件に関するマイクロソフトの公式ガイドまたはその他の記事は、インターネットで見つけることができますが、次の点についてアドバイスします。
すべてのCreate %SOME_GDI_OBJECT%を検索し、対応するDeleteObject (またはHDCオブジェクトのReleaseDC)があるかどうかを確認します。ある場合は、呼び出されないシナリオがあるかもしれません。
このメソッドにはわずかに改善されたバージョンがあり、追加の最初のステップが含まれています。
GDIViewユーティリティをダウンロードします。 彼女はタイプごとに特定の数のGDIオブジェクトを表示できますが、驚くべきことは、すべての合計が最後の列の値と一致しないことです。 少なくともなんらかの方法で検索エリアを絞り込むのに役立つ場合は、これに注意を払わないようにすることができます。
私が取り組んでいるプロジェクトのコードベースは900万行を超え、サードパーティライブラリとほぼ同じ数、GDI関数の数百の呼び出し、数十のファイルに広がっています。 手作業で何も見逃すことなくこれを分析することは単純に不可能であることに気付く前に、私は多くのエネルギーとコーヒーを費やしました。
何を提供しますか?
この方法が長すぎて余分な体の動きを必要とするように思われる場合は、以前の絶望のすべての段階をまだ行っていません。 前の手順を数回試すことができますが、それでも解決しない場合は、このオプションを割引かないでください。
リークを探して、「そして、リークするオブジェクトはどこにあるのでしょうか?」と思いました。API関数が呼び出されたすべての場所にブレークポイントを設定することは絶対に不可能でした。 さらに、.netフレームワークまたはサードパーティのライブラリのいずれかでこれが発生しなかったという完全な確実性はありませんでした。 数分間のグーグル検索で、 Api Monitorユーティリティにアクセスしました。これにより、システム機能の呼び出しを記録およびデバッグできました。 GDIオブジェクトを生成するすべての関数のリストを簡単に見つけ、Api Monitorでそれらを正直に見つけて選択し、ブレークポイントを設定しました。
その後、彼はVisual Studioでデバッグのプロセスを開始し、ここでプロセスツリーで選択しました。 最初のブレークポイントはすぐに機能しました:
課題が多すぎました。 私はすぐにこのストリームを止めてしまい、別の何かを考え出す必要があることに気付きました。 関数からブレークポイントを削除し、ログを見ることにしました。 これらは何千もの課題でした。 それらを手動で分析できないことが明らかになりました。
タスク:削除に対応しないGDI関数の呼び出しを見つけます。 ログには、必要なものすべてが含まれます。時系列の関数呼び出しのリスト、戻り値、およびパラメーター。 Create%SOME_GDI_OBJECT%関数の戻り値を取得し、この値を引数としてDeleteObject呼び出しを見つける必要があることがわかりました。 Api Monitorですべてのエントリを選択し、テキストファイルに貼り付けて、TAB区切り文字付きのCSVのようなものを取得しました。 これを解析するプログラムを書くことを考えていたVSを起動しましたが、ロードする前に、データベースにデータをエクスポートし、興味のあるものを取得するクエリを作成することをお勧めしました。 それは私が非常に迅速に質問し、それらへの回答を得ることができたため、正しい選択でした。
CSVからデータベースにデータをインポートするための多くのツールがあるので、そこで停止しません( mysql 、 mssql 、 sqlite )。
私は次の表を得ました:
-- mysql code CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
API呼び出しから削除されたオブジェクトのハンドルを取得するmysql関数を作成しました。
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
最後に、現在のすべてのオブジェクトを見つけるクエリ:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(厳密に言えば、すべての削除呼び出しをすべての作成呼び出しに見つけるだけです)
呼び出しは図にすぐに表示され、削除は見つかりませんでした。
最後の質問は残ります。これらのメソッドがどこで呼び出されているかをコードのコンテキストでどのように見つけることができますか? そして、ここで1つのトリッキーなトリックが私を助けました:
- VSでデバッグするためにアプリケーションを実行します。
- Api Monitorで見つけて選択します。
- 目的のApi関数を選択し、ブレークポイントを設定します。
- 目的のパラメーターで呼び出されるまで、辛抱強く「次へ」をクリックします。 (条件付きブレークポイントはどのように対不足しました
- 目的のコールに到達したら、VSに移動して[すべて切断]をクリックします。
- VSデバッガーは、リークしているオブジェクトが作成された場所で停止し、削除されない理由を見つけるためにのみ残ります。
(コードは例としてのみ書かれています)
要約:
アルゴリズムは長くて複雑で、多くのツールを使用しますが、巨大なコードベースでのエラーの愚かな検索よりもはるかに高速に結果を得ることができました。
ここは、読むのが面倒だった人や、読んでいる間にすべてがどのように始まったのかをすでに忘れていた人向けです。
- GDIラッパーオブジェクトでメモリリークを検索する
- 存在する場合は、削除して最初の手順を繰り返します。
- そうでない場合は、API関数の呼び出しを直接探します。
- あまり多くない場合は、オブジェクトが削除されないシナリオを探してください。
- 多数ある場合や追跡できない場合は、Api Monitorをダウンロードして、GDI関数への呼び出しを記録するように設定する必要があります。
- VSでデバッグアプリケーションを実行する
- リークを再生します(これにより、キャッシュされたオブジェクトがログで目を汚さないようにプログラムが初期化されます)。
- Api Monitor'omを接続します。
- リークを再現します。
- ログをテキストファイルにコピーし、手元にあるデータベースにインポートします(mysqlの記事のスクリプトですが、RDBMSに簡単に適応できます)。
- CreateメソッドとDeleteメソッドを一致させ(SQLスクリプトはこの記事の前半にあります)、Delete呼び出しを持たないメソッドを見つけます
- Api Monitorにブレークポイントを設定して、目的のメソッドを呼び出します。
- メソッドが必要なパラメーターで呼び出されるまで、続行を押します。 条件付きブレークポイントの不足を叫びます。
- 必要なパラメーターを指定してメソッドが呼び出されたら、VSで[すべて解除]をクリックします。
- このオブジェクトが削除されない理由を見つけてください。
この記事が誰かの時間を大幅に節約し、役に立つことを願っています。