본문 바로가기

My Study/Programming&Theory

Driver에서 시스템 스레드 사용하기 ( 동기화 이벤트 오브젝트 )

 보통 우리가 유저어플에서 스레드 생성할 때 사용하는 함수는 CreateThread입니다. 쫌더 생각해서 동기화까지 고려해 준다면 _beginthreadex를 써주는게 더 낫지요. 이번에는 커널에서 동작하는 드라이버에서 스레드를 생성시켜 보겠습니다.

커널에서 스레드 생성시키는 함수는 PsCreateSystemThread입니다.
NTSTATUS PsCreateSystemThread(
  __out      PHANDLE ThreadHandle,
  __in       ULONG DesiredAccess,
  __in_opt   POBJECT_ATTRIBUTES ObjectAttributes,
  __in_opt   HANDLE ProcessHandle,
  __out_opt  PCLIENT_ID ClientId,
  __in       PKSTART_ROUTINE StartRoutine,
  __in_opt   PVOID StartContext
);
커널에서 생성시키는 스레드는 기본적으로 시스템 스레드라고 합니다. 
보통 유저어플에서 스레드를 생성시키면 해당 스레드는 프로세스 아래서 운영됩니다.
하지만 커널에서는 기본적으로 "SYSTEN"이라는 프로세스 아래서 운용되게 됩니다.
그래서 시스템 스레드라고 합니다. 위 4번째 인자로 특정 프로세스의 핸들 값을 주게 되면 해당 프로세스 아래서 운용되는데
이렇게 되면 이 스레드는 더이상 시스템 스레드가 아닙니다. 보통은 NULL 값을 줍니다.

이 점을 빼고는 유저어플에서 생성하는 스레드와 다를게 없습니다.

먼저 스레드 생성하는 코드를 보시겠습니다.
DriverEntry에서는 스레드를 생성시켜 해당 스레드가 종료 될 때까지 기다리고 있습니다.
그리고 스레드 내부에서는 1초에 한번씩 DbgPrint 함수를 호출하고 있습니다. 총 10번을 말이죠.

유저어플과 다른 점이 또 하나 있습니다.
유저 어플에서 스레드를 생성시켜 해당 스레드가 끝날 때까지 기다리는 코드를 작성할 때

_beginthreadex 함수를 호출하고 리턴 된 스레드 핸들 값을 가지고
WaitForSingleObject 를 호출합니다.

반면에 위 코드를 보시면 아시겠지만 얻은 스레드 핸들을 가지고 KeWaitForSingleObject 함수를 호출하는게 아닌
ObReferenceObjectByHandle 함수에서 스레드 핸들을 가지고 스레드 오브젝트를 구하고 있습니다.
그래서 그렇게 구한 스레드 오브젝트를 인자로 넘겨줘서 사용합니다.
그리고 위 함수를 호출하면 스레드 오브젝트 참조횟수가 증가합니다. 그래서 해당 오브젝트를 다 쓰고 나면 반드시
ObDereferenceObject함수를 사용해 참조횟수를 감소시켜주어야 합니다.

IO_REMOVE_LOCK 구조체를 사용한 이유는 코드를 좀 더 안정화 시키기 위해서 사용했습니다.
보통 IO관리자가 제공하는 함수를 사용할 경우에는 이미 함수 속에 참조횟수를 조절하는 함수들이 사용되어 있습니다.
ObReferenceObject, ObDereferenceObject 들이죠.

하지만 스레드를 생성 한다던지, Custon Timer DPC 루틴을 사용하는 경우에 사용되는 함수들은 이러한 기능이 없습니다.
PsCreateSystemThread, KeSetTimberEx 함수들이죠.

그래서 이러한 함수들을 그냥 막 썼을 때 생길 수 있는 문제점이 있습니다.
드라이버가 스레드를 생성시켜놓고 스레드는 동작하고 있는데 드라이버는 메모리에서 제거되버린 경우입니다.
스레드는 작동하고 있는데 코드들이 메모리에서 제거되버렸으므로 스레드는 갑자기 이상한 명령어들을 읽게되면서
블루스크린이 발생할 수도 있습니다. 해당 함수들은 IO 관리자가 지원하는 함수들이 아니어서 IO관리자 입장에서 봤을 때 드라이버 개발자가 스레드를 만들었는지 안만들었는지 알 길이 없습니다.

