미니 드라이버, 미니포트 드라이버 및 드라이버 쌍
Minidrivers, Miniport Drivers, and Driver Pairs(원본 Microsoft Learn 링크)
아래는 위 문서의 번역이다.
미니드라이버(minidriver) 또는 미니포트 드라이버(miniport driver)는 드라이버 쌍(driver pair)의 한쪽 절반 역할을 한다. (미니포트, 포트)와 같은 드라이버 쌍을 사용하면 드라이버 개발을 더 쉽게 할 수 있다. 드라이버 쌍에서는 하나의 드라이버가 여러 장치에 공통적으로 필요한 일반 작업을 처리하고, 다른 드라이버가 특정 장치에 특화된 작업을 처리한다. 장치 특화 작업을 담당하는 드라이버는 미니포트 드라이버, 미니클래스 드라이버, 미니드라이버 등 다양한 이름을 가진다.
Microsoft는 일반(general) 부분을 제공하며, 특정 장치에 맞는 드라이버는 일반적으로 하드웨어 공급업체(IHV)가 제공한다. 이 내용을 이해하려면 디바이스 노드와 디바이스 스택, I/O 요청 패킷(IRP)에 대한 개념을 알고 있어야 한다.
모든 커널 모드 드라이버는 DriverEntry라는 함수가 있어야 하며, 드라이버가 로드된 직후 호출된다. DriverEntry 함수는 DRIVER_OBJECT 구조체의 여러 멤버를 드라이버가 구현한 함수의 포인터로 채운다. 예를 들어, DriverEntry는 DRIVER_OBJECT의 Unload 멤버에 아래 그림처럼 Unload 함수 포인터를 기록한다.

DRIVER_OBJECT 구조체의 MajorFunction 멤버는 IRP(I/O Request Packet)를 처리하는 함수 포인터들의 배열이다. 다음 그림과 같이, 드라이버는 이 MajorFunction 배열의 여러 요소에 드라이버가 구현한 함수 포인터를 기록하여 다양한 종류의 IRP를 처리한다.

IRP는 IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_PNP 같은 상수로 표시되는 Major Function Code에 따라 분류된다. 이러한 Major Function Code 상수는 MajorFunction 배열에서 인덱스로 사용된다. 예를 들어, 드라이버가 IRP_MJ_WRITE를 처리하는 디스패치 함수를 구현했다면, MajorFunction[IRP_MJ_WRITE] 요소에 그 디스패치 함수의 포인터를 기록해야 한다.
일반적으로 드라이버는 MajorFunction 배열의 일부 요소들만 채우고, 나머지 요소는 I/O 매니저가 제공하는 기본 디스패치 함수 포인터 값으로 남겨둔다. 다음 예시는 !drvobj 디버거 확장을 사용하여 parport 드라이버의 함수 포인터를 확인하는 방법을 보여준다.
0: kd> !drvobj parport 2
Driver object (fffffa80048d9e70) is for:
\Driver\Parport
DriverEntry: fffff880065ea070 parport!GsDriverEntry
DriverStartIo: 00000000
DriverUnload: fffff880065e131c parport!PptUnload
AddDevice: fffff880065d2008 parport!P5AddDevice
Dispatch routines:
[00] IRP_MJ_CREATE fffff880065d49d0 parport!PptDispatchCreateOpen
[01] IRP_MJ_CREATE_NAMED_PIPE fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE fffff880065d4a78 parport!PptDispatchClose
[03] IRP_MJ_READ fffff880065d4bac parport!PptDispatchRead
[04] IRP_MJ_WRITE fffff880065d4bac parport!PptDispatchRead
[05] IRP_MJ_QUERY_INFORMATION fffff880065d4c40 parport!PptDispatchQueryInformation
[06] IRP_MJ_SET_INFORMATION fffff880065d4ce4 parport!PptDispatchSetInformation
[07] IRP_MJ_QUERY_EA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL fffff880065d4be8 parport!PptDispatchDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff880065d4c24 parport!PptDispatchInternalDeviceControl
[10] IRP_MJ_SHUTDOWN fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP fffff880065d4af4 parport!PptDispatchCleanup
[13] IRP_MJ_CREATE_MAILSLOT fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER fffff880065d491c parport!PptDispatchPower
[17] IRP_MJ_SYSTEM_CONTROL fffff880065d4d4c parport!PptDispatchSystemControl
[18] IRP_MJ_DEVICE_CHANGE fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA fffff80001b6ecd4 nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP fffff880065d4840 parport!PptDispatchPnp
디버거 출력에서 parport.sys가 드라이버의 진입점인 GsDriverEntry를 구현하고 있음을 확인할 수 있다. 드라이버가 빌드될 때 자동으로 생성된 GsDriverEntry는 일부 초기화를 수행한 후, 드라이버 개발자가 구현한 DriverEntry를 호출한다.
또한 parport 드라이버가(DriverEntry 함수 내에서) 다음과 같은 주요 함수 코드들에 대한 디스패치 함수 포인터를 제공하고 있음을 확인할 수 있다:
- IRP_MJ_CREATE
- IRP_MJ_CLOSE
- IRP_MJ_READ
- IRP_MJ_WRITE
- IRP_MJ_QUERY_INFORMATION
- IRP_MJ_SET_INFORMATION
- IRP_MJ_DEVICE_CONTROL
- IRP_MJ_INTERNAL_DEVICE_CONTROL
- IRP_MJ_CLEANUP
- IRP_MJ_POWER
- IRP_MJ_SYSTEM_CONTROL
- IRP_MJ_PNP
MajorFunction 배열의 나머지 요소들은 기본 디스패치 함수인 nt!IopInvalidDeviceRequest의 포인터를 가진다.
디버거 출력에서 parport 드라이버는 Unload와 AddDevice에 대한 함수 포인터는 제공했지만 StartIo에 대한 함수 포인터는 제공하지 않았음을 알 수 있다. AddDevice 함수는 특이한데, 그 함수 포인터가 DRIVER_OBJECT 구조체에 저장되지 않는다. 대신 DRIVER_OBJECT 구조체의 확장에 있는 AddDevice 멤버에 저장된다. 아래 그림은 parport 드라이버가 DriverEntry 함수에서 제공한 함수 포인터들을 보여준다. parport가 제공한 함수 포인터들은 음영 처리되어 있다.

