티스토리 툴바


posted by 동건이 2007/04/30 10:35

WDM 디바이스 드라이버 골격

1. 신 윈도우 디바이스 드라이버 모델, WDM

WDM은 윈도우 2000 혹은 윈도우 9x에 적용될 새로운 디바이스(USB와 IEEE 1394 등)를 위한 디바이스 드라이버 모델이다. 주목할만한 점은 플러그 앤 플레이를 지원하기 때문에 기존 NT 드라이버와 달리 I/O 관리자가 직접 디바이스 자원을 할당하고 그 결과를 드라이버에 알려주는 방식을 채택했다는 것이다. 따라서 드라이버 개발자가 일일이 하드웨어 자원을 찾아 할당하는 번거로운 과정이 필요없다. 하지만 WDM은 최근 선보인 일부 디바이스만 지원하므로 기존 드라이버를 모두 대체하지 못하며 기존 디바이스에 대한 드라이버를 만들려면 해당 운영체제에 맞는 드라이버를 작성해야 한다.

WDM 드라이버는 기존 NT 드라이버를 기반으로 하는 실행 파일(sys 형태)로 작성된다. 대부분의 소스 코드가 윈도우 NT와 비슷하므로 먼저 NT 드라이버 작성에 필요한 사항을 알아야 한다. 즉, WDM 작성시 관련 매뉴얼에 찾는 내용이 없다면 NT 드라이버 작성법을 참조하면 된다는 뜻이다. 그러므로 NT 드라이버에 대해 전혀 모른다면 상당한 고초를 겪을 것이다. 이를 위해 WDM 드라이버 작성에 바로 들어가지 않고 NT 드라이버 모델에서 알아야 할 몇 가지 내용을 정리한 다음 WDM 드라이버 작성에 대해 설명하겠다.


 

WDM DDKWDM DDK는 마이크로소프트 홈페이지에서 윈도우 98용과 윈도우 2000용을 구할 수 있다. 윈도우 2000용은 베타 버전에 맞춰 DDK가 배포되고 있으며 윈도우 NT 4.0에서는 사용할 수 없다. 

DDK를 구해 설치한 다음 DDK의 bin 디렉토리를 실행 경로에 넣어놓는 것이 여러모로 편하다. 또한 DDK로 컴파일하기 전에 bin 디렉토리의 setenv.bat을 실행해 라이브러리 관련 환경을 설정해야 한다. 그런데 환경 설정 과정에서 환경 변수를 상당히 많이 할당하므로 config.sys에 다음 문장을 추가해 환경 변수가 사용할 버퍼를 충분히 할당하는 것이 좋다.

shell=c:\win98\command.com /e:4096 /p

setenv.bat을 실행하기 전에 비주얼 C++의 bin 디렉토리에 있는 vcvars32.bat을 실행해 비주얼 C++도 설정해야 한다. 필자는 이들 배치 파일을 autoexec.bat에서 실행하도록 만들었다. 또한 윈도우 NT에서는 비주얼 C++를 설치하는 과정에서 환경 변수를 설정할 것인지 확인하고 전부 알아서 설정해 편리하지만, DDK의 경우에는 별도로 취할 수 있는 방법이 없으므로 직접 입력해야 한다.

 

계층형 드라이버
윈도우 NT에서도 유닉스와 마찬가지로 유저 모드와 커널 모드로 구분되어 소프트웨어가 실행된다. 즉, 유저 모드 프로그램은 Win32 시스템에서 실행되며 커널 모드 프로그램은 하드웨어 관련 소프트웨어를 다룬다. 윈도우 NT는 다양한 CPU를 지원하고 여러 개를 사용할 수 있으므로 직접 디바이스 제어하는 드라이버 모델인 VxD 드라이버(대신 호환성은 떨어진다)를 작성할 수 없다. 대신 드라이버는 HAL이라는 최종 인터페이스를 통해 실제 디바이스와 통신한다. 또한 디바이스 종류에 따라 작성하는 드라이버 구조도 차이가 있다.

 

계층형 구조를 가진 NT 드라이버의 중요한 특징은 특정 드라이버가 자신의 현재 입출력 요청을 처리할 수 있는 다른 드라이버에 넘겨 대신 처리하도록 요청할 수 있다는 점이다. 물론 처리 결과는 요청한 드라이버가 다시 받는다. 윈도우 NT는 이를 응용해 특정 디바이스에 대한 드라이버를 하드웨어에 근접한 순으로 여러 층에 걸쳐 구성하고 상위 드라이버가 애플리케이션에서 입출력 요청을 받아 차례대로 하위 드라이버에게 처리 요청을 전달하는 방식을 사용한다.

