기술아티클

Windows 커널에서 C++ 사용하기 (상)

2024.05.21






들어가며

 

국내에서는 커널 개발에 C++을 적용하기 위한 노력이 상대적으로 부족한 반면, 해외에서는 오래전부터 일부 개발자들을 중심으로 C++을 커널에서 활용하기 위한 노력이 있었습니다. 필자 역시 개인적으로 이에 대한 연구를 지속해왔으며, 이 과정에서 해외의 몇몇 아티클이 큰 도움이 되었습니다. 그러나 이 글들은 작성된 시기가 오래되어 현재의 환경에 일부 부합하지 않는 점이 있거나, 내용이 산재되어 있어 어느 정도 체계적인 정리가 필요했습니다.

 

자사에서는 커널 드라이버에 C++을 적용 후 개발 속도와 안정성 측면에서 의미 있는 성과를 보았으며, 이러한 경험을 바탕으로 최신 도구를 기준으로 커널 개발에 C++을 활용하는 방법을 안내하고자 이렇게 글을 작성하게 되었습니다. 이 글이 커널 개발에 C++을 적용하고자 하는 분들에게 실질적인 도움이 되기를 바랍니다.

 

 

 

 


 

Windows 커널에서 C++이 가지는 장점

 

커널 드라이버 개발에는 전통적으로 C가 선호되고 있습니다. 하지만 C++을 사용하면 객체지향 프로그래밍의 장점을 충분히 활용할 수 있으며, 이는 코드의 재사용성, 확장성, 유지 보수성을 크게 향상시킵니다. 커널 드라이버를 C++로 작성하는 것은 초기에 몇 가지 기술적 문제에 직면할 수 있지만 이러한 문제들은 어느 정도 해결이 가능합니다. 이 글은 초기 기술적 문제를 해결하는 방법에 초점을 두고 있습니다.

 

내용을 진행하기 전에 다음의 "Microsoft: C++ for Kernel Mode Drivers: Pros and Cons" 문서를 읽어보는 것을 추천합니다. 이 문서에서는 Microsoft 관점에서 C++을 사용하여 커널 드라이버를 개발하는 것의 장단점에 대해 설명합니다. 주로 C++ 사용의 문제점에 대해 다루고 있으므로, 문제의 전반적인 내용을 하나의 문서에서 파악할 수 있습니다.

 

 

문서에 따르면, Microsoft는 커널 드라이버에서 C++ 사용을 보증하지도 금지하지도 않으며, 알려진 문제와 위험을 인지하고 스스로의 책임하에 선택적으로 사용할 것을 권장합니다. 개인적으로, 이 문서는 Microsoft의 공식 입장이다 보니 다소 보수적인 관점에서 작성되었다고 생각합니다. 문서에서 경고하는 이슈는 기술적으로 해결이 가능하거나 생각보다 큰 문제가 되지 않는 것도 있으며, 그 외 문제들도 커널 환경과 C++의 특성을 이해하면 어렵지 않게 회피할 수 있다고 생각하기 때문입니다. 앞으로 이어지는 내용을 참고하신 후, C++의 적용 여부와 사용범위를 각자 결정해 보시기 바랍니다.

 

자사의 경험에 따르면, C++ 도입으로 커널 드라이버의 안정성이 크게 향상되었습니다. C++을 활용하고 객체 단위로 개발 산출물을 관리함으로써 블루스크린 뿐 아니라 자원의 누수, 버퍼 오버플로우와 같은 치명적인 시스템 이슈가 거의 발생하지 않게 되었습니다. 또한, 검증된 객체의 재사용이 점차 누적되고, 코드의 개발 및 유지 보수 과정이 단순화되면서 사람의 실수로 인한 오류 또한 크게 줄어들었습니다. 이러한 긍정적인 경험을 바탕으로, 여러분도 C++의 적용을 고려해 보시기를 권장합니다. 보다 안정적이고 효율적인 드라이버 개발이 가능해질 것입니다.

 

 

 

 


 

