2009년 11월 3일 화요일

디바이스드라이버 기초2

NTSTATUS의 정의

typedef LONG NTSTATUS;

예제:

typedef
NTSTATUS
(*PIO_COMPLETION_ROUTINE) (
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp,
    IN PVOID Context
    );

VxD

하드웨어 디바이스를 에뮬레이션하거나 추상화시켜 하드웨어에 대한 독점적인 액세스가 필요한 애플리케이션에서 사용할 수 있도록하는 메커니즘이다.

윈도우 NT 드라이버

CPU의 특권 레벨 또는 슈퍼바이저 레벨(인텔 아키텍쳐의 Ring 0)에서 실행되는 32비트의 모듈 컴포넌트이다. 윈도우 NT 드라이버는 커널에게 신뢰를 받아 무제한의 자유를 누릴 수 있으며 VxD가 메모리에 로드되면 VMM의 일부가 되듯이 윈도우 NT 드라이버도 로드된 다음에는 커널의 일부가 되어 커널과 동일한 권한을 누릴 수 있다. 이와 함께 윈도우 NT 드라이버는 로드되고 나면 I/0 Manager의 관리를 받으며 I/O Manager는 드라이버를 로드하고 관리하는 기초 환경을 제공한다.


1. 드라이버를 로드하는 방법

드라이버를 로드하기 전에 드라이버가 미리 시스템에 설치 되어 있어야 한다. 대부분의 드라이버는 시스템의 레지스터리에 설치되며 그 위치는 \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services이다. 드라이버와 서비스는 유저 모드 측면에서 볼 때는 똑같은 것으로 취급하며 이런 원리로 서비스 API를 이용해 드라이버를 제어 할 수 있다.

// 드라이버를 연다...

hDevice = CreateFile(“\\\\.\\드라이버이름”,
                        GENERIC_READ | GENERIC_WRITE,
                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                        0,
                        OPEN_EXISTING,
                        FILE_FLAG_OVERLAPPED,
                        0);

2. 유저 모드에서 보는 드라이버 (디바이스 드라이버가 유저 모드의 프로세스에게 어떻게 자신을 나타내는가에 대한 문제)

 Win32 API인 QueryDosDevice 함수를 이용해 이런정보(Device Name)를 얻어온다.(Query.c소스) 일단 디바이스를 찾아내고 핸들을 이용해 디바이스를 열었다면 그 다음 단계는 드라이버가 제공하는 API가 어떤 것인지 알아내는 것이다. 드라이버의 기본 기능은 ReadFile, WriteFile과 같은 API함수와 IOCTL(I/O control codes)이 제공하며 ReadFile/WriteFile에 의해 제공되지 않는 기능은 DeviceIoControl함수로 제공한다.

3. 유저 모드에서의 드라이버 엑세스

MBR(Master Boot Record)이라고 알려진 디스크의 0번 섹터를 읽어오는 프로그램

hDriver = CreateFile(“\\\\.\\PhysicalDrive0”,
                        GENERIC_READ | GENERIC_WRITE,
                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                        0,
                        OPEN_EXISTING,
                        0,
                        0);

MBR을 읽기 위해서는 우선 첫째 디스크를 열어야 한다.여기서 PhysicalDrive0이라면 QueryDosDevice함수에서 얻어오는 심벌이름인데 첫째 하드디스크를 의미한다. 드라이버에 대한 핸들은 파일의 0번째 바이트를 가리키도록 초기화 되므로 그대로 ReadFile에 넘겨줘 디스크의 첫째 바이트부터 512바이트까지를 읽도록 한다. 그러면 버퍼에는 0번 섹터의 MBR 내용이 담긴다.

ReadFile(hDriver, &data, 512, &dwBytesRead, NULL);

디바이스에 대한 제어는 관리자 레벨에서만 가능하다. 그러므로 앞의 방식대로 작성한 프로그램에서는 보안상 문제는 없다. 하지만 사용자 계정에서도 MBR을 직접 액세스하는 애플리케이션을 작성하려면 윈도우 NT 디바이스 드라이버를 직접 작성해야 한다.

4. 윈도우 NT 디바이스 드라이버의 작성 방법

4.1 디바이스 오브젝트의 생성

로드, 언로드 할 수 있는 기본적인 윈도우 NT 드라이버를 만들고 몇 개의 IOCTL의 구현을 해본다. 간단히 말해 커널 모드 드라이버는 다른 Win32 애플리케이션이나 DLL과 똑 같은 PE 포맷의 프로그램이다. Win32 DLL의 엔트리 함수는 DllMain이지만 드라이버는 DriverEntry를 사용한다. I/O Manger는 드라이버가 시작될 때 DriverEntry를 호출하며 이 함수에는 DRIVER_OBJECT에 대한 포인터와 드라이버 레지스트리 목록에 대한 포인터가 함께 전달된다.

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DeviceObject, IN PUNICODE_STRING RegitryPath)

/**
루틴 설명:이 루틴은 드라이버가 윈도우 NT에 의해 로드되면 호출된다.
인자:
        DriverObject - 시스템에 의해 만들어진 드라이버 오브젝트를 가리킴
        RegitryPath - 이 드라이버를 위한 서비스 노드 이름을 가리킴

리턴값: 초기 작업의 상태
**/

{

////// 생략

}

DriverEntry는 DRIVER_OBJECT 구조체를 원하는 값으로 초기화 한다. 보통 드라이버에 대한 디바이스 오브젝트를 생성하기 위해 I/O Manger의 IoCreateDevice를 호출한다.

