윈도우 디바이스 드라이버 프로그래밍 기초
여기에 있는 내용들은 전부 Windows Driver Model에서 배운 내용들이다.
만일 드라이버를 처음 접해서 아래 내용이 전혀 이해가 안되더라도 그냥 외워서 따라할 수 있도록 정리해놨다.
경험이 쌓여가면서 하나씩 이해가 될 것이다.
IRP를 완료시킬 때는 STATUS_PENDING
이라는 상태 코드를 써서는 안된다.
디스패치 루틴에서 STATUS_PENDING
을 리턴할 수는 있지만 IoStatus.Status
에 STATUS_PENDING
을 할당한 뒤 I/O를 완료시키면 안된다.
IoCompleteRequest를 호출 하기 전에는 IRP에 대해 지정한 취소 루틴을 제거해야 한다.
Driver Verifier는 이런 경우를 적절히 찾아내서 버그체크를 띄워준다.
단, Irp가 큐잉된다면(StartPacket
을 한다던지 Cancel-Safe Queue에 들어간다던지) 디큐될 때에 자동으로 취소 루틴이 제거된다.
Irp를 종료할 때, IoStatus.Status와 리턴 값은 일관성이 있어야 한다.
즉 IoStatus.Status
는 성공 코드를 넣고 실패 코드로 리턴하는 경우가 있으면 안된다.
Driver Verifier
가 이 오류 또한 적절히 발견해줄 것이다.
디스패치 루틴에서 STATUS_PENDING
을 리턴하기 전에는 Irp를 Pending으로 Marking해준다.
반대로 Irp를 Peding으로 마킹해 주지 않으면 STATUS_PENDING
으로 리턴하면 안된다.
둘은 항상 짝이 맞춰져서 다녀야 한다.
IoStartPacket을 호출하고 나면 더이상 IRP를 건드리지 않는다.
함수가 리턴되자마자 Irp는 이미 완료되었을 수 있다.
STATUS_MORE_PROCESSING_REQUIRED
를 리턴하지 않는 모든 완료 루틴에 대해서 다음의 코드를 넣는다.
if(Irp->PendingReturned)
{
IoMarkIrpPending(Irp);
}
완료루틴을 설정하지 않았을 경우에는 시스템이 Irp를 완료시켜가면서 상위 스택으로 Pending 비트를 복사해주지만,
완료 루틴이 설정되어 있으면 시스템이 이 작업을 해주지 않기 때문에 해당 완료 루틴에서 직접 PendingReturned
값을 체크하여 Irp를 Pending으로 마크해주어야 하기 때문이다.
(어떤 경우라도 시스템은 PendingReturned
변수만큼은 항상 잘 셋팅해준다)
만약 잘 이해되지 않는다면 그냥 외우기만 해도 좋다. 이는 언제나 참이다.
단, 완료 루틴에서만이다. 디스패치 루틴과 착각하면 안된다.
Lower Driver로 Irp를 건네주는 과정에서 완료루틴을 설정한다면 반드시 IoCopyCurrentIrpStackLocationToNext
를 호출해야 한다.
IoSkipCurrentIrpStackLocation
은 안된다.
Next Driver에 Irp를 전달한 이후에는 더 이상 Irp는 자신의 소유가 아니며 건드리지 말아야 한다.
그 Irp는 다른 드라이버나 쓰레드에서 free되거나 완료되었을 수 있다.
하지만 만일 드라이버가 스택 아래로 Irp를 건네준 이후에 그 Irp에 접근하고 싶다면 반드시 완료 루틴을 설정해야만 한다.
Io 매니저가 해당 완료루틴을 호출해 줄 때 완료루틴 내에서 잠시 다시 Irp는 내 드라이버의 소유가 되고, Irp에 액세스 할 수 있다.
만약 Next Driver가 Irp를 완료 시킨 후에, 내 드라이버의 디스패치 루틴이 그 Irp에 접근해야만 한다면 완료루틴은 반드시 STATUS_MORE_PROCESSING_REQUIRED
를 리턴해야만 한다.
이는 Irp의 소유를 디스패치 루틴에게로 건네준다.
IO 매니저는 Irp의 처리를 중지하고 디스패치 루틴에게 궁극적으로 해당 Irp의 Completion을 맡긴다.
디스패치 루틴은 이후에 IoCompleteRequest
를 호출해서 Irp를 완료시킬 수도 있고 처리를 더 하기 위해 Pending으로 마킹할 수 있다.
드라이버는 다음과 같은 경우 반드시 STATUS_PENDING
을 리턴해야만 한다.
- Irp가 완료되기 전에 디스패치 루틴이 리턴하는 경우
- 해당 Irp가 다른 쓰레드에서 완료되는 경우
Arbitrary 쓰레드에서는 비동기적 Irp만을 생성할 수 있다.
NonArbitrary 쓰레드에서는 동기 Irp도 생성할 수 있다.
IRQL이 Dispatch Level 이상인 경우에는 쓰레드를 대기 시켜서는 안된다.
즉, KeWaitFor- 같은 함수 들을 사용할 수 없다는 이야기이다.
단, 쓰레드를 재우지 않고 단순히 오브젝트가 시그널 되었는지 Peek 해보는 것은 상관없다.
대기 함수들에 타임아웃값을 0으로 넘겨주면 Peek만 시도할 수 있는데, 유저모드에서는 WaitFor함수 패밀리에 dwMilliseconds
를 0으로 건네주면 된다.
커널 모드(KeWaitFor 함수 패밀리)에서는 유저모드처럼 그냥 0을 넣어버려서는 안된다.
KeWaitFor 함수에 NULL 포인터를 전달하게 되면 Peek하겠다는 것이 아니라 무제한 기다리겠다는 의미이다.
반드시 LARGE_INTEGER
값을 0으로 셋팅해서 함수에 넘겨주어야만 한다.
쓰레드를 재우면 안되는 이유는, IRQL이 Dispatch Level 이상이 되면 다시 재스케줄링 될 수 없기 때문이다.
이는 비단 WaitFor 함수 패밀리뿐 아니라, 쓰레드를 대기 상태로 만드는 모든 함수를 사용 할 수 없다는 의미임을 알아야 한다.
IRQL이 디스패치 레벨 이상이 된 경우에는 특정 데이터를 참조하기 위해 페이지 폴트를 일으켜서는 안된다.
페이지 폴트는 소프트웨어 예외 중 하나인데 사실 이는 디스패치 레벨 상태에서는 페이지 폴트 뿐만이 아니라 모든 경우의 Exception이 발생해서는 안된다는 의미이다.
이는 위에서 설명한 대기 함수를 사용할 수 없는 이유와 같다.
PAGED_CODE
매크로를 잘 사용하자.
#pragma alloc_text(PAGE, Function Name)
위와 같은 디렉티브를 선언했다면 그 함수의 시작부에 PAGED_CODE
매크로를 사용한다.
이는 반드시 한 쌍으로 있어야 한다.(둘다 있거나 둘 다 없어야 한다.)
Pagable에 대한 의미는 따로 설명하지 않는다.
PAGED_CODE
에 대한 더 자세한 내용은 여기에 써두었다.
커널 스택은 x86에서 12K만큼 할당된다. amd64에서는 24K이다.
단지 자신의 드라이버에서만 커널 스택을 사용할 것이라 착각해서는 안된다.
자신의 드라이버의 상위에 다른 필터 드라이버들이 있을 경우, 해당 필터드라이버가 사용하고 남은 커널 스택만큼을 받아서 사용하게 된다.
따라서 평소엔 잘 돌아가던 드라이버가 백신같은 필터 드라이버가 설치된 환경에서는 블루스크린이 발생하기도 한다.
이것은 모든 드라이버에서 최대한 스택을 아껴써야 함을 의미한다.
유저모드에서 하듯이 다음 줄처럼 코딩 해서는 안되며
WCHAR sz[MAX_PATH]; // 지역변수 하나가 커널 스택의 약 5%를 써버렸다.
좀 귀찮더라도 아래와 같은 방법으로 수정되어야 한다.
PWCHAR sz = ExAllocatePoolWithTag(PagedPool, sizeof(WCHAR) * MAX_PATH, POOL_TAG);
if(sz == NULL)
{
// Process the error.
}
__try
{
// Do something
}
__finally
{
if(sz)
{
ExFreePoolWithTag(sz, POOL_TAG);
}
}
커널 스택은 pagedpool에 할당됨을 알고 있어야 한다.
이는 함수가 대기 상태로 되면 시스템에 의해서 언제라도 Page out 될 수 있다는 의미이다.
Event 객체는 NonPagedPool에 할당되어야 한다.
스택에 할당했을 경우에는 KeWaitFor 함수의 WaitMode
인자로 KernelMode
를 전달함으로써 이 스택 메모리가 Page out 되지 않도록 할 수 있다.
DriverEntry가 아닌 곳에서 디바이스를 생성한다면, 초기화가 끝난 이후에 DO_DEVICE_INITIALIZING
플래그를 지워줘야 한다.
이는 AddDevice 루틴도 예외가 아니다.
deviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
이것을 지워주지 않는다면, CreateFile
같은 Api로 장치를 열 수 없다.
만약 DriverEntry
에서 만드는 경우에는 리턴되자마자 IoManager
가 알아서 플래그를 지워주므로 생략해도 상관없다.
캔슬 큐에 집어 넣고 pend 상태로 처리한 Irp는 나중에 완료하기 직전에 꼭 Cancel Queue에서 제거해주어야 한다.
이 때 Cancel Queue에서 빼면서 리턴값을 꼭 검사해서 실제로 Queue에 존재했었고 제거된 경우에만 이 Irp를 완료시켜야 한다.
// pIrpContext는 Insert할 때 넣어주었던 그 변수를 사용해야 한다.
PIRP pPendedIrp = IoCsqRemoveIrp(&g_pDevExt->cancelIo.CancelSafeQueue, pIrpContext);
if(pPendedIrp)
{
DriverLog(SHOW, "pIrp = 0x%08X\n", pIrp);
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = information;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}
이렇게 해야 여러 요청이 동시에 들어왔을 때의 미묘한 동기화 문제를 피할 수 있다.
그렇지 않으면 같은 Irp를 2번 이상 완료시킬 수 있다
함께 읽으면 좋은 글: