기술아티클

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

2024.07.16






들어가며

 

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

 

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

 

 

 

※ 이 글은 Windows 커널에서 C++ 사용하기 (상) 후속 글입니다.


 

런타임: 예외 처리와 RTTI

 

예외(exception)를 커널에서 사용할지 여부는 논란의 여지가 있으며, 이 글을 읽는 분들 각자의 판단에 따라 적용 여부를 결정하시기 바랍니다. 커널에서 예외를 사용하지 않더라도 C++을 충분히 효과적으로 활용할 수 있으므로, 이 챕터는 참고 용도로만 봐주시기 바랍니다.

 

커널에서 예외를 사용하는 것에는 몇 가지 중요한 문제점이 있습니다. 첫째, 예외 처리는 커널 스택을 과도하게 사용하며, 이는 스택이 부족한 커널에서는 좋지 않은 영향을 줄 수 있습니다. 둘째, 핸들링 되지 않은 예외는 블루스크린을 발생시킬 수 있습니다. 셋째, 예외는 SEH (Structured Exception Handling)와 함께 사용할 수 없어서 일부 구간에서는 코드 작성 시 문제가 발생할 수 있습니다.

 

이러한 여러 문제들로 인해 Visual Studio에서는 기본적으로 /kernel 플래그를 사용하도록 강제되어 있으며 /kernel 플래그가 적용되면 아래와 같은 빌드 에러와 함께 예외 사용이 차단됩니다.

 

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>C:\kmcpp\source\sys\kmcpp.cpp(135,5): error C2980: C++ exception handling is not supported with /kernel

1>Generating Code...

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

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

 

Microsoft가 이와 같이 /kernel 플래그를 강제한 이유는 커널 환경의 특성과 관련이 있습니다. 앞서 언급했듯 커널은 작은 스택 크기를 가지고 있으며, 예외 처리가 필요로 하는 스택 사용량은 이러한 제한을 초과할 수 있습니다. 또한 예외 처리 메커니즘은 예외가 발생할 때마다 스택을 풀어내고(stack-unwinding), 예외 객체를 생성하고, 적절한 Catch 블록을 찾기 위해 RTTI를 적용하며 호출 스택을 거슬러 올라가는 복잡한 과정을 포함하기 때문에 Microsoft는 /kernel 플래그를 통해 커널에서 예외를 사용하지 말라는 입장을 명확히 했습니다.

 

필자는 여러 가지 이유에서 커널에서 예외의 사용을 선호하지 않지만, 혹시나 커널에서 예외 메커니즘의 구현을 결정했다면 아래의 방법으로 /kernel 플래그를 강제로 비활성 할 수 있습니다.

 

그림1. Property Manager 메뉴 위치

 

Visual Studio의 Property Manager 를 열어서 Property Sheet (.props) 파일을 생성합니다. 그리고 생성된 Property 파일을 열어서 아래와 같이 DisableKernelFlag 속성을 추가합니다. 이때, /kernel 플래그와 함께 정의되는 _KERNEL_MODE 정의가 사라지므로 주의가 필요합니다.

 

<?xml version="1.0" encoding="utf-8"?> 
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <DisableKernelFlag>true</DisableKernelFlag>
  </PropertyGroup>

</Project>

 

이렇게 생성된 Property Sheet 파일을 Property Manager를 통해 커널 드라이버 프로젝트와 연결하면 /kernel 플래그가 제거되면서 아래와 같이 빌드 에러가 변경됩니다.

 

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>C:\files\codes\kmcpp\source\sys\kmcpp.cpp(135,5): error C2220: the following warning is treated as an error

1>C:\files\codes\kmcpp\source\sys\kmcpp.cpp(135,5): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc

1>Generating Code...

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

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

 

에러 메시지의 안내대로, 예외 옵션의 활성화를 위해 아래와 같이 Project 속성을 변경합니다.

 

설정1. 예외 설정

 

이후의 빌드 메시지는 다음과 같이 변경됩니다.

 

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 _CxxThrowException referenced in function DriverEntry

1>kmcpp.obj : error LNK2001: unresolved external symbol __CxxFrameHandler4