// 드라이버의 이름을 uszDriverString에 넣는다.
RtlInitUnitcodeString(&uszDriverString, L"\\Device\\MSJDrvr");

// 디바이스 오브젝트를 생성하고 초기화 한다.
ntStatus = IoCreateDevice(DriverObject,
        0,
        &uszDriverString,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &pDeviceObject);

대부분의 드라이버는 드라이버가 정의한 디바이스 익스텐션에 자신만의 데이터를 정의한다. 디바이스 익스텐션에 할당되는 메모리는 IoCreateDevice를 호출할 때의 둘째인자에 의해 생성되고 넷째 인자의 값에 따라 디바이스 형식이 정해진다. 이때 이 값도 관리자 계정이 아닌 계정에서의 드라이버 액세스 여부가 결정되는 중요한 부분이다. 윈도우 NT 드라이버는 관리자에 의해 설치 돼야 한다.

 

액세스가 제한되는 디바이스 종류

액세스가 허용되는 디바이스 종류

액세스가 허용되는 디바이스 종류

FILE_DEVICE_CD_ROM_FILE_SYSTEM

FILE_DEVICE_BEEP

FILE_DEVICE_SERIAL_PORT

FILE_DEVICE_DISK

FILE_DEVICE_CD_ROM

FILE_DEVICE_SCREEN

FILE_DEVICE_DISK_FILE_SYSTEM

FILE_DEVICE_CONTROLLER

FILE_DEVICE_SOUND

FILE_DEVICE_NETWORK

FILE_DEVICE_DATALINK

FILE_DEVICE_STREAMS

FILE_DEVICE_NETWORK_FILE_SYSTEM

FILE_DEVICE_DFS

FILE_DEVICE_TAPE

FILE_DEVICE_TAPE_FILE_SYSTEM

FILE_DEVICE_INPORT_PORT

FILE_DEVICE_TRANSPORT

FILE_DEVICE_VIRTUAL_DISK

FILE_DEVICE_KEYBOARD

FILE_DEVICE_UNKNOWN


FILE_DEVICE_MAILSLOT

FILE_DEVICE_VIDEO


FILE_DEVICE_MIDI_IN

FILE_DEVICE_WAVE_IN


FILE_DEVICE_MIDI_OUT

FILE_DEVICE_WAVE_OUT


FILE_DEVICE_MOUSE

FILE_DEVICE_8042_PORT


FILE_DEVICE_MULTI_UNC_PROVIDER

FILE_DEVICE_NETWORK_REDIRECTOR


FILE_DEVICE_NAMED_PIPE

FILE_DEVICE_BATTERY


FILE_DEVICE_NETWORK_BROWSER

FILE_DEVICE_BUS_EXTENDER


FILE_DEVICE_NULL

FILE_DEVICE_MODEM


FILE_DEVICE_PARALLEL_PORT

FILE_DEVICE_VDM


FILE_DEVICE_PHYSICAL_NETCARD



FILE_DEVICE_PRINTER



FILE_DEVICE_SCANNER



4.2 디바이스 오브젝트가 생성된 후

유저 모드 애플리케이션 측면에서 나타나는 이름을 만들어야 하며 다음가 같이 IoSymbolicLink를 이용하면 된다.

// 디바이스 이름을 uszDeviceString에 넣는다.
RtlInitUnicodeString(&uszDeviceString, L"\\DosDevices\\MSJDrvr");

// 사용자가 접할 이름에 대해 심볼 링크를 생성한다.
ntStatus = IoCreateSymbolicLink(&uszDeviceString, &uszDriverString);

이런 방식으로 한 개 또는 그 이상의 심볼 링크를 생성한 후, 다음 레지스트리 경로에 유저모드의 앨리어스를 만든다.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Dos Devices

레지스트리 경로에 있는 값을 들여다 보면 NUL, PRN, AUX와 같은 MS DOS의 디바이스가 들어 있으며 이에 관련된 데이터는 디바이스 이름이나 심볼 링크오 같은 스트링이다. 예를 들어 NUL은 \Device\Null이라는 앨리어스를 갖고 있다. 따라서 이런 방법으로 만들고 싶은 앨리어스를 쉽게 만들 수 있다. 끝으로 DRIVER_OBJECT 구조체의 멤버는 함수 핸들러의 주소로 채워주면 된다. 대부분의 초기화는 다음과 같이 정의되어 있는 DRIVER_OBJECT 멤버에 관련된 것이다.

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

이는 I/O Manager 가 메시지를 분배하는데 사용할 함수에 대한 포인터의 배열이다. 이 구조체는 차후에 윈도우 NT의 버전이나 드라이버에 관계없이 호환성을 유지하며 컨트롤 메시지를 마음대로 추가하기 위해 구조체의 마지막 부분에 위치한다.

// 구조체를 IRP 핸들러에 대한 포인터로 초기화
    DriverObject->DriverUnload                         = MSJUnloadDriver;
    DriverObject->MajorFunction[IRP_MJ_CREATE]         = MSJDispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          = MSJDispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MSJDispatchIoctl;

I/O Manager는 유저 모드의 애플리케이션에서 메시지와 내부 컨트롤 메시지를 분배하는데 이 포인터를 이용한다. 예를 들어 Win32 애플리케이션이 CreateFile을 호출해 드라이버를 열었을 때 I/O Manager는 DriverObject->MajorFunction[IRP_MJ_CREATE]가 가리키는 곳에 위치한 함수를 호출한다. 이 함수 내부에서는 필요한 작업을 통해 드라이버를 마음대로 초기화 할 수 있다. 마찬가지로 Win32 애플리케이션이 드라이버에 대한 핸들을 닫을 때 DriverObject->MajorFunction[IRP_MJ_CLOSE]가 호출되고 드라이버가 정지 했을 때(시스템이 셧다운 되거나 사용자가 'NET STOP 드라이버 이름'을 입력했을 때, 제어판에서 디바이스를 정지시켰을 때 등)는 DriverObject->DriverUnload가 가리키는 함수가 호출된다.

