심플스 - 프로그램과 책 이야기로 가득한 곳, (Simples.Kr)

  

SetEvent 함수는 언제 실패할까?

Windows Research 조회 수 15591 추천 수 0 2010.09.04 12:11:59
 
작업을 하다가 다음에 예제코드와 비슷한 코드를 작성하게 되었습니다.

thread1
SetEvent(.......); // 이벤트를 시그널해줍니다.

thread2
WaitForSingleObject(.....); // 시그널 될 때까지 기다립니다.

매우 전형적인 코드입니다.
thread2에서는 기다리고 thread1는 이벤트를 시그널해줍니다.

그러나 여기서 한가지 문제가 생긴 부분은 너무 극단적인 상황까지 고려하는게 아닌가 생각되지만
SetEvent 함수에 원형은 다음과 같습니다.

BOOL WINAPI SetEvent(
__in HANDLE hEvent
);

위에서 가장 중요한 부분은 리턴값이 BOOL이라는 것입니다.
즉, 이 함수는 실패할 수 있습니다.

실패할 수 있는 경우에 대해서 생각해보면 바로 떠오르것은 핸들을 잘 못 넘겼을 경우입니다.
하지만 그 외에 경우가 생긴다면 골치아파집니다.
특히나 소켓 프로그래밍이라면 상당히 짜증나겠죠.
예를 들어 메모리가 부족할 경우에 SetEvent가 실패해버리면 다른쪽에 스레드는 무한대기할지도 모릅니다.
물론 메모리가 부족할 경우라는 것은 만약에 그럴지도 모른다는 경우이고 MSDN에 경우에는
GetLastError를 통해서 리턴값이 FALSE일 경우에는 에러코드를 확인하라고 나왔으며 정작
어떠한 경우에 에러가 발생할 수 있는지는 적혀있지 않습니다.

하지만 우리는 최악에 상황까지 고려해야할 경우 저 함수가 어떠한 경우에 실패할 수 있는지에 대해서
알아보고 그에 대한 상황에 대처할 수 있어야 할것입니다.

그렇다면 저 함수가 어떠한 에러코드를 리턴하는지 알려면 결국엔 내부구현을 알아야 합니다.
따라서 해당 함수에 내부구현을 지금부터 따라가보겠습니다.

우선 SetEvent 함수는 Kernel32.dll에 있습니다.
다음과 같이 말입니다.

.text:7DD71653 ; BOOL __stdcall SetEventStub(HANDLE hEvent)
.text:7DD71653                 public _SetEventStub@4
.text:7DD71653 _SetEventStub@4 proc near
.text:7DD71653
.text:7DD71653 hEvent          = dword ptr  8
.text:7DD71653
.text:7DD71653                 mov     edi, edi
.text:7DD71655                 push    ebp
.text:7DD71656                 mov     ebp, esp
.text:7DD71658                 pop     ebp
.text:7DD71659                 jmp     short _SetEvent@4 ; SetEvent(x)
.text:7DD71659 ; ---------------------------------------------------------------------------
.text:7DD7165B                 align 10h
.text:7DD71660
.text:7DD71660 ; BOOL __stdcall SetEvent(HANDLE hEvent)
.text:7DD71660 _SetEvent@4:                            ; CODE XREF: SetEventStub(x)+6
.text:7DD71660                                         ; WerpRecoveryInProgress(x)+7D
.text:7DD71660                 jmp     ds:__imp__SetEvent@4 ; SetEvent(x)
.text:7DD71660 _SetEventStub@4 endp

익스포트 되어 있는 SetEvent 함수는 SetEventStub 함수를 호출하고 실제 내부에 _SetEvent@4
로 점프하게 됩니다.
그리고 __imp__SetEvent 를 참조하여 다시 어딘가로 점프하게 됩니다. ( 임포트 테이블 )

그 어딘가는 결국엔 Kernelbas.dll 에 있는 SetEvent 함수입니다.
다음과 같이 생겼습니다.

