VCLのアトムの誤用と微妙なバグ

画像

バグ検索



システムの長時間の稼働と集中的なデバッグの後、Delphicコントロールの不適切な動作に関連するバグに悩まされていました。 リストの更新が停止し、ボタンが押され、入力フィールドがフォーカスを失い始めました。 そしてすべてが悲しく、IDEを再起動しても助けにはなりませんでした。 さらに、IDEを再起動した後、IDE自体も同様に失敗し始めました。 再起動する必要がありました。

今日それは私を悩ませ、私は彼女を探し始めました。 私は役に立たないと言わなければなりません。

ウィンドウメッセージを誓約したので、私は何が悪かったのか分析し始めました。

Control.pasモジュールには次の行があることがわかりました。

function FindControl(Handle: HWnd): TWinControl; var OwningProcess: DWORD; begin Result := nil; if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and (OwningProcess = GetCurrentProcessId) then begin if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom))) else Result := ObjectFromHWnd(Handle); end; end;
      
      





GetProp(ハンドル、MakeIntAtom(ControlAtom))は常に0を返します。ControlAtomが何らかの理由で0であることがすぐに明らかになり、 GlobalFindAtom(PChar(ControlAtomString))も0を返します。

ControlAtomStringControlAtomは 、モジュールの初期化セクションで呼び出されるInitControlsプロシージャで初期化されます。

 procedure InitControls; var UserHandle: HMODULE; begin {$IF NOT DEFINED(CLR)} WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]); WindowAtom := GlobalAddAtom(PChar(WindowAtomString)); ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]); ControlAtom := GlobalAddAtom(PChar(ControlAtomString)); RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString)); {$IFEND}
      
      





ControlAtomStringは正しく読み込まれますが、 ControlAtomはゼロ読み込まれます。 ここにはエラーのチェックがないため、かなり遅れて戻ってきました。 GlobalAddAtomの後にGetLastErrorを挿入すると、 ERROR_NOT_ENOUGH_MEMORYが返されます。 また、MSDNでGlobalAddAtomの説明を注意深く読んでいる場合は、次のことに気付くことができます。

グローバルアトムは、アプリケーションの終了時に自動的に削除されません。 GlobalAddAtom関数の呼び出しごとに、GlobalDeleteAtom関数の対応する呼び出しが必要です。




すべてがすぐに明らかになります。 アプリケーションが正しく終了しない場合、グローバルアトムは流れ去ります。 そして、猫は名前付き原子を泣きました:0xC000-0xFFFF、つまり16383のみです。 誤った完了時にVCLを使用してDelphiで記述された各dllおよび各exe-shnikは、リークしたグローバルアトムを残します。 より正確に言うと、インスタンスごとに2-3原子:

Controls.pasのControlAtomWindowAtom 、およびDialogs.pasの WndProcPtrAtom



回避策



作成された原子を見るのは難しくありません。 グローバル文字列アトムをリストする単純なアプリケーションのコードは次のとおりです。

 program EnumAtomsSample; {$APPTYPE CONSOLE} uses Windows, SysUtils; function GetAtomName(nAtom: TAtom): string; var n: Integer; tmpstr: array [0..255] of Char; begin n := GlobalGetAtomName(nAtom, PChar(@tmpstr[0]), 256); if n = 0 then Result := '' else Result := tmpstr; end; procedure EnumAtoms; var i: Integer; s: string; begin for i := MAXINTATOM to MAXWORD do begin s := GetAtomName(i); if (s <> '') then WriteLn(s); end; end; begin try EnumAtoms; ReadLn; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
      
      





VCLプロジェクトを開始し、タスクマネージャーを介して釘を打つことにより、アトムが確実に流れるようにすることができます。



原子はグローバルであるため、作成者に関係なくそれらを釘付けできます。 原子が漏れていることを判断することをどうにかして学ぶことは残っています。

原子の名前に注意を払えば、

WndProcPtrAtomはWndProcPtr [HInstance] [ThreadID]です

ControlAtomはControlOfs [HInstance] [ThreadID]です

WindowAtomはDelphi [ProcessID]です

すべての場合において、アトムは特定の接頭辞+ HEXの1つまたは2つの32ビット数によってDelphiによって作成される可能性が高いことを理解できます。 さらに、ProcessIDまたはThreadIDがHEXに書き込まれます。 システムにそのようなプロセスまたはスレッドがあるかどうかを簡単に確認できます。 そうでない場合は、明らかに漏えいした原子があり、それを解放するリスクがあります。 はい、はい、チャンスをつかみます。 実際、このIDを持つスレッド/プロセスが存在しないことを確認し、アトムを削除しようとすると、このプロセスはまったく同じIDで表示され、Delphiプロセスになることがあります。 検証と削除の間にこれが発生した場合、有効なアプリケーションからアトムを破ります。 チェックの間隔で、必ずDelphicプロセスを作成し、正確に同じIDを使用し、アトムを初期化する時間を確保する必要があるため、状況は非常にまれです。 この問題を解決するためのその他の回避策(VCLコードを編集せずに)は表示されません。



