如何防止DEP杀死我的JITted异常处理程序?

时间:2014-02-13 14:16:43

标签: windows delphi compiler-construction exception-handling jit

我正在开发一个到目前为止似乎工作正常的JIT编译器,除了一个问题:当代码引发异常并且异常处理程序在JITted例程中时,操作系统会立即终止进程。当我关闭DEP时不会发生这种情况,所以我认为它与DEP有关。

当DEP关闭时,异常处理程序正确运行,我确保在JITted例程上调用VirtualProtect,保护值为PAGE_EXECUTE_READ,然后使用{{1}验证它}。

在调试器下对此进行测试会报告致命错误发生在引发异常的位置,而不是稍后,我认为这意味着发生了类似的事情:

  • 引发例外
  • SEH查找最近的异常处理程序
  • SEH看到最近的异常处理程序是在JITted代码中并立即吓坏了
  • Windows杀死任务

有没有人知道我可能做错了什么,以及如何让DEP接受我的异常处理程序?执行JITted代码本身没有任何问题。

编辑:这是生成存根的Delphi代码。它分配内存,加载基本代码,修复跳转修复和尝试块,然后将内存标记为可执行文件。这是DWS项目中外部函数JIT正在进行的工作的一部分。

VirtualQuery

重现问题:

  • 从SVN
  • 查看最新版本的DWS
  • 在\ test文件夹
  • 中构建LanguageTests.exe
  • 停用所有测试,然后在标题function MakeExecutable(const value: TBytes; const calls: TFunctionCallArray; call: pointer; const tryFrame: TTryFrame): pointer; var oldprotect: cardinal; lCall, lOffset: nativeInt; ptr: pointer; fixup: TFunctionCall; info: _MEMORY_BASIC_INFORMATION; begin result := VirtualAlloc(nil, length(value), MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE); system.Move(value[0], result^, length(value)); for fixup in calls do begin ptr := @PByte(result)[fixup.offset]; if fixup.call = 0 then lCall := nativeInt(call) else lCall := fixup.call; lOffset := (lCall - NativeInt(ptr)) - sizeof(pointer); PNativeInt(ptr)^ := lOffset; end; if tryFrame[0] <> 0 then begin ptr := @PByte(result)[tryFrame[0]]; if PPointer(ptr)^ <> nil then asm int 3 end; PPointer(ptr)^ := @PByte(result)[tryFrame[2] - 1]; ptr := @PByte(result)[tryFrame[1]]; if PPointer(ptr)^ <> nil then asm int 3 end; PPointer(ptr)^ := @PByte(result)[tryFrame[3]]; end; if not VirtualProtect(result, length(value), PAGE_EXECUTE_READ, oldProtect) then RaiseLastOSError; VirtualQuery(result, info, sizeof(info)); if info.Protect <> PAGE_EXECUTE_READ then raise Exception.Create('VirtualProtect failed'); end; 标题下的列表底部启用测试。
  • 运行测试仪。如果DEP关闭,它应该工作。如果DEP打开,它将按照描述崩溃。

编辑2:以下是生成的机器代码例程的转储:

dwsExternalFunctionTests

这被设计为与以下Delphi代码等效(如果不相同):

//preamble
02870000 55               push ebp
02870001 89E5             mov ebp,esp
02870003 83C4F4           add esp,-$0c
02870006 51               push ecx
02870007 53               push ebx
02870008 56               push esi
02870009 57               push edi
0287000A 8BDA             mov ebx,edx
0287000C 8B33             mov esi,[ebx]
0287000E 31C0             xor eax,eax
//setup exception frame
02870010 55               push ebp
02870011 685D008702       push $0287005d
02870016 64FF30           push dword ptr fs:[eax]
02870019 648920           mov fs:[eax],esp
//procedure body
0287001C 31C9             xor ecx,ecx
0287001E 894DF8           mov [ebp-$08],ecx
02870021 8B06             mov eax,[esi]
02870023 8B5308           mov edx,[ebx+$08]
02870026 8B38             mov edi,[eax]
02870028 FF5710           call dword ptr [edi+$10]
0287002B 8945FC           mov [ebp-$04],eax
0287002E 8B4604           mov eax,[esi+$04]
02870031 8B5308           mov edx,[ebx+$08]
02870034 8D4DF8           lea ecx,[ebp-$08]
02870037 8B38             mov edi,[eax]
02870039 FF571C           call dword ptr [edi+$1c]
//call to a native routine. This routine raises an exception
0287003C 8B55F8           mov edx,[ebp-$08]
0287003F 8B45FC           mov eax,[ebp-$04]
02870042 E8CD1FE6FD       call TestStringExc
//cleanup
02870047 31C0             xor eax,eax
02870049 5A               pop edx
0287004A 59               pop ecx
0287004B 59               pop ecx
//exception handler: a try/finally block to clean
//up a string variable used in the body of the code
0287004C 648910           mov fs:[eax],edx
0287004F 6864008702       push $02870064
02870054 8D45F8           lea eax,[ebp-$08]
02870057 E86870B9FD       call @UStrClr
0287005C C3               ret 
0287005D E98666B9FD       jmp @HandleFinally
02870062 EBF0             jmp $02870054
//more cleanup
02870064 5F               pop edi
02870065 5E               pop esi
02870066 5B               pop ebx
02870067 59               pop ecx
02870068 8BE5             mov esp,ebp
0287006A 5D               pop ebp
0287006B C3               ret 

