C++로 윈도우즈 커널 드라이버를 개발할 때의 이슈에 관한 오래된 글 하나를 번역했습니다.
한마디로 요약하면...
쓰고 싶으면 on your risk로 사용해라. 우린 책임 안진다.
이정도 되겠네요.
몇가지 포인트를 더 짚어보자면... 이정도 되겠습니다.
커널모드 드라이버를 개발할 때는 가급적 C++ 의 "고급" 기능은 사용하지 말고 기본적인 기능만 써라.
사용해도 되는 기능
그냥 C++ 컴파일러를 "Super-C"로 사용하는 것은 괜찮다.
C로 명시적인 번역이 가능한 기능들 (POD : Plain Old Data) : 멤버 함수와 멤버 변수가 있는 단순한 클래스 정도는 써도 된다네요.
가급적 안쓰는게 좋은 기능
(도대체 뭘 쓰라는 건지)클래스의 상속
템플릿 클래스
new / delete
가상함수
캐스트 및 할당 연산자
C++ 스타일의 예외 처리 (try ~ catch)
클래스 인스턴스를 전역으로 선언한 경우 생성자가 호출되지 않습니다. (실제로 해보면... 호출 안되는게 아니라 그냥 빌드가 안됩니다만... 이건 DDK 버전에 따라 다를지도 모르겠네요.)
개발자가 빌드된 결과 오브젝트 코드가 커널 환경에 적합하게 생성되었는지 직접 검토해야 한다는 얘기를 끊임없이 하고 있네요.
(뭐라구?)
본문에서 상기 C++ 고급 기능들을 쓰지 말라는 이유 중 가장 큰 건 메모리 페이징 문제입니다.
1. 개발자가 명시적으로 작성하지 않은 컴파일러가 추가해주는 코드들은 어느 메모리 영역에 생성될 지 개발자가 컨트롤할 수 없음.
2. 코드가 개발자의 의도와 다르게 NonPaged 메모리에 올라가야 할 코드가 Paged 메모리에 로딩되면 DISPATCH_LEVEL에서 문제가 발생할 수 있음.
원문 : http://download.microsoft.com/download/5/b/5/5b5bec17-ea71-4653-9539-204a672f11cf/kmcode.doc
1. 번역자 : kuaaan (kuaaan at naver dot com)
2. 출처의 기재 여부와 상관 없이 재배포를 금지합니다. 참고하길 원하시는 분께서는 URL로 링크를 거시기 바랍니다. (번역이 잘못된 부분 등이 발견되었을 때 원본을 수정하기 위해서입니다.)
3. 오역된 부분은 알려주시면 즉시 반영하겠습니다.
4. 번역된 내용에 대해 번역자는 일체 책임을 지지 않습니다.
C++ for Kernel Mode Drivers: Pros and Cons
Introduction
객체지향적인 특징을 가진 C++은 의미론적인 면에서 Microsoft® Windows® Driver Model (WDM) 및 Windows Driver Foundation (WDF) 와 잘 어울려 보이고, 개발자에게 제공되는 추가적인 편의성과 표현력이 매력적이기도 합니다. 하지만 현재 이용 가능한 Microsoft 컴파일러로 커널모드 C++ 드라이버를 작성하는 경우 일부 기술적인 이슈들이 문제를 야기할 소지가 있습니다.
많은 개발자들은 C++ 컴파일러가 표준 C 컴파일러보다 특정 규칙들을 보다 엄격하게 적용하고 드라이버의 맥락에서 사용하기에 안전한 몇 가지 추가 기능을 제공하기 때문에 전체 C++ 언어를 사용하지 않고 C++ 컴파일러를 “super C"로 사용합니다 . 이 방법으로 C++ 컴파일러를 사용하면 일반적으로 커널 모드 코드에서 잘 작동 할 것으로 예상됩니다. 커널 모드 코드에서 문제점을 나타내는 것들은 non-POD (POD : C++ 표준에 정의 된 "plain old data") 클래스 및 상속, 템플릿 및 예외와 같은 "고급" C++ 기능입니다. 이러한 문제는 C++ 언어의 고유 속성 때문이라기보다는 C++ 의 구현과 커널 환경 때문입니다.
Microsoft는 C++로 윈도우즈 운영체제 군의 커널모드 드라이버를 작성하는 일과 관련된 이슈를 조사하고 있습니다. 이 백서는 C++로 드라이버를 작성하는 과정에서 경험하는 trade-off와 관련된 Microsoft 개발자들의 경험을 공유합니다.
이 백서의 정보는 Windows Server 2003 서비스 팩 1 (SP1) DDK에서 커널 모드 드라이버를 만들기위한 표준 Windows 드라이버 개발 키트 (DDK) 빌드 환경에 적용됩니다. DDK 또는 Longhorn Driver Kit (LDK)와 함께 제공되는 컴파일 환경이나 컴파일러 이외의 컴파일러를 사용하는 경우 여기에 언급 된 문제가 개발 환경에 적용되는지 여부 및 추가 문제가 있는지 여부를 결정해야 합니다. 이것을 결정하는 정보는 컴파일러 제공 업체의 설명서로 사용할 수 있지만 아래에 설명 된 것처럼 생성 된 코드와 링크 map을 검사해야 할 가능성이 더 큽니다.
이 백서에서는 C++로 커널 모드 드라이버를 작성하는 방법을 설명하지 않습니다. 이 백서에서는 커널 모드 드라이버 작성의 기본 원칙을 이해하고 있다고 가정합니다. 커널 모드 드라이버 작성에 대한 일반 정보는 커널 모드 아키텍처 안내서 및 Windows DDK 문서의 장치 별 정보를 참조하십시오.
커널모드 코드는 데이터가 손상되거나, 시스템이 불안정해지거나, 운영 체계가 crash되는 일이 없도록 하기 위해 다음과 같은 사항들을 고려해야 합니다.
커널은 내부적으로 메모리 페이지를 관리합니다.
시스템이 올바르게 동작하는 동시에 메모리를 최소한으로 사용하게 하기 위해 두가지 상반된 요구사항을 관리해야 합니다.
페이징이 허용되지 않을 때 코드를 실행하려면 코드와 데이터가 메모리에 있어야 합니다. 즉, 시스템이 IRQL DISPATCH_LEVEL 이상에서 실행될 때 현재 실행중인 루틴, 호출하는 루틴 또는 액세스하는 데이터를 포함하는 페이지 (그리고 나머지 함수 호출 체인 전체)는 IRQL이 DISPATCH_LEVEL 아래로 떨어질 때까지 메모리에 Lock되어 있어야 합니다. 그렇지 않으면 페이지 폴트가 발생하고 시스템이 Crash됩니다.
사용자 응용 프로그램에서 사용할 수있는 메모리 양을 늘리려면 드라이버가 코드 및 데이터 세그먼트를 가급적이면 Page 가능하게 만들어야 합니다. 이렇게하면 시스템 성능이 향상 될 수 있습니다.
프로세서의 모든 리소스가 항상 사용 가능하지는 않습니다.
x86 시스템에서 부동 소수점 및 멀티미디어 단위는 명시적으로 요청된 경우를 제외하면 커널 모드에서 사용할 수 없습니다. 부적절하게 사용하려고 시도하면 높은 IRQL에서 부동 소수점 fault가 발생하여 시스템이 crash될 수도 있고, fault가 발생하지 않더라도 임의 프로세스에서 데이터 손상이 발생할 수 있습니다. 부적절한 부동 소수점 사용은 다른 프로세스에서도 데이터 손상을 일으킬 수 있습니다. 이러한 문제는 디버그하기 어려운 경우가 많습니다.
Intel Itanium 시스템에서는 일부 부동 소수점 레지스터를 사용할 수 없습니다.
리소스, 특히 스택은 엄격하게 제한되어 있습니다. 사용자 공간에서 "저렴"한 자원이더라도 커널에서는 값이 비싸거나 획득하기 위해 또다른 방법이 필요할 수 있습니다. 특히, 커널 스택의 크기는 3 페이지에 불과합니다.
모든 표준 라이브러리 (C 또는 C++)가 커널 모드에서 지원되는 것은 아닙니다.
커널 모드 빌드 환경과 함께 제공되는 C / C++ 표준 라이브러리는 Win32® API 에 종속적이지 않고 커널 모드 요구사항에 맞추어 작성되어야 하기 때문에 그 버전이 유저 모드의 것과 일치하지 않을 수도 있습니다. 커널모드 표준 라이브러리는 기능이 제한적이거나 커널 모드의 다른 속성에 의해 제한적으로 구현될 수도 있습니다.
유저 모드에서 구현된 라이브러리 루틴은 커널 모드에서 작동하지 않을 수 있습니다. 일부는 link되지 않을수도 있고, 일부는 실행되지 않을수도 있으며, 일부는 동작하는 것처럼 보이지만 의도하지 않은 부작용이 있을 수도 있습니다.
C++ 컴파일러를 사용해 커널모드 코드 작성하기
컴파일러가 올바른 오브젝트 코드를 생성했더라도, 그것이 당신이 예상했던 코드가 아니거나 당신이 예상한대로 조합된 코드가 아닐 수도 있다는 사실을 기억해야 합니다. C에서도 그렇기는 하지만, C보다는 C++에서 문제가 될 가능성이 더 큽니다. 당신은 오브젝트 코드가 당신의 예상대로 생성되었는지를 검사하거나, 적어도 커널 환경에서 올바르게 동작하는지를 확인해야 합니다.
현재 사용 가능한 C++ 컴파일러의 결과물은 모든 플랫폼 및 윈도우즈 버전에서 커널 모드로 올바르게 작동한다고 보증할 수 없습니다. C++ 의 "고급" 기능을 사용한 코드일수록 상호 운용성 문제가 발생할 위험이 커집니다.
커널모드 코드의 핵심 영역
다음 영역은 커널 모드 드라이버에서 특별한 주의가 필요합니다. 이것은 두 언어 (C 및 C++) 모두에 적용되지만, C++ 컴파일러가 자동으로 더 많은 작업을 수행하기 때문에 문제가 발생했음을 알아차리기 어렵다는 면에서, C++ 코드의 문제 발생 가능성이 더 높습니다.
부동 소수점 명령어는 적절하게 보호되어야합니다 (예 : KeSaveFloatingPointState 및 KeRestoreFloatingPointState 또는 Windows DDK 문서에 설명 된 다른 메커니즘).
InterlockedXxx 함수는 생성 된 코드에 Memory Barrier Instruction을 삽입해야합니다. 컴파일 결과물을 점검하여 필요한 보호 barrier가 생성되었는지 확인하십시오. (역자 주 : memory barrier 는 cpu에게 특정 연산의 순서를 강제하도록 하는 insturction을 말함)
volatile 키워드의 의미를 정확하게 이해하여, volatile object에 대해 의도된 만큼의 indirection (역자 주 : indirection = dereferencing)이 일어나도록 해야 합니다. 때로는 volatile 항목이 포인터이기도 하고 때로는 객체 자체이기도하고 때로는 포인터와 객체가 모두 volatile일 수도 있습니다. volatile 키워드를 잘못 적용하는 것은 흔히 발생하는 오류이므로 이 키워드 사용 시 조심스럽게 검토하십시오. 예를 들어 volatile 위치에 대한 non-volatile 포인터를 사용하려는 경우, 코드가 non-volatile 위치에 volatile 포인터를 사용하지 않도록 주의 깊게 코드를 검토하해야 합니다.
스택 프레임이 엄격하게 제한됩니다. 예를 들어 x86 시스템에서 스레드 당 사용가능한 전체 스택은 12K입니다.
함수 소스코드에서 불분명하게 점프하거나 메모리를 사용하면 예상치 못한 페이지 폴트가 발생할 수 있습니다. 특히, 컴파일러는 (사용 시점에서) 메모리에 존재하는지 여부가 즉시 확인되지 않는 함수나 데이터 개체를 생성할 수 있습니다. 예상치 못한 개체에 대한 자세한 내용은 뒤에 나오는 “Code in Memory” 부분을 참조하십시오.
코드가 메모리에 남아있도록 하기 위해 인라인 함수 (및 __forceinline)를 사용하는 경우 그 결과는 컴파일러의 최적화 규칙과 관련되어 있습니다.
인라인 될 것으로 기대한 함수가 인라인되지 않을 수 있습니다. 이 경우에 이 함수를 사용하면 page fault가 발생할 수 있습니다.
컴파일러는 당신이 예상치 못한 함수에 대해 인라인 코드를 생성 할 수도 있습니다.
어떤 구문(가장 명백한 예를 들면 varargs)은 어떤 상황에서도 컴파일러가 함수를 인라인으로 만드는 것을 방지합니다.
어떠한 C++ 구문이 안전한가? (혹은 위험한가?)
현재 커널 모드 코드에서 사용하기 위해 C++의 어떤 기능들이 "완전히 안전한"지에 대한 엄격하고 검증 가능한 정의를 제공 할 수는 없지만, 일반적으로 어떤 구문이 안전하고 어떤 구문이 안전하지 않은 지에 대한 유용한 지침이 제공됩니다.
경험적인 원칙중 하나는 C++ 코드를 재배치하여 C구문으로 만들어낼 수 있다면 그 코드는 안전하다는 것입니다. for 문 안에서 선언된 변수들을 포함하여 여유있게 재배열하는 것이 한 예입니다.
C++에서 보다 엄격한 data type 체크는 기술적으로 가능하지만 의미적으로는 틀린 구문을 걸러낼 수 있습니다. 이러한 엄격한 type 체크는 드라이버의 신뢰도를 향상시키는 데 도움이 됩니다.
클래스 상속 또는 템플릿, 예외 또는 동적 유형 지정과 관련된 모든 것이 위험할 수 있습니다. 이러한 구문을 사용하려면 생성 된 오브젝트 코드를 매우 조심스럽게 분석해야 합니다. 클래스 사용을 POD 클래스로 제한하면 위험이 크게 줄어 듭니다.
생성된 코드를 검토하기
C의 원래 디자인 목표 중 하나는 어떠한 오브젝트 코드가 생성될 지 예측하기 쉽다는 것이며, 따라서 커널 모드 작업에 매우 적합합니다. C++은 훨씬 복잡한 언어이므로 결과적으로 커널 환경에서 안전하게 동작하도록 개발하기가 훨씬 더 어렵습니다.
C++로 드라이버를 작성하려면 컴파일러에서 생성된 코드를 이해하고 그 코드가 커널 모드 요구 사항을 충족하는지, 그리고 이 문서에서 설명하는 문제가 없는지 확인하십시오. 코드와 데이터가 적절한 위치에 있고 커널에서 안전한 라이브러리만 사용되었는지 확인할 수 있도록 오브젝트 코드를 읽어 보고 링크 map을 스캔할 준비를 하십시오. 페이징 가능 여부, 인라인 함수 및 프로그램 순서가 올바른지 코드를 체크하십시오.
소스 코드가 완료 될 때까지 기다리는 것이 아니라 지금 바로 코드 읽기 및 테스트를 시작하는 것이 좋습니다. C++로는 극복 할 수 없는 문제가 발생하면 대체 솔루션을 찾아 구현할 시간을 갖을 수 있도록 초기 프로토 타입을 확인하고 문제가 될 수 있는 사용법을 테스트하십시오.
커널모드 드라이버 관련 C++ 이슈
Microsoft 개발자들은 C++이 커널 모드 드라이버에서 특별한 문제를 많이 발생시키는 분야를 다수 발견했습니다.
메모리의 코드
커널 모드 드라이버를 작성하기 위해 C++을 사용할 때 가장 심각한 문제는 메모리 페이지, 특히 데이터보다는 메모리의 코드를 관리하는 것입니다. 사이즈가 큰 드라이버는 페이지 할 수 있어야 하며 paged 코드가 항상 메모리에 존재하지는 않는다는 점이 중요합니다. 시스템이 페이징이 발생할 수 없는 상태가 되기 전에 필요한 모든 코드가 메모리에 상주해야 합니다.
C++ 컴파일러가 POD가 아닌 클래스와 템플릿에 대한 코드를 생성하는 방식은 함수를 실행하는 데 필요한 모든 코드가 어디로 갈지 모르기 때문에 코드를 안전하게 페이지 하기 어렵게 만듭니다. 컴파일러는 적어도 다음 객체에 대한 코드를 자동으로 생성합니다. 이 객체들은 "out of line"으로 놓여 있으며, 개발자는 삽입 된 부분을 직접 제어 할 수 없기 때문에 필요한 경우 페이지 아웃 될 수 있습니다.
생성자, 소멸자, 캐스트 및 할당 연산자와 같은 컴파일러 생성 코드 (이것들을 명시적으로 제공할 수도 있지만, 제공해야 한다는 것은 인지해야 합니다.)
계층 구조의 다양한 클래스 들을 변환하는 데 사용되는 조정자(Adjustor) thunk 코드
가상함수 호출을 구현하는데 사용되는 가상함수 thunk
기본 클래스와 다형성을 관리하는 데 사용되는 가상 함수 테이블 thunk
명시적으로 인스턴스화되지 않는 경우, 처음 사용할 때 배치되는 템플릿 코드 본체
가상함수 테이블 본체
C++ 컴파일러는 이러한 엔티티가 메모리에 배치되는 위치를 직접 제어하기위한 메커니즘을 제공하지 않습니다. 메모리 배치를 제어하는 데 필요한 pragma는 C++를 염두에 두고 설계되지 않았습니다. #pragma alloc_text는 (몇 가지 이유로) 멤버 함수의 이름을 확인할 수 없기 때문에 멤버 함수의 위치를 지정하는 데에는 사용할 수 없습니다. #pragma code_seg의 경우 컴파일러에서 생성 된 함수, 확장 된 템플릿 본문 및 컴파일러에서 생성 된 thunk에 대해서는 범위가 모호합니다. 가상 함수 테이블은 컴파일러 관점에서 볼 때 코드도 아니고는 데이터 또한 아니기 때문에 위치를 제어하는 메커니즘이 전혀 없습니다. (이것들은 자체적인 섹션에 배치됩니다.)
헤더의 함수가 인라인으로 선언되었지만 컴파일러에서 인라인 코드를 생성하지 않는 경우, 함수가 사용되는 위치에 따라 둘 이상의 코드 세그먼트에서 함수가 생성 될 수 있습니다. 클래스 템플릿이 인스턴스화 될 때는 최초로 사용되는 섹션에서 생성되고, 어떤 섹션에 생성될 지 아직 명확하지 않은 시점도 존재합니다. 이 두 가지 문제로 인해 코드가 페이지되어선 안될 때 페이징 될 수 있으며 그 반대의 경우도 발생할 수 있습니다.
클래스 계층 구조가 사용중인 경우 자식 클래스에 액세스 할 때 부모 클래스의 코드가 메모리에 있어야하는지 여부는 코드가 어느 섹션에 배치되는지 뿐만 아니라 자식 클래스에서 부모 클래스의 어느 함수가 호출되는지 (그리고 컴파일러에서 인라인 할 수 있는지)여부에 따라서도 달라집니다. 또한 자식 클래스가 부모 클래스 메서드를 사용하지 않는 메서드를 제공하는 경우 부모 클래스 코드가 메모리에 있을 필요는 없습니다. 그러나 언제가 그런 경우인지를 아는 것은 어렵습니다. 또한 계층 구조 및 클래스들와 함께 사용되는 thunk도 메모리에 함께 상주해야 할 수 있습니다.
스택
컴파일러는 임시 객체를 생성하거나, 함수 호출 정리를 지연시키거나 기타 스택을 숨겨진 방식으로 사용하여 스택에 추가 데이터를 생성하곤 합니다. 단일 함수가 스택을 사용하는 방식 자체에도 C와 C++ 간에는 약간의 차이점이 있지만 일반적으로 더 많은 함수 호출을 발생시키는 추가 메커니즘 때문에 C++은 더 많은 총 스택을 사용합니다. 스택 공간이 제한된 환경에서는 어떤 프로그래밍 언어를 사용하든 스택 크기를 항상 염두에 두어야합니다.
예외는 또한 스택에 영향을 줍니다. 이 백서 뒷부분의 "Exceptions and RTTI (예외와 RTTI)"를 참조하십시오.
동적 메모리
Driver Verifier와 같은 드라이버 개발 도구는 tag된 메모리를 사용하여 드라이버의 메모리 사용을 검증합니다. operator new와 operator delete를 사용하여 메모리를 할당하고 해제하면 드라이버 코드의 메모리 누수 및 기타 문제를 감지 할 수 있는 이러한 도구의 기능이 약화됩니다.
유저 레벨에서 operator new와 operator delete는 편리하지만 다중 메모리 풀이나 태그 메모리를 사용하는 드라이버에서는 성가신 일이 될 수 있습니다. "placement new"는 추가 피연산자를 지정할 수 있기 때문에 재정의된 new 연산자에 메모리풀을 지정하거나 태그를 생성하는 데 필요한 정보를 전달할 수 있지만, 메모리 함수를 직접 사용하는 것보다 딱히 쉽지는 않습니다. 태그나 풀 타입 등 추가 인수를 전달할 수 있는 "placement delete”가 없기 때문에 delete 연산자를 사용할 때 태그(또는 필요한 경우 메모리 control)를 전달할 방법이 없으며 따라서 메모리 해제 시점에서 해제 작업이 의도대로 된 것인지 확인할 방법이 없고, 결과적으로 메모리 태그의 이점 중 상당 부분을 활용할 수 없게 됩니다. 태그를 제공하지 않고 메모리를 삭제하는 것은 가능하지만 각각의 경우 드라이버 코드에서 태그를 사용하지 않는 위험과 단점이 그로 인한 편리함보다 큰지를 결정해야 합니다.
메모리 추적 도구는 대개 할당을 수행한 함수의 리턴 주소를 기록합니다. 일부 C++ 컴파일러는 new 연산자를 함수로 구현하므로 모든 할당이 단일 위치에서 시작되는 것처럼 보이게 되고 메모리 추적 도구를 그러한 목적으로 사용할 수 없게 됩니다. 이 문제를 해결할 수는 있지만 메모리를 직접 할당하는 것보다 이득인지는 개발자가 스스로 판단해야 합니다.
라이브러리
라이브러리를 만들고 사용하는 데에는 여러 가지 다른 문제가 있습니다:
Export된 C++ 함수의 이름이 릴리스마다 다를 수 있습니다.
사용자 모드에서 사용할 수 있는 모든 함수가 커널 모드 라이브러리에서 사용 가능한 것은 아닙니다.
표준 템플릿 라이브러리(STL)는 단일 DLL의 데이터 개체로 사용되도록 설계되었습니다.
C 함수가 함수 이름만으로 export되는 것과 달리 C++ 함수는 전체 특징(signature)을 기반으로 exort되니다. C++ 함수의 이름은 타입 정보를 포함하기 위해 "Mangling"되어 signature의 일부가 됩니다. 이름 맹글링에 대한 규칙이 상당히 안정적이지만 컴파일 된 이름이 릴리즈 될 때마다 동일 할 것이라는 보장은 없습니다. 따라서 extern "C"함수로 나타낼 수있는 함수는 사용할 수 있지만 C++ 함수를 한 릴리스에서 다음 릴리스로 라이브러리에 안정적으로 explort할 수는 없습니다. 참고로, .def 파일을 사용하면 문제를 완화하는 데 도움이 될 수 있습니다. 전체 특징(signature)에 기반하여 export하는 C++ 과 달리 extern "C" 함수는 이름만을 기준으로 고유하다는 점을 주목하시기 바랍니다.
모든 라이브러리 함수가 커널 모드에서 사용할 수있는 것은 아니며, 특히 "고급" C++ 언어 기능과 관련된 함수들이 그렇습니다. 표준 템플릿 라이브러리(STL)은 다양한 크기의 배열과 같은 많은 C++ 개념을 구현하는 "일반적인" 방법입니다. 그러나 표준 템플릿 라이브러리가 존재한다고 혹은 사용할 수 있다고 가정하는 것은 안전하지 않습니다. 표준 템플릿 라이브러리의 대부분은 헤더에 소스 코드로 구현되어 있지만 때때로 라이브러리 함수나 커널 환경에 없거나 사용할 수 없는 다른 기능을 사용합니다.
Standard Template Library는 또한 사용하는 각 데이터 객체가 하나의 DLL에만 존재한다고 가정합니다. 대부분의 경우 DLL 너머로 POD 객체에 대한 참조를 전달하지만, list와 같은 복잡한 구조에 대한 참조를 전달하면 진단하기 어려운 런타임 오류가 발생할 수 있습니다. 알려진 이슈 중 하나는 메모리를 할당한 DLL이 아닌 다른 DLL에서 메모리를 free하면 (적어도 debug 빌드에서는) 오류가 발생할 수 있다는 것이며, “end of list" 표시가 DLL마다 다르므로 예기치 않은 오류(runaway list search)가 발생할 수 있다는 점입니다. 이러한 문제점을 인식하고 이를 예방하는 조치를 취해야합니다.
커널 모드 드라이버에서 표준 템플릿 라이브러리 기능을 사용하는 것은 권장하지 않습니다. 표준 템플릿 라이브러리가 존재하는지, 그리고 그냥 작동하는지 가정 할 수 없기 때문입니다. 커널 모드 코드의 경우, 데이터 구조가 어떻게 구현되었는지 정확히 이해하는 것이 그 데이터 구조가 커널 공간의 요구 사항을 위반하지 않는지 확인하는데 도움이 됩니다. 그렇게 보면 별도로 구현된 라이브러리가 일반적인 표준 템플릿 라이브러리 함수보다 더 작으면서 더 좋을 수도 있습니다.
예외와 RTTI (Exceptions and RTTI)
C++ 예외를 사용하는 것이 매력적이지만 커널 모드로 구현하기는 어렵습니다. C++ 예외를 사용하려면 Kernel-mode-safe한 라이브러리가 필요하지만 이런 것은 현재 존재하지 않습니다. 또한 예외가 발생할 때 생성되는 예외 레코드는 매우 제한된 스택의 큰 개체이기 때문에 피할 수없는 런타임 문제를 나타냅니다. x86 시스템에서 예외 레코드는 (보통 일반적인 스택 프레임과 비교하면 크더라도) 특별히 크지는 않지만 Intel Itanium 시스템에서는 사용 가능한 24K 스택의 3K에서 4K 또는 1/6에서 1/8로 상당히 큽니다. 64 비트 플랫폼에 대한 드라이버의 이식성을 유지하기 위해 x86 아키텍처에서도 매우 제한된 방식으로 예외를 사용해야합니다. rethrow 연산자는 스택에 여러 예외 레코드를 생성 할 수 있습니다. 공간상의 문제는 남아 있지만 구조화 된 예외 처리 (__try / __ except / __ finally)는 커널 모드에서 사용할 수 있습니다. C++ 예외에는 구조적 예외 처리로 간단하게 매핑 할 수 없는 많은 의미 적 미묘한 요소가 있습니다. 런타임 유형 정보 (RTTI)에는 커널 모드에서 현재 C++ 용으로 존재하지 않는 라이브러리가 필요합니다. 지금까지 커널 모드 코드에서는 이런 요구사항이 거의 없었습니다. 이러한 요구사항이 없었던 것이 그것보다 선행하는 다른 문제의 결과인지 또는 커널 모드에서 유용하지 않기 때문인지는 알 수 없습니다.
컴파일러 버전
C++ 언어 표준은 안정적이지만 구현 기술은 여전히 진화하고 있습니다. 따라서 컴파일러 버전은 생성된 코드가 작동하는 방식을 변경시킬 수 있습니다. 이러한 변경은 사용자 모드 코드에는 영향을 미치지 않지만 드라이버 개발자에게 노출되는 기본 구현이 많아지는 커널 모드 코드에 영향을 줄 수 있습니다. 커널 모드 코드의 버전 간 상호 운용성은 보장되지 않습니다.
일반적으로 C++ 대신 C로 인터페이스를 작성하여 두 드라이버 또는 드라이버와 운영 체제 간의 인터페이스를 신중하게 제어해야 합니다. 그렇지 않으면 C++ 구현의 버전 간 비 호환성으로 인해 상호 운용성 오류가 발생할 수 있습니다.
Static 및 Global 변수 및 초기화
C++ static 변수는 (global이든 local이든) 드라이버에 여러 가지 문제를 일으킵니다.
C++ 표준은 local로 선언된 static 변수가 처음 사용할 때 (scope에 처음 진입할 때) 초기화 될 수 있게 합니다. 이것이 구현되는 방식은 초기화 중에 race condition이 발생할 수 있고 스레드간에 의도하지 않은 데이터 공유가 발생할 위험이 특히 높습니다. static으로 선언 된 변수는 스레드 별 static이 아닌 global한 static으로 선언되어 있기 때문입니다. 스레드간에 공유되는 global하게 static한 데이터에 대해서는 global 범위에서 명시적으로 초기화를 수행하여 상황에 따라 적절하게 접근이 보호되도록 하는 것이 가장 좋습니다.
C++ global 개체가 초기화를 요구하더라도(global 생성자), 생성자를 호출 할 메커니즘이 없습니다. 생성자를 필요로하는 global 개체는 사용하지 말아야하며 그렇지 않으면 생성자가 호출되도록 하는 메커니즘을 개발해야 합니다. 웹에는 이 문제를 해결했다고 주장하는 몇몇 자료가 있으며, 그 중 한 가지 솔루션이 도움이 될 수 있습니다.
전역 객체의 초기화 순서는 C++ 표준에 의해 지정되지 않으므로 생성자를 호출하는 메커니즘이 있더라도 초기화 순서가 드라이버 코드에 의해 명시적으로 제어되거나 중요하지 않아야합니다.
Summary
Microsoft는 커널 모드 드라이버에 C++ 사용을 보증하지도 금지하지 않습니다. 이 보수적 입장은 이 백서에 설명 된 문제와도 관련되어 있고, 모든 플랫폼을 지원해야하는 측면과도 연관되어 있습니다. 커널 모드에서 C++로 개발을 시도하기 전에 이 백서에 설명되어있는 알려진 문제와 위험을 알고 있어야하며 아직 밝혀지지 않은 다른 문제에 대해 주의해야 합니다.
Microsoft는 C++을 커널에서 더욱 사용할 수 있게 만드는 방법을 적극적으로 연구하고 있습니다. 사용자 모드 코드에 적용 할 수있는 모든 C++ 기능을 커널 모드 코드에서 사용할 수 있는지 여부는 아직 알려져 있지 않습니다.
커널 모드 드라이버를 개발하기 위해 C++을 사용하려면 다음 사항에 유의하십시오.
C++ 컴파일러를 "수퍼-C"로 사용하는 것이 일반적으로 가능할 것으로 예상되지만 컴파일러를 그렇게 사용하는 것은 개발자 자신의 책임입니다.
현재 문제가있는 C++ 구문을 기계적으로 식별하는 것은 비현실적이므로 개발자는 컴파일러 출력을 신중하게 분석하여 생성 된 코드가 커널 모드에 적합한 지 확인해야 합니다.
C++의 사용을 결정하기 전에, 그것이 제대로 작동하는 지 주의 깊게 평가해야합니다. 특히, 개발 과정 초기에 C++ 구문을 테스트하여 C++ 구문이 이 백서에 설명 된 문제를 일으키지 않거나 그렇지 않으면 커널 모드 드라이버 작성 원칙을 위반하지 않도록 해야 합니다.
이 백서에서 논의 된 문제 중 일부는 개발이 끝날 때까지 명백해지지 않을 수 있으며,이를 해결하기 위해서는 코드를 완전히 다시 작성해야 할 수도 있습니다.
가장 잘 드러나지 않는 문제 중 일부는 드라이버 테스트 중 재현해내기가 극히 어렵 기 때문에, 결함을 내재한 드라이버가 문제없이 오랜 시간 동안 문제 없이 동작하는 것처럼 보이다가 갑자기 임의의 시간에 고장난 것처럼 보일 수도 있습니다. 신중한 분석의 필요성을 다시 한번 강조합니다.
주의 깊게 코딩하고 생성 된 코드를 면밀히 검토함으로써 많은 문제를 피할 수 있습니다. 하지만 나머지 문제는 극복하기가 매우 어렵습니다. 그것들에 대해서는 개발자의 입장에서 특별한 주의와 신중한 분석이 필요합니다.
'번역' 카테고리의 다른 글
[번역] X64 에 푹 빠져보기 (X64 Deep Dive) (4) | 2013.07.30 |
---|