Home | Info | Research | Blog | Repos | Messages | Contact Me

 


포팅 작업을 할 때 의외로 중요한 부분이 시간에 관련된 부분입니다. 별것 아닌것 같지만 특정 시간 주기로 루틴이 실행된다던가, 데이터에 시간이 포함된다면 시간 함수 처리를 잘못하면 포팅을 하고 나서도 제대로 동작이 되지 않을 때가 많습니다.

먼저 포팅에 앞서 유저 모드와 커널 모드의 시간 처리 기준에 대해 이해할 필요가 있습니다.
 - 유저 모드 : 밀리초, 100나노초
 - 커널 모드 : 100나노초

따라서 유저 모드와 커널 모드의 처리 기준이 다르다면 변환이 필요합니다. 하지만 대부분의 시간 함수는 변환 없이 사용할 수 있습니다. 이 변환기준은 대부분 동기화 객체의 대기 타임아웃 처리 정도에 사용됩니다.

typedef struct _FILETIME {
    DWORD dwLowDateTime;
    DWORD dwHighDateTime;
} FILETIME, *PFILETIME, *LPFILETIME;

void GetSystemTimeAsFileTime(LPFILETIME lpSystemTimeAsFileTime)
{
    KeQuerySystemTime((PLARGE_INTEGER)&lpSystemTimeAsFileTime);
}

FILETIME과 같은 64비트 형태의 시간 구조체는 100나노초 단위입니다. 커널 모드에서도 64비트 형태의 LARGE_INTEGER 구조체를 사용하고 있으므로 변환 절차 없이 바로 사용할 수 있습니다.