계층형 드라이버는 여러 개의 미니 드라이버와 소수의 버스/클래스 드라이버가 계층을 이루고 있다. 즉 특정 디바이스에 통달한 드라이버 마스터가 만든 하위의 버스 클래스 드라이버와 클래스 드라이버는 특정 디바이스의 기본 입출력 기능만 제공하고 복잡한 디바이스 제어는 모두 숨긴다. 그러면 드라이버 개발자는 클래스 드라이버가 제공하는 입출력 패턴을 사용해 자신이 작성할 드라이버를 보다 간단하게 만드는 것이다.


필터 드라이버필터 드라이버는 계층형 드라이버 중간의 적절한 위치에 드라이버를 착탈식으로 삽입시켜 이 드라이버가 입출력 요구를 처리하도록 만든 구조이다. 바로 WDM 드라이버가 필터 드라이버에 해당한다. WDM 드라이버는 디바이스를 설치함과 동시에 동작하며 디바이스가 컴퓨터에서 제거되면 정지하고 제거된다.

 

2. 입출력 처리, IRP

드라이버는 다른 사용자나 다른 드라이버의 요구에 응답해 디바이스에 지시하거나 디바이스에서 특정 값을 읽어들이는 역할을 한다. 이것이 디바이스 제어의 핵심이다. 즉, 마우스와 같은 단방향 디바이스는 디바이스에서 값을 읽어들이고 하드 디스크와 같이 양방향 디바이스는 사용자 지시에 따라 디바이스를 읽거나 쓰기를 한다. 윈도우 9x에서 특정 포트에 값을 기록하는 것은 쉽다. 즉 드라이버를 만들지 않아도 VMM이 대신 처리해주므로 간단한 프로그램은 무리없이 동작되며 VxD도 특별한 디바이스가 아니면 쉽게 값을 읽고 쓸 수 있다. 이는 윈도우 9x가 인텔 기반만 지원하기에 가능한 일이다.

하지만 윈도우 NT는 여러 CPU를 지원하기 때문에 특정 CPU의 어셈블리어를 사용한 직접적인 입출력 루틴을 적용할 수 없다. 대신 윈도우 NT가 정한 방법만 사용해야 한다. 윈도우 NT에서는 디바이스에 대한 입출력 처리를 패킷 단위로 처리하며 드라이버 개발자는 이 패킷을 구조체 IRP(I/O Request Packets)를 사용해 생성, 전달, 처리할 수 있다. IRP는 애플리케이션이나 다른 드라이버에서 전달받아 현재 드라이버에서 패킷 처리를 완료하거나 다른 하위 드라이버에 전달해 처리를 요청할 수도 있다.

IRP는 입출력에 관계된 변수를 포함한 가변 크기 구조체이며 NonPaged pool에 생성해야 한다. IRP 구조는 wdm.h에서 확인할 수 있으며 다양한 변수를 포함하지만 드라이버 작성에 중요한 것은 IO_STATUS_BLOCK과 IO_STACK_LOCATION 정도이다.

Paged pool과 NonPaged pool
디바이스 드라이버가 사용할 수 있는 메모리 영역을 가리키는 것으로, 사용 가능한 4GB 메모리 중 최상단에 위치한다. 용어에서 짐작할 수 있듯이 페이징 가능한 가상 메모리와 물리 메모리에 맵핑되는 가상 메모리를 나타낸다.

IO_STATUS_BLOCK은 IRP의 마지막 상태를 나타내며 Status와 Information, 두 멤버로 구성되어 있다. Status에는 IRP의 마지막 입출력 요구 처리 결과를 기록하며 현재 드라이버에서 입출력 처리를 마치면 결과값을 기록한다. Information에는 실제 데이터 전송이 있을 때 전송된 데이터 크기를 기록하며 그외의 경우 요구 처리가 성공적이면 0을 넣고 다른 값을 넣으면 실패로 간주된다.

 IO_STACK_LOCATION은 처리할 것과 그 종류에 관한 정보를 보관한다. 미리 정의한 입출력 요청에 대한 코드값(MajorFunction)이 들어있으며 드라이버 개발자는 이에 따라 맞는 처리를 하면 된다. MajorFunction은 윈도우 프로시저처럼 메시지를 switch/case문으로 처리하지 않고 각 MajorFunction마다 처리 함수를 할당한다. 그리고 윈도우 메시지처럼 처리할 IRP에 따라 정의되어 있다. 어떤 MajorFunction 코드는 다시 여러 개의 MinorFunction 코드값을 가질 수도 있는데 일반 윈도우 메시지에 딸린 wParam과 비슷하다고 이해하면 될 것이다.

<표 1> IO_STACK_LOCATION 멤버

IO_STACK_LOCATION 멤버

설명

MajorFunction

입출력 요청 코드. IRP_MJ_xxx 형태로 정의된다.

MinorFunction

MajorFunction의 세부 코드값. IRP_MN_xxx 형태로 정의된다.

union

구조체 Read

IRP_MJ_READ 요청이 왔을 때 참조한다.

구조체 Write

