KeAcquireSpinLock과 KeReleaseSpinLock
Table of Contents
Abstract
Windows의 커널 레벨 어플리케이션에서 공유 자원으로의 접근을 동기화하는 스핀 락과 스핀 락을 사용해 상호배제를 제공하는 KeAcquireSpinLock, KeReleaseSpinLock 함수의 사용법에 대해 알아본다. 또한 두 함수의 기본 원리인 IRQL에 대해서도 간략하게 알아본다.
Ke*SpinLock
Spin Lock
스핀 락은 Windows 커널 모듈에서 동기화 메커니즘을 제공하는 불투명한(opaque) 구조다. 공유 자원에 접근하는 스레드들의 동기화를 맞춰주는 뮤텍스에서 잠금을 획득한 스레드만이 공유 자원에 접근할 수 있는 것처럼, 스핀 락을 획득한 스레드만이 공유 자원에 접근할 수 있도록 한다.
Windows에서 스핀 락은 KSPIN_LOCK이라는 타입명으로 정의되어 있으며, WDK 10.0.22000.1 기준으로 wdm.h에 정의된 KSPIN_LOCK은 아래와 같다.
//
// Spin Lock
//
typedef ULONG_PTR KSPIN_LOCK;
typedef KSPIN_LOCK *PKSPIN_LOCK;
IRQL
IRQL은 Interrupt ReQuest Level이라는 의미로, 인터럽트 간 우선순위라고 생각하면 된다. 커널 레벨에서 실행되는 모든 스레드는 IRQL을 가진다. 가장 낮은 IRQL인 PASSIVE_LEVEL(0)을 가지는 스레드가 실행 중인 상태에서 힙 영역을 참조했는데, 이 영역이 현재 메모리에 로드되지 않은 상태(Page Fault)라면 Paging Pool에서 해당 영역 데이터를 가져오기 위한 루틴이 동작하게 된다. 이 루틴은 APC_LEVEL(1)을 가진 루틴이기 때문에 기존 스레드는 실행이 중지되고 컨텍스트 스위칭이 일어나며 페이지 폴트를 해결하는 루틴이 실행된다. 페이지 폴트가 해결되면 다시 원래 스레드로 돌아가며 IRQL도 낮아지게 된다.
아래 표는 Windows의 IRQL 목록이다. 보통 DISPATCH_LEVEL보다 높은 IRQL은 보기 힘들다. 네트워크 필터링 어플리케이션을 만드는 WFP의 필터 엔진 루틴도 DISPATCH_LEVEL이다(그래서 콜아웃 루틴도 DISPATCH_LEVEL이다).
https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities
IRQL | IRQL Values | Desc. | ||
x86 | IA64 | AMD64 | ||
PASSIVE_LEVEL | 0 | 0 | 0 | User threads and most kernel-mode operations. |
APC_LEVEL | 1 | 1 | 1 | Asynchronous procedure calls and page faults. |
DISPATCH_LEVEL | 2 | 2 | 2 | Thread scheduler and deferred procedure calls (DPCs). |
CMC_LEVEL | N/A | 3 | N/A | Correctable machine-check level (IA64 platforms only). |
DIRQL | 3-26 | 4-11 | 3-11 | Device interrupts. |
PC_LEVEL | N/A | 12 | N/A | Performance counter (IA64 platforms only). |
PROFILE_LEVEL | 27 | 15 | 15 | Profilling timer for releases earlier than Windows 2000. |
SYNCH_LEVEL | 27 | 13 | 13 | Synchronization of code and instruction streams across processors. |
CLOCK_LEVEL | N/A | 13 | 13 | Clock timer. |
CLOCK2_LEVEL | 27 | N/A | N/A | Clock timer for x86 hardware. |
IPI_LEVEL | 29 | 14 | 14 | Interprocessor interrupt for enforcing cache consistency |
POWER_LEVEL | 30 | 15 | 14 | Power failure |
HIGH_LEVEL | 31 | 15 | 15 | Machine checks and catastrophic erros; profilling timer for Windows XP and later releases. |
KeAcquireSpinLock & KeReleaseSpinLock
KeAcquireSpinLock을 호출하면 먼저 현재 스레드의 IRQL을 DISPATCH_LEVEL(2)로 상승시킨 후 KSPIN_LOCK 잠금을 획득한다. 상승된 IRQL 덕분에 컨텍스트 스위칭이 일어나지 않으며 안전하게 잠금을 획득할 수 있게 된다. 그 후엔 공유 자원에 접근해 원하는 작업을 하면 된다. 작업을 마친 후에는 반드시 KeReleaseSpinLock으로 잠금을 해제하고 IRQL을 원상태로 복구해야 한다.
아래는 KeAcqurieSpinLock과 KeReleaseSpinLock의 원형이다.
void KeAcquireSpinLock(
SpinLock,
OldIrql
);
VOID
KeReleaseSpinLock (
_Inout_ PKSPIN_LOCK SpinLock,
_In_ _IRQL_restores_ KIRQL NewIrql
);
KeAcquireSpinLock만 조금 다른 걸 볼 수 있는데 KeAcquireSpinLock은 사실 KeAcquireSpinLockRaiseToDpc 함수를 호출하는 매크로다. KeAcquireSpinLockRaiseToDpc에 스핀 락 변수의 주소값과 KIRQL 변수의 주소값을 넘겨주면 스핀 락 변수의 값을 조정하고, KIRQL 변수엔 원래 IRQL을 백업해준다.
//
// These functions are imported for ARM, ntddk, ntifs, nthal, ntosp, and wdm.
// They can be inlined for the system on AMD64.
//
#define KeAcquireSpinLock(SpinLock, OldIrql) \
*(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)
Usage of Ke*SpinLock
Initialize
스핀 락을 사용하기 전에 스핀 락 변수를 초기화 해야한다. 스핀 락 변수 초기화는 KeInitializeSpinLock 함수를 사용한다.
KSPIN_LOCK lock;
KeInitializeSpinLock(&lock);
Acquiring a spin lock
스핀 락 잠금은 KeAcquireSpinLock 매크로를 사용해 얻을 수 있다. 사용하기 전에 원래 IRQL을 저장할 KIRQL 변수를 생성해야 한다. KeAcquireSpinLock의 파라메터로는 KSPIN_LOCK과 KIRQL 변수의 주소값을 넘겨준다.
KIRQL irql;
KeAcquireSpinLock(&lock, &irql);
Releasing a spin lock
잠금을 풀지 않으면 해당 공유 자원을 함께 사용하는 모든 스레드가 무한 대기 상태에 빠진다. 이는 자칫 Windows 자체를 멈추게 만든다(BSOD가 아니라 마우스, 키보드 등 모든 입력이 먹히지 않는 먹통 상태가 된다). Release 시에는 KIRQL 변수의 주소값을 넘기지 않아도 된다.
KeReleaseSpinLock(&lock, irql);
Result
Source Code
예제 코드
#include <ntddk.h>
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT driver_object,
_In_ PUNICODE_STRING registry_path
)
{
UNREFERENCED_PARAMETER(driver_object);
UNREFERENCED_PARAMETER(registry_path);
KIRQL irql;
KSPIN_LOCK lock;
KeInitializeSpinLock(&lock);
// current irql
KdPrint(("[spinlock_sample] Current IRQL is %u\n", KeGetCurrentIrql()));
// increase an irql to dispatch level
KdPrint(("[spinlock_sample] ----- Acquire a spin lock (make an irql to dispatch(2))\n"));
KeAcquireSpinLock(&lock, &irql);
{
KdPrint(("[spinlock_sample] Increased IRQL is %u\n", KeGetCurrentIrql()));
}
KeReleaseSpinLock(&lock, irql);
KdPrint(("[spinlock_sample] ----- Release a spin lock (recover an irql(%u))\n", irql));
// current irql
KdPrint(("[spinlock_sample] Recovered IRQL is %u\n", KeGetCurrentIrql()));
return STATUS_UNSUCCESSFUL;
}