관리 메뉴

kisoo

WDK Filter Manager Model(11.03.29) 본문

01.About Programming /2.Kernel Lab

WDK Filter Manager Model(11.03.29)

JamesK78 2011. 3. 29. 14:58

 [펌] http://john6.springnote.com/pages/3329381

실습 보고서
  • pipelining과 multi-processing(multi-threading)과는 혼란을 일으키지 않도록 하자.
  • 무서울 정도로 했던 말을 그대로 옮긴다. (녹음 당하는 느낌)
  • Re-entrant & Threads
  • 이제 뜬 구름은 그만 잡자. 코드 봅시다.
  • 찾아서 공부하는 시간이 시작되었습니다.

 

Filter Manager Model

일단 큰 그림을 볼 수 있는 페이지 (마이크로소프트)

http://www.microsoft.com/whdc/driver/filterdrv/default.mspx

 

anti-virus filters, backup agents, encryption products 등을 작성할 때 파일 필터 드라이버를 활용해야 했다.

예를 들면, anti-virus에서는 I/O를 감시하다가 exe나 doc와 같은 파일이 열리거나 쓰여질 때 signature 체크를 함으로써 실시간 감시를 할 수 있을 것이다.

I/O 메니저가 FSD(File System Driver)에게 IRP나 Fast I/O 요청을 할 때 그것을 가로채서 이것저것 추가 작업을 수행하는 것이 필터 드라이버가 하는 일.

작업을 하다보면 가로채기만 하는 것이 아니라 새로운 IRP를 만들 때도 있다.

작성은 매우 주의해야 한다. 버그가 있으면 바로 블루 스크린이나 데드락으로 이어진다.

모든 I/O가 통하는 곳에 서 있는 것과 다름 없으므로 길막지 않도록 주의하자. 성능에 치명상을 줄 수도 있다.

 

Lagacy Filter가 가지는 몇 가지 문제점

  • 스택의 순서를 컨트롤하는데 취약했다
  • 언로드가 지원되지 않았다 (재부팅 필요)
  • 인터페이스가 복잡했다 (패스트 I/O, IRP)

 

Filter Manager Model의 장점

  • 체인으로 된 디스패치 루틴을 콜백 기반으로 변경했다. 스택 오버 플로우 문제를 해결하는데 도움을 준다.
  • Fast I/O든 IRP든 같은 방식으로 가로챌 수 있다. 즉 모든 오퍼레이션에 대해 같은 인터페이스를 제공한다.
  • 동적 로드와 언로드가 된다. 언로드가 된다!
  • 효율적 pass through가 가능
  • load 순서가 결정되어 있다
  • 효율적이고 안전한 유저와 커널간의 통신

 

Mini Filter

필터 메니저 모델로 작성된 필터 드라이버를 미니 필터라고 부른다.

Mini filter를 쓴다고 해서 기존 legacy filter를 쓰지 못하는 것은 아니다. 같이 동작 가능하다.

재부팅 없이 unload와 upgrade가 가능하다는 점은 매우 큰 장점!

 

Filter manager는 third-party filter driver를 개발하는 작업을 기존에 비해 단순하게 만들어 준다.

기존 filter driver model이 가지고 있던 문제도 다수 해결하였다.

 

마이크로스프트가 제공하던 필터들은 모두 미니필터로 전환되었다. (롱혼 시절. 즉 비스타가 나오기 전에)

레거시 필터와 함께 동작 가능하지만 미니필터만 남는 것이 목적이다.

시작하는 기본 코드가 이미 5000라인에 가까웠던 것은 큰 약점이었는데, 이젠 아니다.

 

 

altitude

각 미니 필터 드라이버는 할당된 '고도'를 가지는데 유니크 식별자로써 - 다른 미니 필터와 비교해서 I/O 스택의 - 어디에 미니 필터가 로드될 것인지를 결정한다.

'고도'는 MS에 의해 할당되고 관리된다.

 