IRP_MJ_WRITE 요청이 왔을 때 참조한다.

구조체 DeviceIoControl

IRP_MJ_INTERNAL_DEVICE_CONTROL이나 IRP_MJ_DEVICE_CONTROL 요청이 왔을 때 참조한다.

계층형 드라이버에서는 최상단 드라이버에서 최하위 드라이버로 내려갈수록 IRP 크기가 증가한다. 이는 현재 스택 위치에 적합한 IO_STACK_LOCATION이 IRP에 덧붙여지기 때문이며 이런 이유로 계층별 드라이버와 IO_STACK_LOCATION은 1대1로 대응된다고 볼 수 있다. 각 드라이버는 자신에 맞는 IO_STACK_LOCATION을 구해 처리하고 다음 하위 드라이버에 전달하면 된다. 또한 DDK에는 IO_STACK_LOCATION을 적용하기 위한 매크로를 정의해 놓았다. 매크로는 wdm.h에서 찾을 수 있다.


<그림 5> 드라이버와 IRP

<표 2> IO_STACK_LOCATION을 돕기 위한 매크로

이름

설명

IoGetCurrentIrpStackLocation

전달받은 IRP에서 스택 위치를 얻는다.

IoMarkIrpPending

현재 IRP가 앞으로 더 처리될 내용할 있음을 표시한다.

IoGetNextIrpStackLocation

하위 드라이버의 스택 위치를 구한다.

IoSetNextIrpStackLocation

하위 드라이버에 스택 위치를 지정한다.

IoSetCompletionRoutine

하위 드라이버가 입출력 처리를 마쳤을 때 실행될 함수를 지정한다.

 

DRIVER_OBJECT프로그램 내부의 드라이버에 대해 알아보자. 드라이버를 표현하는 객체는 wdm.h에 구조체로 선언되어 있으며 내부 멤버는 특정 이벤트가 발생했을 때 드라이버 시작과 종료, 각 요청 내용을 처리할 함수(Dispatch Routine)의 포인터 변수와 드라이버 이름을 저장할 버퍼로 구성되어 있다.

DriverEntry()드라이버 시작 함수로, 드라이버가 메모리에 로드될 때 제일 먼저 호출된다. 기존 버전과 DriverEntry()와 다른 점은 원래 이 함수가 하던 일을 AddDevice()에서 처리한다는 것이다. 즉, DRIVER_OBJECT를 초기화해 드라이버가 처리할 IRP에 대응되는 처리 함수(디스패치 함수)를 지정한다.

NTSTATUS
DriverEntry (
   IN PDRIVER_OBJECT driver_object,
   IN PUNICODE_STRING reg_path )

첫째 인자로 넘어오는 DRIVER_OBJECT 포인터는 구조체이며 이 구조체의 멤버 변수는 드라이버가 처리할 메시지에 1대1로 대응되는 함수의 포인터이다. 드라이버 개발자는 자신이 사용할 메시지에 대한 함수 포인터를 넘겨주면 DriverEntry()에서 할 일은 끝난 샘이다. <표 3>은 관련 메시지에 대한 설명이다.

<표 3> DriverEntry() 관련 이벤트

이벤트

설명

중요도

DriverExtension->AddDevice

디바이스가 PC에 연결되었을 때 호출되며 드라이버 객체를 사용해 논리 디바이스(FDO)를 생성한다. 그리고 애플리케이션에서 드라이버를 사용할 경우, 애플리케이션에서 드라이버를 찾을 수 있도록 심볼릭 링크를 생성하는 일을 한다.

필수

DriverUnload

디바이스가 PC에서 제거되어 드라이버가 필요없어져 드라이버가 해제될 때 호출된다.

옵션

 

<표 4> IRP_MJ 관련 메시지

MajorFunction의 IRP_MJ_xx

설명

중요도

IRP_MJ_PNP

논리 객체 생성 후, 드라이버가 실제 동작할 때의 모든 일을 담당한다. 디바이스 제거시 드라이버에서 사용한 자원 처리도 여기서 한다. 여러 개의 마이너 코드를 갖고 있으며 WDM 드라이버의 핵심 코드도 이곳에 있다.

필수

IRP_MJ_DEVICE_CONTROL

애플리케이션과 드라이버 간의 블럭 데이터 통신을 위해 사용된다. 드라이버 개발자가 통신 코드를 직접 정의한다.

옵션

IRP_MJ_CREATE

애플리케이션에서 드라이버로 연결하는 통로(애플리케이션이 사용할 자원을 제공). Win32 API인 CreateFile()과 연결된다.

옵션

IRP_MJ_CLOSE

애플리케이션이 드라이버와 연결을 끊을 때 사용한다. Win32 API인 CloseFile()과 연결.

옵션

IRP_MJ_POWER

디바이스에 관련된 전원 처리 마이너 코드를 처리한다.

옵션

 

