2010-09-27 15 views
6

¿Es posible enganchar en la terminación del hilo en Windows? IOW, me gustaría recibir una notificación si un hilo dentro del proceso (que no está interesado en otros procesos y sus hilos) ha terminado (normalmente o, más importante, a la fuerza).Creación/finalización del hilo de enganche

De forma alternativa, también podría servir el enganche en la creación del hilo.

Justificación: Tengo una biblioteca que maneja cierta información en cada subproceso (piénselo como un caché por subproceso de todo el proceso para obtener cierta información). Cuando finaliza un hilo, tengo que eliminar toda la información específica del hilo del caché. [Las asociaciones de caché se implementan utilizando ID de subproceso que puede reutilizarse para futuros subprocesos.]

No hay problema con el orden de ejecución "normal" ya que el usuario de biblioteca separará el subproceso actual de la biblioteca y borrará el estado. Los problemas comienzan a aparecer si alguien mata el hilo que posee el recurso en caché.

Respuesta

4

Puede usar algo como Detours para hacer el enganche API de Win32 APIs como TerminateThread.

No estoy viendo por qué tienes que hacer esto, sin embargo. Parece que necesita borrar la caché asociada al hilo cuando el hilo muere para que pueda volver a usar esa ranura si aparece otro hilo con el mismo ID. ¿Es esto correcto?

Si es así, ¿no podría borrar la asociación de caché en DllMain cuando obtiene el evento DLL_THREAD_ATTACH? Esta es esencialmente tu nueva notificación de hilo. En este punto, sabes que tienes un hilo nuevo, ¿entonces no es seguro eliminar el caché asociado existente?

La otra alternativa que podría funcionar es thread-local storage (TLS). Puede utilizar las API de Win32 como TlsAlloc/TlsSetValue para almacenar información específica de subprocesos. También puede definir una variable con __declspec(thread) para que el compilador administre el TLS por usted. De esta forma, cada hilo mantiene su propio caché. El código sigue siendo el mismo para cada hilo, pero los accesos a los datos son relativos al hilo.

+1

DLL_THREAD_ATTACH funcionaría si la biblioteca en cuestión se implementó como una DLL pero no lo es. Enganchar TerminateThread es una posibilidad, de acuerdo. – gabr

1

mención DLL_THREAD_ATTACH Chris me dio una idea ...

Básicamente, la asociación de caché con identificador de hilo es una cosa mala. Tengo que volver a trabajar en mi biblioteca para que un subproceso establezca inicialmente algún tipo de manejador y luego administre asociaciones usando este manejador.

0

Boost proporciona boost::this_thread::at_thread_exit() que le permite proporcionar código arbitrario para ejecutar cuando sale el hilo actual. Si llama esto a cada hilo, cuando se cierre normalmente, se ejecutará el código. Si un hilo termina por la fuerza con TerminateThread, entonces no se ejecuta más código en ese hilo, por lo que no se invocan las funciones at_thread_exit. La única forma de manejar estos casos sería enganchar TerminateThread, aunque esto no necesariamente manejará el caso de que otro proceso finalice sus subprocesos.

0

La única manera de fiable hacer esto es en una DLL que engancha DLL_THREAD_ATTACH y DLL_THREAD_DETACH. Ver la discusión anterior here.

5

La mejor manera es llamar al WaitForSingleObject con la MANIJA del hilo (llame a OpenThread usando la identificación del hilo para obtener el MANGO).

+2

¡Muy buena idea! – gabr

+0

Muy interesante de hecho. ¿Tal evento se activará solo en la terminación del hilo, o en otras situaciones también (cualquiera que sea)? – rustyx

+0

Se disparará solo en la terminación del hilo –

1

Supongo que si realmente quieres bastante mal, puedes usar la API de depuración (por ejemplo, WaitForDebugEvent, ContinueDebugEvent),.Obtendrás un EXIT_THREAD_DEBUG_EVENT cuando salga un hilo.

No puedo decir que sea exactamente una manera directa o limpia de hacerlo, pero si no se te ocurre ninguna otra cosa, es mejor que nada.

5

Si su programa está en una dll, puede configurarlo para manejar el método DllMain. Esto se llama cuando un subproceso o proceso comienza/finaliza.