.text:7D86013D ; BOOL __stdcall SetEvent(HANDLE hEvent)
.text:7D86013D                 public _SetEvent@4
.text:7D86013D _SetEvent@4     proc near               ; CODE XREF: OutputDebugStringA(x)+1F7
.text:7D86013D
.text:7D86013D hEvent          = dword ptr  8
.text:7D86013D
.text:7D86013D                 mov     edi, edi
.text:7D86013F                 push    ebp
.text:7D860140                 mov     ebp, esp
.text:7D860142                 push    0
.text:7D860144                 push    [ebp+hEvent]
.text:7D860147                 call    ds:__imp__NtSetEvent@8 ; NtSetEvent(x,x)
.text:7D86014D                 test    eax, eax
.text:7D86014F                 jl      short loc_7D860156
.text:7D860151                 xor     eax, eax
.text:7D860153                 inc     eax
.text:7D860154                 jmp     short loc_7D86015E
.text:7D860156 ; ---------------------------------------------------------------------------
.text:7D860156
.text:7D860156 loc_7D860156:                           ; CODE XREF: SetEvent(x)+12
.text:7D860156                 push    eax
.text:7D860157                 call    _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
.text:7D86015C                 xor     eax, eax
.text:7D86015E
.text:7D86015E loc_7D86015E:                           ; CODE XREF: SetEvent(x)+17
.text:7D86015E                 pop     ebp
.text:7D86015F                 retn    4
.text:7D86015F _SetEvent@4     endp

디컴파일 하면 다음과 같습니다.
BOOL __stdcall SetEvent(HANDLE hEvent)
{
  int v1; // eax@1
  BOOL result; // eax@2

  v1 = NtSetEvent(hEvent, 0);
  if ( v1 < 0 )
  {
    BaseSetLastNTError(v1);
    result = 0;
  }
  else
  {
    result = 1;
  }
  return result;
}

NtSetEvent 함수를 호출하고 있고 값이 0보다 작으면 에러입니다.
당연히 NtSetEvent는 ntdll.dll에 있겠고 시스템 콜을 호출하게 될것입니다.

그리고 그 시스템콜은 ntoskrnl.exe에 있을것이고 이름도 NtSetEvent일것입니다.
그 구현코드는 다음과 같습니다. ( 여기서부터는 디컴파일한 결과만 보여드리겠습니다. )

NTSTATUS __stdcall NtSetEvent(HANDLE EventHandle, PULONG PreviousState)
{
  char v2; // al@1
  PULONG v3; // edi@1
  PULONG v4; // ecx@3
  LONG v5; // esi@7
  NTSTATUS v7; // eax@6
  int AccessMode; // [sp+14h] [bp-20h]@1
  CPPEH_RECORD ms_exc; // [sp+1Ch] [bp-18h]@1
  PVOID Event; // [sp+18h] [bp-1Ch]@6

  v2 = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  LOBYTE(AccessMode) = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  ms_exc.disabled = 0;
  v3 = PreviousState;
  if ( PreviousState )
  {
    if ( v2 )
    {
      v4 = PreviousState;
      if ( PreviousState >= MmUserProbeAddress )
        v4 = MmUserProbeAddress;
      *v4 = *v4;
    }
  }
  ms_exc.disabled = -2;
  v7 = ObReferenceObjectByHandle(EventHandle, 2u, ExEventObjectType, AccessMode, &Event, 0);
  PreviousState = (PULONG)v7;
  if ( v7 >= 0 )
  {
    v5 = KeSetEvent((PRKEVENT)Event, 1, 0);
    ObfDereferenceObject();
    if ( v3 )
    {
      if ( (_BYTE)AccessMode )
        *v3 = v5;
      else
        *v3 = v5;
    }
  }
  return (NTSTATUS)PreviousState;
}

NtSetEvent에 유출 된 Windows 2000 소스코드는 다음과 같습니다.
NTSTATUS
NtSetEvent (
    IN HANDLE EventHandle,
    OUT PLONG PreviousState OPTIONAL
    )

/*++

Routine Description:

    This function sets an event object to a Signaled state and attempts to
    satisfy as many waits as possible.

Arguments:

    EventHandle - Supplies a handle to an event object.

    PreviousState - Supplies an optional pointer to a variable that will
        receive the previous state of the event object.

Return Value:

    TBS

--*/