AddDevice()윈도우 NT에서는 실제 디바이스를 직접 제어하지 못한다. 드라이버도 예외는 아니어서 물리 디바이스 객체를 다루는 것이 아니라 물리 디바이스와 대응되는 논리 디바이스 객체를 만들어 사용한다. AddDevice()에서는 DriverEntry()에서 받은 물리 디바이스 객체(PDO)를 사용해 PDO의 복사본인 논리 디바이스 객체(FDO)를 생성해야 한다.

NTSTATUS
add_device (
   IN PDRIVER_OBJECT driver_object,
   IN PDEVICE_OBJECT pdo
   );

부가적으로 일반 애플리케이션이 이 드라이버를 사용할 때 드라이버와 연결 가능한 심벌 이름을 생성한다. 여기서 FDO를 생성하는 방법을 알아보자.

FDO 생성함수 인자의 접두사인 IN과 OUT은 단순히 선언문으로, 어떤 작업을 하지는 않으며 단지 외부에서 값을 들여오는 인자인지 바깥으로 값을 보내는 인자인지 알려주는 역할만 한다. 상당히 쓸만한 아이템으로 다른 사람의 소스 코드를 보는데 큰 도움이 된다.

NTSTATUS
IoCreateDevice(
   IN PDRIVER_OBJECT DriverObject,
   IN ULONG DeviceExtensionSize,
   IN PUNICODE_STRING DeviceName OPTIONAL,
   IN DEVICE_TYPE DeviceType,
   IN ULONG DeviceCharacteristics,
   IN BOOLEAN Exclusive,
   OUT PDEVICE_OBJECT* DeviceObject
);

생성을 마치면 현재 드라이버를 제어할 디바이스 드라이버 스택에 추가해야 한다. IoAttachDeviceToDeviceStack()은 생성된 FDO를 드라이버 스택에 추가시키며 성공적으로 수행되면 바로 다음 드라이버의 포인터를 넘겨준다.

<표 5> 논리 디바이스의 인자

인자명

설명

DriverObject

DriverEntry()에서 넘어온 드라이버 객체

DeviceExtensionSize

드라이버가 사용할 정보 구조체인 DeviceExtension의 크기. 사용자가 직접 정의한다(크기도 매번 다르다).

DeviceName

드라이버 이름으로, 부가 기능이다. 이름은 유니코드 문자열 형태로 바꿔야 한다.

DeviceType

드라이버 종류를 나타낸다(키보드나 마우스 등). 불확실하면 FILE_DEVICE_UNKNOWN을 사용한다.

DeviceCharacteristics

디바이스 속성에 결정한다. DDK 도움말에 명시되어 있으며 해당 사항이 없는 경우엔 0으로 설정한다.

Exclusive

현재 디바이스가 입출력을 한 번에 하나씩만 처리할 것인지 명시한다(일반 형태가 아닌 OEM 입출력인 경우에는 TRUE로 명시한다).

*DeviceObject

새로이 만들어진 FDO를 저장할 포인터

DEVICE_EXTENSION윈도우 NT의 디바이스 드라이버는 객체지향 구조로 되어있으며, 엄밀히 말하면 객체를 사용한다. 그러나 객체와 객체를 제어하는 함수는 있지만 정확한 속성은 정의되어 있지 않다. 객체 속성이라 할 수 있는 DEVICE_EXTENSION 구조체는 드라이버 개발자가 정의해야 한다. 구조체 작성에 특별한 원칙은 없으며 WDM 드라이버에서 공통되는 내용은 디바이스 이름과 다음 드라이버의 포인터, 그리고 FDO를 정의하는 것으로 시작된다.

 

3. 디바이스 이름과 심볼릭 링크 이름 생성/등록

디바이스에 할당된 이름은 모두 유니코드 형태를 가지며 DeviceName에 이름을 삽입하려면 일반 문자열을 UNICODE_STRING으로 바꿔야한다. UNICODE_STRING은 구조체로, 다음 세 가지 변수를 갖고 있다.

USHORT

Length

현재 Buffer에 들어있는 문자열 길이

USHORT

MaximumLength

buffer가 감당할 수 있는 최대 문자열 길이. Length와 동일하게 사용

PWSTR

Buffer

실제 문자열

디바이스 이름을 다음과 같이 설정한다. 모든 디바이스 이름에는 L"\\Device\\"가 붙게 된다.

UNICODE_STRING unistr_dev_name;
PCWSTR base_name = L"\\Device\\";
ULONG len;

len = uni_strlen(base_name) + uni_strlen(device_name);

unistr_dev_name.MaximumLength = static_cast((len + 1) * sizeof(WCHAR));
unistr_dev_name.Length = static_cast(len * sizeof(WCHAR));
unistr_dev_name.Buffer = new (PagedPool) WCHAR[len + 1];

