본문 바로가기
Programming/7. Device Driver

드라이버 개발 핵심 - 17화 MDL

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

MDL — 유저 메모리를 안전하게 커널에서 접근하는 방법

📅 2026 ⏱ 읽기 약 13분 🏷 MDL / MmProbeAndLockPages / DMA / Direct I/O
12화에서 Direct I/O 방식을 선택하면 MDL을 통해 버퍼에 접근한다고 했어요. MDL(Memory Descriptor List)은 처음 보면 복잡해 보이지만, "유저 버퍼의 물리 페이지 목록"이라고 이해하면 훨씬 친숙해져요. 대용량 I/O와 DMA를 다루려면 MDL을 반드시 알아야 합니다.

왜 MDL이 필요한가

유저 모드 버퍼는 가상 주소 공간에 있어요. 이 가상 주소가 가리키는 물리 페이지들은 메모리 어디에나 흩어져 있을 수 있고, 더 나쁜 건 언제든 스왑 아웃될 수 있다는 거예요.

커널 드라이버나 DMA 하드웨어가 이 버퍼에 안전하게 접근하려면:

  1. 물리 페이지들을 "잠가서" 스왑 아웃되지 않게 해야 해요
  2. 해당 물리 주소 목록을 커널 가상 주소로 매핑해야 해요

이 두 작업을 처리하는 구조체가 바로 MDL이에요.

MDL 구조체

typedef struct _MDL { struct _MDL *Next; // 다음 MDL (연결 리스트) CSHORT Size; // MDL 자체 크기 (가변) CSHORT MdlFlags; // 상태 플래그 (잠김 여부 등) struct _EPROCESS *Process; // 이 MDL을 소유한 프로세스 PVOID MappedSystemVa; // 커널에서 접근하는 가상 주소 PVOID StartVa; // 버퍼 시작 페이지의 가상 주소 ULONG ByteCount; // 버퍼 크기 ULONG ByteOffset; // 페이지 내 시작 오프셋 // 이후로 물리 페이지 번호(PFN) 배열이 이어짐 } MDL;

MDL 뒤에 PFN(Physical Frame Number) 배열이 붙어있어요. 버퍼가 걸쳐 있는 물리 페이지들의 번호를 순서대로 담고 있습니다.

MDL 사용 패턴 — 직접 만드는 경우

드라이버에서 유저 버퍼를 직접 MDL로 만들어 쓰고 싶을 때의 패턴이에요:

NTSTATUS MapUserBuffer(PVOID userBuffer, ULONG length, PMDL *outMdl) { PMDL mdl = NULL; NTSTATUS status = STATUS_SUCCESS; // 1. MDL 생성 (유저 가상 주소와 크기로) mdl = IoAllocateMdl( userBuffer, // 유저 모드 가상 주소 length, // 크기 FALSE, // Secondary buffer? 보통 FALSE FALSE, // Charge quota? 보통 FALSE NULL); // 연결할 IRP (없으면 NULL) if (!mdl) return STATUS_INSUFFICIENT_RESOURCES; // 2. 물리 페이지 잠금 + PFN 배열 채우기 __try { MmProbeAndLockPages( mdl, UserMode, // 유저 모드 버퍼임을 명시 IoReadAccess);// 읽기 접근 (IoWriteAccess, IoModifyAccess) } __except(EXCEPTION_EXECUTE_HANDLER) { IoFreeMdl(mdl); return GetExceptionCode(); } // 3. 커널 가상 주소로 매핑 PVOID kernelVa = MmGetSystemAddressForMdlSafe( mdl, NormalPagePriority | MdlMappingNoExecute); if (!kernelVa) { MmUnlockPages(mdl); IoFreeMdl(mdl); return STATUS_INSUFFICIENT_RESOURCES; } // 이제 kernelVa로 유저 버퍼에 안전하게 접근 가능! // RtlCopyMemory(kernelVa, someData, length); *outMdl = mdl; return STATUS_SUCCESS; } // 사용 후 정리 VOID FreeMappedBuffer(PMDL mdl) { if (mdl->MdlFlags & MDL_PAGES_LOCKED) { MmUnlockPages(mdl); } IoFreeMdl(mdl); }
⚠️ MmProbeAndLockPages는 반드시 __try/__except로 감싸야 해요 유저 포인터가 유효하지 않으면 예외가 발생해요. 예외 처리 없이 쓰면 커널이 예외를 못 받아서 BSOD가 나요. 커널에서 유저 메모리를 다루는 코드는 항상 구조적 예외 처리(SEH)로 보호해야 합니다.

Direct I/O에서 IRP가 자동으로 만들어주는 MDL

DO_DIRECT_IO 플래그를 설정한 드라이버에서는, I/O Manager가 IRP를 만들 때 자동으로 MDL을 생성해서 Irp->MdlAddress에 넣어줘요. 드라이버는 그냥 꺼내 쓰면 됩니다:

NTSTATUS DispatchWrite(PDEVICE_OBJECT DevObj, PIRP Irp) { UNREFERENCED_PARAMETER(DevObj); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); // I/O Manager가 만들어 준 MDL PMDL mdl = Irp->MdlAddress; if (!mdl) { Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_INVALID_PARAMETER; } // 커널 주소로 매핑 PVOID buffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority); ULONG length = stack->Parameters.Write.Length; if (!buffer) { Irp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_INSUFFICIENT_RESOURCES; } // 이제 buffer로 유저가 넘긴 데이터를 직접 읽음 DbgPrint("[MyDrv] Write %lu bytes\n", length); // ... 처리 Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = length; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }

MDL과 DMA

하드웨어 DMA(Direct Memory Access) 컨트롤러는 물리 주소가 필요해요. MDL의 PFN 배열에서 물리 주소를 꺼낼 수 있기 때문에, 드라이버는 MDL을 DMA 어댑터에 전달해서 하드웨어가 직접 메모리에 접근하게 합니다. 이게 고성능 I/O 드라이버(NIC, NVMe 등)가 MDL을 쓰는 주된 이유예요.

💡 MDL 체인 하나의 IRP에 여러 MDL을 체인으로 연결할 수 있어요 (MDL.Next 포인터). 대형 scatter/gather I/O에서 여러 버퍼 조각을 하나의 IRP로 처리할 때 씁니다.

✅ 17화 요약
  • MDL은 유저 버퍼의 물리 페이지 목록 + 잠금 정보를 담는 구조체입니다.
  • IoAllocateMdl → MmProbeAndLockPages → MmGetSystemAddressForMdlSafe 순서로 사용해요.
  • MmProbeAndLockPages는 반드시 __try/__except로 감싸야 합니다.
  • DO_DIRECT_IO 드라이버에서는 I/O Manager가 Irp->MdlAddress에 MDL을 자동으로 넣어줍니다.
  • MDL은 DMA 하드웨어에 물리 주소를 제공하는 핵심 수단이기도 합니다.
다음 화
18화 — 인터럽트, ISR, DPC: 하드웨어 신호를 처리하는 두 단계 →

#MDL #MmProbeAndLockPages #DirectIO #DMA #커널메모리매핑