윈도우 커널 모드 포팅의 정석 6편 - 동기화 함수 구현

저작권 안내
  • 책 또는 웹사이트의 내용을 복제하여 다른 곳에 게시하는 것을 금지합니다.
  • 책 또는 웹사이트의 내용을 발췌, 요약하여 강의 자료, 발표 자료, 블로그 포스팅 등으로 만드는 것을 금지합니다.

커널 모드 드라이버에서 큰 비중을 차지하는 부분은 아무래도 동기화 객체 부분일 것입니다.

유저 모드에서 사용할 수 있는 동기화 객체는 커널 오브젝트인 것도 있고 아닌 것도 있습니다. 커널 오브젝트로 존재하는 동기화 객체는 커널 모드 함수를 사용하여 구현을 하고, 그렇지 않은 경우 대체할 수 있는 함수를 사용하여 구현합니다.

하지만 어떻게 해서든 유저 모드와 동일하게 구현하기 어려운 경우가 있습니다. 이 때에는 불가피하게 원본 소스 코드를 수정하여, 커널 모드에 적합하게 동기화 객체를 사용해야 합니다.

CreateMutex
HANDLE
CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner,
    LPCWSTR lpName)
{
    LARGE_INTEGER timeOut = {0, };
    PRKMUTEX mutex = ExAllocatePoolWithTag(NonPagedPool, sizeof(KMUTEX), 'xtum');

    KeInitializeMutex(mutex, 0);

    if (bInitialOwner == TRUE)
        KeWaitForSingleObject(mutex, Executive, KernelMode, FALSE, &timeOut);

    return (HANDLE)mutex;
}

뮤텍스는 커널 모드 동기화 객체입니다. 따라서 커널 모드 함수를 그대로 사용합니다. 리턴하는 변수는 실제 핸들 값이 아니라 뮤텍스 오브젝트라는 점을 주의합니다. 즉 원본 소스코드 수정을 최소화 하기 위해서입니다.

단 뮤텍스를 사용하면서 이름을 만들어 서로 구분해주었다면, 복잡하게 CreateMutex 함수를 구현할 필요 없이 원본 소스 코드를 수정하여 상황에 맞게 커널 모드 동기화 함수를 직접 사용해야 합니다.

CreateSemaphore
HANDLE
CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCSTR lpName)
{
    PRKSEMAPHORE semaphore = 
        ExAllocatePoolWithTag(NonPagedPool, sizeof(KSEMAPHORE), 'ames');

    KeInitializeSemaphore(semaphore, lInitialCount, lMaximumCount);

    return (HANDLE)semaphore;
}

세마포어 또한 커널 모드 동기화 객체이므로 동일한 방법으로 구현합니다. 단 이름을 만들어야 할 경우 뮤텍스와 마찬가지로 원본 소스 코드를 수정합니다.

CloseHandle
BOOL
CloseHandle(
    HANDLE hObject)
{
    if (MmIsAddressValid(hObject) == TRUE)
        ExFreePool(hObject);
    else
        ZwClose(hObject);

    return TRUE;
}

앞서 CreatreMutex에서 뮤텍스 오브젝트를 위해 동적으로 메모리를 할당했습니다. 그러므로 메모리를 해제해주어야 하는데, 기존에 ZwCreateFile과 같은 함수를 이용하여 커널 핸들을 만들었을 수 있습니다. 이런 경우를 대비하여 커널 모드 핸들이면 ZwClose 함수로 핸들을 닫아주고, 메모리 주소라면 해제 해주도록 합니다. ObIsKernelHandle이라는 유용한 함수가 있지만 안타깝게도 비스타 부터 사용이 가능합니다. 아쉽지만 MmIsAddressValid 함수를 이용하여 메모리 주소인지 판단합니다. 핸들이 아무리 숫자가 크더라도 메모리로 판단될 일은 없습니다.