if (unistr_dev_name.Buffer == NULL)
{
   nt_status = STATUS_INSUFFICIENT_RESOURCES;
   goto $exit_proc;
}

RtlZeroMemory (unistr_dev_name.Buffer, unistr_dev_name.MaximumLength);

uni_strcpy(unistr_dev_name.Buffer, base_name);
uni_strcat(unistr_dev_name.Buffer, device_name);

유저 모드 프로그램이 드라이버를 사용하려면 커널 모드 드라이버에서 유저 모드 프로그램이 드라이버를 찾을 수 있는 이름이 필요하다. 디바이스에 정확한 심볼 이름을 명시할 경우에는 심볼 이름을 생성해야 하고 그렇지 않으면 guid를 사용해 심볼로 사용할 인터페이스를 등록해야 한다. 디바이스 심볼명은 디바이스 이름과 마찬가지 방법으로 만들고 새로 등록하려면 IoCreateSymbolicLink()를 사용한다. 등록하려는 심볼과 같은 이름이 있는 경우 등록할 수 없으므로 이름짓는데 주의하기 바란다(참고로 등록된 이름을 삭제하는 함수는 IoDeleteSymbolicLink()이다).

//
// 심볼명이 주어진 심볼 등록의 예
//
NTSTATUS nt_status;
UNICODE_STRING unistr_sym_link;
PCWSTR base_win_name = L"\\DosDevices\\";
ULONG len;

len = uni_strlen(base_win_name) + uni_strlen(link_name);

unistr_sym_link.MaximumLength = static_cast((len+1) * sizeof(WCHAR));
unistr_sym_link.Length = static_cast(len * sizeof(WCHAR));
unistr_sym_link.Buffer = new (PagedPool) WCHAR[len+1];

if (unistr_sym_link.Buffer == NULL)
{
   // 메모리 해제
   return STATUS_INSUFFICIENT_RESOURCES;
}

RtlZeroMemory(unistr_sym_link.Buffer, unistr_sym_link.MaximumLength);

uni_strcpy(unistr_sym_link.Buffer, base_win_name);
uni_strcat(unistr_sym_link.Buffer, link_name);

// 심볼릭 링크 생성 요청

nt_status = IoCreateSymbolicLink (&unistr_sym_link, &unistr_dev_name);

if (! NT_SUCCESS(nt_status))
{
   // 만약 이름이 중복되는 경우

   if (nt_status == STATUS_OBJECT_NAME_COLLISION)
   {
      // 중복될 경우의 처리
   }
   return nt_status;
}

//
// 심볼명이 주어지지 않은 경우 등록의 예
//

NTSTATUS nt_status = STATUS_SUCCESS;
PUNICODE_STRING s_dev_link_name;

//
// guid 기반 심볼릭 링크를 등록한다.
//

nt_status = IoRegisterDeviceInterface (
   pdo,
   (LPGUID)p_guid,
   NULL,
   s_dev_link_name );

if (! NT_SUCCESS(nt_status))
{
   return nt_status;
}

//
// 디바이스 링크 활성화
//

nt_status = IoSetDeviceInterfaceState(s_dev_link_name, TRUE);

return nt_status;
ext->top_of_stack_device_object = IoAttachDeviceToDeviceStack (fdo, pdo)

 

4. 디바이스의 구동과 작동 메시지

메이저 펑션과 마이너 펑션IRP Major Function은 '무엇을 한다'에 해당하는 부분으로, 디바이스가 작동하는 큰 맥락을 이룬다. 각 Major Function은 사용할 것에 한해 각 실행 함수(Dispatch Function)가 배당되어야 하며 세부 동작은 각 Marjor Function에 딸린 Minor Function을 참조해야 한다(드라이버 제작에 필요한 몇 가지 Major Function은 앞에서 설명되었다). 디스패치 함수의 프로토타입은 다음과 같다.

NTSTATUS
dispatch_pnp (
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp )

FDO는 디바이스에 명령을 내리기 위해 사용되었고 IRP는 디바이스에 내릴 명령을 기록하거나 현재 함수에서 처리하기 위한 것이다. 우선 할 일은 현재 IRP에서 드라이버가 위치한 IO_STACK_LOCATION을 구하는 것이다. 그리고 스택 정보에서 마이너 함수를 추출한다.