Windows 커널과 C++ 런타임

 

C++의 많은 기능은 런타임 라이브러리에 의존하여 동작합니다. 런타임은 메모리 할당 및 해제, 파일과 콘솔 입출력, 프로그램 시작과 종료 시 실행되는 코드를 처리하는 메커니즘을 제공합니다. 또한, 정적 객체의 생성과 소멸 관리, 예외 처리와 같은 기능을 지원합니다.

 

문제는 커널에서는 대부분의 필수 런타임을 사용할 수 없다는 점입니다. 따라서 커널에서 C++을 사용하기 위해서는 사용자가 직접 런타임의 일부 기능을 구현해야 하며, 이는 경우에 따라서 조금 까다로운 과정일 수 있습니다. 다음은 커널에서 C++ 사용을 위해 구현을 고려할 만한 런타임 기능입니다.

 

  • 동적 메모리 관리:
    커널 메모리를 사용하는 전역 new, delete 연산자를 정의해야 합니다. C++을 사용하는 데 있어 가장 기본적이며 필수적인 기능입니다.

     

  • 정적 객체 지원:

    런타임은 정적 객체를 초기화하고 종료하는 역할을 담당합니다. 커널에서 정적 객체를 사용하려면 .CRT라는 특수 섹션에서 정적 객체의 초기화와 종료 코드를 관리하는 기능을 정의해야 합니다.

     

  • 예외 처리와 RTTI:

    커널에서 예외를 사용하려면 try-throw-catch 로 이어지는 예외 처리 메커니즘을 구현해야 합니다. 이 과정에서 예외 객체의 타입을 확인하고 적합한 catch 블록을 선택할 수 있도록 하는 RTTI (Runtime Type Information)를 선택적으로 구현할 수 있습니다.

 

MSVC (Microsoft Visual C++)에는 사용자용 런타임 라이브러리의 소스 코드가 포함되어 있습니다. 우리는 위에 나열된 런타임 기능을 커널에서 구현할 때, MSVC의 사용자용 런타임 코드를 복사하여 원본 형태를 최대한 유지한 채 커널로 변환할 수 있습니다. 또한, 필요한 부분만 선택적으로 차용하거나 원본 런타임 코드의 동작을 분석하여 직접 구현할 수도 있습니다. 만약 여러분이 커널에서 C++을 사용하려는 목적 중 하나가 STL (Standard Template Library)의 사용이라면, 원본 런타임 코드를 최대한 유지하면서 필요한 부분만 커널 코드로 변환해 사용하는 것을 권장합니다. 원본 런타임 코드의 경로는 다음과 같습니다.

 

  • Universal C Runtime 원본 코드: 

    <Windows Kits>\Source\<SDK Versions>\
     

  • Visual Studio C Runtime 원본 코드:

    <Visual Studio>\VC\Tools\MSVC\<VC Versions>\

 

이제 위에서 언급한 런타임 기능을 커널에서 구현하는 방법을 순차적으로 알아보고, 이렇게 구현한 런타임을 기반으로 C++의 핵심 기능인 STL 컴포넌트를 커널에서 사용하는 방법에 대해 간략히 다루겠습니다.

 

 

 

 


 

런타임: 동적 메모리 관리

 

커널에서 C++을 사용하는 데 필요한 최소한의 설정입니다. 커널에서는 전역 메모리 할당(new) 및 해제(delete) 연산자가 제공되지 않으므로, 아래와 같이 사용자가 직접 구현해야 합니다. 여기서는 범용성을 위해 Non-Paged Pool을 사용하여 페이지-아웃 되지 않는 메모리 풀에서 메모리를 할당했습니다.

 

void* __cdecl operator new(
    __in const size_t size
    ) { 
    void* ptr = nullptr;
    if (size) { 
        ptr = ::ExAllocatePoolWithTag(NonPagedPool, size, __tag); 
    }
    return ptr;
}