그래서 개발자는 IO관리자가 모르는 콜백 루틴 사용했을 경우에는 IO_REMOVE_LOCK구조체 관련 함수를 사용해서 참조횟수를 조절해 주면 됩니다. 그런 이유로 썼습니다.
typedef struct _IO_REMOVE_LOCK {
    BOOLEAN      Removed;
    BOOLEAN      Reserved [3];
    __volatile LONG     IoCount;
    KEVENT       RemoveEvent;
} IO_REMOVE_LOCK;

그리고 유저어플에서 스레드를 사용하는 이유와 다르게 커널에서만의 이유가 따로 있습니다.
바로 IRQL 때문에 스레드를 만들어서 사용해야 하는 이유도 있습니다.

보통 PASSIVE_LEVEL을 보장받지 못하는 함수에서  PASSIVE_LEVEL을 보장받아야 하는 코드 작성하는 경우입니다.
예를 들어서 IRP를 처리하는 IRPDispatch함수는 PASSIVE_LEVEL을 보장하지 못합니다.
또는 DPC 큐에 모듈을 넣는 경우, IRQL을 높혀서 사용하는 경우.. (스핀락..) 등등입니다.
만약 이러한 곳에서 스케쥴링이 일어날만한 코드를 실행할 경우 블루스크린이 발생할 수도 있습니다.
스케쥴링이 일어날만한 코드는 PASSIVE_LEVEL을 보장받아야 하죠.

그렇기 때문에 PASSIVE_LEVEL을 보장해주는 스레드를 사용하는 것입니다.
스레드는 평상시 대기하고 있다가 드라이버가 스레드에게 작업을 지시하면 스레드는 코드를 실행하면 되는 것입니다.
평상시 대기하고 있다가 지시를 받으면 활동한다??
이렇게 스레드를 구현하려면 어떻게 해야할까요??
바로 동기화 이벤트 오브젝트를 사용하면 됩니다. 드라이버에서 가장 많이 사용되는 이벤트 오브젝트 이지요 :-)
자세한 설명은 하지 않겠습니다. 
유저어플에서 사용하는 이벤트 오브젝트와 내용은 같습니다. 사용하는 인자만 조금 다를 뿐이지요.
여기서도 간단한 샘플 코드를 제가 만들어 봤습니다.
DriverEntry를 보시면 먼저 KeInitializeEvent 함수로 이벤트 초기화를 해줍니다.
두번째 인자로 저 값을 전달하면 자동 리셋모드 입니다. signal -> non-signal로 자동으로 바뀝니다.
세번째 인자는 FALSE 값을 전달하면 기본으로 non-signal 상태로 이벤트가 생성이 됩니다.

그 아래를 보시면 5초에 한번씩 KeSetEvent 함수를 사용해 해당 이벤트를 signal 상태로 바꿔줍니다.
그러면 스레드에서는 KeWaitForSingleObject에서 멈춰있다가 이벤트가 signal상태가 되면 
해당 함수를 빠져나가 그 아래 코드를 진행하게 되죠. 그러면서 이벤트는 자동으로 non-signal상태가 되고
스레드에서는 또 다시 이벤트가 signal 될때까지 기다릴 것입니다.

한번 실행시킨 결과를 보시겠습니다.


정리하는 마음으로 써봤는데 뭔가 설명이 복잡하군요. 잘못된 내용이 있으면 지적 바랍니다 ^^;
그리고 책을 보다가 재미있는 함수가 있던데
KeStackAttachProcess 함수입니다.
해당 함수는 스레드에서 사용하는 함수인데 이 함수를 호출하면 스레드는 첫번째 파라미터로 전달된 프로세스 아래서 실행되게 됩니다. 잘 활용하면 스레드가 이리저리 빌붙어서 다니는거죠.. =_=;;??
아무튼 이러한 함수도 있다라는 것입니다.