typedef struct _SYSTEMTIME {
    WORD wYear;
    WORD wMonth;
    WORD wDayOfWeek;
    WORD wDay;
    WORD wHour;
    WORD wMinute;
    WORD wSecond;
    WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;

void GetSystemTime(LPSYSTEMTIME lpSystemTime)
{
    LARGE_INTEGER systemTime;
    TIME_FIELDS timeFields;

    KeQuerySystemTime(&systemTime);

    RtlTimeToTimeFields(&systemTime, &timeFields);
}

SYSTEMTIME 구조체와 같이 연,월,일, 시, 분, 초, 요일을 세분화 한 것은 먼저 64비트 형식의 시간을 구한 다음 RtlTimeToTimeFields과 같은 함수로 변환해주면 됩니다.

#include <time.h>

errno_t localtime_s(struct tm * _Tm, const time_t * _Time)
{
    LARGE_INTEGER systemTime, localTime;
    TIME_FIELDS timeFields;

    KeQuerySystemTime(&systemTime);
    ExSystemTimeToLocalTime(&systemTime, &localTime);

    RtlTimeToTimeFields(&localTime, &timeFields);

    _Tm->tm_sec = timeFields.Second;
    _Tm->tm_min = timeFields.Minute;
    _Tm->tm_hour = timeFields.Hour;
    _Tm->tm_mday = timeFields.Day;
    _Tm->tm_mon = timeFields.Month;
    _Tm->tm_year = timeFields.Year;
    _Tm->tm_wday = timeFields.Weekday;

    return 0;
}

WinAPI가 아닌 CRT 함수의 경우 해당 CRT 함수의 구조체에 맞게 필드를 채워주어야 합니다. 먼저 64비트 형태의 시간을 구한 다음 ExSystemTimeToLocalTime 함수를 이용하여 로컬 타임으로 변환합니다. 그리고 RtlTimeToTimeFields으로 연,월,일... 형태로 변환한 뒤 tm 구조체에 맞게 값을 채워줍니다.



Windows Driver Kit(WDK) 7.0.0 정식 버전이 발표되었습니다.

이번 WDK 7.0.0은 Windows 7에 대응하는 버전입니다. 7.0.0 버전 부터는 Windows 2000 지원이 제외되었습니다.

그동안 Microsoft Connect를 통해서 WDK 베타 버전을 받을 수 있었지만, 이제부터 Microsoft Connect를 통하지 않고 Microsoft Download 페이지에서 직접 받을 수 있습니다.

WDK 공식 페이지 : http://www.microsoft.com/whdc/DevTools/WDK/WDKpkg.mspx

WDK 다운로드 페이지 : http://www.microsoft.com/downloads/details.aspx?FamilyID=2105564e-1a9a-4bf4-8d74-ec5b52da3d00&displaylang=en
TAG WDK, Windows 7



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

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

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

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에서 사용할 수 없는 함수가 있는지 살펴봐야 합니다.



이번에는 가장 기본적인 메모리 할당 함수를 구현해보도록 하겠습니다.

유저 모드에서는 보통 CRT 함수인 malloc을 많이 사용합니다. 또한 HeapAlloc, LocalAlloc 등의 함수들도 사용됩니다.

커널 모드에서는 이 함수들을 ExAllocatePoolWithTag 함수로 대체할 수 있습니다. 단 한가지 주의해야 할 점은 유저 모드에서는 IRQL 이라는 개념이 없기 때문에 메모리 함수 호출에 아무런 제약이 없습니다. 하지만 커널 모드에서는 IRQL에 따라서 할당할 수 있는 메모리 종류에 제약이 있습니다.

malloc
void *malloc(size_t _Size)
{
    POOL_TYPE poolType;
    void *mem;

    if (KeGetCurrentIrql() <= DISPATCH_LEVEL)
        poolType = NonPagedPool;
    else
        poolType = PagedPool;

    mem = ExAllocatePoolWithTag(poolType, _Size, 'llam');

    return mem;
}

IRQL이 DISPATCH_LEVEL 이상일 때에는 NonPagedPool로 할당합니다. DISPATCH_LEVEL 미만일 때에는 PagedPool로 할당합니다. 유저 모드에서 포팅한 커널 모드 루틴의 경우 대부분 PASSIVE_LEVEL에서 동작할 것이지만, 스핀락을 사용하여 동기화를 하는 부분이 있다면 DISPATCH_LEVEL이 됩니다. 이런 경우를 고려해야 합니다.

free
void free(void *_Memory)
{
    ExFreePool(_Memory);
}

free등의 메모리 해제 함수는 ExFreePool 함수를 이용하면 됩니다.

realloc
void *realloc(void *_Memory, size_t _NewSize)
{
    POOL_TYPE poolType;
    void *mem;

    if (KeGetCurrentIrql() <= DISPATCH_LEVEL)
        poolType = NonPagedPool;
    else
        poolType = PagedPool;

    mem = ExAllocatePoolWithTag(poolType, _NewSize, 'laer');

    if (_Memory != NULL)
    {
        RtlCopyMemory(mem, _Memory, _NewSize);
        ExFreePool(_Memory);
    }

    return mem;
}

realloc 계열 함수도 위 처럼 구현할 수 있습니다. realloc 함수는 할당된 메모리의 크기를 조절하는 함수입니다. 실제 realloc 구현에서는 연속된 빈 메모리 블럭이 있을 때 다시 메모리를 할당하지 않고 크기를 늘리게 됩니다. 연속된 빈 메모리 공간이 없을 때에는 다른 곳에 메모리를 할당하고, 내용을 복사한 후 원본 메모리는 해제하게 됩니다.

실제 realloc과 동일하게 구현하려면 상당히 복잡해지므로, 새로운 메모리를 할당하고 내용을 복사한 뒤 원본 메모리를 해제하는 방식으로 처리합니다. realloc 함수는 많이 사용되지 않기 때문에 이런 방식으로 처리해도 큰 문제는 없습니다.

calloc
void *calloc(size_t _NumOfElements, size_t _SizeOfElements)
{
    POOL_TYPE poolType;
    void *mem;

    if (KeGetCurrentIrql() <= DISPATCH_LEVEL)
        poolType = NonPagedPool;
    else
        poolType = PagedPool;

    mem = ExAllocatePoolWithTag(poolType, _NumOfElements * _SizeOfElements, 'llac');

    RtlZeroMemory(mem, _NumOfElements * _SizeOfElements);

    return mem;
}

calloc은 할당한 메모리를 0으로 초기화 합니다. 실제 calloc 구현과 다소 차이가 있을 수 있습니다. 하지만 포팅을 할때에는 내부 구현어떻게 되었든 해당 함수의 결과 값이 동일하게 나오느냐가 중요합니다. 포팅이 끝난 후 성능 문제가 발생한다면 그때 가서 최적화를 하는 방법으로 진행합니다.



이제부터는 호환 레이어 위에서 각 기능들을 구현할 차례입니다. 이번에는 파일 I/O 함수 구현 방법에 대해 알아보도록 하겠습니다.

유저 모드에서 사용할 수 있는 파일 I/O 함수는, 똑같이 커널 모드에서도 사용할 수 있습니다. 당연한 말이겠지만, 커널 모드에서 파일 I/O 함수를 제공해 주기 때문에 유저 모드에서 사용할 수 있는 것입니다. 실제로 유저 모드에서 사용하는 함수들은 빈 껍데기일 뿐입니다. 실제로는 커널 모드 함수가 모든 일을 처리합니다.

따라서 유저 모드 함수에 사용되는 각종 옵션들도 커널 모드에서 그대로 사용할 수 있습니다. 단 WDK 스타일에 따라 각종 매크로 정의나 구조체 형태가 다를 수 있지만, 기능은 동일합니다.

주요 유저 모드 파일 I/O 함수에 해당하는 커널 모드 함수는 다음과 같습니다.

1. CreateFile - ZwReadFile
2. ReadFile - ZwReadFile
3. WriteFile - ZwWriteFile
4. CloseHandle - ZwClose
5. DeleteFile - ZwDeleteFile

위 함수들 처럼 1:1 대응이 되는 함수들이 있는 경우에는 해당 함수를 그대로 사용하면 되지만 1:1 대응이 되지 않는 함수들이 있습니다. 이러한 함수들은 커널 모드 함수를 조합하여 동일한 기능을 하도록 구현해주어야 합니다.

GetFileSize
DWORD GetFileSize(
    HANDLE hFile,
    LPDWARD lpFileSizeHigh)
{
    NTSTATUS status;
    IO_STATUS_BLOCK iosb;
    FILE_STANDARD_INFORMATION fileStandardInfo;
    DWORD fileSize = 0;

    status = ZwQueryInformationFile(
        hFile,
        &iosb,
        &fileStandardInfo,
        sizeof(fileStandardInfo),
        FileStandardInformation);

    if (NT_SUCCESS(status))
    {
        *lpFileSizeHigh = fileStandardInfo.EndOfFile.HighPart;
        fileSize = fileStandardInfo.EndOfFile.LowPart;
    }

    return fileSize;
}

커널 모드에는 GetFileSize에 정확히 대응되는 함수가 없기 때문에 ZwQueryInformationFile 함수를 이용합니다. FILE_STANDARD_INFORMATION 구조체를 이용하면 파일 크기를 구할 수 있습니다.

SetFilePointer
DWORD SetFilePointer(
    HANDLE hFile,
    LONG lDistanceToMove,
    PLONG lpDistanceToMoveHigh,
    DWORD dwMoveMethod)
{
    NTSTATUS status;
    IO_STATUS_BLOCK iosb;
    FILE_POSITION_INFORMATION filePositionInfo;
    FILE_END_OF_FILE_INFORMATION fileEndOfFileInfo;
    DWORD ret = INVALID_SET_FILE_POINTER;

    switch (dwMoveMethod)
    {
    case FILE_BEGIN:

        filePositionInfo.CurrentByteOffset.HighPart = *lpDistanceToMoveHigh;
        filePositionInfo.CurrentByteOffset.LowPart = lDistanceToMove;

        status = ZwSetInformationFile(
            hFile,
            &iosb,
            &filePositionInfo,
            sizeof(filePositionInfo),
            FilePositionInformation);

        break;

    case FILE_CURRENT:

        status = ZwQueryInformationFile(
            hFile,
            &iosb,
            &filePositionInfo,
            sizeof(filePositionInfo),
            FilePositionInformation);

        if (NT_SUCCESS(status))
        {
            filePositionInfo.CurrentByteOffset.HighPart += *lpDistanceToMoveHigh;
            filePositionInfo.CurrentByteOffset.LowPart += lDistanceToMove;

            status = ZwSetInformationFile(
                hFile,
                &iosb,
                &filePositionInfo,
                sizeof(filePositionInfo),
                FilePositionInformation);
        }

        break;

    case FILE_END:

        status = ZwQueryInformationFile(
            hFile,
            &iosb,
            &fileEndOfFileInfo,
            sizeof(fileEndOfFileInfo),
            FileEndOfFileInformation);

        if (NT_SUCCESS(status))
        {
            filePositionInfo.CurrentByteOffset.HighPart =
                fileEndOfFileInfo.EndOfFile.HighPart;
            filePositionInfo.CurrentByteOffset.LowPart =
                fileEndOfFileInfo.EndOfFile.LowPart;

            filePositionInfo.CurrentByteOffset.HighPart += *lpDistanceToMoveHigh;
            filePositionInfo.CurrentByteOffset.LowPart += lDistanceToMove;

            status = ZwSetInformationFile(
                hFile,
                &iosb,
                &filePositionInfo,
                sizeof(filePositionInfo),
                FilePositionInformation);
        }

        break;
    }

    if (NT_SUCCESS(status))
    {
        status = ZwQueryInformationFile(
            hFile,
            &iosb,
            &filePositionInfo,
            sizeof(filePositionInfo),
            FilePositionInformation);

        if (NT_SUCCESS(status))
        {
            *lpDistanceToMoveHigh = filePositionInfo.CurrentByteOffset.HighPart;
            ret = filePositionInfo.CurrentByteOffset.LowPart;
        }
    }

    return ret;
}

SetFilePointer 함수는 기본적으로 ZwQueryInformationFile, ZwSetInformationFile 함수와 FILE_POSITION_INFORMATION 구조체를 이용합니다. 그리고 FILE_END 옵션을 처리하기 위해 FILE_END_OF_FILE_INFORMATION 구초제를 이용합니다.

GetFileAttributes
DWORD GetFileAttributes(
    LPCTSTR lpFileName)
{
    NTSTATUS status;
    HANDLE fileHandle;
    UNICODE_STRING fileName;
    OBJECT_ATTRIBUTES objectAttributes;
    IO_STATUS_BLOCK iosb;
    FILE_ATTRIBUTE_TAG_INFORMATION fileAttributeTagInfo;
    DWORD ret = INVALID_FILE_ATTRIBUTES;

    RtlInitUnicodeString(&fileName, lpFileName);

    InitializeObjectAttributes(
        &objectAttributes,
        &fileName,
        OBJ_CASE_INSENSITIVE| OBJ_KERNEL_HANDLE,
        NULL,
        NULL);

    status = ZwOpenFile(
        &fileHandle,
        FILE_ALL_ACCESS,
        &objectAttributes,
        &iosb,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        FILE_OPEN);

    if (NT_SUCCESS(status))
    {
        status = ZwQueryInformationFile(
            fileHandle,
            &iosb,
            &fileAttributeTagInfo,
            sizeof(fileAttributeTagInfo),
            FileAttributeTagInformation);

        ret = fileAttributeTagInfo.FileAttributes;

        ZwClose(fileHandle);
    }

    return ret;
}

GetFileAttributes는 매개변수로 핸들을 받는 것이 아니라 파일명을 받기 때문에 일단 ZwOpenFile을 이용하여 핸들을 얻습니다. 그리고 ZwQueryInformationFile 함수와 FILE_ATTRIBUTE_TAG_INFORMATION 구조체를 이용하여 구현합니다.

SetFileAttributes 및 기타 함수들도 ZwQueryInformationFile 함수를 이용하면 어렵지 않게 구현할 수 있습니다.



포팅을 쉽게 하려면 원본 소스 코드를 최소한으로 수정해야 합니다. WDK에 없는 함수를 구현할 호환 레이어를 작성해야 합니다.

이제 부터 매크로와 인라인 함수를 이용하여 에러를 막았던 부분을 구현해줄 차례입니다. 호환 레이어는 호환 레이어 소스를 원본 소스 코드 빌드 설정에 추가하는 것도 좋지만, lib(라이브러리) 형태로 작성하는 것이 편리합니다.

TARGETNAME=libcompat
TARGETTYPE=LIBRARY

SOURCES= \
    io.c \
    sync.c \
    time.c \
    memory.c

커널 모드용 라이브러리를 만들기 위해 TARGETTYPE을 LIBRARY로 설정해줍니다.

TARGETLIBS=..\libcompat\objchk\i386\libcompat.lib

이후 포팅할 드라이버의 sources 파일에서 위와 같이 라이브러리를 지정해주면 됩니다.

SQLite for Windows Kernel의 경우 각 플랫폼 별로 호환 레이어가 제공되었기 때문에 윈도우 커널용 호환 레이어를 원본 소스 코드에 추가하는 방식으로 포팅하였습니다. 이 처럼 플랫폼 별로 호환 레이어를 제공해 주는 경우, 이 호환 레이어를 이용하는 것이 편리합니다.

io.c
#include "compat.h"

HANDLE
CreateFileW(
    LPCWSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile
    )
{
    ... 생략 ...
    ZwCreateFile( );
    ... 생략 ...
}

BOOL
ReadFile(
    HANDLE hFile,
    LPVOID lpBuffer,
    DWORD nNumberOfBytesToRead,
    LPDWORD lpNumberOfBytesRead,
    LPOVERLAPPED lpOverlapped
    )
{
    ... 생략 ...
    ZwReadFile( );
    ... 생략 ...
}

앞서 제작했던 compat.h를 include 해주는 것이 중요합니다. compat.h에 각종 자료형과 구조체를 정의해주었기 때문에 이 부분을 이용해야 합니다. (여기서 실제 구현은 생략하겠습니다.)

함수를 구현하였다면 에러를 막기 위해 compat.h에 추가하였던 매크로 및 인라인 함수를 삭제합니다. 즉, 함수를 구현하여 추가할 때마다 그 함수에 해당하는 매크로, 인라인 함수를 삭제해주는 방식으로 포팅을 진행합니다.

구현할 함수가 늘어날 때마다, io.c, sync.c, time.c, memory.c 등 함수 종류에 맞는 소스 파일을 추가하면 됩니다.



커널 모드 포팅 작업에서 가장 중요한 것은 빌드(컴파일)하기 입니다.

포팅을 결심하고나서도 컴파일 할 때의 무수한 에러와 경고 때문에, 대부분 이 단계에서 포기를 하게 됩니다.

유저 모드에서 동작하던 프로그램들은 빌드 환경이 Visual Studio 및 Platform SDK에 맞추어져 있습니다. 따라서 이 부분을 WDK(DDK) 환경에 맞게 수정해주고 에러와 경고를 줄여나가야 합니다.

그러므로 이번 단계에서는 일단 빌드만 가능하도록 수정합니다. 따라서 정상 동작은 되지 않습니다.

커널 모드 포팅의 원칙은 원본 소스코드를 최소한으로 수정하는것입니다. 그렇기 때문에 원본 소스를 직접 수정하지 않고 공통 헤더 파일과 호환 레이어를 사용하여 원본 소스코드 외부에서 모든 포팅 작업을 하게 됩니다.

1. 공통 헤더 파일 작성
먼저 포팅을 진행할 소스트리에서 최상위 헤더 파일을 찾습니다. 그리고 이제부터 작성할 compat.h를 include 합니다. 최상위 헤더 파일이 없거나, 헤더 파일이 여러개로 나뉘어져 있을 경우, 각각의 헤더 파일에서 compat.h를 include 해줍니다. (compat.h는 Compatibility를 의미합니다.)

compat.h
#include <ntddk.h>
 

WDK 환경에서 빌드를 해야 하므로 가장 기본적인 ntddk.h 헤더 파일을 include 합니다. 그리고 유저 모드용 헤더 파일(windows.h 등)은 include를 모두 삭제합니다.

2. 에러 줄여나가기 - 자료형, 매크로 상수
이제부터 공통 헤더 파일 compat.h를 이용하여 에러를 줄여나갑니다. ntddk.h만 include 하고 빌드를 하면, 각종 자료형과 매크로 상수가 정의되어 있지 않아 에러가 발생할 것입니다.

compat.h
typedef unsigned long DWORD;
typedef unsigned short WORD;

typedef struct _FILETIME {
    DWORD dwLowDateTime;
    DWORD dwHighDateTime;
} FILETIME, *PFILETIME, *LPFILETIME;

#define MAX_PATH 260

ntddk.h에는 없는 자료형과 매크로 상수를 위와 같이 정의해줍니다.

3. 에러 줄여나가기 - 일반 함수
각종 자료형과 매크로 상수 뿐만 아니라, 함수에서도 에러가 발생할 것입니다. 일단 에러를 막는 것이 중요하므로 매크로를 이용하여 빈 껍데기 함수를 만들어줍니다.

compat.h
#define CreateFileW(a,b,c,d,e,f,g) 0
#define SetFilePointer(a,b,c,d) 0

원본 함수의 매개변수 개수와 똑같 만들어줍니다. 그리고 오른쪽 부분을 0으로 지정하면 아무일도 하지 않는 빈껍데기 함수가 됩니다.

4. 에러 줄여나가기 - 레퍼런스를 매개변수로 받는 함수
앞의 일반 함수는 일반 변수 및 상수를 매개변수를 받아 특정 동작을 하고 끝나버리기 때문에 매크로를 이용하여도 문제가 없습니다. 하지만 변수의 레퍼런스를 매개변수로 받는 함수는 특정 값을 얻고자 할 때 사용하는 경우가 대부분입니다. 따라서 함수가 호출된 이후에도 해당 변수는 계속 사용되기 때문에, 매크로로 빈껍데기 함수를 만들어 처리해버리면 해당 변수가 초기화 되지 않았다는 에러가 발생합니다.

compat.h
/* 매개 변수는 int */
#define ExampleFunctionA(a) *a = 0

/* 매개 변수는 LARGE_INTEGER, 이후 LowPart만 사용한다고 했을 때 */
#define QueryPerformanceCounter(a) *a.LowPart = 0

/* 매개 변수는 LARGE_INTEGER, 이후 LowPart, HighPart 모두 사용한다고 했을 때 */
#define QueryPerformanceCounter(a) *a.LowPart = 0;*a.HighPart = 0;

이때는 참조 연산자를 사용하여 매개변수를 초기화 해주어야 합니다. 멤버가 많은 구조체일 때에는 이후에 사용되는 모든 멤버를 초기화 해주어야 에러가 발생하지 않습니다.

compat.h
__inline void GetSystemTimeAsFileTime(__out LPFILETIME lpSystemTimeAsFileTime)
{
}

매번 매개변수를 초기화 하지 않으려면 인라인 함수를 이용합니다. 이처럼 인라인 함수를 이용하여 함수 형태만 만들어주면 초기화 에러는 발생하지 않습니다.


위와 같은 방식으로 WDK에 존재하지 않는 자료형, 매크로 상수, 함수 등을 모두 처리하여 에러가 하나도 발생하지 않고 빌드를 성공하였다면, 이제부터 각각의 함수를 구현할 호환 레이어를 만들어야 합니다.



안녕하세요. 이재홍입니다.

윈도우 드라이버 개발을 하다 보면 유저 모드에서 돌아가는 응용프로그램의 기능을 커널 모드 드라이버에서 지원해줘야 하는 경우가 있습니다.

하지만 유저 모드 응용프로그램의 기능을 커널 모드 드라이버로 옮긴 다는 것은 말처럼 쉬운 일이 아닙니다.

포팅에 있어서 가장 먼저 해야 할 일은 포팅이 가능한지 여부를 판단하는 것입니다. 대략적으로 포팅 가능 여부는 아래 기준으로 판단할 수 있습니다.


1. 로직으로만 구성되어있는가?
암호화 알고리즘과 같이 로직으로만 구성된 것은 커널 모드로 포팅이 가능합니다. 즉 수학적, 논리적 계산으로 결과값을 도출해내는류의 프로그램은 WDK를 이용하여 빌드만 정상적으로 성공한다면 큰 어려움 없이 포팅할 수 있습니다.

또한 암호화 알고리즘 이외에도 DB 라이브러리(예 : SQLite)와 같이 로직으로만 구성된 프로그램도 포팅이 가능합니다. 이 경우에는 WDK를 이용한 빌드 뿐만 아니라. I/O 부분을 신경써줘야 합니다.

자료 구조를 위한 알고리즘(스택, 큐, B+ Tree, AVL Tree, Name-Value Pair 등)도 포팅이 가능합니다.

2. 유저 모드 응용 API에 얼마나 의존하는가?
I/O를 위한 기본 API (CreateFile, ReadFile, WriteFile)이 외에 다양한 기능을 제공하는 응용 API를 얼마나 많이 사용하고 있느냐에 따라 포팅 가능 여부가 결정됩니다.

대부분의 윈도우 기본 API는 커널 모드 API와 1:1 매칭이 되어 포팅에 큰 어려움이 없습니다. 그래서 파일 I/O등 기본 API만 사용한다면 큰 어려움 없이 포팅이 가능합니다.

하지만 유저 모드에만 유효한 GDI 및 DirectX , 세션, 오디오, 사운드, COM 관련 API를 상당수 사용하고 있다면, 커널 모드 포팅에 어려움이 있습니다. 이러한 응용프로그램은 커널 모드로 포팅해도 별 의미가 없는 경우가 대부분입니다.


이 두가지 사항으로 커널 모드 포팅이 가능하다고 판단하여, 한참 포팅을 진행하다가 난관에 봉착하는 경우가 있습니다. 특정 API가 커널 모드에서 매치가 되지 않을 때입니다. 이 때는 해당 API의 기능을 할 수 있도록 다른 API를 사용하여 구현을 해야 합니다.

다음 글 부터는 실제로 포팅을 진행할 때 빌드 방법과 구현 방법을 알아보도록 하겠습니다.



저번 글에서는 세마포어를 이용하여 Condition Variable을 구현해보았습니다.

이번에는 커널 모드 이벤트를 이용하여 Condition Variable을 구현해보도록 하겠습니다.

기존 세마포어를 이용한 구현이 있음에도 불구하고 이벤트로 또다시 구현한 이유는 세마포어를 이용한 것 보다 이벤트를 이용하는 것이 성능이 좀더 좋기 때문입니다. 또한 세마포어를 이용한 것은 객체의 최대 갯수를 정해야 하지만 이벤트를 이용한 것은 객체의 최대 갯수를 정하지 않아도 됩니다.

구현 원리는 Notification Event를 이용하는 것입니다. 이벤트는 2가지가 있는데 Synchronization Evnet와 Notification Event가 있습니다. 유저 모드에서는 Synchronization Event를 Auto-reset Evnet(자동 리셋 이벤트), Notification Evnet를 Manual-reset Event(수동 리셋 이벤트)라고 부릅니다.

즉 Synchronization Event는 대기 상태에서 해제 되는 순간 자동으로 Reset 됩니다. 하지만 Notification Event는 대기 상태가 해제되더라도 계속 해제 상태로 머물러 있게 됩니다. 그래서 이 Notification Event의 특성을 이용하여 Condition Variable의 대기하고 있는 모든 객체를 깨우는 WakeAll 기능을 구현하는 것입니다. 이벤트를 Set 해주고 나서 대기하고 있는 모든 객체가 해제되고 마지막에 이벤트를 Reset 해주는 것입니다.

#define CONDITION_VARIABLE_LOCKMODE_SHARED 0x1

typedef struct _KCONDITION_VARIABLE {
    KEVENT Event;
    BOOLEAN Wake;
    BOOLEAN WakeAll;
    ULONG Waiters;
} KCONDITION_VARIABLE, *PKCONDITION_VARIABLE;

VOID
KeInitializeConditionVariable (
    __out PKCONDITION_VARIABLE ConditionVariable
    )
{
    KeInitializeEvent(&ConditionVariable->Event, NotificationEvent, FALSE);
    ConditionVariable->WakeAll = FALSE;
    ConditionVariable->Wake = FALSE;
    ConditionVariable->Waiters = 0;
}

KCONDITION_VARIABLE 구조체와 초기화 함수 입니다. 구조체는 이벤트와 대기하고 있는 객체를 하나만 깨울 것인지 모두 깨울 것인지 결정하는 플래그와 대기하고 있는 객체의 수를 저장하는 변수로 구성되어 있습니다. 앞서 설명한 것과 같이 이벤트는 Notification Evnet로 설정합니다.


NTSTATUS
KeSleepConditionVariableMX (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PKMUTEX Mutex,
    __in_opt PLARGE_INTEGER Timeout
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();
    ULONG order;

    // 순서 저장
    order = ConditionVariable->Waiters;

    while (1)
    {
        InterlockedIncrement(&ConditionVariable->Waiters);

        KeReleaseMutex(Mutex, TRUE);
        status = KeWaitForSingleObject(&ConditionVariable->Event, Executive, KernelMode,
            FALSE, Timeout);

        KeWaitForMutexObject(Mutex, Executive, KernelMode, FALSE, NULL);

        InterlockedDecrement(&ConditionVariable->Waiters);

        if (status == STATUS_TIMEOUT)
        {
            return status;
        }

        if (ConditionVariable->WakeAll)
        {
            // 대기하고 있는 객체가 더 이상 없을 경우 이벤트를 리셋한다.
            if (ConditionVariable->Waiters == 0)
            {
                ConditionVariable->WakeAll = FALSE;
                KeResetEvent(&ConditionVariable->Event);
            }

            // 아직 대기하고 있는 객체가 있을 경우 이벤트를 리셋하지 않고
            // 빠져나간다.
            break;
        }
        else if (ConditionVariable->Wake)
        {
            // 순서가 가장 빠른 객체만 깨운다.
            if (order == 0)
            {
                ConditionVariable->Wake = FALSE;
                KeResetEvent(&ConditionVariable->Event);

                break;
            }
        }
        // 순서가 0이 아닌 경우 루프를 빠져나가지 않고 자신의 순서를 올린다.
        order--;
    }

    return status;
}

뮤텍스와 함께 사용하는 Condition Variable 대기 함수입니다. 세마포를 이용한 구현 보다 조금 복잡합니다. 하지만 원리는 간단합니다.

먼저 대기하고 있는 객체를 하나만 깨우기 위해서 현재 대기하고 있는 객체의 순서를 저장합니다.  Notification Event를 이용했기 때문에 대기하고 있는 모든 이벤트가 해제되어 루프를 돌 것입니다. 여기서 순서가 0이라면 이벤트를 Reset 하고 루프를 빠져나갑니다. 0이 아니라면 자신의 순서를 올리고 계속 대기합니다.

모든 객체를 깨울 때에는 그냥 루프를 빠져나가면 됩니다. 그리고 맨 마지막에 해제되는 객체 쪽에서 이벤트를 Reset 해줍니다.


NTSTATUS
KeSleepConditionVariableRS (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PERESOURCE Resource,
    __in_opt PLARGE_INTEGER Timeout,
    __in ULONG Flags
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();
    ULONG order;

    // 순서 저장
    order = ConditionVariable->Waiters;

    while (1)
    {
        InterlockedIncrement(&ConditionVariable->Waiters);

        ExReleaseResourceLite(Resource);
        status = KeWaitForSingleObject(&ConditionVariable->Event, Executive, KernelMode,
            FALSE, Timeout);

        if (Flags == CONDITION_VARIABLE_LOCKMODE_SHARED)
            ExAcquireResourceSharedLite(Resource, TRUE);
        else
            ExAcquireResourceExclusiveLite(Resource, TRUE);

        InterlockedDecrement(&ConditionVariable->Waiters);

        if (status == STATUS_TIMEOUT)
        {
            return status;
        }

        if (ConditionVariable->WakeAll)
        {
            // 대기하고 있는 객체가 더 이상 없을 경우 이벤트를 리셋한다.
            if (ConditionVariable->Waiters == 0)
            {
                ConditionVariable->WakeAll = FALSE;
                KeResetEvent(&ConditionVariable->Event);
            }

            // 아직 대기하고 있는 객체가 있을 경우 이벤트를 리셋하지 않고
            // 빠져나간다.
            break;
        }
        else if (ConditionVariable->Wake)
        {
            // 순서가 가장 빠른 쓰레드만 깨운다.
            if (order == 0)
            {
                ConditionVariable->Wake = FALSE;
                KeResetEvent(&ConditionVariable->Event);

                break;
            }
        }
        // 순서가 0이 아닌 경우 루프를 빠져나가지 않고 자신의 순서를 올린다.
        order--;
    }

    return status;
}

이번에는 리소스와 함께 사용하는 Condition Variable 대기 함수입니다. 리소스를 Shared로 사용할 때에는 Flags에 CONDITION_VARIABLE_LOCKMODE_SHARED를 지정해줍니다


VOID
KeWakeConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    if (ConditionVariable->Waiters > 0)
    {
        ConditionVariable->Wake = TRUE;
        KeSetEvent(&ConditionVariable->Event, IO_NO_INCREMENT, FALSE);
    }
}