void* __cdecl operator new[](
    __in const size_t size
    ) { 
    void* ptr = nullptr;
    if (size) { 
        ptr = ::ExAllocatePoolWithTag(NonPagedPool, size, __tag); 
    }
    return ptr;
}

void __cdecl operator delete(
    __in void* ptr
    ) {
    if (ptr) { 
        ::ExFreePoolWithTag(ptr, __tag); 
    }
}

void __cdecl operator delete[](
    __in void* ptr
    ) { 
    if (ptr) { 
        ::ExFreePoolWithTag(ptr, __tag); 
    }
}

 

추가로, 필요하다면 아래와 같이 POOL_TYPE을 선택할 수 있도록 연산자를 오버로딩 할 수 있습니다. 이 오버로딩 된 연산자는 커널 드라이버 개발에 적합하도록 Paged Pool 또는 Non-Paged Pool을 선택적으로 할당할 수 있도록 했습니다.

 

void* __cdecl operator new(
    __in const size_t size, 
    __in const POOL_TYPE pool_type
    ) { 
    void* ptr = nullptr;
    if (size) { 
        ptr = ::ExAllocatePoolWithTag(pool_type, size, __tag); 
    }
    return ptr;
}

void* __cdecl operator new[](
    __in const size_t size, 
    __in const POOL_TYPE pool_type
    ) { 
    void* ptr = nullptr;
    if (size) { 
        ptr = ::ExAllocatePoolWithTag(pool_type, size, __tag); 
    }
    return ptr;
}

 

이처럼 간단히 전역 메모리 할당 및 해제 연산자를 구현했습니다. 필요에 따라 'placement new' 연산자를 정의해야 할 수도 있으며, C++14/17/20 등으로 C++ 버전이 올라가면서 새롭게 도입되는 전역 연산자를 추가로 오버로딩해야 할 수도 있습니다.

 

이제는 객체가 Paged Pool에 할당되도록 강제하는 방법을 알아보겠습니다. 우리는 앞서 정의한 전역 new 연산자를 사용하여 객체를 Non-Paged Pool에 할당할 수도 있고, 오버로딩 된 new(POOL_TYPE) 연산자로 Paged Pool에 할당할 수도 있습니다. 하지만 특정 상황에서는 POOL_TYPE 선택의 여지를 주지 않고 Paged Pool에서 객체가 할당되도록 강제해야 하는 상황도 있을 수 있습니다. 이 경우에는 아래와 같은 base_paged_object 객체를 만들어 상속하는 방법을 권장합니다.

 

// base paged object.
class base_paged_object {

public:

    // constructor.
    base_paged_object() { PAGED_CODE(); }

    // destructor.
    virtual ~base_paged_object() {}

    //
    // operators.
    //

    void* operator new(
        __in const size_t size
        ) { 
        PAGED_CODE();
        void* ptr = nullptr;
        if (size) {
            ptr = ::ExAllocatePoolWithTag(PagedPool, size, __tag);
        }
        return ptr;
    }

    void* operator new[](
        __in const size_t size
        ) { 
        PAGED_CODE();
        void* ptr = nullptr;
        if (size) {
            ptr = ::ExAllocatePoolWithTag(PagedPool, size, __tag);
        }
        return ptr;
    }

    void operator delete(
        __in void* ptr
        ) {
        PAGED_CODE();
        if (ptr) {
            ::ExFreePoolWithTag(ptr, __tag);
        }
    }

    void operator delete[](
        __in void* ptr
        ) { 
        PAGED_CODE();
        if (ptr) {
            ::ExFreePoolWithTag(ptr, __tag);
        }
    }

    /* ... */
};

 

이렇게 정의한 base_paged_object 객체를 사용하는 방법은 간단합니다. Paged Pool에서 동작해야 할 객체를 만들 때 base_paged_object 객체를 상속받도록 하면 파생 클래스의 인스턴스가 Paged Pool에 할당됩니다.

 

// 사용자 정의 클래스 (Paged).
class my_paged_object: public base_paged_object { /* ... */ };