1>kmcpp.obj : error LNK2001: unresolved external symbol "const type_info::`vftable'" (??_7type_info@@6B@)

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

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

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

 

이제는 참조된 함수인 _CxxThrowException, 그리고 컴파일러의 예외 설정에 따라 다르지만__CxxFrameHandler 및 __GSHandlerCheck 등의 기능을 순차적으로 구현하면 됩니다. 예외의 구현은 매우 복잡하지만, 다행히도 원본 런타임의 Platform 별 소스코드가 제공되고 있습니다. 이 소스코드 중 커널에서 현실적으로 유의미한 arm64 와 x64를 커널에서 동작할 수 있도록 수정하여 사용합니다.

 

  • ARM64 예외 원본 코드:
    <Visual Studio C Runtime>\crt\src\arm64\
     

  • x64 예외 원본 코드:
    <Visual Studio C Runtime>\crt\src\x64\

 

예외 메커니즘을 구현하는 것은 기술적으로 어렵고 플랫폼에 종속적인 작업입니다. 예외 처리는 커널 동작에 상당한 부담을 줄 수 있기 때문에, Windows 커널의 구조와 런타임의 동작 메커니즘을 정확히 이해하고 신중하게 사용할 필요가 있습니다. 아래는 커널에서 예외가 동작하는 모습입니다.

 

그림2. 예외 동작 모습

 

예외와 관련하여 RTTI에 대해서도 언급하겠습니다. RTTI는 런타임 시점에 객체의 타입을 확인할 수 있게 해주는 기능입니다. RTTI의 주요 구성 요소는 dynamic_cast 연산자와 typeid 연산자입니다.

 

  • dynamic_cast 연산자:
    dynamic_cast는 객체의 포인터나 참조를 베이스 클래스에서 파생 클래스로 변환할 때 변환 가능 여부를 런타임 시점에 검사합니다. 변환에 실패하면 포인터의 경우 nullptr을 반환하고, 참조의 경우는 bad_cast 예외를 throw 합니다.

     

  • typeid 연산자:
    typeid는 특정 타입에 대한 정보를 담고 있는 type_info 객체를 반환합니다. 이는 객체의 실제 타입을 판단할 때 사용됩니다.

 

예외 처리와 RTTI는 기능적으로 직접적인 연관성은 없지만, 어느 정도는 의존성이 있습니다. 예외 처리 과정에서 RTTI가 필수적이지는 않지만, catch 블록에서 예외 객체의 타입을 확인할 때 RTTI가 선택적으로 사용될 수 있습니다. 또한, RTTI 기능만으로는 예외 처리를 필요로 하지 않지만, dynamic_cast를 사용할 때 bad_cast 예외가 발생할 수 있어 예외 처리와 연결됩니다. 따라서 예외를 구현한 경우에는 비교적 구현이 간단한 RTTI도 함께 고려해 보시기 바라며, 예외를 사용하지 않는다면 특별한 이유가 없는 한 RTTI를 구현할 필요는 없어 보입니다.

 

이제 RTTI를 구현하는 방법에 대해 알아보겠습니다. RTTI를 사용하기 위해서는 아래와 같이 Project 속성에서 RTTI를 활성화해야 합니다.

 

설정2. RTTI 설정

 

이때, /kernel 플래그가 활성화되어 있다면 아래와 같이 빌드 에러가 발생합니다. RTTI를 사용하려면 앞서 예외의 경우와 마찬가지로 /kernel 플래그를 비활성 해야 합니다. 이는 RTTI가 예외와 같이 커널에서 성능상의 문제가 발생할 여지가 있음을 암시하는 것일 수도 있고, RTTI 내에서 bad_cast 예외를 throw 하기 때문일 수도 있습니다. 어떤 이유이건 예외와 RTTI는 모두 /kernel 플래그가 비활성화되어 있을 때만 사용할 수 있습니다. 따라서 현실적으로 이 두 기능을 함께 사용하거나, 혹은 둘 다 사용하지 않는 것이 합리적입니다.

 

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

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