드라이버 쌍을 사용하여 더 쉽게 만들기
시간이 지나면서, Microsoft 내외부의 드라이버 개발자들이 Windows Driver Model(WDM)에 대한 경험을 쌓자, 디스패치 함수에 대해 몇 가지 사실을 깨닫게 되었다:
- 디스패치 함수는 대부분 보일러플레이트 코드이다. 예를 들어, IRP_MJ_PNP 디스패치 함수의 대부분 코드는 모든 드라이버에서 동일하다. 실제로 개별 하드웨어를 제어하는 개별 드라이버에만 필요한 PnP 코드 부분은 아주 작은 일부이다.
- 디스패치 함수는 복잡하며 올바르게 구현하기 어렵다. 스레드 동기화, IRP 큐잉, IRP 취소 같은 기능을 구현하는 것은 매우 도전적이며 운영체제가 동작하는 방식에 대한 깊은 이해가 필요하다.
드라이버 개발자들이 더 쉽게 작업할 수 있도록 Microsoft는 여러 기술 특화 드라이버 모델을 만들었다. 처음 보면 기술별 모델들은 상당히 다른 것처럼 보이지만, 자세히 보면 그중 많은 모델이 다음 패러다임에 기반하고 있음을 알 수 있다:
- 드라이버는 두 부분으로 나뉜다: 일반 처리를 담당하는 부분과 특정 장치에 특화된 처리를 담당하는 부분.
- 일반 부분은 Microsoft가 작성한다.
- 특정 부분은 Microsoft 또는 독립 하드웨어 공급업체가 작성할 수 있다.
Proseware와 Contoso라는 두 회사가 장난감 로봇을 만들며, 이 로봇이 WDM 드라이버를 필요로 한다고 가정해보자. 그리고 Microsoft가 GeneralRobot.sys라는 일반 로봇 드라이버를 제공한다고 하자. 이 경우 Proseware와 Contoso는 각자의 로봇 요구사항을 처리하는 작은 드라이버를 작성할 수 있다. 예를 들어 Proseware는 ProsewareRobot.sys를 작성할 수 있으며, ProsewareRobot.sys와 GeneralRobot.sys라는 드라이버 쌍을 결합하여 하나의 WDM 드라이버를 만들 수 있다. 마찬가지로 ContosoRobot.sys와 GeneralRobot.sys라는 드라이버 쌍도 하나의 WDM 드라이버가 될 수 있다.
가장 일반적인 형태로 말하면, (specific.sys, general.sys) 형태의 드라이버 쌍을 사용하여 드라이버를 만들 수 있다는 아이디어이다.
드라이버 쌍에서의 함수 포인터
(specific.sys, general.sys) 쌍에서 Windows는 specific.sys를 로드하고 그 DriverEntry 함수를 호출한다. specific.sys의 DriverEntry 함수는 DRIVER_OBJECT 구조체에 대한 포인터를 받는다. 일반적으로 DriverEntry는 MajorFunction 배열의 여러 요소에 디스패치 함수 포인터를 채울 것이라고 예상된다. 또한 DriverEntry가 DRIVER_OBJECT 구조체의 Unload 멤버(그리고 필요하다면 StartIo 멤버)와 드라이버 객체 확장에 있는 AddDevice 멤버를 설정할 것이라고도 예상된다. 그러나 드라이버 쌍 모델에서는 DriverEntry가 반드시 이러한 작업을 수행하지는 않는다. 대신 specific.sys의 DriverEntry 함수는 DRIVER_OBJECT 구조체를 general.sys가 구현한 초기화 함수로 전달한다. 다음 코드 예제는 (ProsewareRobot.sys, GeneralRobot.sys) 쌍에서 초기화 함수가 호출되는 방식을 보여준다.
PVOID g_ProsewareRobottCallbacks[3] = {DeviceControlCallback, PnpCallback, PowerCallback};
// DriverEntry function in ProsewareRobot.sys
NTSTATUS DriverEntry (DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
// Call the initialization function implemented by GeneralRobot.sys.
return GeneralRobotInit(DriverObject, RegistryPath, g_ProsewareRobottCallbacks);
}
GeneralRobot.sys의 초기화 함수는 DRIVER_OBJECT 구조체(및 그 확장)의 적절한 멤버와 MajorFunction 배열의 적절한 요소에 함수 포인터를 기록한다. 개념은 다음과 같다: I/O 관리자가 IRP를 드라이버 쌍에 보낼 때, IRP는 먼저 GeneralRobot.sys가 구현한 디스패치 함수에 도달한다. GeneralRobot.sys가 IRP를 자체적으로 처리할 수 있다면, 특정 드라이버인 ProsewareRobot.sys는 관여할 필요가 없다. GeneralRobot.sys가 IRP 처리의 일부만 처리할 수 있는 경우에는 ProsewareRobot.sys가 구현한 콜백 함수 중 하나의 도움을 받는다. GeneralRobot.sys는 GeneralRobotInit 호출에서 ProsewareRobot 콜백에 대한 포인터를 전달받는다.
DriverEntry가 반환된 후 어느 시점에서, Proseware Robot 디바이스 노드에 대한 디바이스 스택이 구성된다. 디바이스 스택은 다음과 같을 수 있다.