윈도우 NT 4.0에 정의된 주요 메시지는 다음 표와 같다.

IOCTL의 메이저 메시지

IRP_MJ_CREATE

IRP_MJ_QUERY_VOLUME_INFORMATION

IRP_MJ_QUERY_SECURITY

IRP_MJ_CREATE_NAMED_PIPE

IRP_MJ_SET_VOLUME_INFORAMTION

IRP_MJ_SET_SECURITY

IRP_MJ_CLOSE

IRP_MJ_DIRECTORY_CONTROL

IRP_MJ_QUERY_POWER

IRP_MJ_READ

IRP_MJ_FILE_SYSTEM_CONTROL

IRP_MJ_SET_POWER

IRP_MJ_WRITE

IRP_MJ_DEVICE_CONTROL

IRP_MJ_DEVICE_CHANGE

IRP_MJ_QUERY_INFORMATION

IRP_MJ_INTERNAL_DEVICE_CONTROL

IRP_MJ_QUERY_QUOTA

IRP_MJ_SET_INFORMATION

IRP_MJ_SHUTDOWN

IRP_MJ_SET_QUOTA

IRP_MJ_QUERY_EA

IRP_MJ_LOCK_CONTROL


IRP_MJ_SET_EA

IRP_MJ_CLEANUP


IRP_MJ_FLUSH_BUFFERS

IRP_MJ_CREATE_MAILSLOT



5. IRP(I/O Request Packet)의 이해와 디바이스에 대한 요청을 처리하는데 필요한 지식

드라이버를 실행하는데 중요한 메시지는 IRP_MJ_DEVICE_CONTROL, IRP_MJ_READ, IRP_MJ_WRITE 등이다. IRP_MJ_READ와 IRP_MJ_WRITE는 Win32 애플리케이션이 드라이버에 대해 ReadFile 함수와 WriteFile 함수를 호출한 결과물로 Win32 애플리케이션은 이들 함수에 입출력을 위한 버퍼, 읽거나 쓸 바이트 수, 동작될 오프셋 등을 함께 전달한다. 드라이버의 입출력 함수가 호출되면 유저 모드의 여러 인자가 IRP라는 패키지로 포장되는데 여기는 호출자에게 필요한 입출력 관련 내역이 담긴다. 그 외에도 I/O Manager는 IRP를 시스템 전체의 입출력 요청을 추적하는데 사용한다. 이를 통해 IRP를 제어하는 일은 디바이스 드라이버가 아닌 I/O Manager의 책임으로 넘어간다. <이점이 바로 드라이버 계층과 필터 드라이버가 운영되는 핵심이다.> IRP는 유저 모드 애플리케이션의 윈도우 메시지와 같은 역할을 한다. 대부분의 윈도우용 애플리케이션이 이벤트에 의해 구동되는 방식인 것처럼 윈도우 NT 드라이버도 IRP에 대해 마찬가지 방식으로 처리한다. 예를 들어 드라이버의 입출력 함수는 다음과 같이 정의된다.

NTSTATUS DispatchReadWrite(IN PDEVICE_OBJECT DeviceObject, IN OUT Irp)

IN, OUT 키워드는 함수의 인자가 입력을 위한 것인지 출력을 위한 것인지 나타내는 것이다. 첫째 인자는 DriverEntry 내부에서 생성한 디바이스 오브젝트로, I/O Manager는 언제나 제어 함수에게 이 포인터를 넘겨 주므로 굳이 다른 곳에서 찾을 필요는 없다. 둘째 인자는 IRP에 대한 포인터로, 이 구조체는 입출력 요청을 처리하는데 필요한 정보가 포함되어 있다. Win32 유저 모드 애플리케이션에서 다음과 같이 호출하면 그 내용이 I/O Manager까지 전달되어 IRP 스택과 함께 IRP를 만들게 된다.

MBR data;
DWORD dwBytesRead;

// Read sector 0 off of the drive...
ReadFile(hPhysicalDrive, &data, 512, &dwBytesRead, NULL);

IRP 스택은 드라이버에게 전달될 인자를 담게 되는데 단일 모드 드라이버에 대해서는 당연히 현재 드라이버에 대한 스택만 갖게 된다. 하지만 계층 드라이버나 필터 드라이버인 경우에는 여러 계층의 드라이버에 대한 각 스택 프레임이 존재하게 된다. 드라이버의 스택 위치에 대한 포인터는 다음처럼 호출해 알아낼 수 있다.

PIO_STACK_LOCATION irpSp;
...
irpSp=IoGetCurrentIrpStackLocation(Irp);

IoGetCurrentIrpStackLocation은 IRP에서 현재의 스택 포인터를 얻어 내는데 쓰는 매크로이고 I/O Manager가 관리한다. IO_STACK_LOCATION은 IOCTL에서 전달된 인자가 담긴 유니언을 포함하는 구조체이다. 윈도우 NT 드라이버에서 흔히 접하는 것이 읽기와 쓰기 디바이스 입출력 컨트롤에 대한 것으로 읽기에 대해서는 다음과 같은 인자를 전달한다.

irpSp->Parameters.Read.ByteOffset
irpSp->Parameters.Read.Length

