Windows 커널 & 드라이버 시리즈 — 16화
커널 메모리 풀 관리 — NonPaged vs Paged, 안전한 할당과 해제
드라이버에서 동적으로 메모리를 할당할 때 malloc()을 쓰면 안 돼요. 커널 환경에서는 ExAllocatePool 계열 함수를 써야 하는데, 어느 풀을 쓸지 잘못 선택하면 BSOD로 직결됩니다. 이번 화에서 올바른 선택 기준을 잡아드릴게요.
두 가지 커널 메모리 풀
| 구분 | NonPaged Pool | Paged Pool |
|---|---|---|
| 스왑 여부 | 절대 디스크로 스왑되지 않음 | 디스크로 스왑될 수 있음 |
| 접근 가능 IRQL | 모든 IRQL (DISPATCH_LEVEL 포함) | PASSIVE_LEVEL / APC_LEVEL만 가능 |
| 사용 가능한 크기 | 상대적으로 제한적 (RAM 일부) | 더 많이 사용 가능 (가상 메모리 활용) |
| 사용 대상 | 인터럽트/DPC 컨텍스트에서 쓰는 데이터 | 일반 드라이버 로직에서 쓰는 대형 데이터 |
ExAllocatePool2 — 현대적인 할당 함수
과거에는 ExAllocatePoolWithTag를 많이 썼는데, Windows 10 2004부터는 ExAllocatePool2가 권장돼요. 이유는 ExAllocatePool2가 할당된 메모리를 자동으로 0으로 초기화해주고, 실패 시 처리가 더 명확하기 때문이에요.
// NonPaged Pool 할당 (4바이트 태그 포함) PVOID buffer = ExAllocatePool2( POOL_FLAG_NON_PAGED, // NonPaged Pool 1024, // 크기 (바이트) 'rDyM'); // 태그: 4문자 (리틀엔디언: MyDr) if (buffer == NULL) { // 할당 실패 처리 (ExAllocatePool2는 실패 시 NULL 반환) return STATUS_INSUFFICIENT_RESOURCES; } // 메모리 사용... RtlZeroMemory(buffer, 1024); // 사실 ExAllocatePool2는 이미 0 초기화해줌 // 해제 ExFreePoolWithTag(buffer, 'rDyM'); buffer = NULL; // 해제 후 NULL로 설정하는 습관!풀 플래그 선택
// NonPaged Pool POOL_FLAG_NON_PAGED // Paged Pool POOL_FLAG_PAGED // NonPaged Pool (NX — 실행 불가 페이지로 할당, 보안 강화) POOL_FLAG_NON_PAGED_EXECUTE // 코드를 담을 때만 사용 // 예외 발생 모드 (실패 시 NULL 대신 예외를 던짐 — 거의 안 씀) POOL_FLAG_RAISE_ON_FAILURE
⚠️ 구형 ExAllocatePoolWithTag를 아직 쓰는 코드가 많아요
레거시 코드에서는 ExAllocatePoolWithTag(NonPagedPool, size, tag)를 자주 보게 될 거예요. 동작하지만, Windows 10 이상에서는 ExAllocatePool2 사용을 권장해요. 단, ExAllocatePoolWithTag는 메모리를 초기화하지 않으므로 RtlZeroMemory나 초기화 코드가 필요합니다.
풀 태그 (Pool Tag) — 디버깅의 생명줄
태그는 4바이트 값으로, 메모리 누수나 손상 디버깅 시 "이 메모리가 어느 드라이버가 할당한 것인지" 추적하는 데 써요. 아무 값이나 써도 되지만, 드라이버 이름을 딴 고유한 값을 쓰는 게 좋습니다.
// 태그 정의 관례: 4글자 ASCII, 리틀엔디언으로 저장됨 // 'MyDr'을 리틀엔디언으로: 0x72446D4D → WinDbg에서 MyDr로 보임 #define MYDRV_POOL_TAG 'rDyM' // 소스에서는 역순으로 써야 WinDbg에서 바르게 보임 PVOID buf = ExAllocatePool2(POOL_FLAG_NON_PAGED, 256, MYDRV_POOL_TAG); // ... 사용 ExFreePoolWithTag(buf, MYDRV_POOL_TAG);
💡 WinDbg에서 풀 태그로 메모리 누수 추적하기
!poolused 2 MyDr 명령으로 'MyDr' 태그로 할당된 메모리 총량을 볼 수 있어요. 드라이버 언로드 후에도 이 값이 0이 아니면 메모리 누수가 있는 거예요.
구조체 할당 패턴
typedef struct _MY_DATA { ULONG Count; WCHAR Name[64]; LIST_ENTRY ListEntry; } MY_DATA, *PMY_DATA; // 할당 PMY_DATA data = (PMY_DATA)ExAllocatePool2( POOL_FLAG_NON_PAGED, sizeof(MY_DATA), MYDRV_POOL_TAG); if (!data) return STATUS_INSUFFICIENT_RESOURCES; // ExAllocatePool2는 자동 0 초기화, 추가 초기화가 필요한 것만 설정 data->Count = 1; RtlStringCbCopyW(data->Name, sizeof(data->Name), L"MyDevice"); // ... 사용 후 ExFreePoolWithTag(data, MYDRV_POOL_TAG);메모리 누수 방지 패턴
커널에서 메모리 누수는 시스템이 오래 실행될수록 가용 풀을 갉아먹어요. 심해지면 시스템이 BSOD로 죽기도 합니다. 다음 패턴을 지키면 누수를 방지할 수 있어요:
NTSTATUS DoSomething(VOID) { PVOID buf = NULL; NTSTATUS status = STATUS_SUCCESS; buf = ExAllocatePool2(POOL_FLAG_NON_PAGED, 512, MYDRV_POOL_TAG); if (!buf) { status = STATUS_INSUFFICIENT_RESOURCES; goto Cleanup; } // ... 작업 Cleanup: if (buf) { ExFreePoolWithTag(buf, MYDRV_POOL_TAG); buf = NULL; } return status; }커널 코드에서는 이런 goto 기반의 정리 패턴(Cleanup 레이블)이 오히려 권장돼요. C++의 RAII나 예외를 쓸 수 없는 환경에서 가독성 있게 자원을 정리하는 방법이거든요.
할당 실패를 절대 무시하지 마세요
커널에서 메모리 할당은 실패할 수 있어요. 시스템이 메모리 압박을 받는 상황이라면 ExAllocatePool2는 NULL을 반환합니다. 반환값 검사를 빠뜨리면 NULL 포인터를 dereference해서 즉시 BSOD가 나요.
📌 Pool 크기 한도
Windows는 NonPaged Pool과 Paged Pool 각각에 크기 한도를 설정해요. 시스템 RAM과 설정에 따라 다르지만, 드라이버 하나가 풀 전체를 독점하면 다른 구성요소가 메모리를 못 받아서 시스템이 불안정해집니다. 실제로 드라이버 메모리 누수가 BSOD로 이어지는 이유가 이것입니다.
✅ 16화 요약
- NonPaged Pool: 스왑 불가, 모든 IRQL에서 접근 가능. 인터럽트/DPC 데이터에 사용.
- Paged Pool: 스왑 가능, PASSIVE/APC 레벨에서만 접근 가능. 대형 데이터에 사용.
- 할당은 ExAllocatePool2, 해제는 ExFreePoolWithTag를 씁니다.
- 풀 태그를 항상 붙여서 메모리 누수 추적이 가능하게 하세요.
- 할당 반환값은 반드시 NULL 체크하고, 사용 후 반드시 해제하세요.
다음 화
17화 — MDL(Memory Descriptor List): 유저 메모리를 안전하게 커널에서 쓰는 방법 →
#NonPagedPool #PagedPool #ExAllocatePool2 #메모리태그 #커널메모리
'Programming > 7. Device Driver' 카테고리의 다른 글
| 드라이버 개발 핵심 - 18화 인터럽트, ISR, DPC (0) | 2026.06.08 |
|---|---|
| 드라이버 개발 핵심 - 17화 MDL (0) | 2026.06.05 |
| 드라이버 개발 핵심 - 15화 스핀락과 동기화 기법 (0) | 2026.06.03 |
| 드라이버 개발 핵심 - 14화 IRQL 완전 정복 (0) | 2026.06.02 |
| 드라이버 개발 핵심 - 13화 IRP 디스패치 루틴 구현 (0) | 2026.06.01 |