#1 2011-06-08 12:44:51

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,718
Website

Intercepting exceptions: a patch to rule them all

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.


One patch to rule them all

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 sad
// 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

Board footer

Powered by FluxBB