{

    PVOID Event;
    KPROCESSOR_MODE PreviousMode;
    LONG State;
    NTSTATUS Status;
#if DBG

    //
    // Sneaky trick here to catch sleazy apps (csrss) that erroneously call
    // NtSetEvent on an event that happens to be somebody else's
    // critical section. Only allow setting a protected handle if the low
    // bit of PreviousState is set.
    //
    OBJECT_HANDLE_INFORMATION HandleInfo;

#endif

    //
    // Establish an exception handler, probe the previous state address if
    // specified, reference the event object, and set the event object. If
    // the probe fails, then return the exception code as the service status.
    // Otherwise return the status value returned by the reference object by
    // handle routine.
    //

    try {

        //
        // Get previous processor mode and probe previous state address
        // if necessary.
        //

        PreviousMode = KeGetPreviousMode();
#if DBG
        if ((PreviousMode != KernelMode) &&
            (ARGUMENT_PRESENT(PreviousState)) &&
            (PreviousState != (PLONG)1)) {
            ProbeForWriteLong(PreviousState);
        }
#else
        if ((PreviousMode != KernelMode) && (ARGUMENT_PRESENT(PreviousState))) {
            ProbeForWriteLong(PreviousState);
        }
#endif

        //
        // Reference event object by handle.
        //

#if DBG
        Status = ObReferenceObjectByHandle(EventHandle,
                                           EVENT_MODIFY_STATE,
                                           ExEventObjectType,
                                           PreviousMode,
                                           &Event,
                                           &HandleInfo);
        if (NT_SUCCESS(Status)) {

            if ((HandleInfo.HandleAttributes & 1) &&
                (PreviousState != (PLONG)1)) {
#if 0
                //
                // This is a protected handle. If the low bit of PreviousState is NOT set,
                // break into the debugger
                //

                DbgPrint("NtSetEvent: Illegal call to NtSetEvent on a protected handle\n");
                DbgBreakPoint();
                PreviousState = NULL;
#endif
            }
        } else {
            if ((KeGetPreviousMode() != KernelMode) &&
                (EventHandle != NULL) &&
                ((NtGlobalFlag & FLG_ENABLE_CLOSE_EXCEPTIONS) ||
                 (PsGetCurrentProcess()->DebugPort != NULL))) {

                Status = KeRaiseUserException(STATUS_INVALID_HANDLE);

            }
        }
#else
        Status = ObReferenceObjectByHandle(EventHandle,
                                           EVENT_MODIFY_STATE,
                                           ExEventObjectType,
                                           PreviousMode,
                                           &Event,
                                           NULL);
#endif

        //
        // If the reference was successful, then set the event object to the
        // Signaled state, dereference event object, and write the previous
        // state value if specified. If the write of the previous state fails,
        // then do not report an error. When the caller attempts to access the
        // previous state value, an access violation will occur.
        //

        if (NT_SUCCESS(Status)) {
            State = KeSetEvent((PKEVENT)Event, ExpEventBoost, FALSE);
            ObDereferenceObject(Event);
            if (ARGUMENT_PRESENT(PreviousState)) {
                try {
                    *PreviousState = State;

                } except(ExSystemExceptionFilter()) {
                }
            }
        }

    //
    // If an exception occurs during the probe of the previous state, then
    // always handle the exception and return the exception code as the status
    // value.
    //

    } except(ExSystemExceptionFilter()) {
        return GetExceptionCode();
    }

    //
    // Return service status.
    //

    return Status;
}


거의 똑같다고 봐도 되겠습니다.
Windows 7에 있는코드인데 윈도우즈2000소스와 거의 똑같습니다.
여기서 PreviousState에 경우에는 인자를 NULL로 넘겼으니 상관이 없습니다.
체크할 필요가 없습니다.

가장 먼저 체크해야 할 부분은 ObReferenceObjectByHandle 함수입니다.
이 함수가 언제 실패하느냐 여부를 알아야 합니다.

WDK 문서를 보면 다음과 같은 에러코드가 정의되어 있습니다.

STATUS_SUCCESS <= 성공했습니다.

STATUS_OBJECT_TYPE_MISMATCH <= 핸들에 대한 타입이 잘 못 되었을 경우인데 API에서 해줌으로

이 에러가 날 경우는 없습니다.

STATUS_ACCESS_DENIED <= 접근거부에러인데 우리가 만든 핸들을 우리가 접근하므로 이 에러는 발생하지 않습니다.

