작업을 하다가 다음에 예제코드와 비슷한 코드를 작성하게 되었습니다.
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 <= 접근거부에러인데 우리가 만든 핸들을 우리가 접근하므로 이 에러는 발생하지 않습니다.