このような漏出したグローバルな原子をクリーニングするためのコンソールツールを作成しました。
このツールのコードは次のとおりです。
 program AtomCleaner; {$APPTYPE CONSOLE} uses Windows, SysUtils; const THREAD_QUERY_INFORMATION = $0040; function OpenThread(dwDesiredAccess: DWORD; bInheritHandle: BOOL; dwThreadId: DWORD): THandle; stdcall; external kernel32; function ThreadExists(const ThreadID: Cardinal): Boolean; var h: THandle; begin h := OpenThread(THREAD_QUERY_INFORMATION, False, ThreadID); if h = 0 then begin Result := False; end else begin Result := True; CloseHandle(h); end; end; function TryHexChar(c: Char; out b: Byte): Boolean; begin Result := True; case c of '0'..'9': b := Byte(c) - Byte('0'); 'a'..'f': b := (Byte(c) - Byte('a')) + 10; 'A'..'F': b := (Byte(c) - Byte('A')) + 10; else Result := False; end; end; function TryHexToInt(const s: string; out value: Cardinal): Boolean; var i: Integer; chval: Byte; begin Result := True; value := 0; for i := 1 to Length(s) do begin if not TryHexChar(s[i], chval) then begin Result := False; Exit; end; value := value shl 4; value := value + chval; end; end; function GetAtomName(nAtom: TAtom): string; var n: Integer; tmpstr: array [0..255] of Char; begin n := GlobalGetAtomName(nAtom, PChar(@tmpstr[0]), 256); if n = 0 then Result := '' else Result := tmpstr; end; function CloseAtom(nAtom: TAtom): Boolean; var n: Integer; s: string; begin Result := False; s := GetAtomName(nAtom); if s = '' then Exit; WriteLn('Closing atom: ', IntToHex(nAtom, 4), ' ', s); GlobalDeleteAtom(nAtom); Result := True; end; function ProcessAtom(nAtom: TAtom): Boolean; var s: string; n: Integer; id: Cardinal; begin Result := False; s := GetAtomName(nAtom); n := Pos('ControlOfs', s); if n = 1 then begin Delete(s, 1, Length('ControlOfs')); if Length(s) <> 16 then Exit; Delete(s, 1, 8); if not TryHexToInt(s, id) then Exit; if not ThreadExists(id) then Exit(CloseAtom(nAtom)); Exit; end; n := Pos('WndProcPtr', s); if n = 1 then begin Delete(s, 1, Length('WndProcPtr')); if Length(s) <> 16 then Exit; Delete(s, 1, 8); if not TryHexToInt(s, id) then Exit; if not ThreadExists(id) then Exit(CloseAtom(nAtom)); Exit; end; n := Pos('Delphi', s); if n = 1 then begin Delete(s, 1, Length('Delphi')); if Length(s) <> 8 then Exit; if not TryHexToInt(s, id) then Exit; if GetProcessVersion(id) = 0 then if GetLastError = ERROR_INVALID_PARAMETER then Exit(CloseAtom(nAtom)); Exit; end; end; procedure EnumAndCloseAtoms; var i: Integer; begin i := MAXINTATOM; while i <= MAXWORD do begin if not ProcessAtom(i) then Inc(i); end; end; begin try EnumAndCloseAtoms; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
      
      







実行するだけで、漏れた原子はきれいになります。 チェックしてください。たぶん今、あなたはすでにシステム内の原子を漏らしています。



結論として



コードを調べると、これらのグローバルアトムはSetPropおよびGetProp関数にのみ使用されていることがわかりました 。 Delphi開発者がアトムを使用することにした理由は完全に理解できません。 結局のところ、これらの関数はどちらも文字列へのポインターで正常に機能します。 アトムはそれで初期化されるため、それ自体にすでに存在する一意の文字列を送信するだけで十分です。

VCLコードでのこのような比較のロジックも理解できません。

GlobalFindAtom(PChar(ControlAtomString))= ControlAtomの場合

両方の変数は1か所で初期化されます。 文字列は一意になります(HInstanceとThreadIDから)。 検証は常にTrueを返します。 残念ながら、Delphiは現在、あらゆる種類のFMXの新機能を推進しています。 彼らがこのバグを修正することはまずありません。 個人的には、QCの修正方法を知りながらQCについて報告したくはありません。 しかし、どういうわけかあなたはそれと共に生きなければなりません。 ご希望の方は、アプリケーションの開始時に上記のツールのコードを実行できます。 私の意見では、これはリークした原子を待つよりもあらゆる点で優れています。

まあ、私たち自身の開発では、OSはリークを制御しないため、グローバルアトムを回避する必要があります。



ツール+ソース



All Articles