Programming/Kernel / Driver2008. 11. 30. 16:35


WDM 기반 USB 지원 드라이버 제작
Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:31

우선 DOS사용자 분들은 왜 DDK가 필요하신지 궁금할겁니다.
DDK(Device Development Kit)을 제가 처음 접하게 된것은 1996년 겨울입니다.
이때는 자료도 없어서 고생을 많이 했었죠...
지금이야 자료가 널려서 제가 설자리가 없다는게 문제지만요.
하여튼 DDK는 MS에서 내어놓은 윈도우에서 하드웨어를 관리해주기위한
하나의 드라이버를 만들기 위한 Software package입니다.

그럼 왜 이 DDK가 필요한지 부터 알아야겠죠....
DOS시절과는 달리 윈도우에서는 하드웨어를 사용자가 직접관리하지 않고
OS가 해줍니다. 즉 이를 Kernel이라고 하죠...리눅스와 마찬가지로요...
그러나 이 Kernel이라는 것이 사용자가 쉽게 손을 델수 없게 MS에서는
만들어 놓았기때문에 Kernel과 통신을 할수 있는 부분이 필요하게 되었습니다. 즉 개발자가 드라이

버라는것을 만들어 OS에 등록하면 이를 인식해서
필요한 처리를 해주는 것이죠...즉 메모리 관리아 IO에 관련하여 여러가지
처리들을 해주게 됩니다.

그러나 사용자들의 문제는 여기에서만 그치지 않습니다.
분명 문법은 C또는 C++인데 이게 그처럼 쉽지 많은 않다는 것이죠...

I/O를 위해서는 사용자가 보지 못하는 여러계층이 있습니다.
Hardware이외에 HAL이라는 하드웨어 Access계층과 Interrupt 서비스 그리고 Procedure Call등을 할

수 있는 계층이죠. 이를 통해 I/O를 관리하고
명령을 내릴때는 DriverEntry를 통하고 Dispatch Routine, Unload등을 통해 동작을 하게 됩니다.

즉 I/O Manager는 DriverEntry를 통해 초기화 하고 dispatch Routine를 통해 명령을 전달하고,

Interrupt Service를 통해 OS에 인터럽트가 걸리는걸 처리하고 Defered Procedure Call통해 I/O

Manager에게 하드웨어의 메세지를 전달하게 됩니다.
그리고 마지막으로 Unload를 통해 드라이버를 Unloading를 하게 되지요...
이러한 과정은 일반 Win32프로그램 제작하는 프로그래머들에게는 알지 못하는 생소한 과정입니다

.(Developing Windows NT Device Driver 의 Chapter1참조)

그러나 이 DDK가 C의 상식을 벗어 나는 것은 아니기때문에
꼮 C/C++의 기초를 다지고 있어야 합니다.

다음은 I/O의 User-Level Overview를 하도록 하죠...
가장 중요한 부분입니다.


안녕하세요....
글을 올리기에 좀 오래 걸리는군요...

이번시간엔 User-Level API를 공부해 보도록 하죠...

DDK의 API는 C/C++처럼 그리 복잡하지 않습니다.
그렇다고 단순한것도 아니죠...

이유는 이렇습니다.
처음 C를 접하던 시절에 printf, #include와 같은 문법이 익숙하지 않고
아에 이걸 왜 쓰는지조자 모르는 상황에서 배울때와의 심정이 같습니다.

그런데 이 부분이 DDK를 하면서 또한 번 드끼게 되죠....
바로 그것은 CreateFile, ReadFile, WriteFile, DeviceIOControl
CloseHandle들의 함수 때문이죠...

우선 왜 이러한 함수들이 사용되는지 부터 알아야 겠죠....

대부분의 중요 함수에 File이라는 약자가 붙습니다.
이유는 간단합니다. 하드웨어와 통신을 하기 위한 하나의 맵핑파일을
만드는 것입니다.우리가 서로 다른 프로세서 들끼리 정보를 주고 받기위해서
공유된 맵핑 파일을 만드는것과 동일한 원리죠...
좀 틀리다면 이는 드라이버 구동을 위한 공유된 머신이름이라던지
또는 공유 메모리 들을 지정하게 됩니다.