fltmc command

  1. fltmc help

필터 드라이버를 로드 및 언로드 할 수 있는 툴. WDK를 설치하지 않아도 사용가능한 것으로 보인다.

  1. net start [DriverName] / net stop

으로도 실행하고 멈출 수 있다.

 

Instance

특정 고도에 필터가 달라붙으면 그걸 인스턴스라고 한다.

다수의 필터 인스턴스가 달라붙을 수 있다.

고도는 스트링으로 표현되는 숫자다. 소숫점 표현이 가능하기 때문에 두 개의 기존 필터에 언제나 끼어들 수 있다.

모든 미니필터는 유일한 고도를 가져야 한다.

미니 필터는 고도개념은 동적으로 로드 및 언로드 되는 미니 필터의 처리 순서를 언제나 정확히 알아낼 수 있다 (deterministic).

 

기존 레거시 드라이버와의 깔끔한 동작을 위해, 필터 메니저 프레임이 2개 존재한다. 0부터 1000까지는 레거시 드라이버보다 밑에, 1000부터 9999까지는 레거시 드라이버보다 위에 위치한다.

미니 필터로 들어오는 모든 오퍼레이션은 CallbackData 구조체를 통해 전달된다. IRP로 생각하면 된다.

 

Iopb

Io Parameter Block

 

레거시 필터 드라이버의 가장 큰 약점은 모든 오퍼레이션에 대해 핸들링해야 했다는 것이다.

기존 레거시 드라이버는 모든 오퍼레이션을 처리해야 했다. 하지만 미니필터는 그렇지 않다. 관심 있는 것만 처리하면 된다.

멀티 프로세서 환경이 고려되었다.

 

PreFast

PREFast는 C/C++ 코드의 결점을 찾는 툴이다.

  1. #pragma prefast(disable:__WARNING_...

와 같은 문장이 있는데, PREFast가 경고를 발생하는 것을 막는 선언으로 보인다.

 

  1. > prefast build -ceZ

와 같이 실행하며, 분석 결과 보는 방법은

  1. > prefast view

ALLOC_PRAGMA

  1. #ifdef ALLOC_PRAGMA
    #pragma alloc_text(INIT, DriverEntry)
    #pragma alloc_text(PAGE, PtUnload)
    #endif

와 같은 코드가 있다.

http://www.asmcommunity.net/board/index.php?action=printpage%3Btopic=17154.0

Whole driver's image is nonpageable by default. You don't have to do any special for this. If you want to make some part of the driver to be discardable as needed use section names starting with "PAGE". Initializing code can be placed into "INIT" section and also will be paged.

일반적으로 드라이버 이미지는 페이징되지 않도록 된다는 의미로 보인다. 그런데 드라이버를 상황에 따라 버릴 수 있도록 하기 위해서는 페이징이 가능하도록 하는 것이 좋을 것이다. PAGE라는 이름의 섹션에 두면 페이징이 가능하게 된다. 초기화 코드는 INIT 섹션에 둘 수 있으며 이 역시 페이징이 가능하다.

미니필터 패스스루는 동적 로딩과 언로딩이 가능해야 하기 때문에 페이징을 가능하도록 만든 것이 아닐까 생각해본다.

 

IRP 메이저 함수 코드

http://msdn.microsoft.com/en-us/library/ms806157.aspx

상당히 많으며 파일 시스템의 경우 여기에 명시된 것보다 추가적으로 더 있음에 유의할 것.

 

FLT_REGISTERATION 구조체

http://msdn.microsoft.com/en-us/library/ms793833.aspx

FltRegisterFilter 함수에 전달될 구조체이다. 거의 모든 정보를 담고 있다.

버전, IRP 메이저 함수에 대한 콜백들, 필터 언로드 콜백, 인스턴스의 setup, teardown 관련 callback 등의 정보를 담고 있다.

각각 콜백의 파라미터는 모두 정해져있으며, 위의 링크를 따라가면 모두 찾아갈 수 있다.

 