대기하고 있는 Condition Variable 객체 하나를 깨우는 함수입니다. Wake를 TRUE로 설정해주고 이벤트를 Set 시킵니다.


VOID
KeWakeAllConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    if (ConditionVariable->Waiters > 0)
    {
        ConditionVariable->WakeAll = TRUE;
        KeSetEvent(&ConditionVariable->Event, IO_NO_INCREMENT, FALSE);
    }
}

대기하고 있는 모든 Condition Variable 객체를 깨우는 함수입니다. WakAll을 TRUE로 설정해주고 이벤트를 Set 시킵니다.

condvar_event.zip : Condition Variable 구현과 사용 예제입니다.

예제에는 3개의 쓰레드를 생성하는데, 2번과 3번 쓰레드는 Condition Variable 객체를 대기하고 있고 1번 쓰레드가 KeWakeAllConditionVariable 함수를 이용하여 2, 3번 쓰레드를 깨우도록 되어 있습니다.



안녕하세요. 이재홍입니다.

윈도우는 Windows Vista, Windows Server 2008 부터 유저 모드에서 사용할 수 있는 Condition Variable 함수들이 추가되었습니다. (Windows Vista에 새로 추가된 동기화 기본 형식)

세마포어를 이용하여 커널 모드에서 사용할 수 있는 Condition Variable을 구현해보도록 하겠습니다.