1>cl : command line  error D8016: '/GR' and '/kernel' command-line options are incompatible

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

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

 

예외 메커니즘을 구현할 때와 같은 방법으로 /kernel 플래그를 제거한 후, RTTI를 사용하기 위한 dynamic_cast 코드를 빌드하면 아래와 같이 빌드 에러가 변경됩니다.

 

1>------ Build started: Project: kmcpp, Configuration: Debug x64 ------

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

1>kmcpp.cpp

1>kmcpp.obj : error LNK2019: unresolved external symbol __RTtypeid referenced in function DriverEntry

1>kmcpp.obj : error LNK2019: unresolved external symbol __RTDynamicCast referenced in function DriverEntry

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

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

========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

 

이제 __RTtypeid, __RTDynamicCast 등의 RTTI관련 구현부를 작성할 준비가 되었으며, 예외와 마찬가지로 원본 런타임 코드를 커널 환경에 맞게 수정하여 사용하면 됩니다. 수정해서 사용해야 할 소스코드의 경로는 다음과 같습니다.

 

  • RTTI 원본 코드:
    <Visual Studio C Runtime>\crt\src\vcruntime\rtti.cpp

 

RTTI의 구현이 완료되면 dynamic_cast의 사용은 가능해지나, typeid를 사용하면 또다시 다음과 같은 빌드 에러가 발생합니다. 이는 type_info 객체가 정의되지 않아서 생기는 오류입니다.

 

1>------ Build started: Project: kmcpp, Configuration: Debug x64 ------

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

1>kmcpp.cpp

1>C:\files\codes\kmcpp\source\sys\kmcpp.cpp(166,44): error C2027: use of undefined type 'type_info'

1>C:\files\codes\kmcpp\source\project\predefined C++ types (compiler internal)(215): message : see declaration of 'type_info'

1>C:\files\codes\kmcpp\source\sys\kmcpp.cpp(167,25): error C3536: 'name': cannot be used before it is initialized

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

========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

 

type_info의 원본 런타임 코드의 위치는 다음과 같습니다. 원본 런타임 코드를 수정하기가 번거롭다면, 이어지는 단순화된 코드를 대신해서 사용해도 됩니다. 이 코드는 일부 기능을 지원하지 않지만, 일반적인 용도로 사용하기에는 충분하므로 적용을 고려해 보시기 바랍니다.

 

  • type_info 원본 코드:
    <Visual Studio C Runtime>\include\vcruntime_typeinfo.h

 

// simplified type info.
class type_info { 

public: 

    typedef type_info this_type;

    // constructor (copy / deleted).
    type_info(
        __in const this_type& other
        ) = delete;

    // destructor.
    virtual ~type_info() {}

    // copy operator (deleted).
    this_type& operator =(
        __in const this_type& other
        ) = delete;

    // operator ==.
    bool operator ==(
        __in const this_type& other
        ) const {
        return (0 == ::strcmp(&other._name[1], &_name[1])); 
    }

    // operator !=.
    bool operator !=(
        __in const this_type& other
        ) const {
        return (0 != ::strcmp(&other._name[1], &_name[1])); 
    }

    // before.
    bool before(
        __in const this_type& other
        ) const {
        return (0 < ::strcmp(&other._name[1], &_name[1])); 
    }

    // get raw type name.
    const char* name() const {
        return _name; 
    }

private: 

    void* _data;
    char _name[1]; 
};

 

이상으로 /kernel 플래그를 해제해야만 사용이 가능한 예외와 RTTI의 구현 방법에 대해 알아보았습니다. Microsoft가 /kernel 플래그로 이러한 기능들의 사용을 제한하는 것에서 알 수 있듯이, 커널에서 이 기능들을 사용하는 것에 대해서는 여전히 논란이 있습니다. 자사의 드라이버는 이 기능들을 사용하지 않지만, 기술적인 구현 가능성을 알리고자 이 주제를 다루었습니다.

 

 

 

 


 

Windows 커널에서 STL 사용

 