STATUS_INVALID_HANDLE <= 핸들이 잘 못 된 경우입니다. 이 에러는 소프트웨어 로직상 핸들을 잘 못 넘겼을 경우에 발생하며

현재 우리가 알려고 하는것은 아주 특수한 경우에 발생할 수 있는 에러가 있는지 알기 위함이므로 무시합니다.


따라서 ObReferenceObjectByHandle함수는 핸들을 잘 못 넘기지 않는 이상은 현재로써 에러가 발생하지 않을것이고

핸들을 잘 못 넘기는건 프로그램의 로직상 문제이므로 무시합니다.


그 다음에 호출하는 함수는 KeSetEvent 함수입니다.

이 함수는 WDK에 문서에 보면 이 함수리턴값이 실패값이 들어간다는 말은 나와 있지 않습니다.

그렇다면 좀 더 확실하게 알기 위해서 KeSetEvent 함수 내부를 보아야 합니다.

내부 코드를 보기전에 PKEVENT는 다음과 같이 생겼습니다.


typedef struct _KEVENT {
    DISPATCHER_HEADER Header;
} KEVENT, *PKEVENT, *PRKEVENT;


typedef struct _DISPATCHER_HEADER {
    union {
        struct {
            UCHAR Type;                 // All (accessible via KOBJECT_TYPE)

            union {
                union {                 // Timer
                    UCHAR TimerControlFlags;
                    struct {
                        UCHAR Absolute              : 1;
                        UCHAR Coalescable           : 1;
                        UCHAR KeepShifting          : 1;    // Periodic timer
                        UCHAR EncodedTolerableDelay : 5;    // Periodic timer
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;

                UCHAR Abandoned;        // Queue
                BOOLEAN Signalling;     // Gate/Events
            } DUMMYUNIONNAME;

            union {
                union {
                    UCHAR ThreadControlFlags;  // Thread
                    struct {
                        UCHAR CpuThrottled      : 1;
                        UCHAR CycleProfiling    : 1;
                        UCHAR CounterProfiling  : 1;
                        UCHAR Reserved          : 5;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                UCHAR Hand;             // Timer
                UCHAR Size;             // All other objects
            } DUMMYUNIONNAME2;

            union {
                union {                 // Timer
                    UCHAR TimerMiscFlags;
                    struct {

#if !defined(_X86_)

                        UCHAR Index             : TIMER_EXPIRED_INDEX_BITS;

#else

                        UCHAR Index             : 1;
                        UCHAR Processor         : TIMER_PROCESSOR_INDEX_BITS;

#endif

                        UCHAR Inserted          : 1;
                        volatile UCHAR Expired  : 1;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                union {                 // Thread
                    BOOLEAN DebugActive;
                    struct {
                        BOOLEAN ActiveDR7       : 1;
                        BOOLEAN Instrumented    : 1;
                        BOOLEAN Reserved2       : 4;
                        BOOLEAN UmsScheduled    : 1;
                        BOOLEAN UmsPrimary      : 1;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                BOOLEAN DpcActive;      // Mutant
            } DUMMYUNIONNAME3;
        } DUMMYSTRUCTNAME;

        volatile LONG Lock;             // Interlocked
    } DUMMYUNIONNAME;

    LONG SignalState;                   // Object lock
    LIST_ENTRY WaitListHead;            // Object lock
} DISPATCHER_HEADER;


LONG __stdcall KeSetEvent(PRKEVENT Event, KPRIORITY Increment, BOOLEAN Wait)
{
  LONG result; // eax@2
  void *v5; // ebx@6
  int v6; // edi@6
  LONG v7; // edi@13
  signed int v14; // [sp+10h] [bp-8h]@4
  int v15; // [sp+14h] [bp-4h]@6

  _ESI = Event;
  if ( Event->Header.Type & 0x7F )
  {
    v14 = 0;
LABEL_6:
    v5 = v320;
    LOBYTE(v15) = KeRaiseIrqlToDpcLevel();
    v6 = 0;
    while ( 1 )
    {
      _EAX = _ESI;
      __asm { lock bts dword ptr [eax], 7 }
      if ( !_CF )
        break;
      do
      {
        ++v6;
        if ( v6 & HvlLongSpinCountMask || !(HvlEnlightenments & 0x40) )
          __asm { pause }
        else
          HvlNotifyLongSpinWait(v6);
      }
      while ( (char)*(_DWORD *)&_ESI->Header.Type < 0 );
    }
    v7 = _ESI->Header.SignalState;
    _ESI->Header.SignalState = 1;
    if ( !v7 )
    {
      if ( v14 == v7 )
        KiSignalSynchronizationObject(v5, _ESI);
      else
        KiSignalNotificationObject(v5);
    }
    _EAX = -129;
    __asm { lock and [esi], eax }
    KiExitDispatcher(v5, Wait, 1, Increment, v15);
    return v7;
  }
  result = 1;
  if ( Event->Header.SignalState != 1 || Wait )
  {
    v14 = 1;
    goto LABEL_6;
  }
  return result;
}


내부코드를 보면 Evente 객체에 SignalState 객체를 참고하거나 값을 변경할 뿐 에러를 리턴할 만한 부분은 없습니다.

여기까지 왔다면 이제 결론이 나옵니다.

SetEvent 함수를 사용 할 때 잘 못 된 핸들을 넘기지 않는 이상은 해당 리턴값이 FALSE를 리턴하지 않습니다.

따라서 시스템이 특수한 상황 ( 메모리 부족 ) 에 에러가 발생할 소지가 없으므로 SetEvent 함수는 무조건

성공한다고 가정합니다.

( 주의 : 예제코드에 한해서만 그렇다는 것이며 Event핸들은 여러 프로세스에 의해서 공유되는 코드로 작성하게

  된다면 접근거부 에러도 발생 할 수 있습니다. 그러나 이러한 부분은 프로그래머가 작성하게 되는 코드이므로

  마찬가지로 문제가 발생한다면 로직상에 문제이지 시스템에 특수한 상황에서 발생하는 에러는 아닙니다. )

List of Articles
번호 제목 글쓴이 날짜 조회 수
163 Linux Tip 리눅스 기반 오픈 소스 사이트 모음 삽질신 2009-11-16 70547
162 Linux Tip VirtualBox에 우분투 설치 후에 내부 네트워크 접속 설정하기 file esniper 2010-09-10 35150
161 Windows Research IE Cache 경로 변경하기 lain 2011-06-09 30648
160 Windows Research VMWare 탐지 기법 우회 - 2 [2] lain 2011-04-04 28624
159 Windows Research 화면캡쳐방지는 어떻게 구현될까? [3] lain 2010-09-04 28409
158 PHP Tip php 파싱 라이브러리 - snoopy esniper 2009-03-26 20570
157 Linux Tip Directory index forbidden by Options directive 삽질신 2009-11-16 20106
156 Linux Tip VirtualBox에서 오른쪽 CTRL키 사용하기(VitualBox 호스트키) file [2] esniper 2010-09-10 19529
155 Windows Tip SSH 터널링 - putty의 plink 사용예 esniper 2009-03-26 19423
154 문서자료 고품질의 무료 아이콘들 [3] esniper 2010-09-08 19300
153 문서자료 json 문법 체크 사이트 Lyn 2012-02-28 18735
152 PHP Tip mysql_insert_id() - 마지막 insert 된 아이디 값 esniper 2009-03-26 18267
151 Linux Tip 우분투 설치 후 putty에서 한글 안 깨지도록 설정하기 file esniper 2010-09-10 16997
150 Linux Tip 우분투 ssh or mysql 서버 접속지연이 있는 경우 해결책 esniper 2010-09-10 16365
149 Windows Research 커널레벨에서 IAT Hook lain 2011-06-11 16291
148 Linux Tip 우분투에 IRC서버 설치후 닉네임 길이와 동접자 조절. [1] 오랑캐꽃 2010-11-26 15918
147 Windows Research 유저 레벨에서 API Hook 1부 file [1] lain 2010-09-03 15674
» Windows Research SetEvent 함수는 언제 실패할까? lain 2010-09-04 15591
145 Windows Research Sysinternals - Filemon source code file [5] lain 2010-09-04 15332
144 Windows Research Sysinternals - Regmon source code file [1] lain 2010-09-04 14920

  • 이용약관
  • 개인정보취급방침
  • 사이트맵