Por ejemplo,

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. 

EDIT: La llamada se realiza en el contexto de la rosca de salir, por lo que se puede llamar GetCurrentThreadId() para obtener el id del hilo.

+0

Lo siento, esto está mal. Solo se invoca cuando un proceso o subproceso * que carga el dll * se inicia o finaliza. A menos que cada subproceso en la aplicación cargue esta DLL, no se llamará (y si la DLL puede cargarse por cada subproceso, hay mejores formas de recibir una notificación cuando finalicen). –

+0

Ken, has malentendido. Esta es la API estándar de Windows: se llama al método cuando un hilo sale del proceso en el que se carga el dll. El dll se carga en el proceso, no en el hilo, de modo que una vez que el proceso carga el dll (por cualquier hilo), el dllMain se llama para cada hilo que sale. Consulte http://msdn.microsoft.com/en-us/library/ms682583(v=vs.85).aspx en particular, la parte DLL_THREAD_DETATCH. – mdma

+0

@mdma: No lo creo. Consulte la primera oración de su tema vinculado (** énfasis mío **): "Un punto de entrada opcional en una biblioteca de enlace dinámico (DLL). Cuando el sistema inicia o termina un proceso o hilo, llama al punto de entrada función para cada DLL cargada ** utilizando el primer subproceso del proceso **. " Tenga en cuenta el "primer hilo" en esa oración. No estoy seguro de cómo eso puede ser más claro, pero estaría dispuesto a admitir que me equivoqué si tiene algo que demuestre lo contrario ... –

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. 

Aquí es una versión que no depende de ninguna biblioteca externa:

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. 
+0

Se ve muy bien. ¿Alguna manera de hacerlo sin la biblioteca propietaria? –

+0

Sí, es un poco más complicado , pero factible. Básicamente necesita sobrescribir los primeros dos bytes de LdrShutdownThread con una instrucción de salto a su propio código, luego haga lo que quiera y finalmente haga lo que las instrucciones que sobreescribió lo hicieron y vuelva al LdrShutdownThread original después del instrucciones de salto. El problema es que no puedes cortar todas las instrucciones a la mitad, por lo que debes desmontar el código original de LdrShutdownThread para asegurarte de que copias una serie de instrucciones. ete instrucciones. Se inicia de la misma manera en todas las versiones del sistema operativo, esto se vuelve más fácil ... –

+0

una búsqueda rápida encuentra esto: https://sites.google.com/site/delphibasics/home/delphibasicssnippets/magicapihook después de un rápido vistazo al código, este va a hacer más de lo que necesita (está escrito para poder inyectar dll's y conectar api's en otros procesos) por lo que debería poder extraer la cantidad mínima de código que necesita para conectar la API en su propio proceso. –

5

Usted puede utilizar el evento Win32_ThreadStopTrace WMI para detectar la terminación de cualquier hilo en el sistema.

iniciar la supervisión de este evento debe escribir una frase WQL como esta comprobación

Select * from Win32_ThreadStopTrace Within 1 Where ProcessID=PID_Of_Your_App 

esta muestra

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; 

Ahora utilizar este hilo en su aplicación debe llamar de esta manera

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

y en la función de devolución de llamada

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; 
+0

+1 para otra respuesta de WMI de RRUZ. ¿Es WMI una solución razonable en este caso? ¿No es el techo demasiado rechoncho para un trabajo de tan bajo nivel? Nota: solo voy por el nombre, Windows Management Instrumentation, y no suena como algo para ser usado para liberar variables de subprocesos, por ejemplo. –

+0

@Cosmin, sé que no es muy común utilizar un objeto com para tareas de bajo nivel, como la supervisión de procesos y subprocesos. Por otro lado, el 'WMI' se conoce principalmente al recuperar información del sistema, como el hardware instalado, y piensa así, pero esto es solo una pequeña parte de su poder. personalmente, escribí muchas aplicaciones para monitorear sistemas remotos con éxito usando WMI. con el 'WMI' puede detectar el inicio de hilos, procesos, fallas de hardware, nuevo hardware conectado o desconectado, etc. en resumen como respuesta a su pregunta' ¿Es WMI una solución razonable en este caso? ', sí lo es. – RRUZ

Cuestiones relacionadas