그리고 장비 자체가 register에 들어 가기때문에
device name같은 부분들이 들어가게 됩니다.

그럼 하나씩 들어가볼가요...

우선 CreateFile은 하드웨어 정의에 의한 path를 이용해서 맵핑파일을 샌성하고 이를 통해 핸들을

생성하게 됩니다. 즉 자하철 타고 문들 통과하기 위해
티켓을 하나 생성한다고 생각하시면 됩니다. 따라서 하드웨어와 I/O를 하기 위한 매니저를 하나 생

성하는 것이죠...

ReadFile와 WriteFile의 경우는 생성된 티켓을 이용해서 이곳에 어떠한 데이터를 중것인가, 또는

받을것인가를 지정하는 부분입니다. 그러나 이부분이 I/O에 어떠한 행동을 직접주는 것은 아니기때

문에 데이터를 넣고 빼기 위한 하나의 호출에 불과 합니다.

세번째는 DeviceIOControl입니다. 이 함수가 실제 일을 진행한다고 할수 있는데요.. 이 부분이 되

면 이는 위에서 언급된 함수들을 통해 하드웨어의 버퍼를 통해 데이터를 집어 넣거나 가져오게 되

는 것이죠...

마지막으로 CloseHandle는 원하는 작업이 끝나면 핸들을 넘겨줌으로써 작업을 마치게 됩니다.

지금까지 말했지만 잘 이해가 안가시죠...
말은 잘황하게 했지만 실질적으로 어떻게 이 부분들이 그냥 선언해 놓은 것만으로 동작을 하는지

모든 분들이 궁금해 할겁니다.

그러나 저는 이렇게 말하고 싶습니다. 이전 강좌에서도 말했듯이
드라이버를 사용자가 컨트롤 하는게 아닙니다 즉 커널이 알아서
컨드롤을 해주는 것이죠...따라서 OS와 하드웨가 어떻게 통신을 할것인지
하나의 인터페이스 규약만을 만들어 주는것에 불과 합니다.

그리고 OS에서는 이 함수들을 불러다가 필요한 부분에 호출하는 것이구요...
따라서 드라이버 하나만 가지고 어플리케이션까지 만들수 있는게 아니라
따로 드라이버를 제어하는 응용 프로그램을 제작하셔야 합니다.

즉 이부분은 이 APP를 만들때 사용하는 부분입니다.
시작하기 전에 printf문을 예로 들었습니다.
나만의 하드웨어는 자신만의 인터페이스를 가지고 있기때문에
이를 위해서는 printf라는 '문자열을 화면상에 뿌려라!'
하나의 함수 하나를 만들어 주기 위한 하나의 인터페이스 함수인것이죠...

즉 하드웨어와 통신하기 위해 초기화 하는 함수를 만든다면
int CHardware::OpenController()
{
   RhndFile = CreateFile(
                                        ".SHINGIRUDev",               // Open the

Device "file"
                                        GENERIC_READ,
                                        FILE_SHARE_READ,
                                        NULL,
                                        OPEN_EXISTING,
                                        0,
                                        NULL);
               
    if(RhndFile == INVALID_HANDLE_VALUE)       // Was the read device opened?
    {
        printf( "Unable to open the read device.n");
        return error;
    }

    WhndFile = CreateFile(
                    ".SHINGIRUDev",           // Open the Device "file"
                    GENERIC_WRITE,
                    FILE_SHARE_WRITE,
                    NULL,
                    OPEN_EXISTING,
                    0,
                    NULL);

    if(WhndFile == INVALID_HANDLE_VALUE)       // Was the write device opened?
    {
        printf( "Unable to open the write device.n");
                return error;
    }

        return 0;
}
이런식으로 SHINGIRUDev라는 이름으로 레지스터에 등록된 이름으로 파일을 하나 만드는 경우 입니

다.

그리고 여기에 Read, Write부분을

