ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Windows 커널 IRQL 완벽 가이드: 레벨별 제약사항과 크래시 방지 전략
    윈도우/커널 드라이버 2025. 12. 22. 01:50
    반응형

    개요

    Windows 커널 드라이버를 개발하다 보면 가장 먼저 부딪히는 벽 중 하나가 바로 IRQL(Interrupt Request Level)입니다. IRQL을 제대로 이해하지 못하면 BSOD(Blue Screen of Death)와 친해질 수밖에 없죠. 이 글에서는 각 IRQL 레벨의 특성과 제약사항, 그리고 흔히 발생하는 크래시 시나리오를 정리합니다.

    IRQL이란?

    IRQL은 Windows 커널에서 인터럽트 우선순위를 관리하는 메커니즘입니다. 현재 프로세서의 IRQL보다 낮거나 같은 우선순위의 인터럽트는 마스킹되어 처리가 지연됩니다.

    높은 우선순위
        ↑
        │  HIGH_LEVEL (31)        - 머신 체크, 치명적 오류
        │  POWER_LEVEL (30)       - 전원 실패
        │  IPI_LEVEL (29)         - 프로세서 간 인터럽트
        │  CLOCK_LEVEL (28)       - 클럭 인터럽트
        │  PROFILE_LEVEL (27)     - 프로파일링 타이머
        │  DIRQL (3-26)           - 디바이스 인터럽트
        │  DISPATCH_LEVEL (2)     - 스레드 스케줄링, DPC
        │  APC_LEVEL (1)          - 비동기 프로시저 콜
        ↓  PASSIVE_LEVEL (0)      - 일반 스레드 실행
    낮은 우선순위

    레벨별 상세 분석

    PASSIVE_LEVEL (0)

    가장 낮은 IRQL로, 일반적인 스레드 코드가 실행되는 레벨입니다.

    허용되는 작업:

    • 모든 커널 API 호출
    • Paged Pool 메모리 접근
    • 파일 시스템 I/O
    • 레지스트리 접근
    • 동기화 객체 대기 (KeWaitForSingleObject 등)
    • 유저모드 메모리 접근 (ProbeForRead/Write)

    제약사항:

    • 없음 (가장 자유로운 레벨)

    일반적인 사용처:

    • DriverEntry, DriverUnload
    • IRP_MJ_CREATE, IRP_MJ_CLOSE
    • 대부분의 IOCTL 처리
    • 작업 스레드(Worker Thread)
    // PASSIVE_LEVEL에서만 안전한 코드 예시
    NTSTATUS SafeFileOperation(PUNICODE_STRING FilePath)
    {
        PAGED_CODE();  // PASSIVE_LEVEL 검증 매크로
    
        HANDLE fileHandle;
        IO_STATUS_BLOCK ioStatus;
        OBJECT_ATTRIBUTES objAttr;
    
        InitializeObjectAttributes(&objAttr, FilePath, 
            OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
    
        // 파일 I/O는 반드시 PASSIVE_LEVEL에서
        return ZwCreateFile(&fileHandle, ...);
    }

    APC_LEVEL (1)

    APC(Asynchronous Procedure Call)가 실행되는 레벨입니다.

    허용되는 작업:

    • Non-Paged Pool 메모리 할당/접근
    • Paged Pool 메모리 접근 (주의 필요)
    • 스핀락 획득
    • Fast Mutex 해제

    제약사항:

    • 커널 APC 비활성화됨
    • KeWaitForSingleObject 호출 시 Alertable = FALSE 권장
    • 일부 동기화 프리미티브 사용 불가

    크래시 시나리오:

    // ❌ 위험: APC_LEVEL에서 ERESOURCE 획득 시도
    VOID DangerousApcRoutine(PVOID Context)
    {
        // 현재 IRQL: APC_LEVEL
        ExAcquireResourceExclusiveLite(&SomeResource, TRUE);  
        // ERESOURCE는 내부적으로 APC를 사용하므로 데드락 가능!
    }

    DISPATCH_LEVEL (2) ⚠️ 가장 주의 필요

    DPC(Deferred Procedure Call)와 스레드 스케줄러가 동작하는 레벨입니다. 드라이버 크래시의 대부분이 이 레벨에서 발생합니다.

    허용되는 작업:

    • Non-Paged Pool 메모리 할당/접근
    • 스핀락 획득/해제
    • DPC 큐잉
    • 인터럽트 동기화

    제약사항 (위반 시 즉시 BSOD):

    금지 작업 이유 결과
    Paged Pool 접근 페이지 폴트 처리 불가 IRQL_NOT_LESS_OR_EQUAL
    파일 시스템 I/O 대기 필요 DRIVER_IRQL_NOT_LESS_OR_EQUAL
    레지스트리 접근 대기 필요 시스템 행 또는 크래시
    KeWaitForXxx (Timeout != 0) 스케줄러 호출 불가 IRQL_NOT_LESS_OR_EQUAL
    유저모드 버퍼 접근 페이지 폴트 가능 PAGE_FAULT_IN_NONPAGED_AREA
    ExAllocatePoolWithTag(PagedPool) 페이지 폴트 가능 IRQL_NOT_LESS_OR_EQUAL

    흔한 크래시 패턴:

    // ❌ 패턴 1: Paged 메모리 접근
    VOID DpcRoutine(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2)
    {
        // 현재 IRQL: DISPATCH_LEVEL
        PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)Context;
    
        // ext가 PagedPool에 할당되어 있다면 크래시!
        ext->SomeField = 0;  // IRQL_NOT_LESS_OR_EQUAL
    }
    
    // ❌ 패턴 2: 대기 시도
    VOID SpinlockProtectedCode(PKSPIN_LOCK Lock)
    {
        KIRQL oldIrql;
        KeAcquireSpinLock(Lock, &oldIrql);
        // 현재 IRQL: DISPATCH_LEVEL
    
        LARGE_INTEGER timeout;
        timeout.QuadPart = -10000000;  // 1초
        KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, &timeout);
        // 크래시! DISPATCH_LEVEL에서 대기 불가
    
        KeReleaseSpinLock(Lock, oldIrql);
    }
    
    // ❌ 패턴 3: 문자열 함수 사용
    VOID DpcStringOperation(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2)
    {
        UNICODE_STRING str;
        RtlInitUnicodeString(&str, L"Test");  // OK (스택)
    
        UNICODE_STRING copy;
        RtlDuplicateUnicodeString(0, &str, &copy);  
        // 내부적으로 PagedPool 할당 시도 → 크래시!
    }

    안전한 DISPATCH_LEVEL 코드:

    // ✅ 안전한 DPC 루틴
    VOID SafeDpcRoutine(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2)
    {
        PNONPAGED_CONTEXT ctx = (PNONPAGED_CONTEXT)Context;
    
        // Non-Paged 메모리만 접근
        ctx->Counter++;
    
        // 작업이 복잡하면 Work Item으로 위임
        if (ctx->NeedsComplexWork) {
            IoQueueWorkItem(ctx->WorkItem, WorkerRoutine, 
                DelayedWorkQueue, ctx);
        }
    }

    DIRQL (Device IRQL, 3-26)

    하드웨어 인터럽트 서비스 루틴(ISR)이 실행되는 레벨입니다.

    허용되는 작업:

    • 해당 디바이스 레지스터 접근
    • 인터럽트 스핀락 획득/해제
    • DPC 요청 (IoRequestDpc)
    • 간단한 Non-Paged 메모리 접근

    제약사항:

    • DISPATCH_LEVEL의 모든 제약 + 추가 제약
    • 대부분의 커널 API 호출 불가
    • 가능한 짧게 실행해야 함
    // ✅ 올바른 ISR 패턴
    BOOLEAN DeviceIsr(PKINTERRUPT Interrupt, PVOID Context)
    {
        PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)Context;
    
        // 1. 인터럽트 확인
        if (!IsMyInterrupt(ext)) {
            return FALSE;  // 다른 디바이스 인터럽트
        }
    
        // 2. 인터럽트 비활성화 (하드웨어)
        DisableDeviceInterrupt(ext);
    
        // 3. 최소한의 정보만 저장
        ext->InterruptStatus = ReadInterruptStatus(ext);
    
        // 4. DPC로 실제 처리 위임
        IoRequestDpc(ext->DeviceObject, NULL, ext);
    
        return TRUE;
    }

    HIGH_LEVEL (31)

    최상위 IRQL로, 시스템 크래시 덤프나 NMI 처리에 사용됩니다.

    특징:

    • 모든 인터럽트 마스킹
    • 거의 아무것도 할 수 없음
    • 드라이버에서 직접 사용하지 않음

    IRQL 관련 주요 크래시 코드

    Bug Check Code 원인 해결책
    IRQL_NOT_LESS_OR_EQUAL (0xA) 높은 IRQL에서 Paged 메모리 접근 Non-Paged Pool 사용, IRQL 확인
    DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1) 드라이버에서 잘못된 메모리 접근 메모리 할당 타입 확인
    PAGE_FAULT_IN_NONPAGED_AREA (0x50) Non-Paged로 예상했으나 실제론 Paged 할당 플래그 확인
    SPIN_LOCK_ALREADY_OWNED (0xF) 같은 스핀락 중복 획득 락 계층 구조 검토
    KERNEL_APC_PENDING_DURING_EXIT (0x20) APC_LEVEL에서 스레드 종료 IRQL 복원 확인

    안전한 코딩을 위한 체크리스트

    컴파일 타임 검증

    // PASSIVE_LEVEL 전용 함수 표시
    #pragma alloc_text(PAGE, MyPagedFunction)
    
    NTSTATUS MyPagedFunction()
    {
        PAGED_CODE();  // 디버그 빌드에서 IRQL 검증
        // ...
    }

    런타임 IRQL 확인

    VOID SafeFunction(PVOID Buffer, SIZE_T Size)
    {
        KIRQL currentIrql = KeGetCurrentIrql();
    
        if (currentIrql >= DISPATCH_LEVEL) {
            // Non-Paged 처리 경로
            ProcessAtHighIrql(Buffer, Size);
        } else {
            // Paged 처리 가능
            ProcessAtLowIrql(Buffer, Size);
        }
    }

    Work Item 패턴 (IRQL 낮추기)

    typedef struct _WORK_CONTEXT {
        PIO_WORKITEM WorkItem;
        PVOID Data;
        SIZE_T DataSize;
    } WORK_CONTEXT, *PWORK_CONTEXT;
    
    // DISPATCH_LEVEL에서 호출
    VOID QueueWorkAtPassiveLevel(PDEVICE_OBJECT DeviceObject, PVOID Data)
    {
        PWORK_CONTEXT ctx = ExAllocatePoolWithTag(
            NonPagedPool, sizeof(WORK_CONTEXT), 'kroW');
    
        if (ctx) {
            ctx->WorkItem = IoAllocateWorkItem(DeviceObject);
            ctx->Data = Data;
    
            // PASSIVE_LEVEL에서 실행될 작업 큐잉
            IoQueueWorkItem(ctx->WorkItem, PassiveLevelWorker,
                DelayedWorkQueue, ctx);
        }
    }
    
    // PASSIVE_LEVEL에서 실행됨
    VOID PassiveLevelWorker(PDEVICE_OBJECT DeviceObject, PVOID Context)
    {
        PWORK_CONTEXT ctx = (PWORK_CONTEXT)Context;
    
        // 이제 파일 I/O, 레지스트리 접근 등 가능
        ProcessDataSafely(ctx->Data);
    
        IoFreeWorkItem(ctx->WorkItem);
        ExFreePoolWithTag(ctx, 'kroW');
    }

    Driver Verifier 활용

    IRQL 관련 버그를 잡기 위해 Driver Verifier를 활용하세요.

    :: IRQL 검증 활성화
    verifier /standard /driver YourDriver.sys
    
    :: 특정 검증만 활성화
    verifier /flags 0x2 /driver YourDriver.sys  :: IRQL checking
    
    :: 상태 확인
    verifier /query

    요약 테이블

    IRQL Paged 접근 파일 I/O Wait 스핀락 일반 API
    PASSIVE_LEVEL
    APC_LEVEL ⚠️ ⚠️
    DISPATCH_LEVEL
    DIRQL ⚠️
    HIGH_LEVEL

    결론

    IRQL을 마스터하는 것은 Windows 커널 드라이버 개발의 기본입니다. 핵심은 간단합니다:

    1. DISPATCH_LEVEL 이상에서는 Paged 메모리 접근 금지
    2. DISPATCH_LEVEL 이상에서는 대기(Wait) 금지
    3. 확실하지 않으면 Work Item으로 PASSIVE_LEVEL에서 처리
    4. Driver Verifier로 항상 테스트

    이 규칙만 지켜도 IRQL 관련 BSOD의 90%는 예방할 수 있습니다.


    반응형

    댓글

Designed by Tistory.