NTSTATUS
dispatch_handler (
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp)
{
   NTSTATUS nt_status = STATUS_SUCCESS;
   PDEVICE_OBJECT stack_device_object;
   PDEVICE_EXTENSION dev_ext;
   PIO_STACK_LOCATION irp_stack;

//
// IRP의 스택 로케이션을 구한다.
//

irp_stack = IoGetCurrentIrpStackLocation (irp);

// DEVICE EXTENSION 포인터 구함

dev_ext = fdo->DeviceExtension;
stack_device_object = dev_ext->top_of_stack_device_object;

// -----------------------------------------------------
// 마이너 디스패치 시작
// -----------------------------------------------------

increment_io_count (fdo); // 현재 스택 영역에서 작업 중임을 설정

switch (irp_stack->MinorFunction)
{
   // 마이너 메시지를 처리한다.
}

if (! NT_SUCCESS(nt_status)) // 마이너 메시지 처리 도중 실패한 경우
{
   // 아무 일도 없었던 것처럼 아래로 전달
   irp->IoStatus.Status = nt_status;
   IoCompleteRequest (irp, IO_NO_INCREMENT);
   goto $exit_proc;
}

//
// 마이너 평션을 성공적으로 처리함
//

IoCopyCurrentIrpStackLocationToNext (irp);
nt_status = IoCallDriver (stack_device_object, irp);

$exit_proc :

   decrement_io_count (fdo); // 작업 마침.
   return nt_status;
}

IO_COUNT드라이버 개발자는 드라이버가 입출력에 관계된 작업을 처리할 때 다른 것에 의해 방해받지 않도록 잠금을 설정하는 것이 유리하다(다른 IRP_MJ_xxx 메시지를 처리할 때도 가장 상단에는 락을 설정하고 작업을 마치면 락을 해제해야 한다). InterlockedIncrement()와 InterlockedDecrement()를 사용해 현재 입출력 레퍼런스 카운트를 늘리거나 줄임으로써 락킹을 할 수 있다. 이 함수의 입출력 카운트는 DEVICE_EXTENSION에 변수를 만들어 사용한다. 문제없이 작동되었다면 입출력 변수의 마지막 값은 시작과 동일한 0이 될 것이다.

VOID
increment_io_count (
   IN PDEVICE_OBJECT fdo)
{
   PDEVICE_EXTENSION dev_ext = get_device_extension (fdo);

   dev_ext->io_count_spin_lock.lock ();
   InterlockedIncrement (&dev_ext->pending_io_count);
   dev_ext->io_count_spin_lock.unlock ();
}

LONG
decrement_io_count (
   IN PDEVICE_OBJECT fdo)
{
   PDEVICE_EXTENSION dev_ext = get_device_extension (fdo);;
   LONG io_cnt;

   dev_ext->io_count_spin_lock.lock ();

   io_cnt = InterlockedDecrement(&dev_ext->pending_io_count);

   // trigger no pending io

   if (io_cnt == 1)
      dev_ext->no_pending_io_event.set (EVENT_INCREMENT);

   // trigger remove-device event

   if (io_cnt == 0)
      dev_ext->remove_event.set (EVENT_INCREMENT);

   dev_ext->io_count_spin_lock.unlock ();

   return io_cnt;
}

IRP_MJ_PNPIRP_MJ_PNP는 드라이버 설정이 끝나면 생성되어 현재 드라이버에 전달되며 디바이스를 초기화하고 디바이스를 동작시키며 디바이스가 제거되었을 때 뒷처리를 담당한다. 할 일이 많으므로 여러 개의 Minor Function을 가진다. 일단 마이너 메시지에서 현재 IRP를 다음 스택 드라이버가 처리하도록 한 다음, 자신이 할 일에 따라 순서대로 코드를 작성해야 한다. 다음 예제는 IRP_MN_START_DEVCE에 들어간 메시지 처리 내용이다. 내부 함수인 start_device()를 호출하기 전까지 내용은 Major Function이 Minor Function을 처리하는 것과 같다. 주요 내용은 현재 IRP를 다음 스택 드라이버가 처리하도록 하고 필요에 따라 완료 상태를 보고받는 것이다. 그런 다음 완료 상태를 '완료 루틴'에서 보고받아 나머지 처리를 하도록 구성되어 있다.

{
   start_device_event.init ();

   IoCopyCurrentIrpStackLocationToNext (irp);

   // PDO가 IRP를 모두 처리했을 때 확인할 수 있도록 완료 함수를 지정한다.

   IoSetCompletionRoutine (
      irp,
      irp_completion_routine,
      start_device_event, // 완료 루틴엔 "context"로서 넘겨진다.
      TRUE,                     // 성공시 호출
      TRUE,                     // 에러시 호출
      TRUE );                   // 취소시 호출

   // PDO가 IRP를 처리하게 한다.
   nt_status = IoCallDriver (stack_device_object, irp);

   //
   // PDO가 일을 못마쳤으면 될 때가지 기다리고 그 결과를 완료 루틴에서
   // 확인할 수 있도록 한다(이벤트가 재설정될 때까지 기다림).
   //

   if (nt_status == STATUS_PENDING)
      wait_status = start_device_event.wait ();

   // ** 여기에 들어갈 코드를 '완료 루틴'이 대신한다. **

   //
   // 디바이스를 시작할 준비가 되었음
   //

   nt_status = start_device (fdo);
   // 드라이버 개발자가 작성할 디바이스를 설정하는 루틴
   irp->IoStatus.Status = nt_status;

   IoCompleteRequest (irp, IO_NO_INCREMENT);
}