C++의 핵심 기능 중 하나인 STL 컴포넌트를 커널에서 사용하는 방법을 알아보겠습니다. 분량상 STL 변환의 과정을 다루지는 못하겠지만, 가능한 유효한 내용을 언급하고자 합니다. 주요 목표는 원본 STL 코드를 커널에서 동작하도록 수정하는 것입니다. STL 내의 표준 템플릿 기능은 대부분 별도의 수정 없이도 커널에서 사용할 수 있기 때문입니다. 수정이 필요한 부분은 시스템 기능 관련 기능으로, 파일, 스레드, 동기화 객체 등이 이에 해당합니다. 아래는 원본 STL 코드 경로입니다.

 

  • 원본 STL 헤더:
    <Visual Studio C Runtime>\include\

     

  • 원본 STL 소스:
    <Visual Studio C Runtime>\crt\src\stl\

 

 

STL: 런타임 확장

 

STL 내에서는 런타임 라이브러리를 광범위하게 사용하지만, 커널 드라이버에 제공되는 런타임 기능은 상당히 제약적입니다. 이에 WDK (Windows Driver Kit)는 libcntpr.lib 라이브러리를 통해 커널에서 부족한 런타임 기능의 일부를 지원하고 있습니다만, 특이하게도 libcntpr.lib는 Visual Studio 드라이버 프로젝트의 기본 설정으로는 연결이 되어있지 않습니다. 따라서 STL 컴포넌트를 커널 코드로 변환하기 전에, 드라이버 개발 프로젝트의 링크 목록에 libcntpr.lib 라이브러리를 추가하는 것이 좋습니다.

 

커널 개발 프로젝트에서는 #pragma comment(lib, “libcntpr.lib”) 와 같은 링크 지시어가 동작하지 않으므로, 아래와 같이 프로젝트의 링크 설정에서 직접 라이브러리를 추가해야 합니다.

 

설정3. libcntpr.lib 연결 설정

 

libcntpr.lib 라이브러리는 대략 다음과 같은 기능을 가지고 있습니다.

 

  • 커널 모드 호환성: libcntpr.lib은 커널 모드에서 사용하기 위한 목적으로 만들어졌습니다.
  • 런타임 함수 지원: _memicmp, _atoi64, _strtoi64 등의 일부 런타임 함수가 포함되어 있습니다.
  • 부동소수점 지원: _fltused 변수를 extern 하고 있어서 부동소수점 연산이 가능하도록 합니다.

 

위의 부동소수점 지원 부분은 조금 생소할 수도 있어 내용을 추가하면, 런타임은 float, double 타입의 연산을 진행할 때 부동소수점 연산이 활성화가 되었는지 판단하기 위해서 _fltused 변수를 참조하게 됩니다.

 

 

float f = 0.2f / 0.1f;
double d = 0.2 / 0.1;

 

예를 들어 _fltused 변수를 정의하지 않은 채로 위의 코드가 포함된 커널 드라이버를 빌드하면 아래와 같은 빌드 에러가 발생하게 됩니다.

 

1>------ Build started: Project: kmcpp, Configuration: Debug x64 ------

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

1>kmcpp.cpp

1>kmcpp.obj : error LNK2001: unresolved external symbol _fltused

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

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

========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

 

이 빌드 에러를 제거하려면 아래와 같이 _fltused 변수를 정의하거나, libcntpr.lib 라이브러리를 링크 목록에 포함하면 이 문제가 해결됩니다.

 

extern "C" int _fltused { 0x9875 };

 

STL 컴포넌트를 커널에서 사용하려고 시도하다 보면 libcntpr.lib 에서도 구현되지 않은 몇몇 런타임 기능을 필요로 합니다. 이때는 원본 런타임 코드를 참조하거나 오픈소스 프로젝트에서 해당 부분을 가져올 수 있습니다. 또한, 커널에서는 사용자 모드 API 대신 커널 API를 사용해야 하므로, 파일, 스레드, 동기화 객체 등의 시스템 기능을 적절히 변환하는 작업이 필요합니다. 이러한 과정은 커널에서의 개발 경험이 있다면 크게 어렵지 않을 것이라 생각합니다.

 

 

STL: 예외 제거 설정

 