irpSp->Parameters.Read.ByteOffset은 유저 모드에서 넘겨받은 파일의 포인터 위치를 알려준다.(SetFilePointer를 호출해 설정할 수 있다). irpSp->Parameters.Read.Length에는 ReadFile을 호출할 때 인자로 넘겨준 길이를 나타내는 값이 그대로 넘어온다. 유저 모드의 버퍼의 주소는 다음에 담기게 된다.

Irp->AssociatedIrp.SystemBuffer

이 버퍼는 드라이버에 의해 맵핑되고 사용준비를 마치며 I/O Manager가 사용자를 대신해 유저 모드에서의 전송이나 유저 모드로의 전송을 준비하기 때문에 별도의 매핑이나 설정은 필요없다. 어쨌든 이 포인터는 유저 모드에서의 입력이나 출력에 모두 사용한다는 것에 유념하자. 사실 버퍼을 공유해서 단순히 읽고 쓸 때는 문제 없으나 DeviceIoControl의 호출 시점에는 문제가 생길 수 있다. 그러니 출력 버퍼에 쓰기 전에는 꼭 입력 버퍼에서 필요한 내용을 미리 꺼내와야 한다. 그렇지 않으면 그 안에 있는 중요한 내용이 모두 지워지게 된다.

6. I/O Manager에서 유저 모드 애플리케이션에게 제어를 넘기기

I/O Manager에게 함수를 호출했던 유저 모드 애플리케이션에게 제어를 넘겨준다. 이를 위해 작동이 실패했는지 성공했는지를 확인한다(MSJDrvr.c).

Irp->IoStatus.Status = STATUS_SUCCESS

다음으로 유저 모드 애플리케이션에게 복사할 정보의 바이트 수를 나타내는 IRP 정보에 대한 멤버를 설정한다.

// 유저 모드에 다시 넘겨줄 바이트

Irp->IoStatus.Information = irpStack->Parameters.DeviceIoControl.OutputBufferLength;

마지막으로 I/O Manager에게 IRP에 대한 처리를 끝내도록 지시한다.

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return ntStatus;

I/O Manager는 IoCompleteRequest로 요청을 끝내는지 다른 계층의 드라이버에게 처리를 넘길 것인지 통지해야 한다.

여기까지가 유저 모드에서 윈도우 NT 드라이버까지의 제어의 흐름이다.

7.IOCTL(I/O Manager와 IRP의 처리 등에 중요)

참고 DeviceIoControl

ntddk.h의 IO_STACK_LOCATION 구조체안의 Parameters 유니언내에 다음처럼 정의되어 있다.

        struct {
            ULONG OutputBufferLength;
            ULONG InputBufferLength;
            ULONG IoControlCode;
            PVOID Type3InputBuffer;
        } DeviceIoControl;

MSJDrvr.c는 MSJDispatchIoctl 함수를 구현해 IOCTL_MSJDRVR_GET_STRING이라는 IOCTL을 처리하고 있으며 이 IOCTL은 NTDDK.H에 CTLCODE 매크로를 이용해 정의되어 있다.

#define DEVICE_TYPE ULONG
#define MDL_IO_SPACE 0x0800
#define METHOD_BUFFERED 0
#define FILE_READ_ACCESS (0x0001) // file & pipe
#define FILE_WRITE_ACCESS (0x0002) // file & pipe

#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)

CTL_CODE 매크로는 IOCTL을 정의한다. 각 인자는 I/O Manager와 IRP의 처리 등에 중요한 것으로, IOCTL를 처리하는데 그 값에 구애받지 않는 윈도우 95와는 조금 다르다. CTL_CODE는 인자를 모아 DWORD값을 만들며 그 결과는 다음과 같다.

비트 31                                   16  15         14 13                                           2  1  0
     |-------a---------------|----b---|------------c-------------|-d|
a 디바이스 형식
b 액세스
c 함수코드
d 전송방식

첫째 인자는 드라이버의 디바이스 오브젝트가 생성될 때 명시된 디바이스 형태이며 둘째 인자는 드라이버에 대한 커스텀 컨트롤 코드인 short 값이다. 0x0000부터 0x07ff까지는 이미 예약된 값이므로 사용하면 안되며 사용자가 마음대로 쓸 수 있는 값은 0x0800이상이다. 셋째 인자는 IOCTL이 사용할 데이터 전송 방법이다. 보통 사용하는 값은 METHOD_BUFFERED로, DDK문서에는  '드라이버가 요청에 의해 적은 양의 데이터를 전송할 때 METHOD_BUFFERED를 사용한다고 되어 있다. 이렇게 정의된 이유는 유저 모드에서 커널 모드로의 전송과 그 반대가 모두 I/O Manager에 의해 수행되기 때문이다. 물론 METHOD_BUFFERED방식을 이용하면 데이터의 크기 제약은 없지만 많은 양의 데이터를 전송할 때 시간이 많이 걸린다. 또한 METHOD_BUFFERED 방법의 입력과 출력 버퍼는 공유되며 버퍼는 앞에서 본 것처럼 Irp->AssociatedIrp.SystemBuffer가 가리키게 된다.

8. 디바이스 드라이버의 로드

