【问题标题】:How do I keep DEP from killing my JITted exception handler?如何防止 DEP 杀死我的 JITted 异常处理程序?
【发布时间】:2014-02-13 14:16:43
【问题描述】:

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

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

在调试器下测试报告说致命错误发生在引发异常的地方,而不是稍后,我认为这意味着正在发生这样的事情:

  • 引发异常
  • SEH 寻找最近的异常处理程序
  • SEH 发现最近的异常处理程序在 JITted 代码中并立即吓坏了
  • Windows 终止任务

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

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

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;

重现问题:

  • 从 SVN 查看最新版本的 DWS
  • 在 \test 文件夹中构建 LanguageTests.exe
  • 禁用所有测试,然后启用列表底部dwsExternalFunctionTests 标题下的测试。
  • 运行测试器。如果 DEP 关闭,它应该可以工作。如果 DEP 处于开启状态,它将按照说明崩溃。

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

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

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

function Stub(const args: TExprBaseListExec): Variant;
var
   list: PObjectTightList;
   a: integer;
   b: string;
   //use of a string variable will introduce an implicit try-finally
   //block by the compiler to handle cleanup
begin
   list := args.List;
   a := TExprBase(args[0]).EvalAsInteger(args.exec);
   TExprBase(args[1]).EvalAsString(args.exec, b);
   TestStringExc(a, b);
end;

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

【问题讨论】:

  • 我在 Detours 单元中遇到了类似的问题。我发现调用 VirtualProtect 是不够的。您还必须在此之前调用 VirtualAlloc。它还指出:“指定区域中的所有页面必须在使用 MEM_RESERVE 调用 VirtualAlloc 或 VirtualAllocEx 函数时分配的同一保留区域内”。但我承认,这可能不是因为对这个主题了解得不够多,无法成为任何专家。
  • @Runner: 有问题的内存已分配VirtualAlloc,标志MEM_RESERVE or MEM_COMMIT
  • 好吧,那不是。我用 MEM_COMMIT 和 PAGE_EXECUTE_READWRITE 标志调用它。如果不是,则检查内存是否在受保护区域中。在那之后我没有更多的建议:(
  • 代码是x86还是x64?
  • 完成。这是调用: VirtualAlloc(nil,TrampolineSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE);并且保护也被称为 PAGE_EXECUTE_READWRITE 我确信它们必须匹配

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


【解决方案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;

它可以在禁用和启用 DEP 的 Windows 7 上运行,并且似乎是一小段“JIT 代码”,其中包含 Delphi try-finally 块。会不会是不同/更新的 Windows 平台的问题?

【讨论】:

  • SEH 似乎在这里与 EXECUTE READWRITE 一起工作。我没有使用 EXECUTE_READ 进行测试。该代码用于 Win32 而不是 Win64(大约有 10 年的历史),SEH 在 WIn64 中的工作方式非常不同。
  • 不。将其从 PAGE_EXECUTE_READ 更改为 PAGE_EXECUTE_READWRITE 并不能解决问题。这是 Win32 代码。
  • 在 Windows 7 Pro、SP1 下运行更新后的示例,并激活 DEP,JITted 存根内的异常会终止进程。不知道你的系统和我的系统有什么区别,但它和我的代码有同样的问题。
  • @Mason:我刚刚发现了不同之处,这是我的错:我的应用程序中确实有一个不同的单元链接,它执行了 SetProcessDEPPolicy(0);。显然他们以这种方式“解决”了同样的问题。我现在看到,在调用中启用 DEP ( 1 ) 时问题仍然存在,而禁用 DEP ( 0 ) 时问题就消失了。这个问题肯定与启用了 DEP 的功能有关,很可能是 SafeSEH 之类的东西。
  • @MasonWheeler 您的问题在于 Ntdll.RtlIsValidHandler 在调度异常时验证异常处理程序图像。如果您使用我描述的方法并注册一个 Vectored Exception Handler,您可以自己进行异常调度并完全避免这个问题。
【解决方案2】:

我已经删除了我的其他帖子,并且相信我知道您的问题可能是什么。

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

您需要通过注册 Vectored Exception Handler 并进行自己的异常调度来避免这种情况,因此您根本不必担心这种行为。

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

【讨论】:

  • 谢谢,但未为此进程启用 SafeSEH。
  • 我能做的最好的就是将您推荐给 MSDN:msdn.microsoft.com/en-us/library/windows/desktop/…
  • 我希望我可以用完整的代码发布一个完整的答案,但我从来没有在 Delphi 中使用过 VEH 处理程序,而且我所描述的完整解决方案并不是少量的代码。跨度>
  • 是的,抱歉,我将尝试解释我过去如何在 C++ 中完成类似的操作。如果异常仅与一个函数有关,您可以使用函数指针和后面的函数指针来确定函数的大小,以便检查异常的 EIP。您还可以使用 goto 样式标签,您可以使用它来获取任何直接跟随指令的位置,从而为您的异常创建范围。基本上,如果 VEH 中异常的 EIP 是您选择的任何范围,则您会将异常传递给关联的处理程序。我希望这是有道理的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-02-22
  • 2013-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-08
  • 2011-02-16
相关资源
最近更新 更多