// PagedPool 에서 my_paged_object 객채가 할당 됨.
my_paged_object* obj = new my_paged_object;

 

Non-Paged Pool은 커널에서 사용하는 가상 메모리가 페이지 파일로 페이지-아웃 되지 않도록 보장하기 때문에 유용하게 사용되지만, 이러한 페이지-아웃 되지 않는 특성으로 인해 시스템의 물리 메모리를 지속적으로 점유하게 됩니다. 비록 하드웨어의 발전으로 시스템 물리 메모리의 용량이 충분해져 이러한 점유가 크게 문제되지 않는 경우도 있지만, 가능하면 메모리를 더욱 효율적으로 관리하기 위해 Paged Pool에 대한 지원 방법을 제공하는 편이 좋습니다. Paged Pool을 사용하면 일시적으로 사용하지 않는 메모리를 페이지 파일로 이동할 수 있어, 물리 메모리를 보다 유연하게 관리할 수 있기 때문입니다.

 

이로써 런타임의 동적 메모리 관리 기능이 구현되었으며, 이제는 커널에서 객체를 만들어 사용할 준비가 완료되었습니다. 이는 최소한의 설정으로 커널 드라이버에 객체지향 프로그래밍을 적용할 수 있는 방법입니다. 만약 이후에 설명할 내용들이 번거롭다면, 이 챕터의 내용을 구현하여 커널 드라이버에서 객체를 먼저 사용해 보기를 권장합니다. 이러한 접근은 C++의 객체지향 프로그래밍의 이점을 커널 환경에 적용해 볼 수 있는 좋은 출발점이 될 것입니다.

 

 

 

 


 

런타임: 정적 객체 지원

 

커널 환경에는 정적 객체를 생성하고 제거할 수 있는 기본적인 메커니즘이 없습니다. 따라서 우리는 정적 객체의 초기화 및 종료에 해당하는 런타임 기능을 직접 구현해야 합니다. 이 말이 일반적으로 잘 이해되지 않을 수 있으므로, 아래에 정적 객체를 지원하지 못하여 발생하는 오류를 보면서 내용을 이어가겠습니다.

 

// 사용자 객체 정의.
class my_object { /* ... */ };

// 정적 객체 선언.
static my_object my_static_object;

 

위와 같이 정적 객체를 선언하면 빌드 시 아래와 같은 에러가 발생하게 됩니다. 이 빌드 에러는 정적(static) 객체뿐 아니라 전역(global) 객체를 사용하더라도 동일하게 발생합니다.

 

 

1>------ Rebuild All started: Project: kmcpp, Configuration: Debug x64 ------

1>Building 'kmcpp' with toolset 'WindowsKernelModeDriver10.0' and the 'Desktop' target platform.
1>kmcpp.cpp

1>Generating Code...

1>kmcpp.obj : error LNK2019: unresolved external symbol atexit referenced in function "void __cdecl `dynamic initializer for 'my_static_object''(void)" (??__Emy_static_object@@YAXXZ)

1>.\bin\chk\x64\kmcpp\kmcpp.sys : fatal error LNK1120: 1 unresolved externals

1>Done building project "kmcpp.vcxproj" -- FAILED.

========== Rebuild All: 0 succeeded, 1 failed, 0 skipped ==========

 

 

만약 정적 객체를 사용하지 않아도 된다면 이 챕터의 내용은 생략해도 됩니다. 정적 객체의 대안으로써 전역으로 사용하려는 객체를 드라이버의 초기화 시점에 동적으로(new) 생성하여 사용하는 방법을 생각해 볼 수 있습니다. 하지만 정적 객체의 초기화 메커니즘을 구현하는 것이 크게 어렵지는 않으므로 가능한 구현을 고려해 보셨으면 합니다.

 

