You are not logged in.
In order to let our TSynLog logging class intercept all exceptions, we use the low-level global RtlUnwindProc pointer, defined in System.pas.
Alas, under Delphi 5, this global RtlUnwindProc variable is not existing. The code calls directly the RtlUnWind Windows API function, with no hope of custom interception.
Two solutions could be envisaged:
- Modify the Sytem.pas source code, adding the new RtlUnwindProc variable, just like Delphi 7;
- Patch the assembler code, directly in the process memory.
The first solution is simple. Even if compiling System.pas is a bit more difficult than compiling other units, we already made that for our Enhanced RTL units. But you'll have to change the whole build chain in order to use your custom System.dcu instead of the default one. And some third-party units (only available in .dcu form) may not like the fast that the System.pas interface changed...
So we used the second solution: change the assembler code in the running process memory, to let call our RtlUnwindProc variable instead of the Windows API.
The first feature we have to do is to allow on-the-fly change of the assembler code of a process.
In fact, we already use this in order to provide class-level variables, as stated by SDD-DI-2.1.3.
We've got the PatchCodePtrUInt function at hand to change the address of each a RtlUnWind call.
We'll first define the missing global variable, available since Delphi 6, for the Delphi 5 compiler:
{$ifdef DELPHI5OROLDER}
// Delphi 5 doesn't define the needed RTLUnwindProc variable
// so we will patch the System.pas RTL in-place
var
RTLUnwindProc: Pointer;
The RtlUnwind API call we have to hook is defined as such in System.pas:
procedure RtlUnwind; external kernel name 'RtlUnwind';
0040115C FF255CC14100 jmp dword ptr [$0041c15c]
The $0041c15c is a pointer to the address of RtlUnWind in kernel32.dll, as retrieved during linking of this library to the main executable process.
The patch will consist in changing this asm call into this one:
0040115C FF25???????? jmp dword ptr [RTLUnwindProc]
Where ???????? is a pointer to the global RTLUnwindProc variable.
The problem is that we don't have any access to this procedure RtlUnwind declaration, since it was declared only in the implementation part of the System.pas unit. So it's address has been lost during the linking process.
So we will have to retrieve it from the code which in fact call this external procedure, i.e. from this assembler content:
procedure _HandleAnyException;
asm
(...)
004038B6 52 push edx // Save exception object
004038B7 51 push ecx // Save exception address
004038B8 8B542428 mov edx,[esp+$28]
004038BC 83480402 or dword ptr [eax+$04],$02
004038C0 56 push esi // Save handler entry
004038C1 6A00 push $00
004038C3 50 push eax
004038C4 68CF384000 push $004038cf // @@returnAddress
004038C9 52 push edx
004038CA E88DD8FFFF call RtlUnwind
So we will retrieve the RtlUnwind address from this very last line.
The E8 byte is in fact the opcode for the asm call instruction. Then the called function is stored as an integer offset, starting from the current pointing value.
The E8 8D D8 FF FF byte sequence is executed as "call the function available at the current execution address, plus integer($ffffd88de8). As you may have guessed, $004038CA+$ffffd88de8+5 points to the RtlUnwind definition.
So here is the main function of this patching:
procedure Patch(P: PAnsiChar);
var i: Integer;
addr: PAnsiChar;
begin
for i := 0 to 31 do
if (PCardinal(P)^=$6850006a) and // push 0; push eax; push @@returnAddress
(PWord(P+8)^=$E852) then begin // push edx; call RtlUnwind
inc(P,10); // go to call RtlUnwind address
if PInteger(P)^<0 then begin
addr := P+4+PInteger(P)^;
if PWord(addr)^=$25FF then begin // jmp dword ptr []
PatchCodePtrUInt(Pointer(addr+2),cardinal(@RTLUnwindProc));
exit;
end;
end;
end else
inc(P);
end;
We will cal this Patch subroutine from the following code:
procedure PatchCallRtlUnWind;
asm
mov eax,offset System.@HandleAnyException+200
call Patch
end;
You can note that we need to retrieve the _HandleAnyException address from asm code. In fact, the compiler do not make access from plain pascal code to the functions of System.pas which name begin with an underscore.
Then the following lines:
for i := 0 to 31 do
if (PCardinal(P)^=$6850006a) and // push 0; push eax; push @@returnAddress
(PWord(P+8)^=$E852) then begin // push edx; call RtlUnwind
will look for the expected opcode asm pattern in _HandleAnyException routine.
Then we will compute the position of the jmp dword ptr [] call, via this line:
addr := P+4+PInteger(P)^;
After checking that this is indeed a jmp dword ptr [] instruction (opcodes are FF 25), we will simply patch the absolute address with our RTLUnwindProc procedure variable.
With this code, each call to RtlUnwind in System.pas will indeed call the function set by RTLUnwindProc.
In our case, it will launch the following procedure:
procedure SynRtlUnwind(TargetFrame, TargetIp: pointer;
ExceptionRecord: PExceptionRecord; ReturnValue: Pointer); stdcall;
asm
pushad
cmp byte ptr SynLogExceptionEnabled,0
jz @oldproc
mov eax,TargetFrame
mov edx,ExceptionRecord
call LogExcept
@oldproc:
popad
pop ebp
{$ifdef DELPHI5OROLDER}
jmp RtlUnwind
{$else}
jmp oldUnWindProc
{$endif}
end;
This code will therefore:
- Save the current register context via pushad / popad opcodes pair;
- Check if TSynLog should intercept exceptions (i.e. if the global SynLogExceptionEnabled boolean is true);
- Call our logging function LogExcept;
- Call the default Windows RtlUnwind API, as expected by the Operating System.
Offline