int CHardware::PortWrite(unsigned long port, unsigned long data)
{
    InputBuffer.PortNumber = port;                                // Get the port number
    DataValue = data;                                                        // Get the data

to be written.

    IoctlCode = IOCTL_SHINGIRU_WRITE_PORT_UCHAR;
    InputBuffer.CharData = (UCHAR)DataValue;
    DataLength = offsetof(SHINGIRU_WRITE_INPUT, CharData)+sizeof(InputBuffer.CharData);

    IoctlResult = DeviceIoControl(
                        WhndFile,                // Handle to device
                        IoctlCode,              // IO Control code for Write
                        &InputBuffer,           // Buffer to driver.  Holds port & data.
                        DataLength,             // Length of buffer in bytes.
                        NULL,                   // Buffer from driver.   Not used.
                        0,                      // Length of buffer in bytes.
                        &ReturnedLength,        // Bytes placed in outbuf.  Should be 0.
                        NULL                    // NULL means wait till I/O completes.
                        );


    if(IoctlResult)          // Did the IOCTL succeed?
    {       
                return 0;
    }else {
                MessageBox( NULL, "Ioctl failed, unknown error.", "ERROR!", MB_ICONERROR |

MB_OK );
                return error;
    }

}


또는

int CHardware::PortRead(unsigned long port)
{
    IoctlCode = IOCTL_SHINGIRU_READ_PORT_UCHAR;
    DataLength = sizeof(DataBuffer.CharData);
           
        PortNumber = port;                                                        // Get the

port number to be read

    IoctlResult = DeviceIoControl(
                            RhndFile,            // Handle to device
                            IoctlCode,          // IO Control code for Read
                            &PortNumber,                // Buffer to driver.
                            sizeof(PortNumber),        // Length of buffer in bytes.
                            &DataBuffer,        // Buffer from driver.
                            DataLength,         // Length of buffer in bytes.
                            &ReturnedLength,    // Bytes placed in DataBuffer.
                            NULL                // NULL means wait till op. completes.
                            );

        if(IoctlResult)
        {   
                unsigned long   Data;

        if(ReturnedLength != DataLength)
        {       
                        MessageBox( NULL, "ReturnedLength != DataLength.", "ERROR!",

MB_ICONERROR | MB_OK );
                        return error;
        }else {
                        Data = DataBuffer.CharData;

                        return Data;
                }
        }else {
                  MessageBox( NULL, "Ioctl failed, unknown error.", "ERROR!", MB_ICONERROR |

MB_OK );
               
                return error;
    }
}
이런식으로 I/O를 관리하는 함수를 만드는 것이죠....

보시면 아시겠지만 ReadFile, WriteFile은 없죠?

왜냐면 둘다 처리 하기 위해 Read, Write부분은 모두 CreateFile로 주어 속성만을 바꾸어 주었기

때문에 함수에서는 단순히 핸들만을 가지고 조정하게 됩니다. 그리고
삭제는

int CHardware::CloseController(void)
{
        if (!CloseHandle(RhndFile))                  // Close the Device "file" for read
        {
                MessageBox( NULL, "Failed to close the read device.", "ERROR!", MB_ICONERROR

| MB_OK );
                return error;
        }
        if (!CloseHandle(WhndFile))                  // Close the Device "file" for write
        {
                MessageBox( NULL, "Failed to close the write device.", "ERROR!",

MB_ICONERROR | MB_OK );
                return error;
        }
        return 0;
}
이런식으로 해주어야 합니다.

이 이외에도 보시면 여러 파라미터 들이 있죠.,...
이부분들은 여러분이 만들어낸 하드웨어의 주소라든지
데이터 구조들이기때문에 따러 설명드리지는 않겠습니다.

참고로 제가 예전에 만들었던 부분은

HKEY hkReg;                         // For Reg key

// The following is returned by IOCTL.  It is true if the write succeeds.
BOOL IoctlResult;

// The following parameters are used in the IOCTL call.
HANDLE RhndFile;                //Read Handle to device, obtain from CreateFile
HANDLE WhndFile;                //Write Handle to device, obtain from CreateFile

SHINGIRU_WRITE_INPUT        InputBuffer;        //Input buffer for DeviceIoControl
LONG IoctlCode;
ULONG DataValue;
ULONG DataLength;
ULONG ReturnedLength;        // Number of bytes returned in output buffer
//read
ULONG PortNumber;                // Buffer sent to the driver (Port #)