Condition Variable은 기존 동기화 객체와는 약간 다른 특징을 가지고 있습니다. 뮤텍스 혹은 스핀락의 경우 대기와 해제가 1:1 대응입니다. 즉 한번에 하나씩 대기하고 있는 객체를 깨울 수 있습니다. Condition Variable은 대기하고 있는 하나의 객체를 깨울 수도 있고, 대기하고 있는 여러개의 객체를 한번에 모두 깨울 수도 있습니다.

구현 원리는 세마포어의 특성을 이용하는 것입니다. 세마포어는 대기하고 있는 객체를 원하는 개수 만큼 깨울 수 있는 기능이 있습니다. 따라서 대기하는 객체의 개수를 저장하고 있고, 대기하는 객체를 한번에 깨울 때에는 저장하고 있던 객체의 개수를 이용하는 것입니다.

#define MAXIMUM_WAIT_CONDITION_VARIABLES 1024

typedef struct _KCONDITION_VARIABLE {
    KSEMAPHORE Semaphore;
    KSPIN_LOCK SpinLock;
    ULONG Waiters;
} KCONDITION_VARIABLE, *PKCONDITION_VARIABLE, *PRKCONDITION_VARIABLE;

VOID
KeInitializeConditionVariable (
    __out PKCONDITION_VARIABLE ConditionVariable
    )
{
    ConditionVariable->Waiters = 0;
    KeInitializeSpinLock(&ConditionVariable->SpinLock);
    KeInitializeSemaphore(&ConditionVariable->Semaphore, 0,
        MAXIMUM_WAIT_CONDITION_VARIABLES);
}