런타임은 정적 객체의 초기화 코드를 처리하기 위해 바이너리 이미지 내에 .CRT라는 특수 섹션을 사용합니다. 컴파일러가 초기화가 필요한 정적 객체를 발견하면 해당 객체의 동적 초기화 코드를 이 섹션에 배치합니다. 따라서 여기에 위치한 초기화 함수들을 실행하는 메커니즘을 제공하면 정적 객체의 초기화 지원이 완료됩니다.

 

먼저 우리의 커널 드라이버에 .CRT 섹션을 추가해 보겠습니다. 커널 드라이버는 기본적으로 .CRT 섹션이 존재하지 않으므로, 다음과 같이 커널 드라이버 코드에서 #pragma section 지시어를 통해 직접 .CRT 섹션을 추가하는 과정이 필요합니다. 이 코드는 C/C++의 초기화 및 종료를 지원하기 위한 .CRT 메모리 섹션을 정의하고, 이 섹션의 속성을 읽기 전용으로 지정하는 코드입니다.

 

//
// 초기화 함수 배치를 위한 읽기 전용 CRT 섹션 정의.
//

#pragma section(".CRT$XCA", long, read) // First C++ Initializer
#pragma section(".CRT$XCZ", long, read) // Last C++ Initializer

#pragma section(".CRT$XIA", long, read) // First C Initializer
#pragma section(".CRT$XIZ", long, read) // Last C Initializer

#pragma section(".CRT$XPA", long, read) // First Pre-Terminator
#pragma section(".CRT$XPZ", long, read) // Last Pre-Terminator

#pragma section(".CRT$XTA", long, read) // First Terminator
#pragma section(".CRT$XTZ", long, read) // Last Terminator

 

위에서 정의한 각 섹션의 역할은 다음과 같습니다.

 

  • .CRT$XIA, .CRT$XIZ: C 초기화 함수 포인터의 시작과 끝.
  • .CRT$XCA, .CRT$XCZ: C++ 초기화 함수 포인터의 시작과 끝.
  • .CRT$XPA, .CRT$XPZ: 프로그램 종료 전 호출되는 함수 포인터의 시작과 끝.
  • .CRT$XTA, .CRT$XTZ: 종료 함수 포인터의 시작과 끝.

 

다음으로, 위에서 정의한 .CRT 섹션에 배치될 동적 초기화 및 종료 함수의 형식을 정의합니다.

 

typedef void (__cdecl* _PVFV)(void);
typedef int  (__cdecl* _PIFV)(void);

 

이제 위의 초기화 함수를 각 섹션에 포인터 배열의 형태로 위치시키는 코드를 작성합니다. 아래와 같이 데이터 섹션을 지정하기 위해 #pragma data_seg 지시어를 사용하고, 뒤이어 데이터를 정의하면 해당 데이터는 직전에 지정한 섹션에 위치하게 됩니다.

 

//
// 초기화 함수의 배열 데이터를 각 CRT 섹션에 배치.
//

#pragma data_seg(".CRT$XIA")
_PIFV __xi_a[] = { nullptr };   // First C Initializer

#pragma data_seg(".CRT$XIZ")
_PIFV __xi_z[] = { nullptr };   // Last C Initializer

#pragma data_seg(".CRT$XCA")
_PVFV __xc_a[] = { nullptr };   // First C++ Initializer

#pragma data_seg(".CRT$XCZ")
_PVFV __xc_z[] = { nullptr };   // Last C++ Initializer

#pragma data_seg(".CRT$XPA")
_PVFV __xp_a[] = { nullptr };   // First Pre-Terminator

#pragma data_seg(".CRT$XPZ")
_PVFV __xp_z[] = { nullptr };   // Last Pre-Terminator

#pragma data_seg(".CRT$XTA")
_PVFV __xt_a[] = { nullptr };   // First Terminator

#pragma data_seg(".CRT$XTZ")
_PVFV __xt_z[] = { nullptr };   // Last Terminator

#pragma data_seg()  // (기본 상태로 복원)

 

여기에서 각 배열은 { nullptr }로 초기화되어 있습니다. 이는 배열이 함수 포인터로 채워져야 하지만 현재는 아무런 함수도 할당되지 않았음을 의미하며, 런타임 시 실제 함수의 주소가 할당되게 됩니다.

 

