挂钩线程创建/终止

时间:2010-09-27 08:43:08

标签: windows multithreading hook

是否可以挂钩到Windows上的线程终止? IOW,如果进程中的一个线程(对其他进程及其线程不感兴趣)已经终止(通常或者 - 更重要 - 强制),我希望得到通知。

或者,挂钩创建线程也可以。

基本原理:我有一个库,可以在每个线程的基础上管理一些信息(将其视为某些信息的进程范围的每线程缓存)。线程终止时,我必须从缓存中删除所有特定于线程的信息。 [缓存关联使用线程ID实现,可能会在将来的线程中重用。]

“正常”执行顺序没有问题,因为库用户将从库中分离当前线程以清除状态。如果有人杀死拥有缓存资源的线程,问题就会开始出现。

9 个答案:

答案 0 :(得分:6)

最好的方法是打电话 WaitForSingleObject与线程的HANDLE(使用线程id调用OpenThread来获取HANDLE)。

答案 1 :(得分:5)

如果您的程序在dll中,则可以设置为处理DllMain方法。当线程或进程开始/结束时调用它。

例如,

library MyDLL;

uses
   SysUtils, Windows;

procedure DllMain(reason: integer) ;
var
   dyingThreadId: Cardinal;
begin
   case reason of
     DLL_THREAD_DETACH:
     begin
          dyingThreadId := GetCurrentThreadId();
          // handle thread exit with thread id
     end;
   end;
end; 

begin
   DllProc := @DllMain;
end.

编辑:调用是在退出线程的上下文中进行的,因此您可以调用GetCurrentThreadId()来获取线程的ID。

答案 2 :(得分:5)

您可以使用Win32_ThreadStopTrace WMI事件来检测系统中任何线程的终止。

要开始监控此活动,您必须像这样写一个WQL句子

Select * from Win32_ThreadStopTrace Within 1 Where ProcessID=PID_Of_Your_App

检查此示例

uses
 Classes;

type
   TProcWmiEventThreadeCallBack = procedure(AObject: OleVariant) of object;
   TWmiEventThread    = class(TThread)
   private
     Success      : HResult;
     FSWbemLocator: OleVariant;
     FWMIService  : OleVariant;
     FEventSource : OleVariant;
     FWbemObject  : OleVariant;
     FCallBack    : TProcWmiEventThreadeCallBack;
     FWQL         : string;
     FServer      : string;
     FUser        : string;
     FPassword    : string;
     FNameSpace   : string;
     TimeoutMs    : Integer;
     procedure RunCallBack;
   public
     Constructor Create(CallBack : TProcWmiEventThreadeCallBack;const Server,User,PassWord,NameSpace,WQL:string;iTimeoutMs : Integer); overload;
     destructor Destroy; override;
     procedure Execute; override;
   end;

implementation

uses
 SysUtils,
 ComObj,
 Variants,
 ActiveX;

constructor TWmiEventThread.Create(CallBack : TProcWmiEventThreadeCallBack;const Server,User,PassWord,NameSpace,WQL:string;iTimeoutMs : Integer);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FCallBack       := CallBack;
  FWQL            := WQL;
  FServer         := Server;
  FUser           := User;
  FPassword       := PassWord;
  FNameSpace      := NameSpace;
  TimeoutMs       := iTimeoutMs;
end;

destructor TWmiEventThread.Destroy;
begin
  FSWbemLocator:=Unassigned;
  FWMIService  :=Unassigned;
  FEventSource :=Unassigned;
  FWbemObject  :=Unassigned;
  inherited;
end;


procedure TWmiEventThread.Execute;
const
  wbemErrTimedout = $80043001;
begin
  Success := CoInitialize(nil); //CoInitializeEx(nil, COINIT_MULTITHREADED);
  try
    FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
    FWMIService   := FSWbemLocator.ConnectServer(FServer, FNameSpace, FUser, FPassword);
    FEventSource  := FWMIService.ExecNotificationQuery(FWQL);
    while not Terminated do
    begin
      try
       FWbemObject := FEventSource.NextEvent(TimeoutMs); //set the max time to wait (ms)
      except
       on E:EOleException do
       if EOleException(E).ErrorCode=HRESULT(wbemErrTimedout) then //Check for the timeout exception   and ignore if exist
        FWbemObject:=Null
       else
       raise;
      end;

      if FindVarData(FWbemObject)^.VType <> varNull then
        Synchronize(RunCallBack);

      FWbemObject:=Unassigned;
    end;
  finally
    case Success of
      S_OK, S_FALSE: CoUninitialize;
    end;
  end;
end;

procedure TWmiEventThread.RunCallBack;
begin
  FCallBack(FWbemObject);