FLT_OPERATION_REGISTRATION 구조체의 정의

  1. typedef struct _FLT_OPERATION_REGISTRATION {

        UCHAR MajorFunction;
        FLT_OPERATION_REGISTRATION_FLAGS Flags;
        PFLT_PRE_OPERATION_CALLBACK PreOperation;
        PFLT_POST_OPERATION_CALLBACK PostOperation;

        PVOID Reserved1;

    } FLT_OPERATION_REGISTRATION, *PFLT_OPERATION_REGISTRATION;

2개의 flag를 가진다. Cached I/O에 대한 skip 여부, Paging I/O에 대한 skip 여부.

Callback 정의 모습

  1. CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
        { IRP_MJ_CREATE,
          0,
          PtPreOperationPassThrough,
          PtPostOperationPassThrough },
  2. ...
  3.     { IRP_MJ_OPERATION_END }
    };

끝은 IRP_MJ_OPERATION_END로 끝낸다.

 

프리 오퍼레이션과 포스트 오퍼레이션

IRP의 움직임을 상상해보자.

우선 아래로 내려갔다가 베이스까지 가고나면 다시 올라온다.

우선 내려가는 것을 pre-operation이 처리한다.

다시 올라올 때는 post-operation이 실행된다.

둘은 거의 같은 모습을 가지고 있는데, 우선 (콜백)데이터는 IRP와 거의 같다고 보면 된다.

completeContext 포인터를 통해서 pre-operation이 어떤 정보를 post-operation에게 전달할 수 있다.

  1. typedef FLT_PREOP_CALLBACK_STATUS
    (*PFLT_PRE_OPERATION_CALLBACK) (
        IN OUT PFLT_CALLBACK_DATA Data,
        IN PCFLT_RELATED_OBJECTS FltObjects,
        OUT PVOID *CompletionContext
        );
  2. typedef FLT_POSTOP_CALLBACK_STATUS
      (*PFLT_POST_OPERATION_CALLBACK) (
        IN OUT PFLT_CALLBACK_DATA Data,
        IN PCFLT_RELATED_OBJECTS FltObjects,
        IN PVOID CompletionContext,
  3.     IN FLT_POST_OPERATION_FLAGS Flags
  4.     );

컴플리트컨텍스트의 IN / OUT을 유심히 보도록 하자.

 

Passthrough 분석

DriverEntry

미니필터 역시 드라이버이므로 메인 함수의 역할은 드라이버엔트리가 수행한다. 넘겨 받는 파라미터는 (당연히) 동일하다.

가장 중요한 작업은 FltRegisterFilter 함수를 통해 필터를 등록하는 것이다.

등록이

성공하면 FltStartFiltering 함수를 통해 필터 작업을 시작한다.

성공하지 않았다면 FltUnregisterFilter 함수를 통해 필터 등록을 해제한다.

InstanceSetup

새로운 인스턴스가 볼륨에 생성될 때 호출된다.

디버그뷰 출력)

  1. PassThrough!PtInstanceSetup: Entered
InstanceQueryTeardown

FltDetachVolume이나 FilterDetach의 호출로 수동적으로 인스턴스가 지워질 때 호출되는 콜백. 이 콜백을 지정해두지 않으면 두 함수의 호출은 언제나 실패하게 된다.

다음 코드를 통해 패스스루 인스턴스 중 하나를 떼어내볼 수 있다.

  1. fltmc detach passthrough c: "passthrough instance"

디버그뷰에서의 모습)

  1. PassThrough!PtInstanceQueryTeardown: Entered
    PassThrough!PtInstanceTeardownStart: Entered
    PassThrough!PtInstanceTeardownComplete: Entered
InstanceTeardownStart

인스턴스가 테어다운을 시작하면 호출되는 콜백.

InstanceTeardownComplete

테어다운 작업이 완료되면 호출되는 콜백.

 

PassThrough만 가지고 있는 함수들