먼저 KCONDITION_VARIABLE 구조체와 초기화 함수입니다. 대기하고 있는 객체의 수를 저장하는 변수를 초기화 하고, 스핀락과 세마포어를 초기화 합니다. 스핀락은 Waiters 변수를 보호하기 위해 사용합니다.


NTSTATUS
KeSleepConditionVariableMX (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PKMUTEX Mutex,
    __in_opt PLARGE_INTEGER Timeout
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();

    KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
    ConditionVariable->Waiters++;
    KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);

    KeReleaseMutex(Mutex, TRUE);
   
    status = KeWaitForSingleObject(&ConditionVariable->Semaphore, Executive,
        KernelMode, FALSE, Timeout);

    KeWaitForSingleObject(Mutex, Executive, KernelMode, FALSE, NULL);

    return status;
}

뮤텍스와 함께 사용하는 Condition Variable 대기 함수입니다. 대기할 때 마다 Waiters 변수를 증가시킵니다. Windows Vista(Server 2008)에 있는 SleepConditionVariableCS라는 함수와 비슷한 용도입니다. 이 함수는 크리티컬 섹션을 사용하는 함수입니다.


NTSTATUS
KeSleepConditionVariableRS (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PERESOURCE Resource,
    __in_opt PLARGE_INTEGER Timeout,
    __in ULONG Flags
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();

    KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
    ConditionVariable->Waiters++;
    KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);

    ExReleaseResourceLite(Resource);

    status = KeWaitForSingleObject(&ConditionVariable->Semaphore, Executive,
        KernelMode, FALSE, Timeout);

    if (Flags == CONDITION_VARIABLE_LOCKMODE_SHARED)
        ExAcquireResourceSharedLite(Resource, TRUE);
    else
        ExAcquireResourceExclusiveLite(Resource, TRUE);

    return status;   
}