end;

现在要在您的应用中使用此线程,您必须以这种方式调用它

WmiThread:=TWmiEventThread.Create(
  Log,
  '.',
  '',
  '',
  'root\cimv2',
  Format('Select * from Win32_ThreadStopTrace Within 1 Where ProcessID=%d',[GetCurrentProcessId]),1);

并在回调函数中

procedure TForm1.Log(AObject: OleVariant);
begin        
    { 
      The OleVariant parameter has these properties
      uint32 ProcessID;
      uint8  SECURITY_DESCRIPTOR[];
      uint32 ThreadID;
      uint64 TIME_CREATED;         
    }
    //do your stuff here
    Memo1.Lines.Add(Format('Thread %s terminated ',[AObject.ThreadID]));
end;

答案 3 :(得分:4)

您可以使用类似Detours之类的内容来执行TerminateThread等Win32 API的API级挂钩。

但是,我不明白为什么你需要这样做。听起来你需要在线程死亡时清除线程的关联缓存,以便在另一个具有相同ID的线程出现时重新使用该槽。这是对的吗?

如果是这样,当您收到DllMain事件时,您是否只能清除DLL_THREAD_ATTACH中的缓存关联?这基本上是你的新线程通知。此时,您知道有一个新线程,因此清除现有的关联缓存是否安全?

可能有效的另一种选择是thread-local storage(TLS)。您可以使用TlsAlloc / TlsSetValue等Win32 API来存储特定于线程的信息。您还可以使用__declspec(thread)定义变量,以便编译器为您管理TLS。这样,每个线程都维护自己的缓存。每个线程的代码保持不变,但数据访问是相对于线程的。

答案 4 :(得分:3)

program ThreadExitHook;

{$APPTYPE CONSOLE}

uses
  Windows,
  Classes,
  madCodeHook;

type
  TLdrShutdownThread = procedure; stdcall;

var
  LdrShutdownThreadNext : TLdrShutdownThread;

procedure LdrShutdownThreadCallback; stdcall;
begin
  WriteLn('Thread terminating:', GetCurrentThreadId);
  LdrShutdownThreadNext;
end;

begin
  HookAPI('ntdll.dll', 'LdrShutdownThread', @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
  end).Start;

  ReadLn;

  UnhookAPI(@LdrShutdownThreadNext);
end.

这是一个不依赖于任何外部库的版本:

program Project7;

{$APPTYPE CONSOLE}

uses
  Windows,
  Classes;

{==============================================================================}
function IsWin9x: Boolean;
asm
  MOV     EAX, FS:[030H]
  TEST    EAX, EAX
  SETS    AL
end;
{------------------------------------------------------------------------------}
function CalcJump(Src, Dest: DWORD): DWORD;
begin
  if (Dest < Src) then begin
    Result := Src - Dest;
    Result := $FFFFFFFF - Result;
    Result := Result - 4;
  end else begin
    Result := Dest - Src;
    Result := Result - 5;
  end;