윈도우 95는 VxD에 대해 CreateFile을 사용하면 다른 준비 필요 없이 드라이버를 동적을 로드할 수 있지만, 윈도우 NT 드라이버에 대한 CreateFile 호출은 그 전에 시스템에 의해 드라이버가 로드된 상태여야 한다. 이는 보안상 시스템 관리자가 디바이스 드라이버를 사용하기전에 미리 설치해야 한다는 뜻이다. 정적으로 로드된 윈도우 NT 드라이버는 %SystemRoot%System32\Drivers 디렉토리에 위치한다. 일반적으로 드라이버 파일은 시스템이 발견하기 쉽도록 이곳에 위치해야 하며 이 위치는 레지스트리의 값을 수정해 바꿀 수 있다. 디바이스 드라이버와 서비스는 \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services에 존재한다. 여기에 드라이버의 파일 이름과 같은 이름의 키(.sys는 빼고)를 만들어 주면 된다. 이때 드라이버를 작동하려면 최소한 Type, Start, ErrorControl 등의 DWORD값이 들어갈 공간이 필요하다. Type값은 커널 드라이버에 대해 1로 설정하고 파일 시스템 드라이버에 대해서는 2로 설정한다. Strart 값은 다음에 나열된 것처럼 다섯 가지의 값중 하나가 들어가는데 드라이버를 설치하고도 제어판의 디바이스 애플릿에서 바꿀 수 있다.

Start 값

0 SERVICE_BOOT_START

부트 프로세스의 초기에 윈도우 NT가 드라이버를 시동한다. 특별한 이유가 없는 한 사용하지 않는 것이 좋다.

1 SERVICE_SYSTEM_START

윈도우 NT가 초기화할 때 드라이버를 시작한다.

2 SERVICE_AUTO_START

시스템이 초기화될 때(GUI의 로드가 끝나고 아무도 로그인 하지 않은 상태)

3 SERVICE_DEMAND_START

요구받았을 때 드라이버를 로드한다(제어판의 드라이버 애플릿에서 드라이버를 로드하거나 사용자가 NET START등을 실행한 경우, 애플리케이션이 SCM API를 통해 요구한 경우)

4 SERVICE_DISABLED

드라이버를 실행하지 않은 경우


에러코드

0 SERVICE_ERROR_IGNORE

에러가 로그 파일에 저장

1 SERVICE_ERROR_NORMAL

드라이버가 작동 불능이 되어 SCM이 팝업 윈도우를 띄워 사용자에게 알린 경우

2 SERVICE_ERROR_SERVERE

최근의 설정 상황으로 되돌아갈 수 있다면 부팅이 계속되지만 그렇지 않다면 시스템을 재부팅

3 SERVICE_ERROR_CRITICAL

시스템 시동이 실패한 경우, 그렇지 않다면 시스템 재시동


ErrorControl값은 드라이버 시작이 불가능할 때의 에러 상황에 대한 값이 들어 있는데 드라이버가 부트 프로세스에서 시작되거나 시스템 시동 시에 작동되기 시작할 때 특히 중요하다.

다른 값도 디바이스 드라이버와 함께 사용할 수 있다. 보통 SCM에서 보여주기 위한 디바이스에 대한 설명을 담은 ImagePath 등이 많이 쓰인다. 드라이버가 디폴트로 지정된 장소에 위치하지 않는다면 이 값을 사용해야 한다. 이렇듯 드라이버와 서비스는 설치와 설정의 관점에서는 비슷하게 취급한다.

여기까지가 윈도우 NT의 드라이버 아키텍처에서 특권모드의 동작을 수행하는데 필요한 방법이다.

9. 제한된 자원에의 접근

이제 유저모드 애플리케이션에 유용한 서비스를 제공하는 방법을 살펴보자. 시스템을 보다 견고히 하기 위해 윈도우 NT는 유저모드 애플리케이션에 대해서는 대부분의 하드웨어 자원에 대한 제한을 가한다. 예를 들어 시스템의 입출력 포트는 커널 모드에서만 접근이 허용된다. 이 점이 입출력 포트에 대한 액세스를 입출력 포트 추상화를 위한 VxD의 요구에 대해서만으로 제한하는 윈도우 95와의 차이점이다. 시스템의 대부분의 입출력 포트는 주변기기가 사용한다. 윈도우 NT의 유저 모드 애플리케이션에서는 접근이 제한되는 입출력 포트를 통해 접근할 수 있는 많은 시스템 자원이 있다. 예를 들어  CMOS 메모리에는 시스템에 설치된 램의 크기와 같은 시스템 설정에 관한 내용이 포함되어 있으며 이런 정보를 얻기 위해 포트를 사용하는 것은 윈도우 95와 DOS에서는 당연한 일이었다. 그러나 어셈블리의 IN이나 OUT명령, C 런타임 라이브러리의 _inp나 _outp를 사용해 포트를 사용할 수 있다고 하지만 윈도우 NT의 유저모드에서는 철저히 접근이 제한된다. 물론 커널 모드에서는 무제한 적으로 액세스할 수 있다. 일반적으로 시스템에 설치된 램의 크기는 기본 메모리와 확장 메모리 형태로 제공되며 이 값을 얻기 위해서는 70h 포트에 한 바이트를 쓰고 71h 포트에서 결과를 읽어오면 된다. 70h에 쓰는 값은 다음과 같다.

  • 15h : 기본 메모리의 하위 바이트를 얻어온다.
  • 16h : 기본 메모리의 상위 바이트를 얻어온다.
  • 17h : 확장 메모리의 하위 바이트를 얻어온다.
  • 18h : 확장 메모리의 상위 바이트를 얻어온다.

결과는 short 형태로 KB 단위로 넘어오며 한 번에 한 바이트씩 읽어 온다. 예를 들어 DOS나 윈도우 95에서 CMOS에 담긴 확장 메모리의 크기를 알기 위해서는 다음과 같이 하면 간단히 해결된다.

_outp(0x70, 0x17);
loByte = _inp(0x71);
_outp(0x70, 0x18);
hiByte = _inp(0x71);