이번에는 리소스와 함께 사용하는 Condition Variable 대기 함수입니다. Windows Vista(Server 2008)에 있는 SleepConditionVariableSRW 함수에 대응합니다. 리소스를 Shared로 사용할 때에는 Flags에 CONDITION_VARIABLE_LOCKMODE_SHARED를 지정해줍니다.


VOID
KeWakeConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    KIRQL irql = KeGetCurrentIrql();

    if (ConditionVariable->Waiters > 0)
    {
        KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
        KeReleaseSemaphore(&ConditionVariable->Semaphore, IO_NO_INCREMENT, 1, FALSE);
        ConditionVariable->Waiters--;
        KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);
    }
}

대기하고 있는 Condition Variable 객체 하나를 깨우는 함수입니다. 세마포어의 특성을 이용하는 것인데, KeReleaseSemaphore에 1을 지정하여 대기하고 있는 세마포어 객체 하나를 깨웁니다. 그리고 대기하고 있는 객체의 개수를 감소 시킵니다.


VOID
KeWakeAllConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    KIRQL irql = KeGetCurrentIrql();

    if (ConditionVariable->Waiters > 0)
    {
        KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
        KeReleaseSemaphore(&ConditionVariable->Semaphore, IO_NO_INCREMENT,
            ConditionVariable->Waiters, FALSE);
        ConditionVariable->Waiters = 0;
        KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);
    }
}

대기하고 있는 모든 Condition Variable 객체를 깨우는 함수입니다. KeReleaseSemaphore에는 대기하고 있는 객체의 개수를 지정합니다. 그래서 대기하고 있는 개수 만큼 세마포어 객체를 깨웁니다. 끝으로 대기하고 있는 객체의 개수를 0으로 초기화 합니다.

condvar_semaphore.zip : Condition Variable 구현과 사용 예제입니다.

예제에는 3개의 쓰레드를 생성하는데, 2번과 3번 쓰레드는 Condition Variable 객체를 대기하고 있고 1번 쓰레드가 KeWakeAllConditionVariable 함수를 이용하여 2, 3번 쓰레드를 깨우도록 되어 있습니다.