pass through는 아무 일도 하지 않는 mini filter로써 callbackData를 받기는 하되 아무 처리도 하지 않고 다음으로 연결되어 있는 mini filter에게 전달하기만 한다.

PtPreOperationPassThrough

FLT_PREOP_SUCCESS_WITH_CALLBACK을 반환함으로써 callback data를 다음 minifilter로 전달하는 역할만 수행한다.

반환 값은, 콜백 함수가 잘 수행되었으며 I/O가 완료될 때까지 이후 작업을 계속 진행하라는 의미이며, post-operation도 수행하기를 바란다는 뜻.

CallbackData는 pre-operation과 post-operation callback function에 모두 첫 번째 파라미터로 전달된다.

 

PtOperationStatusCallback

IoCallDriver 함수로부터 해당 오퍼레이션이 리턴되면 이 함수가 호출된다.

IRP가 전달 되는 것에 대한 문서: http://msdn.microsoft.com/en-us/library/ms795781.aspx

 

FltRequestOperationStatusCallback 함수를 통해 등록시킨다.

  1. status = FltRequestOperationStatusCallback( Data, PtOperationStatusCallback, (PVOID)(++OperationStatusCtx) );

pass through에 위와 같이 사용되었다.

PtPostOperationPassThrough

FLT_POSTOP_FINISHED_PROCESSING을 반환하는 일만 수행한다.

 

fltKernel.h에 있는 함수

FltRequestOperationStatusCallback

http://msdn.microsoft.com/en-us/library/aa488702.aspx

 

매크로

UNREFERENCED_PARAMETER() 매크로
  1. UNREFERENCED_PARAMETER(Flags);

와 같은 문장을 볼 수 있는데, 이것은 파라미터를 넘겨 받았으나 현 함수에서 이 파라미터를 이용하지 않는다는 표시를 해주는 것이다. 이렇게 처리하지 않을 경우, 컴파일러가 경고를 발생시킬 수 있는데 그것을 억제하기 위함으로 보인다.

 

PAGED_CODE() 매크로

http://msdn.microsoft.com/en-us/library/aa489904.aspx

A call to this macro should be made at the beginning of every driver routine that either contains pageable code or accesses pageable code.

페이징 가능한 코드를 담고 있거나 페이징 가능한 코드에 접근하는 모든 드라이버 루틴은 시작 시에 이 매크로를 실행해야 한다.

드라이버 루틴의 진입 시에 한 번 체크하는 것이기 때문에 체크 후에 IRQL이 올라가는 것까지 감지하지는 못한다.

  1. #if defined(_PREFAST_)
    void __PREfastPagedCode(void);
    #define PAGED_CODE()        __PREfastPagedCode();
    #elif DBG
    #define PAGED_CODE() {                                                       \
        if (KeGetCurrentIrql() > APC_LEVEL) {                                    \
            KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
            NT_ASSERT(FALSE);                                                    \
        }                                                                        \
    }
    #else
    #define PAGED_CODE()        NOP_FUNCTION;
    #endif

wdm.h에 정의되어 있다. 일부만 가져옴.

위에서 보는 바와 같이 디버그 모드가 아닐 때는 아무것도 하지 않는다.

 

커널 매크로 사용 시 유의 사항
  1. #define RemoveHeadList(ListHead)
  2. (ListHead)->Flink;

  3. {RemoveEntryList((ListHead)->Flink)}

위와 같이 두 개 이상의 문장으로 구성된 매크로가 있다.

따라서 다음과 같이 사용하면 의도한 것과는 다르게 동작할 수 있다.

  1. if(SomethingInList)
  2. Entry = RemoveHeadList(list);

따라서 버릇처럼 if, for 등을 사용할 때 중괄호로 감싸도록 하자.

  1. if(SomethingInList) {
  2. Entry = RemoveHeadList(list);

  3. }

     

이 글은 스프링노트에서 작성되었습니다.

Comments