IRP 구조 완전 분석 — I/O Request Packet의 모든 것
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 — 유저 주소 직접 접근 | 가장 위험. 유저 포인터 검증 필수. 특수 상황 외엔 비권장 |
보류(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); }WinDbg로 IRP 보기
; 특정 IRP 구조체 덤프 dt nt!_IRP <IRP 주소> ; 현재 스레드의 대기 중인 IRP 목록 !irpfind ; 특정 디바이스의 IRP 큐 상태 !devobj <DevObj 주소>- IRP는 고정 헤더와 드라이버별 IO_STACK_LOCATION 배열로 구성됩니다.
- IoGetCurrentIrpStackLocation으로 현재 드라이버의 파라미터를 읽어요.
- 완료 시 IoStatus에 결과를 채우고 IoCompleteRequest를 호출합니다.
- 버퍼 전달 방식은 Buffered(안전), Direct(고성능), Neither(위험) 세 가지입니다.
- 즉시 완료 불가 시 IoMarkIrpPending + STATUS_PENDING으로 비동기 처리합니다.
#IRP #IO_STACK_LOCATION #IoCompleteRequest #BufferedIO #IoStatus