일반적으로 STL 컴포넌트는 예외가 활성화된 상태를 전제로 동작합니다. 따라서 예외의 구현 유무에 따라 커널로의 변환 과정이 달라져야 합니다. 커널 환경에서 예외를 사용하기로 결정했다면, 구현한 예외 메커니즘이 정상적으로 동작하는 한 STL 컴포넌트에서 발생하는 예외에 대해 그다지 신경 쓸 필요가 없습니다. 하지만 예외를 사용하지 않기로 결정했다면, STL 컴포넌트가 발생시키는 수많은 예외 코드를 어떻게 제거해야 할지 고민될 수 있습니다. 이번에는 이 주제에 대해 다뤄보겠습니다.

 

//
// vcruntime.h
//
//      Copyright (c) Microsoft Corporation. All rights reserved.
//
// Declarations used throughout the VCRuntime library.
//

#ifndef _HAS_EXCEPTIONS // Predefine as 0 to disable exceptions
    #ifdef _KERNEL_MODE
        #define _HAS_EXCEPTIONS 0
    #else
        #define _HAS_EXCEPTIONS 1
    #endif /* _KERNEL_MODE */
#endif /* _HAS_EXCEPTIONS */

 

위의 원본 STL 헤더에서 찾을 수 있는 _HAS_EXCEPTIONS 정의를 보면, /kernel 플래그에 의해 설정되는 _KERNEL_MODE 정의의 유무에 따라 예외의 사용 여부를 결정하는 _HAS_EXCEPTIONS 정의의 값이 변경되는 것을 볼 수 있습니다. 여기에서 우리는 몇 가지 중요한 점을 눈치채야 합니다.

 

첫째, _KERNEL_MODE 정의는 /kernel 플래그의 유무에 따라 결정됩니다. 앞서 런타임 챕터에서 언급했듯이, /kernel 플래그가 활성화되면 예외 사용이 강제로 비활성화되면서 _KERNEL_MODE 정의가 설정됩니다. 이로 인해 위의 헤더 파일에서 _HAS_EXCEPTION 정의가 사라지면서, STL 컴포넌트 내에서는 예외가 동작하지 않게 됩니다. 이는 우리가 커널에서 예외를 사용하지 않겠다고 선택한 경우에도, STL 컴포넌트에서 예외를 사용하지 않도록 별도로 설정할 필요가 없음을 의미합니다.

 

둘째, Microsoft는 커널에서 C++의 사용을 권장하지는 않았지만, 사용자가 원하면 C++을 사용할 수 있는 방법을 열어둔 채 STL을 개발했습니다. 이것이 Microsoft 문서에서 언급된 커널에서의 C++ 지원을 위한 중간 단계인지는 확실하지 않지만, 사용자의 선택에 따라 C++을 적절히 활용하라는 의미로 볼 수 있습니다. 중요한 점은, Microsoft의 암묵적인 동의하에 약간의 노력만으로도 커널에서 STL 컴포넌트를 사용할 수 있다는 점입니다.

 

Microsoft의 STL은 예외의 사용 여부에 따라서, 아래와 같이 try-throw-catch 매크로의 동작이 다르게 정의하고 있습니다. 이로 인해 예외를 사용하지 않더라도 STL 컴포넌트를 커널에서 동작하도록 수정하는 과정이 크게 어렵지 않게 되었습니다.

 

//
// xstddef 헤더.
//

#if _HAS_EXCEPTIONS

#define _TRY_BEGIN  try {
#define _CATCH(x)   } catch (x) {
#define _CATCH_ALL  } catch (...) {
#define _CATCH_END  }
#define _RAISE(x)   throw (x)
/* ... */

#else /* no exceptions */

#define _TRY_BEGIN  {{
#define _CATCH(x)   } if (0) {
#define _CATCH_ALL  } if (0) {
#define _CATCH_END  }}
#define _RAISE(x)   (x)
/* ... */

#endif /* _HAS_EXCEPTIONS */

//
// yvals.h 헤더.
//