위 그림에서 보듯이, Proseware Robot의 디바이스 스택에는 세 개의 디바이스 객체가 존재한다. 가장 상단의 디바이스 객체는 필터 드라이버 AfterThought.sys와 연결된 필터 디바이스 객체(Filter DO)이다. 중간의 디바이스 객체는 드라이버 쌍(ProsewareRobot.sys, GeneralRobot.sys)과 연결된 기능 디바이스 객체(FDO)이다. 이 드라이버 쌍은 디바이스 스택의 기능 드라이버로 동작한다. 가장 하단의 디바이스 객체는 Pci.sys와 연결된 물리 디바이스 객체(PDO)이다.
드라이버 쌍은 디바이스 스택에서 하나의 레벨만 차지하며 하나의 디바이스 객체(FDO)에만 연결되어 있음을 주목할 필요가 있다. GeneralRobot.sys가 IRP를 처리할 때 ProsewareRobot.sys에 처리를 요청할 수도 있지만, 이는 IRP를 디바이스 스택 아래로 전달하는 것과는 다르다. 드라이버 쌍은 단일 WDM 드라이버를 구성하며 디바이스 스택의 하나의 레벨에 해당한다. 드라이버 쌍은 IRP를 직접 완료하거나, 아니면 IRP를 디바이스 스택 아래로 전달하여 Pci.sys와 연결된 PDO로 내려보낸다.
드라이버 쌍 예제
노트북 컴퓨터에 무선 네트워크 카드가 있고, 장치 관리자를 확인해 보니 해당 네트워크 카드의 드라이버가 netwlv64.sys임을 알게 되었다고 가정한다. 이때 디버거 확장 기능인 !drvobj를 사용하여 netwlv64.sys의 함수 포인터를 조사할 수 있다.
1: kd> !drvobj netwlv64 2
Driver object (fffffa8002e5f420) is for:
\Driver\netwlv64
DriverEntry: fffff8800482f064 netwlv64!GsDriverEntry
DriverStartIo: 00000000
DriverUnload: fffff8800195c5f4 ndis!ndisMUnloadEx
AddDevice: fffff88001940d30 ndis!ndisPnPAddDevice
Dispatch routines:
[00] IRP_MJ_CREATE fffff880018b5530 ndis!ndisCreateIrpHandler
[01] IRP_MJ_CREATE_NAMED_PIPE fffff88001936f00 ndis!ndisDummyIrpHandler
[02] IRP_MJ_CLOSE fffff880018b5870 ndis!ndisCloseIrpHandler
[03] IRP_MJ_READ fffff88001936f00 ndis!ndisDummyIrpHandler
[04] IRP_MJ_WRITE fffff88001936f00 ndis!ndisDummyIrpHandler
[05] IRP_MJ_QUERY_INFORMATION fffff88001936f00 ndis!ndisDummyIrpHandler
[06] IRP_MJ_SET_INFORMATION fffff88001936f00 ndis!ndisDummyIrpHandler
[07] IRP_MJ_QUERY_EA fffff88001936f00 ndis!ndisDummyIrpHandler
[08] IRP_MJ_SET_EA fffff88001936f00 ndis!ndisDummyIrpHandler
[09] IRP_MJ_FLUSH_BUFFERS fffff88001936f00 ndis!ndisDummyIrpHandler
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff88001936f00 ndis!ndisDummyIrpHandler
[0b] IRP_MJ_SET_VOLUME_INFORMATION fffff88001936f00 ndis!ndisDummyIrpHandler
[0c] IRP_MJ_DIRECTORY_CONTROL fffff88001936f00 ndis!ndisDummyIrpHandler
[0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff88001936f00 ndis!ndisDummyIrpHandler
[0e] IRP_MJ_DEVICE_CONTROL fffff8800193696c ndis!ndisDeviceControlIrpHandler
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff880018f9114 ndis!ndisDeviceInternalIrpDispatch
[10] IRP_MJ_SHUTDOWN fffff88001936f00 ndis!ndisDummyIrpHandler
[11] IRP_MJ_LOCK_CONTROL fffff88001936f00 ndis!ndisDummyIrpHandler
[12] IRP_MJ_CLEANUP fffff88001936f00 ndis!ndisDummyIrpHandler
[13] IRP_MJ_CREATE_MAILSLOT fffff88001936f00 ndis!ndisDummyIrpHandler
[14] IRP_MJ_QUERY_SECURITY fffff88001936f00 ndis!ndisDummyIrpHandler
[15] IRP_MJ_SET_SECURITY fffff88001936f00 ndis!ndisDummyIrpHandler
[16] IRP_MJ_POWER fffff880018c35e8 ndis!ndisPowerDispatch
[17] IRP_MJ_SYSTEM_CONTROL fffff880019392c8 ndis!ndisWMIDispatch
[18] IRP_MJ_DEVICE_CHANGE fffff88001936f00 ndis!ndisDummyIrpHandler
[19] IRP_MJ_QUERY_QUOTA fffff88001936f00 ndis!ndisDummyIrpHandler
[1a] IRP_MJ_SET_QUOTA fffff88001936f00 ndis!ndisDummyIrpHandler
[1b] IRP_MJ_PNP fffff8800193e518 ndis!ndisPnPDispatch
디버거 출력에서 netwlv64.sys가 드라이버의 엔트리 포인트인 GsDriverEntry를 구현하고 있음을 확인할 수 있다. GsDriverEntry는 드라이버가 빌드될 때 자동으로 생성된 함수로, 기본 초기화를 수행한 후 드라이버 개발자가 작성한 DriverEntry를 호출한다.
이 예제에서는 DriverEntry는 netwlv64.sys가 구현하지만, AddDevice, Unload, 여러 디스패치 함수들은 ndis.sys가 구현하고 있다. 따라서 netwlv64.sys는 NDIS 미니포트 드라이버, ndis.sys는 NDIS 라이브러리라고 부르며, 이 두 모듈은 함께 (NDIS 미니포트, NDIS 라이브러리) 쌍을 이룬다.
아래 그림은 무선 네트워크 카드의 디바이스 스택을 보여준다. 드라이버 쌍(netwlv64.sys, ndis.sys)은 디바이스 스택에서 단 하나의 레벨만을 차지하며, 단 하나의 디바이스 객체(FDO)에만 연관되어 있음을 주목한다.

사용 가능한 드라이버 쌍
서로 다른 기술별 드라이버 모델은 드라이버 쌍의 특정 부분과 일반 부분에 대해 다양한 이름을 사용한다. 많은 경우, 쌍의 특정 부분은 “mini”라는 접두어를 가진다. 다음은 사용 가능한 (특정, 일반) 드라이버 쌍들이다.
- (display miniport driver, display port driver)
- (audio miniport driver, audio port driver)
- (storage miniport driver, storage port driver)
- (battery miniclass driver, battery class driver)
- (HID minidriver, HID class driver)
- (changer miniclass driver, changer port driver)
- (NDIS miniport driver, NDIS library)
목록에서 볼 수 있듯이, 여러 모델은 드라이버 쌍의 일반 부분에 대해 class driver라는 용어를 사용한다. 이러한 종류의 class driver는 독립형 class driver와도 다르고 class filter driver와도 다르다.
댓글남기기