end;
{------------------------------------------------------------------------------}
function OpCodeLength(Address: DWORD): DWORD; cdecl; assembler;
const
  O_UNIQUE = 0;
  O_PREFIX = 1;
  O_IMM8 = 2;
  O_IMM16 = 3;
  O_IMM24 = 4;
  O_IMM32 = 5;
  O_IMM48 = 6;
  O_MODRM = 7;
  O_MODRM8 = 8;
  O_MODRM32 = 9;
  O_EXTENDED = 10;
  O_WEIRD = 11;
  O_ERROR = 12;
  asm
    pushad
    cld
    xor edx, edx
    mov esi, Address
    mov ebp, esp
    push    1097F71Ch
    push    0F71C6780h
    push    17389718h
    push    101CB718h
    push    17302C17h
    push    18173017h
    push    0F715F547h
    push    4C103748h
    push    272CE7F7h
    push    0F7AC6087h
    push    1C121C52h
    push    7C10871Ch
    push    201C701Ch
    push    4767602Bh
    push    20211011h
    push    40121625h
    push    82872022h
    push    47201220h
    push    13101419h
    push    18271013h
    push    28858260h
    push    15124045h
    push    5016A0C7h
    push    28191812h
    push    0F2401812h
    push    19154127h
    push    50F0F011h
    mov ecx, 15124710h
    push    ecx
    push    11151247h
    push    10111512h
    push    47101115h
    mov eax, 12472015h
    push    eax
    push    eax
    push    12471A10h
    add cl, 10h
    push    ecx
    sub cl, 20h
    push    ecx
    xor ecx, ecx
    dec ecx
  @@ps:
    inc  ecx
    mov  edi, esp
  @@go:
    lodsb
    mov  bh, al
  @@ft:
    mov  ah, [edi]
    inc  edi
    shr  ah, 4
    sub  al, ah
    jnc  @@ft
    mov al, [edi-1]
    and al, 0Fh
    cmp  al, O_ERROR
    jnz  @@i7
    pop edx
    not edx
  @@i7:
    inc edx
    cmp al, O_UNIQUE
    jz  @@t_exit
    cmp al, O_PREFIX
    jz  @@ps
    add  edi, 51h
    cmp  al, O_EXTENDED
    jz   @@go
    mov edi, [ebp+((1+8)*4)+4]
  @@i6:
    inc  edx
    cmp  al, O_IMM8
    jz   @@t_exit
    cmp  al, O_MODRM
    jz   @@t_modrm
    cmp  al, O_WEIRD
    jz   @@t_weird
  @@i5:
    inc  edx
    cmp  al, O_IMM16
    jz   @@t_exit
    cmp  al, O_MODRM8
    jz   @@t_modrm
  @@i4:
    inc  edx
    cmp  al, O_IMM24
    jz   @@t_exit
  @@i3:
    inc  edx
  @@i2:
    inc  edx
    pushad
    mov  al, 66h
    repnz scasb
    popad
    jnz  @@c32
  @@d2:
    dec  edx
    dec  edx
  @@c32:
    cmp  al, O_MODRM32
    jz   @@t_modrm
    sub  al, O_IMM32
    jz   @@t_imm32
  @@i1:
    inc  edx
  @@t_exit:
    jmp @@ASMEnded
  @@t_modrm:
    lodsb
    mov  ah, al
    shr  al, 7
    jb   @@prmk
    jz   @@prm
    add  dl, 4
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jnz  @@prm
  @@d3:  sub  dl, 3
    dec  al
  @@prmk:jnz  @@t_exit
    inc  edx
    inc  eax
  @@prm:
    and  ah, 00000111b
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jz   @@prm67chk
    cmp  ah, 04h
    jz   @@prmsib
    cmp  ah, 05h
    jnz  @@t_exit
  @@prm5chk:
    dec  al
    jz   @@t_exit
  @@i42: add  dl, 4
    jmp  @@t_exit
  @@prm67chk:
    cmp  ax, 0600h
    jnz  @@t_exit
    inc  edx
    jmp  @@i1
  @@prmsib:
    cmp  al, 00h
    jnz  @@i1
    lodsb
    and  al, 00000111b
    sub  al, 05h
    jnz  @@i1
    inc  edx
    jmp  @@i42
  @@t_weird:
    test byte ptr [esi], 00111000b
    jnz  @@t_modrm
    mov  al, O_MODRM8
    shr  bh, 1
    adc  al, 0
    jmp  @@i5
  @@t_imm32:
    sub  bh, 0A0h
    cmp  bh, 04h
    jae  @@d2
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jnz  @@chk66t
  @@d4:  dec  edx
    dec  edx
  @@chk66t:
    pushad
    mov  al, 66h
    repnz scasb
    popad
    jz   @@i1
    jnz  @@d2
  @@ASMEnded:
    mov esp, ebp
    mov [result+(9*4)], edx
    popad
end;
{------------------------------------------------------------------------------}
function ApiHook(ModName, ApiName: PChar; FuncAddr, HookedApi: Pointer; var MainApi: Pointer): Boolean;
var
  dwCount, Cnt, i, jmp: DWORD;
  P: Pointer;
  hMod, OldP, TMP: Cardinal;