NTSTATUS
irp_completion_routine(
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp,
   IN PVOID context)
{
   c_event event ((PKEVENT)context);

   // Set the input event

   event.set (EVENT_INCREMENT);

   // from book

   if (irp->PendingReturned) IoMarkIrpPending (irp);

   //
   // IoFreeIrp()를 아직 호출하지 않았기 때문에
   // 반드시 STATUS_MORE_PROCESSING_REQUIRED를 리턴해야 한다.
   //

   return STATUS_MORE_PROCESSING_REQUIRED;
}

다음은 IRP_MJ_PNP MajorFunction에서 처리하는 Minor Function을 설명한다. 복잡하지 않은(대게 저가의) 디바이스의 경우 이 정도만 처리해도 문제없이 동작한다.

IRP_MN_START_DEVICEAddDevice()를 실행한 다음 바로 실행되는 Minor Function이다. AddDevice()가 드라이버를 초기화하는 일을 담당하는 것에 비해 Minor Function은 실제 디바이스를 설정하고 초기화하는 내용이 들어가야 한다. 즉, 이전 내용은 드라이버를 초기화하는 정형적인 내용이고 여기서부터는 실제 디바이스에 맞는 설정을 해야 한다. 디바이스 설정 내용은 사용할 디바이스에 따라 다르다(이 메시지는 필수 항목이다).

IRP_MN_REMOVE_DEVICE디바이스가 제거되었을 때 드라이버가 설정한 자원을 이 부분에서 제거한다. 설정할 자원은 디바이스 정보(디스크립터 등)와 FDO이다. 이 메시지는 필수 항목이다.

//
// case IRP_MN_REMOVE_DEVICE :
//

decrement_io_count (fdo);

//
// 읽고 쓰는 irp와 ioctl irp는 전부 실패 처리한다. 그러나 이것이 정상이다.
//
dev_ext->f_device_removed = TRUE;

IoCopyCurrentIrpStackLocationToNext (irp);

nt_status = IoCallDriver(stack_device_object, irp);

decrement_io_count(fdo);

// wait for any io request pending in our driver to
// complete for finishing the remove

dev_ext->remove_event.wait (Suspended, KernelMode, FALSE);

// Delete the link and FDO we created

remove_device (fdo); // 드라이버가 제어하는 디바이스에 관련된 내용 제거

IoDetachDevice (dev_ext->top_of_stack_device_object);

IoDeleteDevice (fdo);

return nt_status; // end, case IRP_MN_REMOVE_DEVICE

//
// remove_device()
//
NTSTATUS
remove_device (
   IN PDEVICE_OBJECT fdo)
{
   NTSTATUS nt_status = STATUS_SUCCESS;
   PDEVICE_EXTENSION dev_ext;

   dev_ext = get_device_extension (fdo);

   //
   // remove the GUID-based symbolic link(guid 이름을 사용한 경우)
   //
   // UNICODE_STRING s_device_link;
   // RtlInitUnicodeString (&s_device_link, dev_ext->s_device_link);
   // nt_status = IoSetDeviceInterfaceState (&s_device_link, FALSE);
   // ASSERT (NT_SUCCESS(nt_status));

   //
   // 드라이버가 제어한 디바이스에 관련된 항목 해제를 이곳에서 한다.
   //

   return nt_status;
}

IRP_MJ_CREATE유저 모드 프로그램에서 CreateFile()을 사용해 드라이버와 연결할 때 보내는 마이너 메시지로, 드라이버가 유저 모드 프로그램과 통신하는데 반드시 필요하다. 입출력 연결 디바이스를 여는 경우 이름을 다시 분리해 연결할 디바이스의 정확한 위치를 지정하고 이에 대한 처리를 IoCompleteRequest()로 마무리 짓는다.

NTSTATUS
create (
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp)
{
   NTSTATUS nt_status = STATUS_SUCCESS;
   NTSTATUS act_status;

   PFILE_OBJECT file_object;
   PIO_STACK_LOCATION irp_stack;
   PDEVICE_EXTENSION dev_ext;

   ULONG i, name_len, ix, uval, umultiplier;

   dev_ext = get_device_extension(fdo);
   interface = dev_ext->USB_interface;

   increment_io_count(fdo);

   irp_stack = IoGetCurrentIrpStackLocation (irp);
   file_object = irp_stack->FileObject;

   // fscontext is null for device

   file_object->FsContext = NULL;
   name_len = file_object->FileName.Length;

   //
   // 실제 디바이스의 입출력 통로를 여는 코드가 있어야 한다.
   //

   irp->IoStatus.Status = nt_status;
   irp->IoStatus.Information = 0;

   IoCompleteRequest (irp, IO_NO_INCREMENT);

   decrement_io_count (fdo);
   return nt_status;
}