ExtendedMomoryInK = (hiByte<<8) | lowByte;

물론 Win32 애플리케이션에서도 _inp와 _outp를 사용할 수 있지만, 윈도우 NT에서 앞의 코드를 사용하면 에러가 발생할 것이다. 윈도우 NT가 유저모드에서 포트에 대한 접근을 차단하기 때문이다. 그러나 앞의 코드를 포트에 대한 입출력이 허용되는 윈도우 NT 드라이버에 옮기면 그대로 사용할 수 있다. MSJDrvr.c에서 MSJReadMemoryStatsFromCMOS 함수에서는 IOCTL_MSJDRVR_READ_CMOS를 처리하는데 이를 이용하고 있으며 다음과 같은 방법으로 기존 메모리와 확장 메모리의 양을 CMOS에서 얻어 구조체에 담아 반환한다.

    // Get base memory bytes from CMOS
    WRITE_PORT_UCHAR((PUCHAR)0x70, 0x15);
    byLowByte = READ_PORT_UCHAR((PUCHAR)0x71);
    WRITE_PORT_UCHAR((PUCHAR)0x70, 0x16);
    byHiByte = READ_PORT_UCHAR((PUCHAR)0x71);

    pMemory->ulBaseMem = (byHiByte << 8) | byLowByte;

    // Get extended memory bytes from CMOS
    WRITE_PORT_UCHAR((PUCHAR)0x70, 0x17);
    byLowByte = READ_PORT_UCHAR((PUCHAR)0x71);
    WRITE_PORT_UCHAR((PUCHAR)0x70, 0x18);
    byHiByte = READ_PORT_UCHAR((PUCHAR)0x71);

    pMemory->ulExtMem = (byHiByte << 8) | byLowByte;

앞의 소스 코드와 비슷하며 _inp와 _outp에 대한 호출을 윈도우 NT 커널이 제공하는 WRITE_PORT_UCHAR와 READ_PORT_UCHAR로 대치했을 뿐이다. 이론적으로 이 함수는 다른 플랫폼에도 그대로 이식할 수 있지만 실제로는 권장하고 싶지 않다. IN이나 OUT부분을 C함수로 둘러쌀 수 있지만 인텔 CPU 기반의 플랫폼이 아닌 곳에서는 포트의 주소값이 완전히 달라지므로 사용하지 말기 바란다. 이와 함께 윈도우 NT 드라이버 아키텍처에서는 하나의 디바이스에 대해 하나의 드라이버를 사용한다는 전제가 있다. 물론 이는 VxD에서도 마찬가지다. 예를 들어 윈도우 95의 VPCID(Virtual Programmable Interrupt Controller Device)는 하나의 IRQ를 하나의 서비스 루틴에 제한한다. 인터럽터 체인과 같은 구조를 사용할 수 있지만 마이크로소프트는 이런 방식을 사용하지 않았다. 즉, 하나의 디바이스 드라이버가 인터럽트에 대한 소유권을 주장하면 다른 디바이스 드라이버는 그 자원에 대한 제어권을 빼올 수 없는 것이다. 일단 이렇게 하면 질서가 생기게 된다. 인터럽트 처리루틴이 체인화되어 있다면 부당한 제한이 생길 소지도 있기 때문이다. 비슷한 제한이 윈도우 NT의 하드웨어 인터럽트 처리 루틴에도 존재한다. I/O Manager는 디바이스가 각자의 드라이버를 가진다는 가정하에 IRQ를 체인 방식으로 연결한다. 대신에 다른 드라이버가 장악하고 있는 IRQ를 체인 방식으로 연결한다. 대신에 다른 드라이버가 장악하고 있는 IRQ 요청을 보고 싶은데 드라이버가 체이닝을 지원하는 인터페이스를 지원하지 않으면 불행하게도 그럴 수 없다(윈도우 NT의 병력 포트 드라이버가 그렇다).

앞서 CMOS를 읽어오는 예제는 성공적으로 수행되지만 윈도우 NT 환경에 걸맞는 다른 방법이 존재한다. CMOS는 HAL이 소유하고 있기 때문에 그 내용은 HalGetBusData의 첫째 인자에 Cmos를 넘겨줘 호출해 구할 수 있다. 이는 MSJDrvr.c의 MSJReadMemoryStatsFromCMOS에 잘 나타나 있다.

10. PhysicalDrive0의 액세스 연습

특수한 액세스를 일반 계정을 통해 행하는 방법을 구현한다. 앞서 설명한 바와 같이 하나의 하드웨어 자원을 두 개 이상의 드라이버가 동시에 액세스할 수 없다. 그렇더라도 디스크에서 한 개의 섹터를 읽어오는 작업을 위해 ATA나 SCSI 디바이스를 직접 액세스하는 기능을 다시 만드는 것은 엄청난 시간 낭비가 된다. 대신 커널 모드 드라이버내에 I/O Manager의 도움으로 새로운 IRP를 생성하는 방법을 사용한다. IRP는 유저 모드 프로세스가 생성한 경우에만 대상이 되는 디바이스 드라이버에 분배된다. 다만 I/O Manager가 드라이버를 호출해 어떤 작업을 할 때는 그것을 제한할 방법이 없음을 명심하자. 앞서 보인 제한이 디바이스 드라이버에서는 합법적인 것이 된다.

10.1 IRP 생성