WaitForSingleObject
DWORD
WaitForSingleObject(
    HANDLE hHandle,
    DWORD dwMilliseconds)
{
    NTSTATUS status;
    LARGE_INTEGER timeOut;
    LONG milliseconds = dwMilliseconds;
    DWORD ret;

    timeOut.QuadPart = -(milliseconds * 10000);

    if (dwMilliseconds == INFINITE)
        status = KeWaitForSingleObject(hHandle, Executive, KernelMode, FALSE, NULL);
    else
        status = KeWaitForSingleObject(hHandle, Executive, KernelMode, FALSE, &timeOut);

    switch (status)
    {
    case STATUS_SUCCESS:
        ret = WAIT_OBJECT_0;
        break;
    case STATUS_TIMEOUT:
        ret = WAIT_TIMEOUT;
        break;
    case STATUS_ABANDONED_WAIT_0:
        ret = WAIT_ABANDONED;
        break;
    default:
        ret = WAIT_FAILED;
    }

    return 0;
}

대기 함수입니다. WaitForSingleObject의 동작을 흉내내기 위해 리턴값도 유저 모드와 동일한 형태로 만들어줍니다.

여기서 주의해야 할 점은 Timeout 값 입니다. 유저 모드에서는 이 값을 밀리초 단위로 사용하지만, 커널 모드에서는 100 나노초 단위로 사용합니다. 따라서 밀리초를 100나노초 단위로 변환해주어야 합니다. 또한 유저 모드에서는 상대 시간만 사용하지만, 커널 모드에서는 절대 시간과 상대 시간을 모두 사용합니다. Timeout 값이 양수일 때에는 절대 시간이며 음수 일 때 상대 시간입니다. 그러므로 밀리초에 10000을 곱하여 100나노초 단위로 만들어준 다음, 음수로 변환하여 상대 시간으로 만들어줍니다.

Critical Section
typedef struct _CRITICAL_SECTION {

    ERESOURCE Resource;

} CRITICAL_SECTION, *LPCRITICAL_SECTION;

VOID
InitializeCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection)
{
    ExInitializeResourceLite(&lpCriticalSection->Resource);
}

VOID
EnterCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection)
{
    KeEnterCriticalRegion();
    ExAcquireResourceExclusiveLite(&lpCriticalSection->Resource, TRUE);
}

VOID
LeaveCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection)
{
    ExReleaseResourceLite(&lpCriticalSection->Resource);
    KeLeaveCriticalRegion();
}

VOID
DeleteCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection)
{
    ExDeleteResourceLite(&lpCriticalSection->Resource);
}

Critical Section은 유저 모드 동기화 객체입니다. 따라서 커널 모드에는 대응되는 것이 없습니다. 이 Critical Section은 Critical Region과 리소스를 이용하여 구현합니다.

Event
HANDLE
CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,
    BOOL bManualReset,
    BOOL bInitialState,
    LPCSTR lpName)
{
    EVENT_TYPE eventType;
    PRKEVENT event = ExAllocatePoolWithTag(NonPagedPool, sizeof(KEVENT), 'tnve');

    if (bManualReset == TRUE)
        eventType = NotificationEvent;
    else
        eventType = SynchronizationEvent;

    KeInitializeEvent(event, eventType, bInitialState);

    return (HANDLE)event;
}

BOOL
SetEvent(
    HANDLE hEvent)
{
    KeSetEvent(hEvent, IO_NO_INCREMENT, FALSE);

    return TRUE;
}

BOOL
ResetEvent(
    HANDLE hEvent)
{
    KeResetEvent(hEvent);

    return TRUE;
}

BOOL
PulseEvent(
    HANDLE hEvent)
{
    KePulseEvent(hEvent, IO_NO_INCREMENT, FALSE);

    return TRUE;
}

이벤트 또한 커널 모드 동기화 객체입니다. CreateEvent 함수에서 객체 이름 부분은 구현되지 않았습니다. 이름을 정해 사용하려면 IoCreateNotificationEvent, IoCreateSynchronizationEvent 함수를 이용하면 됩니다. (활용도는 그다지 놓지 않겠지만)

지금까지 유저 모드 동기화 함수들의 기능을 커널 모드 함수를 사용하여 구현을 하였습니다. 하지만 커널 모드에서 성능을 높이기 위해서는 스핀락과 같은 동기화 객체를 이용하는 것이 좋습니다.

스핀락을 사용하려면 원본 소스 코드를 적절히 수정해야 합니다. 단 주의애햐 할 점은 스핀락을 사용하면 IRQL이 DISPATCH_LEVEL이 되기 때문에 PagedPool 메모리나 DISPATCH_LEVEL에서 사용할 수 없는 함수가 있는지 살펴봐야 합니다.

관련글


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

2009-07-25