非常に有益なメッセージ、エラーの原因、場所、および解決方法がすぐにわかります。
しかし、冗談がなければ、それは何ですか?
もちろん、これは例外ですが、例外の種類もその説明も利用できません-「Runtime error 217」とアドレス、そして自分自身...
正直に言うと、私はこの例外について何とか考えていませんでした。なぜなら、 私のプロジェクトでは、ある日、一連のユーザー全員が217番目のエラーを正確に再現し始めるまで、これはまれな現象です。
しかし、それでも正しい道に行かず、プロジェクトに追加のログ記録レベルを追加しただけです。その結果に応じて、すぐに理由を見つけて修正しました。
しかし、実際には、私はちょうど時間を過ごしました...
そして、先日ビクターフェドレンコフが私に連絡せず、エラー番号217についての彼の考えについて話さなかったなら、彼は将来それを使ったでしょう。
理論と問題分析
理論がなければ、私たちはどこにもいません。
したがって、もちろん、理論的な部分から始めます。
そもそも、SysUtilsモジュールが初期化されていない場合にエラー217が表示されるという情報を得たAlexander Alekseevの記事「Error Handling-Chapter 1.2.2」の一部を読んだ後、原則としてエラーに関する考えを少し更新しました。これはアレクサンダーによって非常に明確に示されています。
フルサイズの画像を表示...
この図に基づいて、大まかな結論を下すことができます。SysUtilsが動作している間、すべての例外は通常の形式で表示する必要があります。
たとえば、実行時エラーに関するメッセージが表示された場合、上記の図から判断すると、フォーム上のイベントハンドラーでエラーが発生した可能性は低いです。 しかし、たとえば、何らかのファイナライズセクション(SysUtilsモジュールのファイナライズセクションの後に実行される)または割り当てられたExitProcessProcプロシージャで発生する可能性がはるかに高くなります。 しかし、もちろん、エラーの原因はどこにでもある可能性があります-前述のイベントハンドラーを含みます。
さて、チェックして、SysUtilsをUnit1モジュールよりも後にファイナライズするコードを書いてください。Unit1モジュールでは、人為的に例外を生成します。
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} initialization finalization raise Exception.Create('finalization exception'); end.
ビルド、実行、フォームを閉じて...ランタイムエラー217。
SysUtilsのファイナライズ後に217が表示されるという主張は完全に正しいですが、ファイナライズコード自体を見てみましょう。
procedure FinalizeUnits; ... begin ... Count := InitContext.InitCount; Table := InitContext.InitTable^.UnitInfo; ... try while Count > 0 do begin Dec(Count); InitContext.InitCount := Count; P := Table^[Count].FInit; if Assigned(P) then ... TProc(P)(); ... end; end; except FinalizeUnits; { try to finalize the others } raise; end; end;
何が起こるかを見てください:FinalizeUnitsプロシージャでは、すべてのファイナライズプロシージャが呼び出され、そのアドレスは初期化された順序でInitContext.InitTable ^ .UnitInfo配列にあります。 一番最初のものは配列の先頭にあります(そして最終化は末尾からです)。
SysUtils + Systemは一番下のどこかにありますが、Unit1モジュールでは私たちは一番上のどこかにいます。
しかし、私たちのモジュールで突然例外が発生して「広範」になり、カタルシスの順序に違反します。
「広範な」FinalizeUnitsが再び呼び出され、例外を引き起こしたモジュールをスキップし、その結果、SysUtilsおよび途中で遭遇したさまざまなクラスデストラクタが破棄されると、メモリマネージャを備えたシステムがヒープにクラッシュします(リストの先頭にあるものの1つが座った後)額にコントロールショットがあります-RAISE、ここで出航しました-こんにちは217。
しかし、モジュールの初期化セクションで例外が発生するとどうなりますか?
はい、すべて同じです:
procedure InitUnits; ... begin ... try ... except FinalizeUnits; raise; end; end;
結論:初期化またはファイナライズのセクションで未処理の例外があると、例外の説明が失われ、エラー217が発生します。
これで理論は、私たちは終了すると思います。
ランタイムエラー217の発生理由を理解しながら、例外メッセージのより馴染みのあるバージョンを手に入れようとします。
モジュールのファイナライズを無効にする
議論の最初に、ビクターはこのエラーを回避するためのかなり効果的な方法を提案しました。
彼の分析は次のとおりです。例外ハンドラーの一般的な初期化はSysUtilsモジュールのInitExceptionsプロシージャで実行され、ファイナライズはDoneExceptionsを呼び出すことで実行されます。
DoneExceptions plus呼び出しを何らかの方法で無効にし、System finalizationブロックへの呼び出しをブロックすることでメモリマネージャーが故障するのを防ぐ場合、受け入れ可能な形式で例外メッセージを受け取ります。
解決策として、次のコードが提案されました。これは最初のモジュールでプロジェクトファイルに接続する必要があります(D2005以降で動作します)。
unit suShowExceptionsInInitializeSections; interface uses SysUtils; implementation uses Windows; // PackageInfo // System InitTable, function GetInitTable: PackageInfo; var Lib: PLibModule; TypeInfo: PPackageTypeInfo; begin Result := nil; Lib := LibModuleList; if not Assigned(Lib) then Exit; // (BPL ), , // / BPL, // if Assigned(Lib^.Next) then Exit; Typeinfo := Lib^.TypeInfo; if Assigned(TypeInfo) then begin // TPackageTypeInfo // PackageInfo // . // IDA , TypeInfo //PackageInfo // PackageInfo //TypeInfo Result := PackageInfo(PByte(TypeInfo) - (LongWord(@PackageInfoTable(nil^).TypeInfo))); end; end; // procedure DisableAllFinalization; var Loop: Integer; OldProtect: LongWord; InitTable: PackageInfo; Table: PUnitEntryTable; begin InitTable := GetInitTable; if Assigned(InitTable) then begin Table := InitTable^.UnitInfo; if Assigned(Table) then // / if VirtualProtect(Table, SizeOf(PackageUnitEntry) * InitTable^.UnitCount, PAGE_READWRITE, OldProtect) then for Loop := 0 to InitTable^.UnitCount - 1 do Table^[Loop].FInit := nil; end; end; initialization finalization // , SysUtils , // . , // ( System), // . // //1. Exception - //2. // // , , // . // , DLL , // if IsLibrary then Exit; // // RTTI. , // , //. DPR , // . // , // . // DisableAllFinalization; end.
正直に言うと、彼は立ちながら拍手を送った。
ここにあります: 最も汚い形式のハック -この脅威が実際に理解している人だけがそのようなことを行うことができます:)
そして、このモジュールは約3時間IT部門の仕事をもたらしました-大変な議論でした:)
しかし、ところで、このコードのロジックを分析しましょう。
その本質は簡単です。Delphiアプリケーションが理解できる形式で、ロードされたモジュール(BPLを含む)に関するデータにアクセスする必要があります。 これは、TLibModule構造体の単方向リストの最上部にアクセスすることにより行われました。 リストの最初の要素は、現在のイメージを記述する構造です。UnitInfo構造に関するデータを取得する必要がある場所から、初期化されたモジュールの数と、PackageUnitEntryレコードの形式での初期化および終了手順のアドレスに関するデータが含まれます。
PackageUnitEntryエントリごとにFInitパラメーターをnilに設定すると、モジュールのファイナライズがブロックされます。
このパラメーターが無効化されると、FinalizeUnitsはハンドラーを呼び出すことができなくなり、その結果、上で書いたのと同じ発生が発生した例外を正しく表示できます。
しかし、その後、すべてがより複雑になります。
よく考えてみる
アイデアは健全で、理由は明らかですが、リソースが解放されず、FastMemは正常に動作しなくなり(ファイナライズ中にリークを収集します)、互換性は十分ではありません。 7、このコードはまったく機能しません。
IT部門での最初の1時間の議論の後、次の結論に達することができました。
そして、彼らは再び議論を始めました-まあ、このアプローチは私たちに合っていませんでした、すべてがうまくいくように見えますが、どういうわけかうまくいきません。
ファイナライズブロックを例外デストラクタのヒープに直接接続するオプションも考慮されましたが、既存のハックに対する追加のハックは誰にも適合しませんでした。
そして、ここで、デバッガーに座って70回目のコードの実行について考えました。
これは...しかし、発生した例外に関するメッセージはどうですか?
そして、ExceptHandlerに制御を移すことで表示されます。コードには秘密はありません。
そして、モジュールのファイナライズを削除することで何をしますか
そうです、彼に同じことをさせます。
ExceptHandler呼び出しをエミュレートしてみましょう。
最初にテストユニットを記述し、プロジェクトに接続します。
unit Test; interface uses SysUtils; var E: Exception; implementation initialization finalization E := AcquireExceptionObject; if E <> nil then begin ShowException(E, ExceptAddr); E.Free; Halt(1); end; end.
起動して...
わかった。
ファイナライズサイクルに統合して、発生した例外を表示し、Halt(1)を呼び出してさらにファイナライズを続けました。
その結果、問題は解決され、有能で文書化され、Delphi 7と互換性がありますが...
しかし、アイデアを開発するのではありませんか?
「誘導エラー」などがあります。 エラーも発生したために発生したエラー。
たとえば、特定のクラスのインスタンスを返す関数Aと、このインスタンスを作業で使用する関数Bなどです。 たとえば、未処理の例外が関数Aで発生し(ファイルへのアクセスがないなど)、クラスを作成しなかった後、どこかでアプリケーションコードを使用して、プロシージャBがこのインスタンスにアクセスし、その結果、アクセス違反が発生します。
初期化/ファイナライズ手順でも同じことが起こります。ファイナライズで発生した例外により、理由が隠されます。
デモンストレーションのために、アプリケーションの初期化時に特定のロガーが作成され、アプリケーションのステージが書き込まれ、最終段階で作業の完了の記録があるようなコードを記述します。
例外を発生させるには、存在しないパスに沿ってロガーを作成します。
uses Classes; var Logger: TFileStream; const StartLog: AnsiString = ' ' + sLineBreak; EndLog: AnsiString = ' ' + sLineBreak; implementation initialization Logger := TFileStream.Create('A:\MyLog,txt', fmCreate); Logger.WriteBuffer(StartLog[1], Length(StartLog)); finalization Logger.WriteBuffer(EndLog[1], Length(EndLog)); Logger.Free; end.
システムにドライブ「A」を持っている人はほとんどいないので、このコードの結果は「ランタイムエラー216」(つまり、217ではなく216)になるか、前の章のコードを接続すると:
001B1593のモジュールProject2.exeの例外EAccessViolation。
モジュール 'Project2.exe'のアドレス005B1593でのアクセス違反。 アドレス00000000の読み取り。
しかし、その理由は最初の例外にあり、これは表示していません。エラーの原因を一瞬で把握することはできません。
この不正を修正するために、コードを少しとかしてこの状態にすることができます:
unit ShowExceptSample; interface uses SysUtils, Classes; implementation type PRaiseFrame = ^TRaiseFrame; TRaiseFrame = packed record NextRaise: PRaiseFrame; ExceptAddr: Pointer; ExceptObject: TObject; ExceptionRecord: PExceptionRecord; end; var // CurrentRaiseList: Pointer = nil; // function GetNextException: Pointer; begin if CurrentRaiseList = nil then CurrentRaiseList := RaiseList; if CurrentRaiseList <> nil then begin Result := PRaiseFrame(CurrentRaiseList)^.ExceptObject; PRaiseFrame(CurrentRaiseList)^.ExceptObject := nil; CurrentRaiseList := PRaiseFrame(CurrentRaiseList)^.NextRaise; end else Result := nil; end; var ExceptionStack: TList; E: Exception; initialization finalization // , ? E := GetNextException; if E <> nil then begin ExceptionStack := TList.Create; try // , while E <> nil do begin ExceptionStack.Add(E); E := GetNextException; end; // , while ExceptionStack.Count > 0 do begin E := ExceptionStack[ExceptionStack.Count - 1]; ExceptionStack.Delete(ExceptionStack.Count - 1); ShowException(E, ExceptAddr); E.Free; end; finally ExceptionStack.Free; end; // Halt(1); end; end.
ここでの考え方は単純です。GetNextException関数は基本的にAcquireExceptionObject呼び出しを繰り返しますが、呼び出しの後、キュー内の次の例外へのリンクは失われず、外部変数の次のフレームのアドレスを記憶します。
その後、すべての例外がリストに入力され(最後の例外がリストの最初になります)、優先度の順にプログラマーに表示されます。その結果、この最初のことが発生したことがすぐにわかります。
そして、彼があらゆる種類のAVに行った後にのみ。
次に、残りのエラーコードについて説明します。
「ランタイムエラー217」で始まったのはなぜですか?
さて、これは最も簡単に再現可能で、技術的には上記のモジュールを使用しているため、考えられるすべてのランタイムエラーの完全に通常の説明が得られます。
reMap: array [TRunTimeError] of Byte = ( 0, { reNone } 203, { reOutOfMemory } 204, { reInvalidPtr } 200, { reDivByZero } 201, { reRangeError } { 210 Abstract error } 215, { reIntOverflow } 207, { reInvalidOp } 200, { reZeroDivide } 205, { reOverflow } 206, { reUnderflow } 219, { reInvalidCast } 216, { reAccessViolation } 218, { rePrivInstruction } 217, { reControlBreak } 202, { reStackOverflow } 220, { reVarTypeCast } 221, { reVarInvalidOp } 222, { reVarDispatch } 223, { reVarArrayCreate } 224, { reVarNotArray } 225, { reVarArrayBounds } { 226 Thread init failure } 227, { reAssertionFailed } 0, { reExternalException not used here; in SysUtils } 228, { reIntfCastError } 229, { reSafeCallError } 235, { reMonitorNotLocked } 236 { reNoMonitorSupport } {$IFDEF PC_MAPPED_EXCEPTIONS} { 230 Reserved by the compiler for unhandled exceptions } {$ENDIF PC_MAPPED_EXCEPTIONS} {$IF defined(PC_MAPPED_EXCEPTIONS) or defined(STACK_BASED_EXCEPTIONS)} { 231 Too many nested exceptions } {$ENDIF} {$IF Defined(LINUX) or Defined(MACOS)} { 232 Fatal signal raised on a non-Delphi thread } , 233 { reQuit } {$ENDIF LINUX or MACOS} {$IFDEF POSIX} , 234 { reCodesetConversion } {$ENDIF POSIX} , 237, { rePlatformNotImplemented } 238 { reObjectDisposed } );
まとめ
このような不注意なコードを使用すると、コード217のエラーが説明したくない内容を取得できます。
ただし、このアプローチは経験豊富なプログラマには馴染みがないとは思いません。
ほとんどの場合、これはハローバイクです。この問題は既に誰かによって既に解決されている可能性が高いのですが、私はこの解決策について知りませんでした。
そうでない場合は、2番目になります。
この記事の共著者および首謀者に対する別の敬意は、ヴィクトル・フェドレンコフです。
がんばって。