본문 바로가기
Programming/6. Design Pattern

드라이버 개발 핵심 - 12화 IRP 구조 완전 분석

by S.W 2026. 5. 30.
Windows 커널 & 드라이버 시리즈 — 12화

IRP 구조 완전 분석 — I/O Request Packet의 모든 것

📅 2026 ⏱ 읽기 약 14분 🏷 IRP / IO_STACK_LOCATION / IoStatus
IRP(I/O Request Packet)는 드라이버 개발에서 가장 많이 보게 될 구조체예요. 앱의 I/O 요청이 커널로 들어오는 순간부터 완료될 때까지, 모든 정보를 이 구조체 하나에 담아서 드라이버들 사이를 돌아다닙니다. 이번 화에서 IRP를 완전히 해부해볼게요.

IRP의 전체 구조

IRP는 고정 크기의 헤더와, 스택에 참여한 드라이버 수만큼의 IO_STACK_LOCATION 배열로 이루어져 있어요:

[ IRP 헤더 (고정) ] - Type, Size - MdlAddress <-- 유저 버퍼의 MDL (Direct I/O 방식) - Flags - AssociatedIrp.SystemBuffer <-- 커널 버퍼 (Buffered I/O 방식) - IoStatus <-- 완료 결과: { NTSTATUS Status, ULONG_PTR Information } - RequestorMode <-- KernelMode or UserMode - PendingReturned <-- 비동기 처리 여부 - CurrentLocation <-- 현재 처리 중인 IO_STACK_LOCATION 인덱스 - Tail.Overlay.Thread <-- 요청한 스레드 ETHREAD 포인터 - ... [ IO_STACK_LOCATION 배열 ] (드라이버 스택 깊이만큼) [0] 최상위 드라이버용 [1] 다음 드라이버용 [2] ... [n] 버스 드라이버용

핵심은 "IRP 헤더는 공유되고, IO_STACK_LOCATION은 각 드라이버가 자신의 것을 따로 갖는다"는 점이에요.

IO_STACK_LOCATION — 각 드라이버의 개인 파라미터

각 드라이버는 자신의 IO_STACK_LOCATION을 통해 이 IRP에 관련된 자신만의 파라미터를 읽습니다. IoGetCurrentIrpStackLocation(Irp)으로 현재 드라이버의 스택 위치를 가져와요.

// IRP 처리 함수에서 스택 위치 꺼내는 패턴 NTSTATUS DispatchRead(PDEVICE_OBJECT DevObj, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); // 읽기 요청의 파라미터 ULONG length = stack->Parameters.Read.Length; // 읽을 바이트 수 ULONG offset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart; // 파일 오프셋 DbgPrint("Read: %lu bytes from offset %lu\n", length, offset); // ... 실제 처리 }

Parameters 유니온은 Major Code에 따라 다른 구조체를 가져요. Read면 Read, Write면 Write, DeviceIoControl이면 DeviceIoControl 구조체를 씁니다.

IoStatus — 결과를 돌려주는 방법

IRP 처리가 끝나면 Irp->IoStatus에 결과를 채우고 IoCompleteRequest를 호출해야 해요:

// IRP 완료 패턴 Irp->IoStatus.Status = STATUS_SUCCESS; // 성공/실패 여부 Irp->IoStatus.Information = bytesRead; // 실제로 읽은 바이트 수 등 IoCompleteRequest(Irp, IO_NO_INCREMENT); // IRP 완료! return STATUS_SUCCESS;

IoCompleteRequest의 두 번째 파라미터는 우선순위 부스트 값이에요. 일반적으로 IO_NO_INCREMENT(0)을 쓰고, 디스크나 네트워크 I/O처럼 대기가 있었던 경우에는 IO_DISK_INCREMENT 등을 씁니다.

버퍼 전달 방식 세 가지

유저 모드 앱의 버퍼를 드라이버에서 안전하게 접근하는 방법이 세 가지 있어요. 디바이스 오브젝트의 Flags로 방식을 선택합니다:

방식Flags 설정버퍼 접근 방법특징
Buffered I/O DO_BUFFERED_IO AssociatedIrp.SystemBuffer — 커널이 복사한 버퍼 간단하고 안전. 소량 데이터에 적합. 복사 오버헤드 있음
Direct I/O DO_DIRECT_IO MdlAddress — MDL로 매핑된 물리 메모리 복사 없이 직접 접근. 대용량 I/O에 적합. MDL 사용법 필요
Neither (플래그 없음) UserBuffer — 유저 주소 직접 접근 가장 위험. 유저 포인터 검증 필수. 특수 상황 외엔 비권장
💡 처음 드라이버에는 Buffered I/O를 쓰세요 DO_BUFFERED_IO를 설정하면 커널이 유저 버퍼를 안전하게 커널 버퍼로 복사해줘요. AssociatedIrp.SystemBuffer에 접근하면 되고, 실수로 유저 포인터를 직접 dereference해서 크래시 날 걱정이 없어요.

보류(Pending) 처리 — 비동기 IRP

IRP를 즉시 완료할 수 없는 경우(예: 데이터가 아직 준비 안 됨), 나중에 처리하겠다고 표시하고 STATUS_PENDING을 반환할 수 있어요:

NTSTATUS DispatchRead(PDEVICE_OBJECT DevObj, PIRP Irp) { // IRP를 큐에 넣고 나중에 처리할 것임을 표시 IoMarkIrpPending(Irp); // IRP를 내부 큐에 저장 (나중에 꺼내서 완료시킬 것) ExInterlockedInsertTailList(&gPendingIrpList, &Irp->Tail.Overlay.ListEntry, &gPendingIrpLock); return STATUS_PENDING; // 아직 완료 안 됨을 I/O Manager에 알림 } // 나중에 데이터가 준비되면... void CompleteQueuedIrp(PIRP Irp, ULONG bytesTransferred) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = bytesTransferred; IoCompleteRequest(Irp, IO_NO_INCREMENT); }
⚠️ IoMarkIrpPending과 STATUS_PENDING을 같이 써야 해요 IoMarkIrpPending을 호출했으면 반드시 STATUS_PENDING을 반환해야 하고, 반대도 마찬가지예요. 한쪽만 하면 시스템 상태가 꼬입니다.

WinDbg로 IRP 보기

; 특정 IRP 구조체 덤프 dt nt!_IRP <IRP 주소> ; 현재 스레드의 대기 중인 IRP 목록 !irpfind ; 특정 디바이스의 IRP 큐 상태 !devobj <DevObj 주소>
✅ 12화 요약
  • IRP는 고정 헤더와 드라이버별 IO_STACK_LOCATION 배열로 구성됩니다.
  • IoGetCurrentIrpStackLocation으로 현재 드라이버의 파라미터를 읽어요.
  • 완료 시 IoStatus에 결과를 채우고 IoCompleteRequest를 호출합니다.
  • 버퍼 전달 방식은 Buffered(안전), Direct(고성능), Neither(위험) 세 가지입니다.
  • 즉시 완료 불가 시 IoMarkIrpPending + STATUS_PENDING으로 비동기 처리합니다.
다음 화
13화 — IRP 디스패치 루틴 구현: DeviceIoControl 완전 정복 →

#IRP #IO_STACK_LOCATION #IoCompleteRequest #BufferedIO #IoStatus