TestStringExc例程的目的是引发异常并确保异常处理程序正确清理字符串。

2 个答案:

答案 0 :(得分:1)

以下代码可能会有所帮助(来自我自己的编译器的存根接口:

function GetExecutableMem(Size: Integer): Pointer;
  procedure RaiseOutofMemory;
  begin
    raise EOutOfResources.Create('UnitProxyGenerator.GetExecutableMem: Out of memory error.');
  end;
var
  LastCommitTop: PChar;
begin
  // We round the memory needed up to 16 bytes which seems to be a cache line amound on the P4.
  Size := (Size + $F) and (not $F);
  //
  Result := MemUsed;
  Inc(MemUsed, Size);
  // Do we need to commit some more memory?
  if MemUsed > MemCommitTop then begin
    // Do we need more mem than we reserved initially?
    if MemUsed > MemTop then RaiseOutOfMemory;
    // Try to commit the memory requested.
    LastCommitTop := MemCommitTop;
    MemCommitTop := PChar((Longword(MemUsed) + (SystemInfo.dwPageSize - 1)) and (not (SystemInfo.dwPageSize - 1)));
    if not Assigned(VirtualAlloc(LastCommitTop, MemCommitTop - LastCommitTop, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) then RaiseOutOfMemory;
  end;
end;

initialization
  GetSystemInfo(SystemInfo);
  MemBase := VirtualAlloc(nil, MemSize, MEM_RESERVE, PAGE_NOACCESS);
  if MemBase = nil then Halt; // VERY BAD ...
  MemUsed := MemBase;
  MemCommitTop := MemBase;
  MemTop := MemBase + MemSize;
finalization
  VirtualFree(MemBase, MemSize, MEM_DECOMMIT);
  VirtualFree(MemBase, 0, MEM_RELEASE);
end.

请注意VirtualAlloc调用中的PAGE_EXECUTE_READWRITE。

当进程运行DEP时,启用以下正确运行:

type
  TTestProc = procedure( out A: Integer ); stdcall;

procedure Encode( var P: PByte; Code: array of Byte ); overload;
var
  i: Integer;
begin
  for i := 0 to High( Code ) do begin
    P^ := Code[ i ];
    Inc( P );
  end;
end;

procedure Encode( var P: PByte; Code: Integer ); overload;
begin
  PInteger( P )^ := Code;
  Inc( P, sizeof( Integer ) );
end;

procedure Encode( var P: PByte; Code: Pointer ); overload;
begin
  PPointer( P )^ := Code;
  Inc( P, sizeof( Pointer ) );
end;

// returns address where exceptiuon handler will be.
function EncodeTry( var P: PByte ): PByte;
begin
  Encode( P, [ $33, $C0, $55,$68 ] );             // xor eax,eax; push ebp; push @handle
  Result := P;
  Encode( P, nil );
  Encode( P, [ $64, $FF, $30, $64, $89, $20 ] );  // push dword ptr fs:[eax]; mov fs:[eax],esp
end;

procedure EncodePopTry( var P: PByte );
begin
  Encode( P, [ $33, $C0, $5A, $59, $59, $64, $89, $10 ] );  // xor eax,eax; pop edx; pop ecx; pop ecx; mov fs:[eax],edx
end;

function Delta( P, Q: PByte ): Integer;
begin
  Result := Integer( P ) - Integer( Q );
end;

function GetHandleFinally(): pointer;
asm
  lea eax, system.@HandleFinally
end;

procedure TForm10.Button5Click( Sender: TObject );
var
  P, Q, R, S, T: PByte;
  A:             Integer;
begin
  P := VirtualAlloc( nil, $10000, MEM_RESERVE or MEM_COMMIT, PAGE_EXECUTE_READWRITE );
  if not Assigned( P ) then Exit;
  try

    // ------------------------------------------------------------------------
    // Equivalent
    //
    // A:=10;
    // try
    //   A:=20
    //   PInteger(nil)^:=20
    // finally
    //   A:=30;
    // end;
    // A:=40;
    //
    // ------------------------------------------------------------------------

    // Stack frame
    Q := P;
    Encode( Q, [ $55, $8B, $EC ] );                  // push ebp, mov ebp, esp

    // A := 10;
    Encode( Q, [ $8B, $45, $08, $C7, $00 ] );
    Encode( Q, 10 );                                 // mov eax,[ebp+$08], mov [eax],<int32>

    // try
    R := EncodeTry( Q );

    // TRY CODE !!!!
    // A := 20;
    Encode( Q, [ $8B, $45, $08, $C7, $00 ] );
    Encode( Q, 20 );                                 // mov eax,[ebp+$08], mov [eax],<int32>

    // REMOVE THIS AND NO EXCEPTION WILL OCCUR.
    Encode( Q, [ $33, $C0, $C7, $00 ] );             // EXCEPTION: xor eax, eax, mov [eax], 20
    Encode( Q, 20 );
    // END OF REMOVE

    // END OF TRY CODE


    EncodePopTry( Q );
    Encode( Q, [ $68 ] );                            // push @<afterfinally>
    S := Q;
    Encode( Q, nil );

    // FINALLY CODE!!!!
    T := Q;
    // A := 30;
    Encode( Q, [ $8B, $45, $08, $C7, $00 ] );
    Encode( Q, 30 );                                 // mov eax,[ebp+$08], mov [eax],<int32>

    // AFter finally
    Encode( Q, [ $C3 ] );                            // ret
    Encode( R, Q );                                  // Fixup try

    // SEH handler
    Encode( Q, [ $E9 ] );                            // jmp
    Encode( Q, Delta( GetHandleFinally(), Q ) - sizeof( Pointer ) ); // <diff:i32>
    Encode( Q, [ $E9 ] );                            // jmp
    Encode( Q, Delta( T, Q ) - sizeof( Pointer ) );  // <diff:i32>

    // After SEH frame
    Encode( S, Q );
    // A := 40;
    Encode( Q, [ $8B, $45, $08, $C7, $00 ] );
    Encode( Q, 40 );                             // mov eax,[ebp+$08], mov [eax],<int32>

    // pop stack frame
    Encode( Q, [ $5D, $C2, $04, $00 ] );         // pop ebp, ret 4

    // ------------------------------------------------------------------------

    // And.... execute
    A := 0;
    try
      TTestProc( P )( A );
    except
      ;
    end;
    Caption := IntToStr( A )+'!1';


    // Dofferent protection... execute
    VirtualProtect( P, $10000, PAGE_EXECUTE_READ, nil );

    A := 0;
    try
      TTestProc( P )( A );
    except
      ;
    end;
    Caption := IntToStr( A ) + '!2';

  finally
    // Cleanup
    VirtualFree( P, $10000, MEM_RELEASE );
  end;
end;

它适用于Windows 7,禁用DEP并启用DEP,似乎是一个带有Delphi try-finally块的“JIT代码”的最小部分。可能是因为不同/更新的Windows平台存在问题吗?

答案 1 :(得分:1)

我删除了我的其他帖子,并相信我意识到你的问题可能是什么。

问题在于ntdll.RtlIsValidHandler,它在根据SAFESEH调度异常时验证你的异常处理程序。

你需要通过注册一个向量异常处理程序来避免这种情况,并进行自己的异常调度,这样你根本不必担心这种行为。

编辑: 我相信你的根本问题是由于某种原因,DEP在内核的KPROCESS结构中设置了ExecuteDispatchEnable和ImageDispatchEnable,这就是为什么你开始遇到这个问题。也许可以通过调用NtSetInformationProcess来设置它们,但鉴于这没有正式记录,我无法很好地了解如何进行此调用。