MSJDrvr.c의 MSJReadDriveZeromasterBootRecord 함수에 이것을 보였다. 우선 대상 디바이스 드라이버의 DEVICE_OBJECT에 대한 포인터를 얻어낸다. 이는 IoGetDeviceObjectPointer에 드라이버의 이름을 담은 유니코드 스트링을 넘겨줘 호출하면 된다.

    // Initialize unicode string
    RtlInitUnicodeString(&uszDeviceName, L"\\DosDevices\\PhysicalDrive0");

    // Get a pointer to PhysicalDrive0
    ntStatus = IoGetDeviceObjectPointer(&uszDeviceName,
                                        FILE_READ_ATTRIBUTES,
                                        &fileObject,
                                        &pDriveDeviceObject);

 IoCreateDevice를 호출할 때 사용한 커널 모드 이름이나 유저 모드의 심볼 이름 두 가지가 모두 유효하다는 점이 재미있다.

 ///////////////////////////////////////////////////////////////////

// DriverEntry 에서...


    NTSTATUS        ntStatus;
    UNICODE_STRING  uszDriverString;
    UNICODE_STRING  uszDeviceString;
    PDEVICE_OBJECT  pDeviceObject;

       
    // Point uszDriverString at the driver name
    RtlInitUnicodeString(&uszDriverString, L"\\Device\\MSJDrvr");

    // Create and initialize device object
    ntStatus = IoCreateDevice(DriverObject,
                              0,
                              &uszDriverString, // 유저 모드의 심볼 이름
                              FILE_DEVICE_UNKNOWN,// 커널 모드 이름 (?)
                              0,
                              FALSE,
                              &pDeviceObject);
////////////////////////////////////////////////////////////////////

SCSI나 ATA 디바이스 드라이버의 이름을 직접 지칭하지 않고 심볼 이름만으로도 원하는 디바이스를 가리킬 수 있기 때문에 작업이 대단히 편리해진다. 또한 디바이스 오브젝트의 포인터가 반환될 때 아무 제한이 없는 상태이다. 예를 들어 디바이스 오브젝트에 대해 모두 알고 있다면 포인터를 통해 직접 디바이스 익스텐션을 액세스할 수도 있다. 하지만 자신이 하는 일에 대한 확신이 없다면 디바이스 익스텐션을 직접 액세스하는 일은 권할 성질의 것이 아니다.

DEVICE_OBJECT에 대한 포인터를 얻었으므로 IRP를 만들어 분배하면 된다. IRP를 만드는 일은 I/O Manager의 IoBuildSynchrononusFsdRequest나 IoBuildAsynchronousFsdRequest, IoBuildDeviceIoControlRequest를 사용한다. 앞의 두 함수는 IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_FLUSH_BUFFERS, IRP_MJ_SHUTDOWN_IOCTL에 제한되고 마지막 함수는 드라이버가 지원하는 IOCTL에 대해 모두 사용할 수 있다. 여기서는 간단히 하기 위해 IoBuildSynchronousFsdRequest를 사용한다.

        KeInitializeEvent(&event, NotificationEvent, FALSE);
           
        sectorNum.LowPart = sectorNum.HighPart = 0;

        pIrp = IoBuildSynchronousFsdRequest(IRP_MJ_READ,
                                            pDriveDeviceObject,
                                            pBuffer, // IoCallDriver 호출의 결과에 따라 작업 결과가 담긴다.
                                            512,
                                            &sectorNum,
                                            &event,
                                            &ioStatus);

// 보충/////////////////////////////////////////////////////////////////////////////////////
VOID
    KeInitializeEvent(
        IN PKEVENT  Event,
        IN EVENT_TYPE  Type,
        IN BOOLEAN  State
        );

KeInitializeEvent -> initializes an event object as a synchronization (single waiter) or notification type event and sets its initial state to Signaled or Not-Signaled.

Parameters
Event
Points to an event object, for which the caller provides the storage.

Type
Specifies the event type, either NotificationEvent or SynchronizationEvent.

State
Specifies the initial state of the event. TRUE indicates Signaled.
//////////////////////////////////////////////////////////////////////////////////////////////////

IoBuildSynchronousFsdRequest에 전달되는 인자는 유저 모드의 ReadFile 인자와 비슷하다. 커널 모드 이벤트 오브젝트는 KeInitializeEvent를 호출해 생성하며 이 이벤트의 핸들은 IRP의 처리가 보류되어 있을 때 막는 목적으로 사용된다. 이제 IRP를 생성했으므로 IoCallDriver를 호출해 I/O Manager에 분배한다.

10.2 IRP 분배

        ntStatus = IoCallDriver(pDriveDeviceObject, pIrp);
 
        if(ntStatus == STATUS_PENDING)
        {
            KeWaitForSingleObject(&event, Suspended, KernelMode, FALSE, NULL);
            ntStatus = ioStatus.Status;
        }

이 작업이 성공하면 IRP를 생성할 때 인자로 넘겨준 버퍼에 작업 결과가 담기게 된다. 이 결과는 호출자에게 그대로 반환되며 이에 따라 호출자가 드라이버를 직접호출한 것처럼 보인다. 즉 직접 작성한 드라이버가 유저 모드 애플리케이션에서는 불가능해 보이는 일을 가능하게 해주는 일종의 프록시로 동작하는 것이다.

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

IoBuildAsynchronousFsdRequest를 이용한 IRP의 비동기 처리방식을 사용하면 커널 모드 드라이버 내부에 IRP 처리가 끝났을 때 호출할 콜백함수를 지정할 수 있으며, 유저 모드 애플리케이션의 동작이 끝났을 때 신호를 받게 되어 보다 유연하게 작업할 수 있다. 이와 함께 비동기 입출력 처리 방식의 기본으로써 유저 모드 입출력에서 OVERLAPPED 구조체의 사용을 분명하게 해 준다.

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 보충자료 1.동적으로 윈도우 NT 드라이버 로드하기