union{
        ULONG  LongData;
        USHORT ShortData;
        UCHAR  CharData;
}DataBuffer;                                // Buffer received from driver (Data)
       
/*************************************************************************
|    Definitions for ENcoder                                             |
*************************************************************************/
/* Addresses */
const int DATA[6]          = { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A }; /* data registers of

LS7166         */
const int CONTROL[6] = { 0x01, 0x03, 0x05, 0x07, 0x09, 0X0B }; /* control registers of

LS7166        */
#define LATCH                        8                /* causes LS7166s to latch counter    

    */

/* Commands */
#define MASTER_RESET        0X20        /* master reset command                         */
#define INPUT_SETUP                0X68        /* command to setup counter input         */
#define ADDR_RESET                0X01        /* command to reset address pointer */
#define LATCH_CNTR                0X02        /* command to latch counter                 */
#define CNTR_RESET                0X04        /* command to reset counter                 */
#define PRESET_CTR            0X08        /* transfer preset to counter                 */
const int QUAD_X[5] = {0XC1, 0XC1, 0XC2, 0XC1, 0XC3}; /* quad multiplier */
const int error                = 0XFF;

/*************************************************************************
|    Definitions for DAC                                                 |
*************************************************************************/
const int da_base_addr =        0x0C;
const int ad_base_addr =        0x10;
const int timer_base_addr = 0x14;
const int io_base_addr =        0x18;
const int CH[4] = { 0xFC, 0xFD, 0xFE, 0xFF };

#define DA_LSB        da_base_addr+0
#define DA_MSB        da_base_addr+1
#define DA_CONT        da_base_addr+2
#define DA_CW        da_base_addr+3

#define DA_CW_DATA                        0x80
#define DA_RANK1                        0xBF
#define DA_RANK2                        0xFF

#define DA_CS1                                0xEF
//#define DA_CS2                                0xDF

#define DA_RESET1                        0xBF
//#define DA_RESET2                        0xF7
#define DA_DISABLE        0x3C
이런식으로 매크로 또는 변수가 지정되어 있습니다.

그럼 다음 시간엔 Driver의 데이터 구조들에 대해 알아보겠습니다.
즉 실제 드라이버를 만들기 위한 드라이버 함수들이죠...이부분이 진짜 DDK부분이 되겠죠...


Driver Data Structures입니다.
이는 DDK에서 만들어진 sys파일이 어떻게 하드웨어와 소프트웨어와
데이터를 주고 받는지에 대한 설명입니다.
즉 다음시간에 다룰 DriverEntry에 대한 서론이라고 할수 있죠..

우선 어떻게 Driver가 시작되는지 알아보죠...
우리는 하드웨어를 구입하면 가장 먼저 하는것이
하드웨어를 OS에 등록하는 것입니다.
일반적으로 Driver를 설치한다고 하죠...
이는 Registery와 sys파일 그리고 기타 app들이 있는 몇가지 부분을
설정또는 복사하는 겁니다.

가장 중요한 부분은 Registry는
HKEY_LOCAL_MACHINECurrentControlSetservicesdeivername
형식으로 들어가게 됩니다.

이에 대해서는 추후에 다시 설명 드릴 기회가 있으니 다음에 설명하도록 하죠..

그러면 다음과 같은 구조를 갖게 됩니다.

Service Control Manager  ----- Registry ------- MyDriver.sys

여기서 Service Control Manager란 App라고 보셔도 됩니다.
원래는 이것의 기능은 OS에 드라이버의 메모리 어드레스를 Loading해주는
역활을 하는데요. 이 부분이 사실 App에서 호출을 하기 때문에 그렇습니다.
그리고 Registry를 접속해서 등록된 Driver를 호출하는 거죠...

즉 Registry를 참조하여 어드레스를 얻어서 Service Control Manager가
드라이버 sys파일을 호출하게 됩니다. 이때 호출되는 것이 DriverEntry입니다.
그리고 이렇게 과정을 거치면 Driver Object를 생성하게 되는 데요.
이를 통해 Interface를 하게 되는거죠..

즉 이전 시간에 했던