#ifdef _DEBUG
#define _RAISE(x) _invoke_watson(_CRT_WIDE(#x), __FUNCTIONW__, __FILEW__, __LINE__, 0)
#else
#define _RAISE(x) _invoke_watson(nullptr, nullptr, nullptr, 0, 0)
#endif

 

_HAS_EXCEPTIONS 정의가 사라지면 _RAISE 매크로가 throw 대신 _invoke_watson 함수를 호출하도록 동작합니다. 이 _invoke_watson 함수는 커널 환경에는 구현되어 있지 않으므로, 아래와 같이 구현부를 직접 작성해야 합니다. 여기서는 별다른 기능 없이 디버깅 트랩만 발생시키도록 하였습니다.

 

extern "C" void __cdecl _invoke_watson(
    const void* pszExpression,
    const void* pszFunction,
    const void* pszFile,
    unsigned int nLine,
    uintptr_t pReserved
    ) {

    UNREFERENCED_PARAMETER(pszExpression);
    UNREFERENCED_PARAMETER(pszFunction);
    UNREFERENCED_PARAMETER(pszFile);
    UNREFERENCED_PARAMETER(nLine);
    UNREFERENCED_PARAMETER(pReserved);

    NT_ASSERT(0);
}

 

위의 _invoke_watson 함수는 원래의 예외 처리 기능을 완전히 대체하지는 못합니다. 예외를 사용하지 않는 환경에서는 객체의 생성자에서 발생하는 오류, 혹은 컨테이너에 요소 삽입 시 발생할 수 있는 자원 부족에 대한 보고를 받을 수 없는 문제를 보안하기 위한 추가적인 검증 기능을 구현해야 합니다.

 

예를 들어, 객체 생성자의 기능이 정상적으로 완료됐는지를 확인하기 위한 상태 검증 기능을 추가하거나, 컨테이너 사용 시 요소의 삽입 전/후로 컨테이너의 크기(size)를 비교하도록 동작하는 오류 검증 기능을 추가할 수 있습니다. 또한, 런타임의 errno 기능을 구현하고, 이것을 활용하는 방법도 생각해볼 수 있을 것 같습니다. 이처럼 객체의 검증 기능을 구현하는 데는 다양한 방법이 있을 수 있으므로, 각자의 방법을 연구하고 적용해 보시기 바랍니다.

 

 

STL: Paged Allocator 적용

 

이번에는 Paged Pool을 사용하는 std::allocator 객체를 구현하고 이를 STL 컨테이너에 적용하는 방법을 알아보겠습니다. std::allocator 객체는 STL 컨테이너가 동적 메모리를 할당할 때 사용하는 기본 메모리 할당자 입니다.

 

앞서 정의한 전역 new, delete 연산자가 기본적으로 Non-Paged Pool을 사용하도록 구현되었기 때문에, std::allocator 객체 역시도 Non-Paged Pool을 할당하도록 동작합니다. 그리고 STL 컨테이너는 데이터를 저장하기 위한 메모리를 할당하고 해제할 때 std::allocator 객체를 경유하므로, 결국 STL 컨테이너에서 사용되는 모든 메모리는 Non-Paged Pool에 할당되게 됩니다.

 

STL 컨테이너가 Paged Pool에 메모리를 할당할 수 있도록 하려면, Paged Pool을 사용하는 추가적인 allocator 객체를 정의하고 STL 컨테이너에서 이를 사용하도록 정의해야 합니다. 우리는 원본 STL의 std::allocator 객체를 복사하고, 원본의 형태를 유지한 채 메모리 할당 및 해제 부분만 수정할 것입니다. 복사한 객체의 이름을 paged_allocator로 명명하여 원본 객체와 구분하겠습니다. 원본의 형태를 유지하는 이유는 paged_allocator 객체를 여러 STL 컨테이너에서 사용해도 문제가 발생하지 않도록 보장하기 위한 것입니다. 원본 std::allocator 객체의 코드는 다음 위치에 있습니다.

 

  • std::allocator 원본 코드:
    <Visual Studio C Runtime>\include\xmemory

 

이어서 paged_allocator 객체에서 실제 메모리의 할당 및 해제를 수행하는 allocate와 deallocate 멤버를 아래와 같이 수정합니다.

 