IRP_MJ_CLOSE유저 모드 프로그램이 드라이버 사용을 마치고 드라이버와 연결을 종료할 때 호출된다. 열린 입출력 디바이스를 닫고 이 메시지에 대한 요청을 IoCompleteRequest()로 마무리 짓는다.

NTSTATUS
close (
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp)
{
   NTSTATUS nt_status;
   NTSTATUS act_status;

   PFILE_OBJECT file_object;
   PIO_STACK_LOCATION irp_stack;

   PDEVICE_EXTENSION dev_ext;

   increment_io_count(fdo);

   dev_ext = get_device_extension(fdo);
   irp_stack = IoGetCurrentIrpStackLocation (irp);
   file_object = irp_stack->FileObject;

   //
   // 열려있는 입출력 통로가 있으면 닫는 코드가 여기에 위치한다.
   //

   decrement_io_count(fdo);
   
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;

   nt_status = irp->IoStatus.Status;

   IoCompleteRequest (irp, IO_NO_INCREMENT);

   return nt_status;
}

IRP_MJ_DEVICE_CONTROL유저 모드 프로그램이 IOCTL 블럭을 사용해 드라이버에게 값을 보낸 디바이스를 제어하거나 디바이스에서 값을 얻어올 때 사용한다. IRP_MJ_READ 혹은 IRP_MJ_WRITE처럼 단일 버퍼 입출력이 아닌 보다 복잡한 정보를 주고받을 때 사용한다. 입출력에 필요한 버퍼는 드라이버 개발자가 구성하며 보통 구조체를 설정해 사용한다. 이 구조체는 드라이버와 애플리케이션이 모두 같이 알고 있어야 한다. 참고로, 도스 시절부터 사용되었으며 도스 관련 시스템 프로그래밍 단행본을 뒤져보면 IOCTL을 사용한 디바이스 제어에 관한 내용을 찾아볼 수 있다.

여러 개의 마이너 메시지를 가질 수도 있는데 이는 미리 정의된 것을 사용하는 것이 아니라 드라이버 개발자가 임의로 결정해 사용한다. 원하는 만큼 얼마든지 마이너 메시지를 정의해 사용할 수 있으며 메시지 정의는 CTL_CODE 매크로를 사용한다.

#define IOCTL_SOME_MINOR_MSG \
   CTL_CODE(FILE_DEVICE_UNKNOWN, \      // IoCreateDevice()에서 정의한 디바이스 종류
   IOCTL_INDEX + 4, \                                   // 개발자가 지정하는 임의의 수
   METHOD_BUFFERED, \                             // 입출력 방법
   FILE_ANY_ACCESS)                                    // 어떤 용도의 입출력인가(지금은 읽고 쓰기 모두)?

다음 예제는 IOCTL 핸들러의 기본 골격이다. 먼저 IRP로부터 스택 드라이버(현재 드라이버)를 구한 다음 IRP와 IRP_STACK 멤버로 입출력 버퍼와 이 버퍼의 크기를 구한다. 입출력 코드는 IRP_STACK에서 구할 수 있다. 요청된 입출력 컨트롤 코드를 마치면 IoCompleteRequest()로 요청 처리를 마무리 짓는다.

NTSTATUS
ioctl_handler (
   IN PDEVICE_OBJECT fdo,
   IN PIRP irp)
{
   NTSTATUS nt_status;

   PUCHAR p_ch = NULL;
   IOCTL_BLOCK* p_ioctl_block = NULL; // 드라이버 작성자가 정의한 구조체

   PVOID p_io_buffer;
   ULONG input_buf_len;
   ULONG output_buf_len;
   ULONG io_ctl_code;
   ULONG length;

   PDEVICE_EXTENSION           dev_ext;
   PIO_STACK_LOCATION         irp_stack;

   increment_io_count (fdo);

   dev_ext = get_device_extension(fdo);

   irp_stack = IoGetCurrentIrpStackLocation (irp);

   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;

   //
   // 호출자의 입출력 버퍼와 그 크기를 구한다(ioctl 명령어하고).
   //

   p_io_buffer = irp->AssociatedIrp.SystemBuffer;
   input_buf_len = irp_stack->Parameters.DeviceIoControl.InputBufferLength;
   output_buf_len = irp_stack->Parameters.DeviceIoControl.OutputBufferLength;

   io_ctl_code = irp_stack->Parameters.DeviceIoControl.IoControlCode;

   // -----------------------------------------------------
   // I O C T L 코 드 디 스 패 치
   // -----------------------------------------------------

   switch (io_ctl_code)
   {
      // -----------------------------------------------------
      // default
      // -----------------------------------------------------

      default :
      nt_status = irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
   }

   IoCompleteRequest (irp, IO_NO_INCREMENT);
   decrement_io_count (fdo);
   return nt_status;