윈도우 NT 드라이버는 윈도우 NT의 서비스와 같은 방식으로 설치하고 관리한다. 당연히 서비스를 관리할 때 사용하는 유틸리티로 드라이버를 시동하고 중지시킬 수 있다.그러므로 SCM이 제공하는 API를 이용해 드라이버를 설치, 시동, 정지, 제거할 수 있다는 것도 별로 놀랄 만한 일은 아니다. 이 API를 사용해 다이나믹하게 드라이버를 로드하고 언로드하는 일도 간단한데, 이는 Dynamic.c에 나타나 있다.

CreateFile을 호출해 드라이버에 대한 핸들을 얻으려면 레지스트리의 \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services에 알맞은 내용을 담고 있어야 한다. 물론 애플리케이션을 설치하면 자동으로 설정되지만 SCM API를 사용해 간단히 수동으로 추가할 수도 있다. 우선 SCM에 대한 핸들을 연다.

    SC_HANDLE   hSCManager;

    // Open Service Control Manager on the local machine...
    hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

앞의 호출은 보안상 아무 사용자나 레지스트리를 수정할 수 없어야 하므로 관리자 계정에서만 사용할 수 있다. SCM을 연 다음에는 CreateService에 드라이버의 이름과 드라이버 파일의 경로를 넘겨줘 호출하면 드라이버를 추가할 수 있다.

    // Create the driver entry in the SC Manager.
    hService = CreateService(hSCManager,            // SCManager database
                             DriverName,            // name of service
                             DriverName,            // name to display
                             SERVICE_ALL_ACCESS,    // desired access
                             SERVICE_KERNEL_DRIVER, // service type
                             SERVICE_DEMAND_START,  // start type
                             SERVICE_ERROR_NORMAL,  // error control type
                             ServiceExe,            // service's binary
                             NULL,                  // no load ordering group
                             NULL,                  // no tag identifier
                             NULL,                  // no dependencies
                             NULL,                  // LocalSystem account
                             NULL                   // no password
                             );

이제 StartService에 CreateService에서 얻은 핸들을 넘겨줘 드라이버를 시동하면 된다.

    // Start the driver!
    bReturn = StartService(hService, 0, NULL);

이런 과정을 거치면 CreateFile에 방금 로드한 드라이버의 이름을 넘겨줄 수 있다. 드라이버를 정지시키고 언로드하는 것도 간단하다. 앞에서 얻은 드라이버에 대한 서비스 핸들을 저장하기 않았다면 SCM을 통해 드라이버를 제어하는데 필요한 서비스 핸들을 OpenService를 호출해서 얻을 수 있다.

    // Open the Service Control Manager for our driver service...
    hService = OpenService(hSCManager, "MSJDrvr", SERVICE_ALL_ACCESS);

우선 드라이버를 정지시켜야 한다. 이것은 SERVICE_CONTROL_STOP을 넘겨 ControlService를 호출하면 된다.

    // Stop the driver.  Will return TRUE on success...
    bReturn = ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus);

마무리를 위해 CreateService를 호출해 만들었던 레지스트리 항목을 삭제해야 한다. 이는 DeleteService를 통해 수행할 수 있다.

    // Delete the driver from the registry...
        bReturn = DeleteService(hService);

서비스와 SCM이 동작하는 방식을 사용하면 윈도우 NT 드라이버를 설치해야 하는 번거로움을 피할 수 있어 보다 유연하게 작동하게 만들 수 있다.

보충자료 2.드라이버를 실행파일에 숨기기

.RC 파일을 열어 다음과 같이 추가한다

MSJDATNT BINRES MOVEABLE PURE "MSJDRVR.SYS"

그리고 애플리케이션의 초기화 부분에서 이 드라이버를 발견해 임시 파일로 저장하는 작업을 하면 된다.

                HRSRC       hRsrc;
                HGLOBAL     hDriverResource;
                DWORD           dwDriverSize;    
                LPVOID          lpvDriver;
                HFILE       hfTempFile;
                OFSTRUCT    of;


                // If we're under NT, load the .SYS resource...
                            if(os.dwPlatformId == VER_PLATFORM_WIN32_NT)
                                    hRsrc = FindResource(ghInst,MAKEINTRESOURCE(MSJDATNT),"BINRES");
                            // Otherwise we're under 95, load the .VXD resource...
                            else
                                    hRsrc = FindResource(ghInst,MAKEINTRESOURCE(MSJDAT95),"BINRES");

                            hDriverResource = LoadResource(ghInst, hRsrc);
                            dwDriverSize = SizeofResource(ghInst, hRsrc);
                            lpvDriver = LockResource(hDriverResource);

                            // Dump the resource out to a file
                if(os.dwPlatformId == VER_PLATFORM_WIN32_NT)
                                hfTempFile = _lcreat("msjdrvr.sys",0);
                else
                                hfTempFile = _lcreat("msjdrvr.vxd",0);

                            _hwrite(hfTempFile, lpvDriver, dwDriverSize);
                            _lclose(hfTempFile);

이제 드라이버를 동적으로 로드하면 된다. 로드한 다음에는 임시 파일을 지워도 상관없다. 물론 이렇게 동적으로 드라이버를 로드하는 등의 작업은 관리자 레벨의 권한을 갖고 있어야 가능하다.

 보충자료3. 제품출시전 시스템 파일에 심볼정보가 포함되지 않도록 한다.

드라이버를 빌드하기 전에 다음과 같은 문장을 실행하도록 한다.

set NTDEBUG = retail

댓글 없음:

댓글 쓰기