다음 지시어는 위에서 정의한 .CRT 섹션을 .rdata 섹션과 병합하도록 링커에 지시합니다. .rdata 섹션은 읽기 전용 데이터 섹션으로, 실행 중에 수정되지 않는 데이터를 저장하는 데 사용됩니다. 여기서는 .CRT 섹션에 위치하는 초기화 함수의 배열을 읽기 전용 데이터 영역에 배치하여, 실행 중에 데이터의 내용이 변경되지 않도록 합니다.

 

//
// 링커에 .CRT 와 .rdata 섹션의 병합을 지시.
//

#pragma comment(linker, "/merge:.CRT=.rdata")

 

다음은 런타임이 동적 초기화 과정에서 사용하는 함수인 _initterm과 _initterm_e를 정의하고 있습니다. 이 함수들은 앞서 정의한 동적 초기화 및 종료 함수의 배열을 순차적으로 호출하는 역할을 합니다. 원본 런타임 코드의 경로는 다음과 같습니다.

 

  • _initterm, _initterm_e 원본 코드:

    <Universal C Runtime>\ucrt\startup\initterm.cpp

 

//
// initterm.cpp
//
//      Copyright (c) Microsoft Corporation. All rights reserved.
//
// _initterm and _initterm_e functions used during dynamic initialization.
//
#include <corecrt_internal.h>

// Calls each function in [first, last).  [first, last) must be a valid range of
// function pointers.  Each function is called, in order.
extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last)
{
    for (_PVFV* it = first; it != last; ++it)
    {
        if (*it == nullptr)
            continue;

        (**it)();
    }
}

// Calls each function in [first, last).  [first, last) must be a valid range of
// function pointers.  Each function must return zero on success, nonzero on
// failure.  If any function returns nonzero, iteration stops immediately and
// the nonzero value is returned.  Otherwise all functions are called and zero
// is returned.
//
// If a nonzero value is returned, it is expected to be one of the runtime error
// values (_RT_{NAME}, defined in the internal header files).
extern "C" int __cdecl _initterm_e(_PIFV* const first, _PIFV* const last)
{
    for (_PIFV* it = first; it != last; ++it)
    {
        if (*it == nullptr)
            continue;

        int const result = (**it)();
        if (result != 0)
            return result;
    }

    return 0;
}

 

이상으로 정적 객체의 초기화 코드를 실행하기 위한 기능을 구현했습니다. 이제는 정적 객체의 종료 코드를 실행하기 위한 atexit 함수를 구현할 차례입니다. atexit 함수는 호출 시점에 종료 함수를 등록하고, 종료 시점에 등록된 종료 함수를 LIFO (Last-In, First-Out) 순으로 호출하는 기능을 가지고 있습니다. 런타임은 정적 객체를 생성하는 시점에 객체의 소멸자를 종료 함수에 등록하므로 이 atexit 함수를 반드시 구현해야 합니다.

 

  • atexit 원본 코드:

    <Visual Studio C Runtime>\crt\src\vcruntime\utility.cpp

 

atexit 함수의 원본 런타임 코드는 위의 경로에서 찾아볼 수 있지만, 다른 코드와의 의존성이 높아 원본 런타임 코드를 수정하는데 약간의 어려움이 있을 수 있습니다. 다음은 atexit 함수의 동작 원리만 차용하여 비교적 단순하게 재 구현한 코드이므로, 원본 런타임 코드의 수정이 번거롭다면 아래의 코드로 대체해 사용해도 됩니다.

 

//
// atexit 초기화/종료 지원.
//

static _PVFV* _exit_list = { nullptr };
static size_t _exit_list_count = 0;
static const size_t _max_exit_list = 256;