begin
  Result := False;
  if IsWin9x then
    Exit;
  P := FuncAddr;
  if P = nil then begin
    hMod := GetModuleHandle(ModName);
    if hMod = 0 then
      hMod := LoadLibrary(ModName);
    P := GetProcAddress(hMod, ApiName);
  end;
  if (P = nil) or (HookedApi = nil) then
    Exit;
  if not VirtualProtect(P, $40, PAGE_EXECUTE_READWRITE, @OldP) then
    Exit;
  if ((Byte(P^) = $68) and (DWORD(Pointer(DWORD(P) + 1)^) = DWORD(HookedApi))) then
    Exit;
  MainApi := VirtualAlloc(nil, $1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if MainApi = nil then
    Exit;
  Cnt := 0;
  for dwCount := 0 to $3F do begin
    Inc(Cnt, OpCodeLength(DWORD(P) + Cnt));
    for i := 0 to Cnt - 1 do
      PByte(MainApi)[i] := PByte(P)[i];
    if Cnt > 5 then
      Break;
  end;
  PByte(MainApi)[Cnt] := $68;
  DWORD(Pointer(DWORD(MainApi) + Cnt + 1)^) := DWORD(P) + Cnt;
  PByte(MainApi)[Cnt + 5] := $C3;
  PByte(MainApi)[Cnt + 6] := $99;
  if (OpCodeLength(DWORD(MainApi)) = 5) and
    ((Byte(MainApi^) = $E8) or (Byte(MainApi^) = $E9)) then
  begin
    jmp := DWORD(P) + DWORD(Pointer(DWORD(MainApi) + 1)^) + 5;
    DWORD(Pointer(DWORD(MainApi) + 1)^) := CalcJump(DWORD(MainApi), jmp);
  end;
  PByte(P)[0] := $68;
  DWORD(Pointer(DWORD(P) + 1)^) := DWORD(HookedApi);
  PByte(P)[5] := $C3;
  VirtualProtect(P, $40, OldP, @TMP);
  Result := True;
end;
{------------------------------------------------------------------------------}
function ApiUnHook(ModName, ApiName: PChar; FuncAddr, HookedApi: Pointer; var MainApi: Pointer): Boolean;
var
  dwCount, Cnt, i, jmp: DWORD;
  P: Pointer;
  hMod, OldP, TMP: Cardinal;
begin
  Result := False;
  if IsWin9x then
    Exit;
  P := FuncAddr;
  if P = nil then begin
    hMod := GetModuleHandle(ModName);
    P := GetProcAddress(hMod, ApiName);
  end;
  if (P = nil) or (MainApi = nil) or (HookedApi = nil) then
    Exit;
  if not VirtualProtect(P, $40, PAGE_EXECUTE_READWRITE, @OldP) then
    Exit;
  if ((Byte(P^) <> $68) or (DWORD(Pointer(DWORD(P) + 1)^) <> DWORD(HookedApi))) then
    Exit;
  Cnt := 0;
  for dwCount := 0 to $3F do begin
    Inc(Cnt, OpCodeLength(DWORD(MainApi) + Cnt));
    if (Byte(Pointer(DWORD(MainApi) + Cnt)^) = $C3) and
      (Byte(Pointer(DWORD(MainApi) + Cnt + 1)^) = $99) then
      Break;
    for i := 0 to Cnt - 1 do
      PByte(P)[i] := PByte(MainApi)[i];
  end;
  if (OpCodeLength(DWORD(P)) = 5) and ((Byte(P^) = $E8) or (byte(P^) = $E9)) then begin
    jmp := DWORD(MainApi) + DWORD(Pointer(DWORD(MainApi) + 1)^) + 5;
    DWORD(Pointer(DWORD(P) + 1)^) := CalcJump(DWORD(P), jmp);
  end;
  VirtualProtect(P, $40, OldP, @TMP);
  VirtualFree(MainApi, 0, MEM_RELEASE);
  Result := True;
end;
{==============================================================================}

type
  TLdrShutdownThread = procedure; stdcall;

var
  LdrShutdownThreadNext : TLdrShutdownThread;

procedure LdrShutdownThreadCallback; stdcall;
begin
  WriteLn('Thread terminating:', GetCurrentThreadId);
  LdrShutdownThreadNext;
end;

begin
  ApiHook('ntdll.dll', 'LdrShutdownThread', nil, @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
    WriteLn('Waking up');
  end).Start;

  ReadLn;

  ApiUnHook('ntdll.dll', 'LdrShutdownThread', nil, @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
    WriteLn('Waking up');
  end).Start;

  ReadLn;
end.

答案 5 :(得分:1)

Chris提到DLL_THREAD_ATTACH给了我一个想法......

基本上,将缓存与线程ID相关联是一件坏事。我必须重新编写我的库,以便线程最初建立某种句柄,然后使用此句柄管理关联。

答案 6 :(得分:1)

我想如果你真的非常想要,你可以使用调试API(例如,WaitForDebugEventContinueDebugEvent),.当线程退出时,你会得到一个EXIT_THREAD_DEBUG_EVENT。

我不能说这是一种简单直接或干净的方式,但如果你不能提出任何其他的东西,它可能总比没有好。

答案 7 :(得分:0)

Boost提供boost::this_thread::at_thread_exit(),允许您提供在当前线程退出时运行的任意代码。如果你在每个线程上调用它,那么当它正常退出时,代码将会运行。如果使用TerminateThread强制终止某个线程,则该线程上不再运行任何代码,因此不会调用at_thread_exit函数。处理这种情况的唯一方法是挂钩TerminateThread,尽管这不一定能处理另一个进程终止你的线程的情况。

答案 8 :(得分:0)

可靠执行此操作的唯一方法是在DLL中挂钩DLL_THREAD_ATTACH和DLL_THREAD_DETACH。请参阅先前的讨论here