// generic paged allocator.
template <class T> class paged_allocator {

public:

    /* ... */

    // deallocate object at _Ptr
    void deallocate(
        __in pointer _Ptr, 
        __in size_type _Count
        ) {
        UNREFERENCED_PARAMETER(_Count);
        ::operator delete(_Ptr, PagedPool, __tag);
    }

    // allocate array of _Count elements
    _DECLSPEC_ALLOCATOR pointer allocate(
        __in size_type _Count
        ) {
        pointer ptr = nullptr;
        if (_Count) {
            ptr = reinterpret_cast<pointer>(
                    ::operator new(_Count * sizeof(value_type), PagedPool, __tag)
                    );
        }
        return ptr;
    }

    /* ... */
};

 

이렇게 정의한 paged_allocator를 STL 컨테이너에 적용하는 방법을 알아보겠습니다. 먼저, std::string에서 Paged Pool을 사용하도록 설정하는 방법입니다. 아래와 같이 std::basic_string에서 새로 만든 paged_allocator를 사용하도록 재정의(typedef) 하면 std::paged_string을 사용할 수 있습니다.

 

// paged string.
typedef basic_string<char, char_traits<char>, paged_allocator<char>> 
        paged_string;

// paged wstring.
typedef basic_string<wchar_t, char_traits<wchar_t>, paged_allocator<wchar_t>> 
        paged_wstring;

 

다음은 std::list, std::map과 같이 템플릿 인자를 필요로 하는 컨테이너에 paged_allocator를 적용하는 방법입니다. 여기서는 typedef 대신 C++11 이상에서 지원하는 Alias Template을 사용하여 컨테이너를 재정의하였습니다. Alias Template은 using 키워드를 사용하여 정의할 수 있습니다.

 

// paged list.
template <class _Ty> using paged_list = 
    list<_Ty, paged_allocator<_Ty>>;

// paged map.
template <class _Kty, class _Ty, class _Pr = less<_Kty>> using paged_map = 
    map<_Kty, _Ty, _Pr, paged_allocator<pair<const _Kty, _Ty>>>;

 

여기서 예를 들지 않은 다른 STL 컨테이너들도 위의 두 가지 방법 중 하나를 사용하여 Paged Pool을 할당하도록 정의하여 사용할 수 있습니다.

 

모든 STL 컴포넌트를 커널로 변환하는 작업은 많은 시간이 소요될 수 있습니다. 하지만 필요한 STL 컴포넌트를 커널 환경에서 동작하도록 점진적으로 확장하고, 실제 사용 환경에서 적용하는 과정을 꾸준히 반복하다 보면 머지않아 커널에서의 개발 효율성이 높아지는 동시에 사람의 실수를 줄이고 시스템 안정성을 높이는 데 STL이 큰 역할을 할 것이라 생각합니다.

 

 

 

선택은 여러분의 몫

 

결국, C++를 사용하여 커널 드라이버를 개발할지 여부는 유연한 사고와 기술에 대한 의욕, 유지 보수 편의와 안정성 향상을 위한 노력에 달려 있다고 생각합니다. C++의 강점을 활용하면 보다 향상된 유지 보수성, 확장성 및 안정성을 얻을 수 있지만, 커널에서의 C++ 사용에는 추가적인 초기 노력이 필요합니다. 이러한 초기의 어려움을 감내할 준비가 되어 있다면, 커널에서도 C++는 분명 강력한 도구가 될 것이며 의미 있는 선택이 될 수 있습니다. 선택은 여러분의 몫입니다.

 

 

📝 3줄 요약

 

  1. 커널에서 new, delete 연산자만 정의하고 사용해도 충분히 좋음

  2. 검증된 객체가 누적될수록 커널 드라이버의 개발과 유지 보수가 쉬워짐

  3. 노력에 따라 더 많은 C++ 기능을 커널에서 사용할 수 있지만 잘 알고 써야 함

 

 

김진호

개발팀

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

추천하는 영감

사이버 공격, AI for Security로 답을 찾자

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

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