// atexit 초기화.
bool init_exit() {
    if (nullptr == _exit_list) {
        _exit_list = new _PVFV[_max_exit_list];
        if (_exit_list) {
            ::memset(_exit_list, 0, (sizeof(_PVFV) * _max_exit_list));
            _exit_list_count = 0;
            return true;
        }
    }
    return false;
}

// atexit 종료.
void do_exit() {
    if (_exit_list) {
        while (_exit_list_count) {
            _PVFV pfexit = _exit_list[--_exit_list_count];
            if (pfexit) { pfexit(); }
        }
        delete[] _exit_list;
    }
}

//
// atexit 구현부.
//

// simplified atexit.
extern "C" int __cdecl atexit(_PVFV const pfexit) {
    if (pfexit) {
        if (_exit_list_count < _max_exit_list) {
            _exit_list[_exit_list_count++] = pfexit;
            return 0; // succeed.
        }
    }
    return -1;
}

 

여기까지 정적 객체를 지원하기 위한 런타임 기능을 구현했습니다. 남은 문제는 드라이버에서 위의 초기화 및 종료 코드를 자동으로 실행하도록 할 수 있는 방법이 없다는 점입니다. 이 문제를 해결하기 위해서는 다음의 두 가지 방법 중 하나를 사용할 수 있습니다.

 

첫 번째는 DriverEntry 와 DriverUnload 함수의 래퍼를 만들고, 이 래퍼에서 정적 객체의 초기화 및 종료 코드를 실행하도록 하는 방법입니다. 두 번째 방법은 DriverEntry 와 DriverUnload 함수에서 명시적으로 정적 객체의 초기화 및 종료 코드를 실행하는 것입니다. 여기서는 보다 단순한 두 번째 방법의 예를 들겠습니다. 먼저 다음과 같은 CRT 초기화 및 종료 함수를 정의합니다.

 

// CRT 초기화 함수 정의.
bool my_crt_init() {
    if (init_exit()) {
        ::_initterm_e(__xi_a, __xi_z);
        ::_initterm(__xc_a, __xc_z);
        return true;
    }
    return false;
}

// CRT 종료 함수 정의.
void my_crt_exit() {
    ::_initterm(__xp_a, __xp_z);
    ::_initterm(__xt_a, __xt_z);
    do_exit();
}

 

이제는 위에서 정의한 초기화 및 종료 함수를 다음과 같이 DriverEntry와 DriverUnload에서 직접 호출하여 정적 객체가 올바르게 초기화되고 종료될 수 있도록 합니다.

 

// DriverEntry.
extern "C" NTSTATUS DriverEntry(
    __in PDRIVER_OBJECT DriverObject, 
    __in PUNICODE_STRING RegistryPath
    ) {

    NTSTATUS status = STATUS_SUCCESS;

    do {

        // CRT 초기화 진행.
        if (false == my_crt_init()) {
            status = STATUS_INSUFFICIENT_RESOURCES;
            break;
        }

        /* ... */

    } while (false);

    /* ... */

    return status;
}        

// DriverUnload.
VOID DriverUnload(
    __in PDRIVER_OBJECT DriverObject
    ) {

    /* ... */

    // CRT 종료 진행.
    my_crt_exit();
}

 

이상으로 정적 객체의 초기화와 종료를 지원하는 런타임 기능을 구현해 보았습니다. 혹시나 여러분이 정적 혹은 전역 객체를 자주 사용하지 않더라도, atexit 함수의 구현은 Singleton 홀더 등의 유틸리티를 만들 때에도 유용하게 사용될 수 있으므로 가능하면 이 챕터의 내용을 구현해 보시기를 권장합니다.

 

 

김진호

개발팀

기술과 자본의 스노우볼 효과를 믿으며, 작은 노력이 뭉쳐 큰 결과를 가져올 것임을 확신합니다

추천하는 영감

보안 전문가가 말하는 CC 인증 파헤쳐 보기

이 글은 AI가 작성했습니다_ 마케터의 위기 의식?

한바탕 휩쓴 방산 해킹 사건, 보안의 다른 '답'을 찾아야 할 때