Windows 커널 & 드라이버 시리즈 — 11화
디바이스 오브젝트와 디바이스 스택 — 드라이버들이 쌓이는 원리
7화에서 I/O 요청이 "드라이버 스택"을 타고 내려간다고 했는데, 그 스택이 어떻게 만들어지는지 이번 화에서 알아볼게요. 디바이스 오브젝트들이 어떻게 서로 연결되는지, 그리고 필터 드라이버가 어떻게 기존 스택에 끼어드는지 — 이게 이해되면 드라이버 아키텍처의 절반은 잡은 거예요.
디바이스 스택의 구조
디바이스 하나에는 여러 드라이버가 관여할 수 있어요. 예를 들어, USB 마우스를 연결하면 다음과 같은 스택이 구성됩니다:
필터 드라이버 (선택)
mouhid.sys — HID 마우스 필터
FDO
↕ IRP 전달 (IoCallDriver)
함수 드라이버 (필수)
hidusb.sys — USB HID 클래스 드라이버
FDO
↕
버스 드라이버
usbhub.sys — USB 허브 드라이버
PDO
↕
버스 드라이버 (루트)
usbhcd.sys — USB 호스트 컨트롤러
PDO
IRP는 맨 위에서 아래로 전달되고, 처리가 완료되면 반대 방향으로 올라갑니다. 각 드라이버는 IRP를 처리하거나, 아래 드라이버로 전달하거나, 둘 다 할 수 있어요.
PDO vs FDO — 두 종류의 디바이스 오브젝트
| 종류 | 이름 | 역할 | 누가 만드나 |
|---|---|---|---|
| PDO | Physical Device Object | 실제 하드웨어 장치를 대표. 버스 드라이버가 장치를 발견하면 생성 | 버스 드라이버 (usbhub, PCI 등) |
| FDO | Functional Device Object | 장치의 실제 기능을 구현. PDO 위에 붙어서 주요 I/O 처리 | 함수 드라이버 (hidusb, disk 등) |
| Filter DO | Filter Device Object | FDO 위나 아래에 붙어서 I/O를 가로채거나 수정 | 필터 드라이버 (보안, 암호화 등) |
📌 WDM에서 PnP 드라이버의 기본 구조
WDM(Windows Driver Model) 기반 드라이버는 IRP_MJ_PNP를 처리해야 해요. PnP Manager가 새 장치를 발견하면, 버스 드라이버에게 PDO를 만들게 하고, 이어서 함수 드라이버에게 FDO를 만들어 스택을 완성하도록 지시합니다.
IoAttachDeviceToDeviceStack — 스택에 끼어들기
필터 드라이버는 기존 스택에 자신의 디바이스 오브젝트를 삽입합니다. 이를 "attach"라고 해요:
// 필터 드라이버의 AddDevice 루틴 (PnP 드라이버 기준) NTSTATUS FilterAddDevice( _In_ PDRIVER_OBJECT DriverObject, _In_ PDEVICE_OBJECT PhysicalDeviceObject) // PDO (버스 드라이버가 만든 것) { PDEVICE_OBJECT filterDO = NULL; NTSTATUS status; // 1. 내 필터 디바이스 오브젝트 생성 status = IoCreateDevice( DriverObject, sizeof(FILTER_DEVICE_EXTENSION), NULL, // 이름 없음 (필터는 보통 익명) PhysicalDeviceObject->DeviceType, 0, FALSE, &filterDO); if (!NT_SUCCESS(status)) return status; // 2. 기존 스택(PDO 위)에 내 DO를 붙인다 PFILTER_DEV_EXT ext = (PFILTER_DEV_EXT)filterDO->DeviceExtension; ext->LowerDevice = IoAttachDeviceToDeviceStack(filterDO, PhysicalDeviceObject); if (!ext->LowerDevice) { IoDeleteDevice(filterDO); return STATUS_NO_SUCH_DEVICE; } // 3. 플래그 동기화 (중요!) filterDO->Flags &= ~DO_DEVICE_INITIALIZING; filterDO->Flags |= (ext->LowerDevice->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO)); return STATUS_SUCCESS; }LowerDevice 포인터가 핵심이에요
IoAttachDeviceToDeviceStack이 반환하는 포인터가 바로 이 필터 아래에 있는 다음 디바이스 오브젝트예요. IRP를 아래로 전달할 때 이 포인터를 씁니다:
// IRP를 아래 드라이버로 그대로 전달하는 패스스루(Pass-through) 패턴 NTSTATUS FilterDispatchAny( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { PFILTER_DEV_EXT ext = DeviceObject->DeviceExtension; // 뭔가 처리하고 싶으면 여기서... // (아무것도 안 하면 순수 패스스루) // 아래 드라이버로 IRP 전달 IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(ext->LowerDevice, Irp); }IoSkipCurrentIrpStackLocation vs IoCopyCurrentIrpStackLocationToNext
IRP를 아래로 전달할 때 두 가지 매크로 중 하나를 선택해야 해요:
| 매크로 | 사용 시점 |
|---|---|
| IoSkipCurrentIrpStackLocation | 현재 스택 위치를 그냥 건너뜀. 완료 루틴(CompletionRoutine)을 등록하지 않을 때 사용 — 가장 가벼운 패스스루 |
| IoCopyCurrentIrpStackLocationToNext | 현재 스택 위치를 다음 위치로 복사. 완료 루틴을 등록할 때 사용 — 완료 시 추가 작업이 필요한 경우 |
⚠️ 이 선택을 틀리면 BSOD 또는 메모리 손상이 납니다
IoSkipCurrentIrpStackLocation 후에 완료 루틴을 등록하면 이미 해제된 스택 위치에 접근하는 문제가 생겨요. 완료 루틴이 필요하면 반드시 IoCopyCurrentIrpStackLocationToNext를 써야 합니다.
WinDbg로 디바이스 스택 확인하기
; 특정 디바이스 오브젝트의 스택 전체 보기 !devstack <DEVICE_OBJECT 주소> ; 드라이버 이름으로 디바이스 찾기 !drvobj \Driver\disk ; 예시 출력: ; !DevObj !DrvObj !DevExt ObjectName ; ffff... \Driver\partmgr ffff... ; ffff... \Driver\disk ffff... DR0 ; > ffff... \Driver\ACPI ffff...✅ 11화 요약
- 디바이스 스택은 PDO(버스) → FDO(함수) → Filter DO(필터) 순서로 쌓입니다.
- 필터 드라이버는 IoAttachDeviceToDeviceStack으로 기존 스택에 삽입됩니다.
- IRP 전달 시 IoSkipCurrentIrpStackLocation(완료 루틴 없을 때) 또는 IoCopyCurrentIrpStackLocationToNext(있을 때) 중 하나를 선택합니다.
- ext->LowerDevice 포인터로 아래 드라이버에 IRP를 전달합니다.
다음 화
12화 — IRP 구조 완전 분석: I/O Request Packet의 모든 것 →
#DEVICE_OBJECT #디바이스스택 #PDO #FDO #IoAttachDevice
'Programming > 7. Device Driver' 카테고리의 다른 글
| 드라이버 개발 핵심 - 14화 IRQL 완전 정복 (0) | 2026.06.02 |
|---|---|
| 드라이버 개발 핵심 - 13화 IRP 디스패치 루틴 구현 (0) | 2026.06.01 |
| 드라이버 개발 핵심 - 10화 DriverEntry와 드라이버 오브젝트 (0) | 2026.05.28 |
| 드라이버 개발 핵심 - 9화 첫 번째 커널 드라이버 (0) | 2026.05.27 |
| Windows 아키텍처 기초 - 8화 개발 환경 구축 (0) | 2026.05.26 |