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

 


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

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

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

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를 사용하여 구현을 해야 합니다.

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