CreateFile, ReadFile, WriteFile, CloseHandle를 통해
DriverEntry에 Open, Read, Write, Close, Unload등 필요한 명령들을 Loading된 메모리를 통해서

전달하는 것이죠...


그럼 이제 DriverEntry를 들어가기 전에 필요한 몇가지 부분을
살펴 보겠습니다.

DDK는 DRIVER_OBJECT라는 구조체를 가지고 있습니다.
이를 이용해서 DriverEntry에서 MajorFunction을 을 통해
CREATE, CLOSE, READ, WRITE POWER, PNP등 여러가지 기능들을 할수 있는것이죠...

가진 기본적으로

  DriverObject->MajorFunction[IRP_MJ_CREATE]          = SHINGIRUDispatch;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]           = SHINGIRUDispatch;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = SHINGIRUDispatch;
    DriverObject->DriverUnload                          = SHINGIRUUnload;
    DriverObject->MajorFunction[IRP_MJ_PNP]            = SHINGIRUDispatchPnp;
    DriverObject->MajorFunction[IRP_MJ_POWER]          = SHINGIRUDispatchPower;
    DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = SHINGIRUDispatchSystemControl;
    DriverObject->DriverExtension->AddDevice           = SHINGIRUAddDevice;

위의 몇가지를 들수 있습니다.
여기서 DriverObject는 DriverEntry에서 PDRIVER_OBJECT로 포인터 형을
받기 때문이녀. IRP_MJ_*로 시작을 하는
몇가지 옵션이 있습니다.
이는 보시면 아시겠지만 MajorFunction을 통해 어떠한 함수들이 호출 된느지를 알려주는 것입니다.

    DriverObject->MajorFunction[IRP_MJ_PNP]            = SHINGIRUDispatchPnp;

위문장을 예로들면 IRP_MJ_PNP를 싱행하기 위해서는 SHINGIRUDispatchPnp함수를 호출하는 것이죠.

3-1에서도 말했다 시피 레지스터를 통해 DriverObject가 만들어 질때 DriverEntry를 통해 장비와

인터페이스를 하게 되는게 DriverEntry에
위와 같은 DriverObject를 만들고 인터페이스를 하는것이죠...

뭐 앞으로 더욱 자세한 내용을 할거기때문에 이는 이쯤에서 설명드리구요..

따라서 이때 각 함수에 IoCreateDevice를 하게 되구요...
이때 DEVICE_OBJECT, 또는 PDEVICE_OBJECT를 통해 IRP나 flag그리고
DeviceExtension, DEVICE_TYPE등을 설정하게 됩니다.
이후에 필요한 작업들을 지시 하는겁니다.

따라서 방금 말한 부분들은 대부분 AddDevice부분에 들어가게 되는것이죠...


NTSTATUS
SHINGIRUAddDevice(
    IN PDRIVER_OBJECT DriverObject,
    IN PDEVICE_OBJECT PhysicalDeviceObject
    )
{
    NTSTATUS                status = STATUS_SUCCESS;
    PDEVICE_OBJECT          deviceObject = NULL;
    PLOCAL_DEVICE_INFO      deviceInfo;
    UNICODE_STRING          ntDeviceName;
    UNICODE_STRING          win32DeviceName;

    PAGED_CODE();

    RtlInitUnicodeString(&ntDeviceName, SHINGIRU_DEVICE_NAME);

    //
    // Create a device object.
    //

    status = IoCreateDevice (DriverObject,
                             sizeof (LOCAL_DEVICE_INFO),
                             &ntDeviceName,
                             SHINGIRU_TYPE,
                             0,
                             FALSE,
                             &deviceObject);

   
    if (!NT_SUCCESS (status)) {

        return status;
    }

..........
}

이외에도 Adapter Objects, Controller Object, Interrupt Object, Timer Objects, DPC Objects들

이 있습니다. 즉 DriverEntry를 통해 생성된 Device Object들을 통해 로드된 메모리를 기점으로 인

터페이스가 이루어 지는거죠...

Posted by skensita
Programming/Kernel / Driver2008. 10. 10. 11:17

Visual Studio 6.0 + vmware + windbg 개발환경 설정 정리 문서
Posted by skensita