Intercepting exceptions: a patch to rule them all
By A.Bouchez on 2011, Tuesday June 7, 22:45 - Pascal Programing - Permalink
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.passource code, adding the newRtlUnwindProcvariable, 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.
Patch a running process
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 another article of this Blog.
We've got the PatchCodePtrUInt function at hand to change the
address of each a RtlUnWind call.
One patch to rule them all
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 RtlUnwind
declaration, since it was declared only in the implementation part
of the System.pas unit. So its address has been lost during the
linking process. Requiescat In Pace.
So we will have to retrieve it from the code which in fact calls this external API, 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($ffffd88d)".
As you may have guessed, $004038CA+$ffffd88d+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 does not let access from plain
pascal code to the functions of System.pas having a name beginning
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 (expected 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 // hidden push ebp at asm level
{$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 set);
- In case of interception, call our logging function
LogExcept with the appropriate parameters;
- Call the default Windows RtlUnwind API, as expected by the
Operating System.
Comments are welcome on our forum, just as usual.