Programming/Kernel / Driver2008. 11. 30. 16:30
컴퓨터를 사서 처음 하는 것은 운영체제와 장치 드라이버 설치일 것이다. 드라이버를 제대로 설치하지 않으면 장치들은 제대로 작동하지 않고 컴퓨터도 오작동하게 된다. 그래서 드라이버는 잘 만들어야 하는 프로그램이다. 하지만 이 분야를 처음 공부하려는 사람들은 아마도 어렵게 느껴질 것이다. 충분한 자료가 부족하고 단기간에 배울 수 없는 분야이기 때문이다. 이번 연재를 통해 장치 드라이버를 공부하려는 독자들에게 조금이나마 도움이 되고자 한다.
이상섭·곽태진 (devguru, 마이크로소프트웨어)
2002/09/27


장치 드라이버란
장치 드라이버는 ‘장치를 구동하는 프로그램’으로 줄여서 드라이버라고도 부른다. 드라이버는 하드웨어와 소프트웨어(운영체제) 중간에 위치하며, 프로그램 중에서 가장 저수준 레벨에서 처리하는 프로그램으로 볼 수 있다. 일반적으로 운영체제에서 많은 부분이 드라이버로 되어 있다고 봐도 될 것이다.

드라이버는 기본적으로 하드웨어를 제어하고 운영하는 프로그램으로 볼 수 있다. 하드웨어 자원인 메모리, I/O, 인터럽트, DMA를 처리해 사용자가 장치를 사용할 수 있게 한다. 그리고 일반적인 애플리케이션에서는 할 수 없는 일이나 운영체제의 기능을 확장할 때도 드라이버는 필요하다. 예를 들면 바이러스 백신, 보안을 위한 파일 암복호화나 네트워크 패킷 필터링을 하기 위해 드라이버를 이용하기도 한다.

드라이버는 운영체제와 밀접하게 연관되어 작동하기 때문에 운영체제에 따라 다르게 만들어진다. 즉, 윈도우·리눅스·Mac OS마다 드라이버를 새로 만들어야 한다. 윈도우에서도 윈도우 9x 계열(95·98·ME)과 윈도우 NT 계열(NT·2000·XP)은 드라이버를 다르게 만들어야 한다. 따라서 장치를 만드는 하드웨어 업체에서는 실제 소비자를 위해 모든 운영체제에 드라이버를 지원해야 하다보니 개발기간이 길어질 수밖에 없어진다.

윈도우 2000 드라이버의 종류
우리는 이번 연재에서 윈도우 2000용 드라이버를 만든다. 먼저 윈도우 2000에서는 어떤 종류의 드라이버가 있는지 알아보자.

사용자 삽입 이미지


<그림 1>은 윈도우 2000의 드라이버 종류를 나타낸 것이다. 각각의 드라이버가 어떤 것인지 간단히 설명해 보겠다.

◆ 가상 장치 드라이버 : x86 플랫폼에서 하드웨어를 액세스하는 DOS 기반 응용 프로그램을 돌아가게 하는 사용자 모드 요소다. 가상 장치 드라이버(VDD, Virtual Device Driver)는 윈도우 98의 VxD와는 다른 것이니 혼돈하지 말기 바란다.

◆ 커널 모드 드라이버 : 일반적인 장치 드라이버를 총칭한다고 보면 된다.

◆ WDM 드라이버 : 전력 관리와 PnP를 처리하는 커널 모드 드라이버다. 윈도우 98과 2000에서 소스 호환되어 드라이버 개발을 쉽게 할 수 있다. WDM 드라이버는 클래스 드라이버와 미니 드라이버로 나뉜다. 클래스 드라이버는 주로 장치를 클래스라는 종류로 나눠 처리하고, 미니 드라이버는 클래스 드라이버에 하드웨어 장치들마다 다르게 처리할 수 있는 도움을 제공한다.

◆ 비디오 드라이버 : 디스플레이와 프린터를 위한 드라이버다.

◆ 파일 시스템 드라이버 : 일반적인 로컬 하드 디스크나 네트워크로 연결된 파일 시스템을 처리한다.

◆ 레거시 드라이버 : 다른 드라이버의 도움 없이 하드웨어 장치를 직접 제어하는 커널 모드 드라이버다. 이 드라이버는 윈도우 2000에서도 작동하는 예전의 윈도우 NT 드라이버를 포함한다.

이번 연재에서는 윈도우 2000용 장치 드라이버에 중에서 WDM 드라이버에 대한 내용보다는 윈도우 NT 계열의 레거시 드라이버에 대한 내용을 다룰 것이다. WDM 드라이버도 레거시 드라이버 기반 위에 PnP와 전력 관리 부분이 추가된 것이기 때문에 기본적인 내용은 레거시 드라이버만 알아도 이해하는 데 충분하다.

윈도우 2000의 내부구조
장치 드라이버는 운영체제와 밀접하게 연관되어 있다고 말했다. 따라서 운영체제 내부를 알고 있어야 드라이버를 개발하는 데 필요한 내용을 이해하기 더 쉽다. 지금부터 윈도우 2000의 전체적인 구조를 살펴보자. 윈도우 2000의 내부구조를 보면, <그림 2>와 같다.


사용자 삽입 이미지


윈도우의 내부는 크게 커널 모드와 사용자 모드로 나눌 수 있다.

◆ 커널 모드 : 프로세서의 특권 레벨(privileged level)로 프로세서의 모든 명령을 처리할 수 있고, 시스템 자원이나 하드웨어들을 직접 액세스할 수 있다. 장치 드라이버나 운영체제의 코드들이 작동하는 모드이기도 하다.

◆ 사용자 모드 : 일반 응용 프로그램이 동작하는 모드로 커널 모드와는 달리 비특권 레벨(nonprivileged level)이기 때문에 하드웨어나 시스템 자원을 직접 이용할 수 없고 시스템 서비스(API)를 이용해 접근해야 한다. 그렇지 않으면 예외(exception)가 발생해 프로그램이 종료한다.

커널 모드는 다시 Executive, 커널, HAL(Hardware Abstraction Layer) 부분으로 나눌 수 있다.

◆ Executive : 윈도우의 기본이 되는 서비스들을 제공한다. 메모리·프로세스·쓰레드를 관리한다. 특히, Executive를 구성하는 것 중에서 I/O 관리자는 장치 드라이버와 관련해서 많은 부분을 처리한다. 그리고 나머지 구성요소도 드라이버에서 사용하는 서비스를 많이 가지고 있다.

◆ 커널 : 주로 저수준 운영체제 기능을 제공한다. 쓰레드 스케쥴링, 인터럽트와 예외 처리, 멀티 프로세서 동기화 등을 맡는다.

◆ HAL : 하드웨어 추상화 계층으로 플랫폼이 어떤 것이든지 상관없이 운영체계가 작동할 수 있게 한다. 이 계층이 있기 때문에 윈도우 NT 계열 운영체제가 멀티 플랫폼을 지원할 수 있게 된다.


장치 드라이버 개발환경
기본적으로 다음의 개발도구들로 개발한다.

◆ 비주얼 C++ 6.0 : 비주얼 C++의 개발 환경을 이용하기 위해 설치하는 것이 아니라 C 컴파일러나 링커를 이용하기 위해 설치한다.

◆ DDK(Driver Development Kit) : 장치 드라이버 개발에 필수적인 툴로 드라이버 개발에 관련된 라이브러리, 헤더 파일, 예제, 문서(도움말도 포함), 개발에 필요한 프로그램 등이 포함되어 있다. DDK는 OS 버전마다 나오기 때문에 개발하려는 OS에 맞는 DDK를 설치해 개발한다. 우리는 이번 연재에서 윈도우 2000용 드라이버를 만들기 때문에 윈도우 2000 DDK를 설치한다. http://www.microsoft.com/ddk에서 내려받을 수 있다(<화면 1>).


사용자 삽입 이미지

◆ 디버거(WinDbg, Soft-ice) : WinDbg는 DDK에 포함되어 있어서 무료로 사용할 수 있다(http://www.microsoft.com/ddk/debugging/). Soft-ice는 컴퓨웨어 누메가(Compuware Numega)에서 나오는 드라이버스튜디오(DriverStudio)에 포함되어 있다.

◆ 에디터 : 소스를 편집할 에디터가 필요하다.

이제 개발환경이 갖춰졌으면, 실제 드라이버 개발에 필요한 내용으로 들어가자.

 
IRQL이란

IRQL(Interrupt Request Levels)은 윈도우 NT부터 나온 개념으로 단일 CPU에서 동기화를 위한 방법으로 사용하고 있다. IRQL은 0~31까지의 값으로 할당한다. 값이 클수록 높은 레벨을 나타내며 현재 실행되고 있는 플랫폼에 따라서 값들이 정해진다. IRQL은 현재 CPU가 실행되고 있는 레벨이 PASSIVE_LEVEL이라면, 그보다 더 높은 레벨의 IRQL만이 인터럽트될 수 있는 규칙을 가진다. 하드웨어 IRQL은 DIRQL(Device IRQL)로 소프트웨어는 로우 레벨 IRQL(PASSIVE_LEVEL=0, APC_LEVEL=1, DISPATCH_LEVEL=2)로 구분할 수 있다.

사용자 삽입 이미지
소프트웨어 IRQL 레벨이 드라이버와 관련이 있기 때문에 그 부분들에 대해 간단히 알아보면 다음과 같다.

◆ DPC 레벨 : 쓰레드 스케쥴링과 DPC(Deferred Procedure Call) 실행 레벨
◆ APC 레벨 : APC(Asynchronous Procedure Call) 레벨
◆ PASSIVE 레벨 : 일반적인 쓰레드의 실행 레벨
 


장치 드라이버 기초 프로그래밍
커널 모드 드라이버는 응용 프로그램과는 많은 부분에서 다르게 작동한다. 간단하게 살펴보면 드라이버는 운영체제(I/O 관리자)에 의해 호출 받는 루틴의 집합이라고 볼 수 있다. 드라이버의 루틴은 I/O 관리자에 의해 호출 받을 때까지 기다린다. I/O 관리자는 주로 다음과 같은 상황에서 드라이버의 루틴을 호출한다.

◆ 드라이버가 로드될 때(DriverEntry 루틴)
◆ 드라이버가 언로드될 때와 시스템이 셧다운될 때
◆ 응용 프로그램이 I/O 시스템 서비스를 호출했을 때
◆ 공유하는 하드웨어 자원이 드라이버에서 사용될 때
◆ 실제 장치가 동작하는 동안에 다양한 곳에서

앞의 상황 중에서 드라이버가 기본적으로 관심을 가져야 할 상황에 처리해야 할 루틴들을 설명하겠다.

DriverEntry 루틴
모든 프로그램은 제일 처음 실행되는 부분이 있어야 한다. C로 프로그램을 짜면 처음 호출을 받아 실행되는 부분이 main 함수다. 드라이버에서도 main 함수처럼 가장 처음 호출을 받는 부분이 DriverEntry가 된다. DriverEntry는 I/O 관리자에 의해 드라이버 로딩시에 호출된다. DriverEntry 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 알아보자.

① I/O 요청을 처리할 장치 객체 생성
② 처리할 IRP(I/O Request Packet)의 MajorFunction에 해당하는 디스패치 루틴들을 등록
③ 응용 프로그램에서 읽기와 쓰기시에 메모리 전략 선택
④ Win32 응용 프로그램이 드라이버로 접근하기 위해 Win32 서브시스템에 심볼릭 링크 생성

<리스트 1> DriverEntry 루틴
NTSTATUS DriverEntry
( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
......

   status = IoCreateDevice ( DriverObject, sizeof(MYDE), → ①
     &uniNtNameString, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
......
......
   if ( NT_SUCCESS(status) )
   {
     DriverObject->MajorFunction[IRP_MJ_CREATE] = SAMPLECreate; → ②

     // MajorFunction에 디스패치 루틴 등록
     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=SAMPLEDeviceControl;

     deviceObject->Flags |= DO_BUFFERED_IO; → ③
......
     status = IoCreateSymbolicLink( &uniWin32NameString, → ④
      &uniNtNameString );
   }

   // 인터럽트 사용시 DPC 초기화 및 사용 객체 초기화 및 메모리 사용을 위함
   // 메모리 할당

   return status;
}

언로드 루틴
일반적으로 드라이버는 로드된 후 시스템이 리부트하기 전까지는 남아 있다. 그러나 드라이버를 사용하다가 언로드해야 할 경우엔 언로드 루틴이 필요하며 DriverEntry에서 언로드 루틴을 등록해야 한다. 언로드 루틴은 I/O 관리자가 드라이버를 메모리에서 제거하기 전에 호출된다. 언로드 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 살펴보자.

◆ 심볼릭 링크를 제거한다.
◆ DeviceObject를 제거한다.

VOID Sample1UnLoad( IN PDRIVER_OBJECT DriverObject )

{
......
// 장치 객체와의 심볼릭 링크를 해제한다.
IoDeleteSymbolicLink( &uniWin32NameString );
IoDeleteDevice( DriverObject->DeviceObject );
}

디스패치 루틴
윈도우 2000에서 I/O는 패킷 드리븐 방식으로 이루어진다. I/O 요청이 있을 때 먼저 I/O 관리자가 그 요청에 해당하는 내용을 가지고 IRP를 만든다. I/O 관리자는 응용 프로그램의 요청(읽기, 쓰기 등)을 받았을 때, 그 요청에 맞는 함수 코드로 바꾼다. 그리고 처리할 요청에 대한 드라이버를 선택하고 적절한 드라이버 내의 디스패치 루틴을 호출한다.

디스패치 루틴은 요청한 내용을 보고 알맞은 처리를 한 후 결과를 I/O 관리자에 반환한다. <표 1>은 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드들을 보여준다.

  Win32 API 함수 코드  
  CreateFile IRP_MJ_CREATE  
  CloseHandle IRP_MJ_CLOSE
IRP_MJ_CLEANUP
 
  ReadFile IRP_MJ_READ  
  WriteFile IRP_MJ_WRITE  
  DeviceIoControl IRP_MJ_DEVICE_CONTROL  


<표 1> 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드

디스패치 루틴에서 기본적으로 처리해야 할 몇 가지 내용은 다음과 같다.

① 드라이버와 관련 있는 IRP 스택 위치에 포인터를 얻기 위해 IoGetCurrentIrpStackLocation 함수를 호출한다.

② I/O 요청에 대한 매개변수들을 가져온다.

③ IoStatus에 반환할 값들을 IRP에 채운다. Status에는 에러 코드를 채우고, Information에는 적절한 값을 채운다. 일반적으로 읽기시에는 응용 프로그램으로 복사할 데이터 크기를 알려준다.

④ I/O 요청에 대한 모든 처리를 끝내고, IRP를 더 이상 사용하지 않기 위해 IoCompleteRequest를 호출한다.

NTSTATUS Sample1Read( IN PDEVICE_OBJECT pDeviceObject, IN PIRP Irp )
{
......
   // 현재 StackLoacation을 구한다.
   pCurrentStack = IoGetCurrentIrpStackLocation( Irp ); → ①

   // 읽을 바이트 수
   usize = pCurrentStack->Parameters.Read.Length; → ②
......
   Irp->IoStatus.Status = STATUS_SUCCESS; → ③
   Irp->IoStatus.Information = usize;

   IoCompleteRequest( Irp, IO_NO_INCREMENT ); → ④
   return STATUS_SUCCESS;
}

앞서 설명한 루틴들은 일반적인 드라이버에서 기본적으로 쓰이는 것들이다. 이외에도 다른 루틴들도 있지만 나머지 루틴들은 다음 기회에 설명하겠다.

장치 드라이버에는 많은 자료구조가 나온다. 장치 드라이버를 공부하는 데 기본적으로 알아야 할 자료들은 DriverObject, DeviceObject, IRP가 있다. 이런 자료구조들을 아는 것이 드라이버를 이해하는 데 밑거름이 되기 때문이다. 그 중에서 이번 회에는 IRP에 대한 내용을 살펴보겠다.


 

 
DriverObject와 DeviceObject의 연관 관계

직렬 드라이버를 예로 들어 DriverObject와 DeviceObect의 연관 관계를 잠시 설명해보겠다. 우리가 직렬 드라이버를 만든다고 가정해 보자. 다들 알다시피 직렬 드라이버는 COM1, COM2, COM3 등의 이름을 가지고 응용 프로그램에서 접근한다. 그러나 실제로 윈도우 2000에는 COM 개수에 따라 직렬 드라이버가 각각 다르게 존재하는 것이 아니다. 하나의 드라이버가 여러 개의 COM 포트를 관리하고 처리한다. 즉, 드라이버에서는 DriverObject가 하나 존재하고, 대신 COM 포트 개수만큼 DeviceObject를 생성하는 것이다. DriverObject가 같다는 것은 IRP 처리 루틴을 같이 공유한다는 뜻이다.

예를 들어 응용 프로그램에서 COM1을 읽는 명령이 내려왔다고 가정해 보자. Win32 서브시스템(API)을 통해 커널 모드로 진입할 것이다. 그리고 명령은 I/O 관리자에 전달될 것이며, I/O 관리자는 해당 명령을 실행할 수 있는 IRP를 생성해 해당 드라이버, 즉 직렬 드라이버에 전달한다. 그리고 DriverObject의 MajorFunction에 해당하는 디스패치 루틴 함수로 분기해 처리하고, 처리 결과를 I/O 관리자에 돌려준다. 여기서 읽기 처리 루틴이 다음과 같다고 하자.

Read(…)
{
......
// 선행 처리 과정
data = InPort ( 0x378 );
......
return data;


이와 같이 코딩되어 있다면 읽기 디스패치 루틴 실행시 0x378(COM1 포트의 주소) 값을 읽어올 것이고 응용 프로그램은 원하던 결과를 얻을 것이다. 그렇다면 응용 프로그램이 COM2 또는 COM3의 값을 읽게 하는 명령이 직렬 드라이버에 내려온다면 어떻게 될까?

그럼 COM 포트들은 같은 DriverObject를 사용하기 때문에 IRP에 관한 루틴을 공유한다. 그럼 앞과 같은 루틴이라면 COM2, COM3 포트의 값을 읽었을 때도 COM1의 값이 얻어질 것이다. 이런 문제를 해결하기 위해 DeviceExtension을 사용한다. DeviceExtension은 DeviceObject만을 위한 메모리 공간이다. 문제가 되던 data = InPort( 0x378 );을 다음과 같이 바꾸면 될 것이다.

data = InPort( DeviceObject->DeviceExtension->Port );

DeviceExtension에는 Port라는 변수가 있을 것이고, 그 변수에는 각 포트 주소가 기억되어 있다. 이와 같이 처리하면, 읽기 디스패치 루틴을 공유해도 각각의 포트 값을 읽어 올 수 있다.
 


IRP
패킷 드리븐 방식으로 I/O 요청을 처리하는 윈도우 NT 계열 운영체제는 처리해야 할 I/O 요청을 IRP를 이용해 처리한다. IRP 자료구조는 다음과 같이 크게 두 부분으로 나눌 수 있다.

◆ 헤더 : I/O 요청에 대한 다양한 정보가 있다. 요청한 I/O의 타입, 크기, 상태 등을 저장한다. buffered I/O를 할 경우에 Associatedirp에 버퍼 포인터, 다이렉트 I/O를 할 경우엔 MdlAddress에 MDL 포인터 관련 정보를 가지고 있다.

◆ I/O 스택 위치 : 함수 코드와 매개변수들을 담고 있다. I/O 스택은 I/O 요청이 어떤 처리를 하느냐에 따라 스택 크기가 결정된다.

예를 들어 플로피 디스크에 파일을 쓰는 I/O 요청이 있으면 적어도 스택 크기는 두 개가 될 것이다. 하나는 파일 시스템 드라이버를 위한 것이고, 다른 하나는 플로피 디스크 드라이버를 위한 것이기 때문이다.

IRP 버퍼 관리
사용자 응용 프로그램들이 읽기나 쓰기 같은 요청을 했을 경우 사용자 쪽의 데이터 버퍼를 처리하는 방법에 따라서 다음과 같이 분류할 수 있다.

◆ Buffered I/O : I/O 관리자는 먼저 호출한 응용 프로그램의 사용자 버퍼와 같은 크기의 Non-paged 풀(pool) 버퍼를 할당한다. 응용 프로그램에서 쓰기시 I/O 관리자는 IRP를 만들 때 할당한 버퍼로 호출한 사용자 버퍼 데이터를 복사한다. 읽기시에 I/O 관리자는 IRP가 끝났을 때 I/O 관리자가 할당한 버퍼에서 사용자 버퍼로 복사한다. 그리고 I/O 관리자가 할당한 버퍼는 해제한다.

◆ 다이렉트 I/O : I/O 관리자는 먼저 사용자 버퍼에 해당하는 물리 메모리 페이지를 잠근다(lock). 그리고 잠근 페이지에 대한 내용을 설명하기 위한 MDL을 만든다. MDL에는 버퍼에 의해 할당한 물리적인 메모리를 지정한다. 그리고 만약 드라이버가 버퍼의 내용을 접근하려고 하면, 시스템 주소 공간으로 버퍼를 맵해 사용할 수 있다.

◆ Neither I/O : I/O 관리자는 어떤 버퍼 관리도 처리하지 않는다. 대신 버퍼 관리는 장치 드라이버의 discreation으로 남겨지고, 장치 드라이버는 I/O 관리자가 다른 버퍼 관리 타입에서 처리하는 과정을 손수 처리할 수 있도록 선택할 수 있다.

꼭 그런 것은 아니지만 일반적으로 드라이버는 호출한 부분의 데이터의 양이 한 페이지(4KB)보다 작으면 Buffered I/O 방식을 사용하고, 그것보다 클 경우엔 다이렉트 I/O 방식을 사용한다. 그리고 파일 시스템 드라이버는 Neither I/O를 주로 사용한다. 데이터를 파일 시스템 캐시에서 사용자의 버퍼로 복사할 때 버퍼 처리에 오버헤드가 없기 때문이다.

드라이버 컴파일과 실행
드라이버를 만들기 위해서는 다음 파일들이 필요하다.

◆ Makefile : DDK에서 사용하는 실제 makefile을 호출하는 내용이 있다. 내용을 수정할 필요는 없다. 일반적으로 DDK 샘플에 있는 파일을 가져다 쓴다.
◆ Sources : 드라이버 컴파일 환경 및 관련 옵션을 정해준다.
◆ 드라이버 소스 파일 : 장치 드라이버 실제 소스
◆ RC(resource) 파일 : 드라이버 버전 정보를 담은 파일

컴파일
이 파일들이 구성됐으면 DDK에 있는 개발환경 Command 창을 띄운다(<화면 2>).


사용자 삽입 이미지


① 빌드 명령을 내린다. 그리고 나면 원하는 폴더에 *.SYS 파일이 생성된다. 그 파일을 \WINNT\System32\Drivers 폴더에 복사한다.

② 드라이버를 로드하려면 레지스트리에 정보를 기록해야 한다. 레지스트리에 직접 기록하든지, 아니면 간단한 *.reg 파일을 생성해 설치해도 된다. 레지스트리에 들어가는 키 위치를 보면 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services 밑에 서브키를 생성한다. 키 이름은 드라이버 이름과 같은 이름으로 생성한다. 예를 들면 드라이버 이름이 SAMPLE.sys라면, SAMPLE이라는 키를 만든다. 키가 만들어 졌으면, 이제 키 밑에 값들을 적는다.

Start = 3
ErrorControl = 1
Type = 1

이 값은 드라이버가 윈도우가 시작되면서 올라오는 게 아니고 우리가 언제라도 올리고 내릴 수 있는, 즉 동적으로 작동하기 위해 정한 값이다.

③ 재부팅한다.

장치 드라이버 실행
Command 창을 하나 띄운다. 그리고 net start sample1을 실행한다. ‘서비스를 시작합니다’라는 메시지가 나오면 드라이버가 제대로 로드됐다는 것을 알 수 있다(<화면 3>). 드라이버를 내리고 싶다면 net stop sample1을 실행한다(<화면 4>).


 
사용자 삽입 이미지


사용자 삽입 이미지

[출처] 장치 드라이버 기초와 프로그래밍

Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:23

Driver 개발을 처음 하시는 분들은 따라할 만한 가이가 없어서 환경을 구축하는데도 시간이 많이 걸립니다. 개발은 말할것도 없구요 그래서 그동안의 자료를 바탕으로 정리를 해 봤습니다.

아래의 가이드를 따라하시면 기초는 세팅이 되었다고 할 수 있습니다.

예제 소스는 키보드 필터 드라이버입니다. DDK의 포함된 예제소스, Ctrl2CapSource, Filemon 공개소스등을 참고하여 만들었습니다.

사용자 삽입 이미지

[키보드 필터 드라이버 UI]



       - 키보드 필터 드라이버에서 참고할만한 사항은 아래와 같습니다. -

  • 필터 드라이버의 기본구조

  • 드라이버 동적로딩

  • IOCTRL을 통한 User Mode Program 과의 통신(키보드 스캔코드 출력)

  • 후킹 시작시 Ctrl2CapSurce와 같이 Caps Lock키를 Left Ctrl 키로 변경시켜 줌


          이후부터의 내용은 다른 분들의 글을 인용한 부분도 있습니다. 양해 부탁드립니다.

  1. SW 설치
    1. 필수 - DDK (NT, 2000, XP 등 해당 OS DDK)
    2. 옵션 - CompuwareDriverStudio

  2. 빌드 환경

    1. DDK를 통한 Build

      시작메뉴의 프로그램에서 설치된 DDK(Development Kits)에 가보면 Build Envrionment > Checked Build Environment, Free Build Environment 4개가 보인다. 그 중 하나 Checked Build Environment 를 클릭하면 필요한 환경변수 세팅 및 경로 세팅을 한 후 cmd창 하나가 실행된다. 이 상태에서 DDK 설치 폴더의 만만한 샘플 폴더(C:\WINDDK\src\input\kbfiltr)로 직접 이동해서 build라고 명령을 치면 Sources라는 파일(확장자 없음)을 참고하여 C파일들을 컴파일 및 링크까지해서 sys파일이 빌드되어진다.

      (Checked Build, Free Build 랑의 차이는 Debug/Release 차이랑 비슷하고, DDK Help 참조)

    1. VC++ 빌드 환경 사용법

      여러 방법이 있겠지만 제일 간단한 방법은 DriverStudio를 설치하면 생성되는 DDK sources to vcproj Converter(SrcToVcProj) 를 사용한다 .

      요걸 실행해서 Sources라는 파일을 Open, 그리고 메뉴의 Convert>Convert DDK dirs/sources를 실행해서 VC++6 Workspace를 선택해서 Convert를 누르면 해당 폴드에 DDK로 빌드 할 수 있게 환경설정이 된 dsw 파일이 생성된다. Sources파일을 Drag&Drop하면 바로 Convert Dialog가 뜬다. 우리가 새롭게 작성할 .c 파일을 특정 폴더에 만들고 Sources파일(DDKsrc 폴더 참고)을 생성하면 된다.

      우린 요걸 사용해서 Checked, Free 모드로 빌드하면 된다. 물론 VC 컴파일/빌드의 F7 단축키를 사용가능.

      빌드하기 전에 DriverStudio를 설치하면 생성되는 툴바 중 왼쪽에서 3번째 DDK Build Settings를 실행해서 DDK Root Dir을 설정해 주고 빌드.

      또 다른 빌드방법은 DriverStudio를 설치하면 생성되는 툴바 중 왼쪽에서 4번째 버턴(Build With DDK..)으로 할 수 있다.

      이 버턴이 활성화 되게 하려면 SrcToVcProj를 사용하여 dsw를 만들 때 해당 폴더에 makefile 파일(DDKsrc 폴더 참조)이 있으면 활성화 되어 컴파일 할 수 있다.

      , Windows 2000 Device Driver, Windows Driver Model 책의 awx 파일로 프로젝트를 만들어 빌드할 수 있지만 환경이 잘 맞지 않아 빌드가 잘 안되었음.

      SoftICE 환경 설정 및 사용법

  1. SoftICE 환경 설정 및 사용법

    1. 초기 SoftICE Windows 설정한다

      "SoftICE Initiallization Settings..." ->"Initialization string" 에 다음과 같이 세팅한다

      X; SET FONT 2; LINES 60; WL 10; WC 35;

      x : 디버거 모드 종료

      set font 2 : 2번째 폰트를 사용

      lines : 줄 수를 50 라인

      wc : 코드 윈도우 줄 수는 35라인.

      wl : 로컬변수 윈도우 줄 수는 10라인

    1. SoftICE 실행한다 – Start SoftICE

      - cmd 창이 떴다가 사라진다.

    2. Symbol Loader를 실행해서 대상 모듈을 “open”하고 심볼을 “load”한다.

      Open할때 파일 형식을 *.*으로 ㅎ고 해당 .sys파일을 선택한다.

    3. Ctrl + D를 눌러 SoftICE 디버거 창을 띄운다.

    4. 소스 파일을 연다.

      FILE * : 모듈에 관계된 모든 소스파일의 목록을 보여 준다.

      FILE test.c : test.c 파일의 내용을 코드 윈도우에 표시한다.

    1. F6키를 눌러 코드 윈도우로 이동한 다음 원하는 위치에서 F9를 눌러 브레이크 포인트를 설정한다.

      BL : 브레이크 포인트 목록을 표시한다.

      BC * : 모든 브레이크 포인트를 삭제한다.

      U 210 : 210번째 라인으로 이동

      T : step into(F8)

      P : step over(F10)

      G : 실행을 계속한다.

      F7 : excute to here

      WD : data window를 표시

      WW : watch window를 표시

      WATCH xx : 특정 xx 변수 값을 watch window에 표시

      Alt+C : 커서를 코드 윈도우로 이동

      Alt+L : 커서를 로컬변수 윈도우로 이동

      Alt+W : 커서를 watch window로 이동

      Enter : 로컬변수 창등에서 구조체의 내부를 표시

      ww : Watch창을 보이게 함.

      wl : 지역변수창을 보이게 함.

    2. 다시 Ctrl + D를 눌러 디버거 창을 닫는다.

      - 런터임시 BP가 걸리면 디버거 창이 뜬다. 위 단축키로 디버깅을 해보아라.


[키보드 필터 드라이버 소스]


Posted by skensita
Programming/Win32 API2008. 11. 19. 11:16
Question
  Win32 API 책(30장메모리 부분)을 보니까 메모리 할당 부분에 malloc와 new가 나오고
가상 메모리 부분에 VirtualAlloc이 나오든데
그럼 malloc하는 부분은 가상 메모리가 아니란 말씀이신가요?
malloc 하는 부분과 VirtualAlloc 하는 부분의 메모리가 다른 부분인가요?

그리고 그 뒤에 힙 부분에 HeapAlloc와 GlobalAlloc, LocalAlloc 가 나오든데
HeapAlloc는 Win32함수이고 다른 두 개는 Win16의 잔재라고 나와 있더군요.
그래서 이 차이점은 알겠는데
제가 듣기론 malloc도 힙 영역에 메모리를 할당한다고 들었는데
그런것 아닙니까? malloc는 힙 아닌 다른 영역에 할당 하나요?
힙 부분에 다른 함수들이 나오니까 다른 영역에 할당 하나 싶어서요.
그리고 HeapAlloc나 GlobalAlloc는 가상 메모리에 할당 하는게 아닌가요?
가상 메모리의 힙 영역에 할당하는 것인가요?

일단 제가 이해 하기로는 모두 실질적인 메모리에 할당 하는 것은 아니고
가상 메모리에 할당하고 이것을 페이징 해서 실제 메모리에 올리고 사용하고
내리고 하는 걸로 알고 있는데 맞나요?
그리고 가상 메모리의 VirtualAlloc가 할당하는 부분과 malloc가 할당하는 영역,
그리고 HeapAlloc가 할당 하는 영역은 다르다.
이런 건가요?
Answer
  가상메모리란 말그대로 실제 메모리가 아닌 가상의 메모리입니다. win32 환경에서
모든 프로세스는 독립적인 4G 공간의 메모리 주소 공간을 가지게 됩니다. 그런데
실제로 4G의 메모리를 탑재하고 있는 컴퓨터는 없으며, 4G * 프로세스의 갯수
만큼의 메모리를 탑재한 컴퓨터는 더더욱 없습니다. 따라서 없는 것을 실제로
눈에 보이는 것처럼 만들기 위해서 몇가지 트릭이 사용됩니다. 가장 큰 트릭이
스와핑과 페이징이죠.

스와핑은 현재 실제 사용중이지 않은 메모리를 디스크로 옮기고, 실제로 필요한
페이지가 없을 경우 디스크로 부터 로딩해서 사용하는 기술입니다. 페이징은 좀
더 복잡한 개념인데, 가상 메모리가 실제 어떠한 물리 메모리로 맵핑 되는지의
정보를 가지고 있고 그것들을 변환하는 기술이라고 보시면 됩니다.

이것과 별개로 힙메모리라는 것이 나오는데, 이것은 프로세스에서 빈번히
사용하는 서로다른 크기의 작은 메모리 할당/해제시의 단편화가 적고 빠르게
되도록 만든 구조의 메모리 입니다. 이러한 메모리가 별도로 존재하는 것은
아니고 소프트웨어적으로 그렇게 관리하는 것을 말합니다. 보통은 특정한 크기의
풀을 만들어 두고 해당 풀의 메모리를 효율적으로 분할해서 쓰는 방식을 많이
사용합니다.

그럼 각각 이것들을 조작하는 API를 살펴보도록 하죠. 가장 저수준의
VirtualAlloc이라는 함수는 실제 가상 메모리를 할당받고 해제하는 함수입니다.
VirtualAlloc함수를 통해서는 실제 메모리 할당 뿐만 아니라 해당 페이지를
예약하는 등의 저수준 제어도 할 수 있습니다.

HeapAlloc이라는 함수는 힙에 메모리를 할당하는 함수입니다. HeapAlloc의 첫번째
인자로 넘어가는 것이 힙 핸들이죠. 힙은 HeapCreate함수를 통해 생성할 수
있으며, win32에서는 프로세스별로 하나의 기본 힙이 제공됩니다.
GetProcessHeap을 통해서 구할 수 있습니다.

malloc, free, new, delete 함수는 위의 API 보다 좀 더 상위의 계층에서 이러한
일들을 하게 됩니다. C 런타임 라이브러리 초기화 코드에서 해당 라이브러리의 힙
관리자 들이 별도의 힙 메모리를 만들고 관리하게 됩니다.

단순하게 이해하기 위해서는 그냥 위의 세가지 종류가 계층 구조라고 생각하면
편합니다. VirtualAlloc이 가장 하부의 계층, 그것을 이용해 HeapAlloc등의 API가
구현되고, 그 상위에 CRT 힙 함수들이 존재하는 것이죠.

실제로 응용 프로그램 계층에서의 대부분의 작업은 CRT 함수로도 충분히
가능합니다. 별도의 자료 구조를 특정 힙에 할당하는 방법을 쓸때에 Heap함수들을
사용하나 그렇게 자주 있는 일도 아니죠. VirtualAlloc을 쓰는 일은 물론 더더욱
없겠죠. 실제로 대용량 메모리를 사용해야 하는 경우에도 Memory mapped file을
통해서 처리하는 것이 일반적입니다.

음... 쭈욱 적고 밑에 질문글을 다시 보니 동문서답한 느낌도 드네용... ㅋㅋㅋ~

결론적으로 말하면 메모리는 하나입니다. 컴퓨터 뜯어보면 하나죠. ㅋㅋ~ 당연히
프로그램에서 사용하는 메모리도 그거 하나입니다. 하지만 편의상 메모리를
나눠서 쓰겠죠. 우리가 용돈을 받았을때 이건 차비, 식비,... 등으로 쪼개
쓰는것과 같다고 보면 됩니다. 컴퓨터도 그런식으로 용도에 맞게 메모리를
효율적으로 사용하기 위해서 가상메모리, 힙, 스택메모리 같은걸로 분리해서
사용합니다~

--
신영진 (Shin, YoungJin)
http://www.jiniya.net


Posted by skensita
Programming/Win32 API2008. 11. 19. 11:13

From Devpia

각 함수의 정의는 다음과 같다.
 
LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes);
HLOCAL LocalAlloc(UINT uFlags, UINT uBytes);
HGLOBAL GlobalAlloc(UINT uFlags, DWORD dwBytes);

셋 다 힙에서 메모리 블록을 할당받는 동일한 기능을 한다.

LocalAlloc과 GalobalAlloc은 Win32에서는 완전히 동일하다. Win16에서는 어플리케이션마다 가지는 Local 힙과 힙 매니저가 가지는 Global 힙이 따로 존재했지만 Win32에서는 따로 존재하지 않는다. 따라서 Win32에서 Local 힙과 Global 힙은 동일하고 함수 자체는 하위 호환을 위해 남아있다.

LocalAlloc과 GlobalAlloc은 동일하므로 LocalAlloc만을 언급하겠다.

HeapAlloc과 LocalAlloc의 차이점을 살펴보자.
첫째로 메모리 블록을 할당하는 힙 자체가 다르다는 것이다. LocalAlloc은 항상 디폴트 힙에서 할당한다. 디폴트 힙은 하나의 어플리케이션이 하나씩 가지는 메인 힙이다. 이는 프로세스가 생성되고 가상 주소 공간이 생겨날 때 같이 생성되는 힙이다. 그 크기는 링크시 실행파일 헤더에 기록이 된다. 링크 옵션으로 그 크기를 변경할 수도 있다. 기본 값은 1MB이다. 반면에 HeapAlloc은 함수의 첫 번째 인자에서 보듯이 임의의 힙에서 할당한다. HeapCreate로 만든 힙일 수도 있고 GetProcessHeap으로 얻는 디폴트 힙일 수도 있다.

둘째로 리턴값을 살펴보면 HeapAlloc은 포인터를 리턴하지만 LocalAlloc은 핸들을 리턴한다. 따라서 LocalAlloc으로 할당받은 메모리 블록을 실제로 사용할 때는 LocalLock 함수로 포인터를 얻어와야 한다. LocalAlloc의 사용이 좀 더 불편하지만 핸들을 사용하므로써 동기화와 같은 기능을 제공한다. 그리고 포인터를 직접 사용하지 않고 포인터를 사용하는 또 다른 (결과적으로) 이유는 다음과 같다.
LocalAlloc의 첫번째 인자는 할당받을 메모리 블록의 특성을 지정하는데, GMEM_MOVEABLE 이라는 특성을 지정할 수 있다. 이는 힙 메모리 단편화 제거를 위해 또는 힙 공간이 부족할 때 힙 관리자가 메모리 블록을 이동할 수 있도록 하게 하는데 이렇게 되면 포인터로 리턴했을 경우 이동된 후 잘못된 참조를 하게 되므로 핸들로서 관리되는 것이다. 실제로 GMEM_MOVEABLE 이 아닌 GMEM_FIXED로 지정하게 되면 리턴값은 그 자체가 포인터이다.

※ 참고로 C run-time library인 malloc은 위의 API 함수들과 같은 기능을 하지만 디폴트 힙이 아닌 자신만의 힙을 사용하고 자신만의 방법으로 힙을 관리한다.

+ VirtualAlloc() 함수는 뭐냐. 이것도 생각해 봐야.

LocalAlloc()과 같은 heap function 은 memory exception 을 가져 올 수
있다는 것과 대용량의 memory 할당에는 적합하지 않다는 것입니다.



=======  From MSDN : "Heap Functions" ============================================
Windows NT 4.0: External factors may cause accesses to heap memory to generate access violations.
One possible cause of an access violation is very limited space in the paging file. Therefore, all
accesses to heap memory should be protected with structured exception handling. This is not
necessary on later versions of Windows.

Windows 95/98/Me: The heap managers are designed for memory blocks smaller than four megabytes. If
you expect your memory blocks to be larger than one or two megabytes, you can avoid significant
performance degradation by using the VirtualAlloc or VirtualAllocEx function instead.

======= From MSDN : "Comparing Memory Allocation Methods" =========================
This topic provides a brief comparison of the following memory allocation methods:

CoTaskMemAlloc
GlobalAlloc
HeapAlloc
LocalAlloc
malloc
new
VirtualAlloc

The following functions are equivalent: GlobalAlloc, LocalAlloc, and HeapAlloc with the handle
returned by the GetProcessHeap function.

The VirtualAlloc function allows you to specify additional options for memory allocation. However,
its allocations use a page granularity, so using VirtualAlloc can result in higher memory usage.

The malloc function has the disadvantage of being run-time dependent. The new operator has the
disadvantage of being compiler dependent and language dependent.

The CoTaskMemAlloc function has the advantage of working well in either C, C++, or Visual Basic.
It is also the only way to share memory in a COM-based application, since MIDL uses CoTaskMemAlloc
and CoTaskMemFree to marshal memory.

GlobalAlloc, LocalAlloc, HeapAlloc 함수들은 궁극적으로 동일한 힙에 메모리를 할당하지만, 약간의 차이를 가지고 있다. HeapAlloc은 메모리가 할당될 수 없으면 예외를 발생시킬 수 있으며, LocalAlloc은 이런 기능이 없다. LocalAlloc은 핸들 할당을 지원함으로써 핸들값을 바꾸지않고 재할당을 통해 기초메모리 (underlying memory) 를 이동시킬 수 있으며, HeapAlloc은 이런 기능이 없다.

각각의 힙 할당자는 서로 다른 기법을 사용해 구현되었기에, 각 함수에 맞게 메모리를 해제해야 한다. 예를 들어, HeapAlloc으로 할당된 메모리는 LocalFree / GlobalFree이 아닌 HeapFree으로 해제되어야 한다.

VirtualAlloc 함수는 메모리 할당에 추가 옵션을 허용한다. 하지만 이 함수는 페이지 입도를 사용하므로, 더 많은 메모리가 소비될 수 있다.

malloc함수는 런타임 의존적, new연산자는 컴파일러/언어 의존적이라는 약점을 가지고 있다.

 

CoTaskMemAlloc함수는 C/C++/Visual Basic 모두에서 잘 작동하는 이점을 가지고 있다. 또한 MIDL (Microsoft Interface Definition Language) 이 메모리를 마샬링*하는데 CoTaskMemAlloc / CoTaskMemFree를 사용하므로, COM 기반 애플리케이션에서 메모리를 공유하는 유일한 방법이다.

 

*마샬링(marshalling) : 하나 이상의 프로그램 또는 연속적이지 않은 저장공간으로부터 데이터를 모은 다음, 이들을 메시지 버퍼에 집어넣고 특정 수신기나 프로그래밍 인터페이스에 맞도록 그 데이터를 조직화하거나, 미래 정해진 다른 형식으로 변환하는 과정.

주로 한 언어로 작성된 프로그램의 출력 매개변수를 다른 언어로 작성된 프로그램의 입력으로 전달해야 하는 경우 필요하다.

 

 VirtualAlloc은 힙을 사용하지는 않습니다.
그리고 힙처럼 연속된 메모리로 제한되지는 않으며
64KB의 조각들로 할당합니다...
그리고 또 하나...
새로운 사실인데...
new가 메모리 할당에 실패했는데 VirtualAlloc함수가 성공하는 경우가 있다는 군요...

VirtualAlloc이라는 함수가 하드에 잡아준다 이건 절대 아닙니다.
사람들이 가상메모리하면 바로 하드 스와핑을 떠올리시는데 그건 아닙니다.
Virtual* 메모리 API는 Win32에서 가장 원시적인(?) 함수라고 보시면됩니다.
(내가 쓸 메모리는 항상 램에 있어야한다 등등은 AWE라는 메모리 기술로
조절할 수 있습니다)

VirtualAlloc은 큰 메모리를 잡을 때 많이 씁니다.
왜냐면 얘들은 allocation granularity와 page size에 맞춰서
까다롭게 commit, decommit을 해줘야하기 때문이죠.

반면 HeapAlloc함수들은 이런 메모리 바운더리 및 얼라인먼트에대한
부담없이 언제든지 원하는 메모리를 뽑아 쓸 수 있도록 설계된 것이죠.
(결국 HeapAlloc도 VirtualAlloc으로 commit된 메모리에서 나오는 것이죠)
new, malloc 등 모든 함수들은 결국 HeapAlloc으로 귀결된답니다.

아무래도 메모리의 경계를 잘못 맞추어서 겪으신 어려움인것 같군요.
priority낮은 쓰레드가 뒤에서 돌면서 가비지 콜렉터 역할을 하며
페이지크기에 맞게 남은 페이지를 decommit해야하는걸로 알고 있습니다.
여튼, 속도는 잘나오겠지만 쓰는 법이 약간 까다롭죠.

결론은 작은 메모리를 빈번하게 할당, 해지해야할 땐
Heap메모리를 써야한다는 것, 큰 메모리를 비교적 적은 횟수에
쓰실 때엔 Virtual메모리를 쓰는게 정석인 듯 하옵니다..

윈도우 메모리 구조는 Programming Apps for Windows란
책에 가장 자세히 설명되어 있습니다^^

Posted by skensita
Programming/Win32 API2008. 11. 19. 10:51
함수 호출 규약

앞 항에서 Add 함수의 어셈블리 코드를 통해 스택 프레임의 실제 모양을 확인해 보았다. 인수는 뒤쪽부터 순서대로 전달하며 인수 전달에 사용한 스택 영역은 호출원이 정리했는데 이는 C/C++ 언어의 기본 호출 규약인 __cdecl의 스택 프레임 모양일 뿐이다. 호출 규약이 바뀌면 스택 프레임의 모양과 관리 방법도 달라질 수 있다.

호출 규약은 호출원과 함수간의 약속이므로 양쪽이 다른 형태로 약속을 할 수도 있는 것이다. 그렇다면 __cdecl이 아닌 다른 호출 규약은 어떻게 스택 프레임을 작성하는지 차이점을 분석해 보자. 호출 규약에 따라 인수를 전달하는 방법과 스택의 정리 책임, 함수의 이름을 작성하는 방법이 달라진다.

 

호출 규약

인수 전달

스택 정리

이름 규칙

__cdecl

오른쪽 먼저

호출원

_함수명

__stdcall

오른쪽 먼저

함수

_함수명@인수크기

__fastcall

ECX, EDX 우선 전달. 나머지는 오른쪽 먼저

함수

@함수명@인수크기

thiscall

오른쪽 먼저, this 포인터는 ecx 레지스터로 전달된다.

함수

C++ 이름 규칙을 따름.

naked

오른쪽 먼저

함수

없음

 

리턴값을 돌려 주는 방식도 호출 규약에 따라 달라질 수 있는데 다행히 현존하는 모든 호출 규약의 리턴 방식은 동일하다. 4바이트의 값을 돌려줄 때는 eax 레지스터를 사용하며 8바이트의 값을 리턴할 때는 edx:eax 레지스터 쌍을 사용한다. 8바이트를 초과하는 큰 리턴값, 예를 들어 구조체 등은 임시 영역에 리턴할 값을 넣어 두고 그 포인터를 eax에 리턴한다.

__stdcall

Add 함수의 호출 규약을 __stdcall로 바꿔 보자. __stdcall은 윈도우즈 API 함수들의 기본 호출 규약이며 비주얼 베이직도 이 호출 규약을 사용한다. __cdecl과 인수를 전달하는 방법은 동일하되 인수 전달에 사용된 스택을 정리하는 주체가 호출원이 아니라 함수라는 점이 다르다. Add 함수의 호출 규약을 바꾸기 위해 다음과 같이 수정해 보자.

 

int __stdcall Add(int a, int b)

{

     int c,d,e;

     c=a+b;

     return c;

}

 

함수 이름앞에 __stdcall 키워드를 삽입하면 이 함수는 __stdcall 호출 규약을 사용한다. main에서 함수를 호출하는 부분이 다음과 같이 변경된다.

 

push 2

push 1

call Add

result=eax

 

인수를 스택에 밀어 넣는 것과 인수를 푸시하는 순서는 동일하다. 단, call Add 다음에 add esp,8 코드가 없어 함수가 리턴된 후에 인수 전달에 사용한 스택을 복구하지 않는다는 점이 __cdecl과 다르다. 인수 전달에 사용한 영역은 이제 Add 함수가 직접 정리한다. 이 함수의 접두, 본체는 __cdecl과 동일하며 접미 부분이 다음과 같이 변경된다.

 

push ebp

....

ret 8

 

복귀 코드가 ret에서 ret 8로 바뀌었으며 복귀하면서 esp를 8만큼 증가시킨다. 이 코드에 의해 함수는 실행을 마치고 복귀함과 동시에 인수 영역을 해제한다. Add 함수 자신이 복귀하면서 스택을 정리하므로 호출원에서는 스택을 정리할 필요가 없다. 호출원은 인수를 순서대로 스택에 푸시한 후 함수만 호출하면 된다.

__cdecl과의 차이점

__cdecl과 __stdcall의 가장 큰 차이점은 스택 정리 주체가 누구인가하는 점인데 사실 이 차이점이 컴파일된 결과 코드에 미치는 영향은 별로 없다. 스택 정리 주체와는 상관없이 스택은 항상 호출 전의 상태로 복구되며 프로그램의 동작도 완전히 동일하다. 실행 속도는 거의 차이가 없으며 프로그램의 크기는 비록 무시할만한 수준이기는 하지만 __stdcall이 조금 더 작다. 왜냐하면 함수를 여러 번 호출하더라도 스택을 정리하는 코드는 함수 끝의 접미에 딱 한 번만 작성되기 때문이다. 반면 __cdecl은 호출원이 스택을 정리하므로 호출할 때마다 정리 코드가 반복되어 프로그램 크기가 조금 더 커진다.

또 다른 중요한 차이점은 가변 인수 함수를 만들 수 있는가 아닌가 하는 점이다. __stdcall은 함수가 직접 스택을 정리하기 때문에 가변 인수 함수를 지원하지 않는다. 함수 접미에 스택 정리 코드를 작성하려면 인수의 총 크기를 미리 알아야 하는데 가변 인수 함수는 전달되는 인수 개수가 가변이므로 이 크기가 고정적이지 않아 접미에서 스택을 직접 정리할 수 없다. 컴파일러가 접미의 ret n 명령에 대해 n을 결정할 수 없는 것이다.

이에 비해 __cdecl은 함수가 스택을 정리할 책임이 없으며 호출원이 함수를 부를 때마다 스택을 정리한다. 함수를 호출하는 쪽에서는 인수를 몇개나 전달했는지 알 수 있으므로 실제 전달한 인수 크기만큼 스택을 정리할 수 있다. 그래서 printf나 scanf같은 가변 인수를 지원하는 함수는 모두 __cdecl 호출 규약을 사용한다. 또한 윈도우즈 API 함수의 기본 호출 규약은 __stdcall이지만 wsprintf는 예외적으로 __cdecl로 작성되어 있다.

호출 규약 중 호출원이 스택을 정리하는 것은 __cdecl밖에 없으며 그래서 가변 인수를 지원할 수 있는 호출 규약도 __cdecl이 유일하다. 가변 인수 함수를 만들려면 반드시 __cdecl 호출 규약을 사용해야 한다. 만약 가변 인수 함수를 __stdcall로 작성하면 컴파일러는 이를 무시하고 __cdecl로 강제로 바꾸어 버린다.

__fastcall

다음은 __fastcall 호출 규약을 테스트해 보자. 함수 정의부를 int __fastcall Add(int a, int b)로 수정하기만 하면 된다. 호출부의 코드는 다음과 같다.

 

mov edx,2

mov ecx,1

call Add

result=eax

 

__fastcall은 인수 전달을 위해 edx, ecx 레지스터를 사용하는데 두 개의 인수를 차례대로 edx, ecx에 대입했다. 만약 인수가 둘 이상이면 세 번째 이후의 인수는 __cdecl과 마찬가지로 스택에 밀어넣을 것이다. 인수 전달을 위해 스택을 쓰지 않고 레지스터를 우선적으로 사용하므로 인수 전달 속도가 빠르다는 이점이 있다. 함수의 코드는 다음처럼 작성된다.

 

push ebp

mov ebp,esp

sub esp,14h

mov [ebp-8],edx             // 첫 번째 인수를 지역변수로

mov [ebp-4],ecx             // 두 번째 인수를 지역변수로

mov eax,[ebp-4]

add eax,[ebp-8]

mov [ebp-0ch],eax               // c는 세 번째 지역변수가 된다.

mov eax,[ebp-0ch]

mov esp,ebp

pop ebp

ret

 

edx, ecx 레지스터를 통해 전달받은 인수 둘을 순서대로 지역변수 영역에 복사한 후 사용하는데 어차피 인수도 지역변수의 일종이므로 이렇게 해도 별 상관이 없다. VC는 fastcall 호출시 ecx, edx로 인수를 넘기기는 하지만 이를 다시 스택의 지역변수로 만드는데 이렇게 되면 fastcall을 하는 의미가 없다. 비주얼 C++은 fastcall을 형식적으로만 지원할 뿐 fastcall의 장점을 취하지는 않는데 이는 컴파일러 구현상 ecx, edx 레지스터가 꼭 필요하기 때문이다. 

스택 정리는 함수가 하는데 Add 함수의 경우 인수가 두 개 뿐이므로 인수 전달을 위해 스택을 사용하지 않았으며 그래서 정리할 내용이 없다. 만약 인수가 세 개라면 제일 끝의 ret는 ret 4가 될 것이다. 레지스터는 스택보다 훨씬 더 빠르게 동작하기 때문에 __fastcall은 이름대로 호출 속도가 빠르다. 대신 이식성에 불리하다는 단점이 있다. 이 호출 규약은 ecx, edx 레지스터를 사용하도록 되어 있는데 이 두 레지스터가 모든 CPU에 공통적으로 존재하는 것이 아니기 때문이다. 그래서 윈도우즈 API는 이 호출 규약을 지원하기는 하지만 사용하지는 않는다. 볼랜드의 델파이가 __fastcall을 사용한다.

thiscall

thiscall은 클래스의 멤버 함수에 대해서만 적용되는데 ecx로 객체의 포인터(this)가 전달된다는 것이 특징이며 나머지 규칙은 __stdcall과 동일하다. 예외적으로 가변 인수를 사용하는 멤버 함수는 __cdecl로 작성되며 이때 this는 스택의 제일 마지막에(그러므로 첫 번째 인수로) 전달된다.

이 호출 규약은 컴파일러가 멤버 함수에 대해서만 특별히 적용하는 것이므로 일반 함수에는 이 호출 규약을 적용할 수 없다. thiscall은 이 호출 규약의 이름일 뿐 키워드가 아니기 때문에 함수 원형에 thiscall이라고 쓸 수도 없다. 멤버 함수이기만 하면 컴파일러가 알아서 thiscall 호출 규약을 적용한다. 객체니 멤버 함수니 this니 하는 것들은 C++편에서 배우게 될 것이다.

__naked

__naked 호출 규약은 컴파일러가 접두, 접미를 작성하지 않는 호출 규약이다. 스택 프레임의 상태 보존을 위해 컴파일러가 어떤 코드도 작성하지 않으므로 접두, 접미는 사용자가 직접 작성해야 한다. 스택은 어셈블리 수준에서만 다룰 수 있으므로 인라인 어셈블리를 사용해야 하며 제약점도 많기 때문에 일반적인 목적으로는 사용되지 않는다.

이 호출 규약이 반드시 필요한 경우는 C/C++이 아닌 언어에서 호출하는 함수를 작성할 때이다. 예를 들어 어셈블리에서는 인수 전달에 스택을 쓰지 않고 범용 레지스터만으로도 인수를 전달할 수 있다. 이런 경우는 C컴파일러가 만들어주는 접두, 접미가 불필요하다. 또한 속도가 지극히 중요한 디바이스 드라이버를 작성할 때도 이 호출 규약을 사용한다. __naked 호출 규약을 사용하려면 함수의 정의부에 __declspec(naked)를 적어주면 된다.

여기서 알아본 호출 규약 외에도 __pascal, __fortran, __syscall 이라는 호출 규약이 있었으나 지금은 지원되지 않는다. 비주얼 C++은 과거와의 호환성을 위해 이 단어들을 키워드로 인정하기는 하지만 실제로 사용할 경우 에러로 처리한다. 이상으로 다섯 가지의 호출 규약에 대해 정리했는데 실제로 사용되고 사용자가 지정할 수 있는 호출 규약은 현실적으로 __cdecl, __stdcall 두 가지밖에 없는 셈이다.

 출처 : WINAPI

Posted by skensita
Programming/C2008. 11. 18. 16:59

0.시작하면서...

 OS를 개발하면서 초반에 어셈블리어로 작성한 코드를 보면, 사용한 어셈블리어 명령이 몇 종류 없는 것을 알 수 있다. 그것도 아주 기초적인 수준의 어셈블리어만 사용했는데, 역으로 말하면 몇가지 종류의 어셈블리어만 알고 있으면 부트로더(Boot Loader), 커널로더(Kernel Loader), 그리고 기타 초기화 함수를 작성할 수 있다.



1.어셈블리어(Assembly Language) 기초 명령

 아래는 기초 명령의 리스트이다(Intel Style의 명령이라 가정한다).


  • mov A, B : B에서 A로 값을 이동
  • cmp A, B : 두 값을 비교하여 결과를 Flags 레지스터에 업데이트
  • rep instruction : insturction을 CX 레지스터의 값 만큼 반복 수행
  • call X : Stack에 Return Address를 삽입하고 jump 수행
  • jmp X : 무조건 해당 주소로 jump
  • je, ja X : 조건 분기 명령. Flags 레지스터의 플레그 값에 따라서 jmp 수행(보통 cmp와 같은 명령어와 함께 사용)
  • push X: 스택에 값을 저장
  • pusha, pushad : 스택에 모든 레지스터 값을 저장. EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI 저장
  • pop X : 스택에서 값을 꺼냄
  • popa, popad : 스택에서 모든 레지스터의 값을 꺼냄. 위의 pushad 명령과 같은 순서의 레지스터 사용
  • add A, B : A에 B의 값을 더함
  • sub A, B : A에서 B의 값을 뺌
  • mul A, B : A에 B의 값을 곱함
  • inc A : A의 값을 1 증가시킴
  • int X : X번째의 Software Interrupt를 발생시킴
  • ret, retn : Stack에 포함된 Return Address를 꺼내서 해당 주소로 복구(보통 Call 명령과 같이 사용)
  • iret, iretd : 인터럽트 처리 시에 모든 처리를 완료하고 다시 태스크로 복구
  • or A, B : A에 B값을 OR
  • xor A, B : A에 B값을 XOR
  • not A : A의 값을 반전(0->1, 1->0)
  • lgdt : GDT를 설정(Intel Architecture 특수 명령)
  • lidt : IDT를 설정(Intel Architecture 특수 명령)
  • lldt : LDT를 설정(Intel Architecture 특수 명령)
  • ltr : Task Register에 TSS를 설정(Intel Architecture 특수 명령)
  • clts : Task Switching 플래그를 0으로 설정(Intel Architecture 특수 명령)
  • cli : 인터럽트 불가 설정
  • sti : 인터럽트 가능 설정
  • fninit : FPU 초기화 명령(x87 Floating Point Unit 관련 명령)
  • ... 기타 등등

 물론 전부를 나열하지는 않았지만 척 봐도 알 수 있는 기본적인 명령어들이다. 물론 성능을 고려한다면 더 많은 어셈블리어 명령어들이 리스트에 포함되겠지만, 성능적인 면을 고려하지 않는다면 위의 함수 정도면 OK다.

 위의 함수에 대한 기본적인 기능들은 Intel Architecture Manual Volume 2 Instruction Set 문서를 참고하면 된다. 위의 명령어를 사용하여 프로그램을 작성하고 싶은 사람은 New Wide Assembler(NASM)을 이용하면 테스트 가능한데, 아래의 참고자료를 참고하면 간단히 함수를 생성하고 빌드 할 수 있다.


 http://nasm.sourceforge.net/ 홈페이지에 가면 컴파일러를 다운받을 수 있고 예제 및 문서도 제공하므로 한번 해보는 것도 괜찮을 듯 하다.


2.호출 규약(Calling Convension)

2.1 stdcall, cdecl, fastcall

 실제로 어셈블리어를 아는 것도 중요하지만 이 함수를 C 언어에서 어떻게 호출하여 사용할 것인가 하는 문제도 중요하다. 흔히들 호출 규약(Calling Convention)이라고 표현하는 이것은 함수를 호출하는 규약인데, 몇가지 방식이 존재한다.


  • stdcall(pascal) 방식 : 스택에 파라메터를 역순으로 삽입하고 함수를 호출. 스택의 정리작업을 호출된 함수에서 수행. 파스칼 언어 및 베이직 언어에서 사용하는 방식
  • cdecl 방식 : 스택에 파라메터를 넣는 방식은 stdcall과 같음. 단 스택의 정리작업을 호출한 함수에서 수행. C언어에서 사용하는 방식
  • fastcall 방식 : 몇개의 파라메터는 레지스터를 통해 넘기고 나머지 파라메터는 스택을 사용하는 방식

 위의 세가지 중에서 보편적인 방식 두가지는 stdcall 및 cdecl 방식이다. 이 두가지 방식의 가장 큰 차이점은 스택의 정리를 누가 하는 가이다.

 stdcall 방식 같은 경우 Callee(호출 된 함수)에서 스택 정리를 하므로 Caller(호출하는 함수)와 Callee 모두 파라메터의 개수를 알고 있어야 정상적인 처리가 가능하다.

 반면 cdecl 방식 같은 경우 Caller에서 스택 정리를 하므로 Callee는 파라메터의 개수를 정확하게 몰라도 된다. 바로 이 점이 C 언어의 가변인자(Variable Argument)를 가능하게 하는 것이다(printf와 같은 함수를 생각해보자).

 가변인자에 대해서는 나중에 알아보고 우리가 사용할 cdecl에 대해서 자세히 알아보자.


2.2 cdecl 분석

 아래는 간단한 C 프로그램을 작성한 것이다.


int DoSomething( int a, int b )
{
    int c;
    c = a+b;
    return c;
}

int main(int argc, char* argv[])
{
    DoSomething( 1, 2 );

}

 간단하게 파라메터 2개를 받아서 그중 첫번째 파라메터를 리턴하는 함수이다. 이것을 cdecl로 해서 컴파일 한 결과 나온 어셈블리어 결과는 아래와 같다.


int DoSomething( int a, int b )
{
    int c;
    c = a+b;
    return c;
   /* 여기가 어셈블리어로 변경된 코드
 
   push ebp
    mov ebp,esp
    push ecx
    mov eax,[ebp+08h]
    add eax,[ebp+0Ch]
    mov [ebp-04h],eax
    mov eax,[ebp-04h]
 
   mov esp,ebp
    pop ebp
    retn
   */
}

int main(int argc, char* argv[])
{
    DoSomething( 1, 2 );
   /* 여기가 어셈블리어로 변경된 코드
    push ebp
    mov ebp,esp
    push 00000002h
    push 00000001h
    call SUB_L00401000
   
add esp,00000008h <== 스택을 정리하는 부분
    pop ebp
    retn
   */
}



 위에서 보면 파라메터를 역순으로 Push 하는것을 알 수 있으며 main 함수에서 "add esp,08" 명령을 통해 스택 정리를 수행함을 볼 수 있다. 여기서 주의해서 봐야 할 부분은 DoSomething 함수에서 어떻게 파라메터에 접근하고 또한 어떻게 함수 내부적으로 사용하는 레지스터를 관리하고 복원하는가 이다.


 아래는 Caller(main)Callee(DoSomething)의 스택의 상태를 표시한 것이다.


 

사용자 삽입 이미지


<Caller와 Callee의 Stack>


 왜 ESP로 접근하지 않고 EBP를 통해 파라메터에 접근하는 것일까? 위의 그림을 보면 왜 ebp + Index로 접근을 하는 지 알 수 있다. 스택의 Top을 의미하는 ESP 레지스터의 경우 코드 중간 중간에 스택을 사용하면서 계속 변하는 값이다. 그 반면에 파라메터의 위치는 항상 고정적이므로 스택의 Top을 이용해서 파라메터에 접근하려면 문제가 발생한다. 따라서 EBP에 ESP의 값을 처음 설정해 두고 EBP를 이용해서 고정된 Offset으로 접근하는 것이.


 이와 같이 하면 스택의 Top이 계속 바뀌더라도 EBP가 초기의 스택 Top의 위치를 가지고 있으므로 EBP + 8, EBP+ 12과 같은 값으로 접근 가능하다. 위에서 초기에 Callee의 Stack에서 Push ebp를 하고 난 뒤에 Stack의 Top은 esp1을 가르키고 있다. 이 값을 ebp에 넣게 되므로 ebp를 이용하면 Parameter에 고정된 Index( 8, 12, 16...)으로 접근을 할 수 있는 것이다.


2.3 stdcall

stdcall의 경우에는 cdecl과 거의 차이가 없고 스택을 정리하는 부분만 차이가 있다.

int DoSomething( int a, int b ) 

    int c; 
    c = a+b; 
    return c; 
   /* 여기가 어셈블리어로 변경된 코드
    push ebp
    mov ebp,esp
    push ecx
    mov eax,[ebp+08h]
    add eax,[ebp+0Ch]
    mov [ebp-04h],eax
    mov eax,[ebp-04h]
    mov esp,ebp
    pop ebp
    retn 08h <== 스택을 정리하는 부분
   */ 
}   
int main(int argc, char* argv[]) 

    DoSomething( 1, 2 ); 
   /* 여기가 어셈블리어로 변경된 코드
    push ebp
    mov ebp,esp
    push 00000002h
    push 00000001h
    call SUB_L00401000
    pop ebp
    retn
   */ 
}  


2.4 프롤로그(prologue) 및 에필로그(epilogue)


 Callee의 스택을 다시 Caller의 스택으로 복원해야 하는데 스택 top을 저장하고 복원하고 하는 작업을 프롤로그(prologue), 에필로그(epilogue)라고 한다. 위에서 본 스택을 복구하는 작업이다.

 만약 우리가 어셈블리어 함수를 만든다면? 그리고 그 함수를 C에서 호출한다면? 아니면 그 반대의 경우라면 어셈블리어 함수를 어떻게 만들어야 할까? 그렇다. 위에서 본 것과 같은 형태 즉 cdecl의 형태를 그대로 따라서 만들면 된다.

 DoSomething() 함수의 프롤로그 에필로그 형태는 아주 일반적인 형태이므로 알아두도록 하자.(꼭 저렇게 구성할 필요는 없지만 일반적이므로 알아두자.)


3.마치면서...

 자 오늘 우리는 어떻게 어셈블리어 함수를 만들어서 파라메터를 넘겨 받을 것이며, 어셈블리어 함수에서 C 함수를 어떻게 호출해야 하는지, 혹은 그 반대의 경우 어떻게해야 하는지에 대해서 알아보았다.




참고자료

1. NASM에서 Macro 사용법

NASM에서 Macro의 작성법은 아래와 같다.

Macros
The built-in macro system has the following syntax:

.macro <name> <args>
<operations>
.endm

Example:


.macro write string  
movw string, %si  
call printstr.
endm


This would be equivalent to the NASM macro:

code : %macro write 1  
          mov si, %1  
          call printstr
          %endmacro


상당히 유용한 기능인것 같다. 코드가 확 줄어버리네. @0@


2. NASM에서 C에서 호출 가능한 함수 작성법

크게 세부분으로 나눌 수 있다.

  • bit 정의 부분
  • global 설정 부분
  • section 설정 부분

각 부분을 정의하는 코드는 아래와 같다.

  1. [bits 32]     <= bit 설정 부분
  2. global _kInit <= 외부로 export할 함수 또는 변수명
  3. extern _main  <= 어셈블리어 파일에서 C 함수 또는 다른 함수를 호출할때 외부에 있다는 것을 알림
  4. section .text <= 섹션 정의부, 코드 영역임을 알림
  5. _kInit:       <= 함수부
       mov      ax, 0x10
       mov      ds, ax
  6.    call     main
  7.    retn 

위와 같이 쓰면 사용할 수 있다.


출처(http://kkamagui.springnote.com
)

Posted by skensita
Programming/Win32 API2008. 11. 15. 19:20

DLL Injection Howto

 

 

 

 Table Of Contents

 

 

1.  개요

2.  Method1: Using Registry

3.  Method1: Using Windows Hook Function

4.  Method2: VirtualAllocEx & CreateRemoteThread

5.  Method3: CreateRemoteThread & WriteProcessMemory

 

 

  

1. 개요

 

- DLL Injection 이란?

 

‘DLL Injection’ 은 이미 실행되어 있는 프로세스로 DLL Code 를 삽입하는 기법들을 말한다. 이 방법은 말 그대로 특정 프로세스를 실행하기에 앞서, 필요한 코드를 프로세스를 실행하는 지점에서 가로챈 뒤 자신의 코드를 먼저 주입(inject, 실행) 하고 제어권을 다시 돌려주는 방법을 뜻하며, 이러한 방법은 ‘Code Injection’ 이라 불리우기도 한다. 이미 실행되어 있는 Process – 현재 Runtime 에서 동작되고 있는 Process - 를 변경하여 사용하기도 하며, 특정 API 를 실행하고자 할 때 Code 를 먼저 Process 상에 Injection 하여 사용하기도 한다. ‘DLL Injection’ 을 통해 구현할 수 있는 기능들은 다음과 같다. 특정 함수를 실행하기에 앞서 필요한 작업을 하는데 사용하거나, 기존 함수들의 동작을 Monitoring 하고자 할 때 사용하며, 또는 기존의 기능을 확장하고자 할 때도 사용할 수 있을 것이다.

 

이 문서에서는 ‘DLL Injection’ 을 구현하고자 할 때 사용하는 여러 가지 방법들을 소개할 것이다.

 

- DLL Injection 을 위해 필요한 기반 지식

 

 WIN32 API 에 대한 기본 지식, DLL Process, Thread 의 동작원리를 알고 있으면 DLL Injection 을 하는데 큰 어려움이 없다. 이 외에도 Windows 에서 Process 를 실행 할 때 DLL 을 호출하는 원리를 알면 된다.


  

일반적으로 DLL 은 한 프로세스 프로그램 - 이 시작될 때 프로그램의 address 영역으로

Mapping 이 이루어 진다. Mapping 이 이루어 질 경우, DLL explicit 하게 호출이 되는지, implicit 하게 호출이 되는지 여부와 상관없이 entry point 함수인 DllMain 을 호출하며 cleanup, initialize 동작을 수행할 것이다. DLL 자체는 DLL 을 호출한 (프로세스의) 쓰레드 address 공간에 자신의 주소를 가지게 될 것이며, 이로 인해 DLL 코드가 프로세스 상의 내부 영역 프로세스가 사용하는 영역 에서 Dispatch 를 수행한다든지, API 를 가로채는 등의 기능을 할 수 있게 되는 것이다.

 

사용자 삽입 이미지


2.
Method1: Using Registry

- Registry: AppInit_DLLs (http://support.microsoft.com/kb/197571)

: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows

 

Registry 를 이용하여 모든 프로세스에서 DLL 을 로드할 수 있다. 위에 언급한 레지스트리 경로에 있는 AppInit_DLLs subkey 에 삽입하고자 하는 DLL 의 경로와 이름을 지정해 놓으면, Windows 에서 USER32.DLL 을 사용하는 모든 프로세스는 이 DLL 을 자동으로 LoadLibrary(…)를 이용하여 실행한다.

 

실제로 이 방법을 사용할 경우는 없을 것이다. 효율적이지 못한 것 – 모든 프로세스가DLL 을 로드하게 되는 부하가 생길 것이며, DLL 코드 내부에서 특정 상태에 따른 처리를 해 주어야 하는 문제가 생기는 것 - 이 첫째 이유라면 그럴 것이고, DLL 코드 내부에서 사용할 수 있는 함수도 KERNEL32.DLL 에서 export 되는 함수에 국한될 것이기 때문이다.

 

만약 테스트를 해 보고자 한다면 Registry 에 값을 쓰고 시스템을 리부팅을 꼭 해야 적용이 될 것이다.

 

 

3. Method2: Using Windows Hook Function

- SetWindowHookEx, UnhookWindowsHookEx

 

HHOOK SetWindowsHookEx(

  int idHook,        // type of hook to install

  HOOKPROC lpfn,     // address of hook procedure

  HINSTANCE hMod,    // handle to application instance

  DWORD dwThreadId   // identity of thread to install hook for

);

 

BOOL UnhookWindowsHookEx(
  HHOOK hhk   // handle to hook procedure to remove
);

 

사용자 삽입 이미지
 

 

 Windows 에서 Hook 함수를 사용하여 DLL Inject 하게 되면, 내부적으로는 Hook Procedure 만이 아니라, Hook Procedure 가 들어 있는 DLL 코드 전체가 프로그램의 코드 영역에 mapping 되기 때문에, DLL 코드가 실행되는 영역이 결국 DLL 을 호출한 프로그램의 내부 영역이 된다. 내부 메시지를 hook 을 걸거나, window procedure hook 을 걸어 필요한 작업을 진행하면 될 것이고 SetWindowLong(…) 을 이용하여 관련 함수를 Dispatch 하는 방법을 사용하면 될 것이다.

 

Windows Hook 을 사용하고자 할 때 코드 작성 방식은 다음과 같다.

 

- A) DLL Shared 영역 지정

: Global 변수들을 Shared 로 지정하여 DLL 을 사용하는 모든 프로그램에 대해 DLL 이 로드되는 시간 동안 DLL 간의 공유 가능한 영역을 지정한다.

 

#pragma data_seg(".hkshared")

HHOOK   gHook   = NULL;        // global hook

HWND    gHwnd   = NULL;        // for subclassing (dll injection window)

UINT    gMsg    = 0;           // hook msg

#pragma data_seg()

#pragma comment(linker, "/SECTION:.hkshared,RWS") // for linker option

 

- B) DLL Code 선언부

: Hook 을 설치, 제거하는 함수와 실제 hook 으로 데이터를 얻었을 때 이를 처리하는 함수를 구현한다.

// install Hook Function

HOOKDLL_API int fnInjectDll(HWND hWnd);

// remove Hook Function

HOOKDLL_API int fnRemoveDll();

 

// Hook Procedure

HOOKDLL_API LRESULT CALLBACK HookProc (

int nCode,

     WPARAM wParam,

     LPARAM lParam

);

 

- C) DLL Main Code

: Dll implicit, explicit 와 상관 없이 항상 호출되는 start code 이다. DLL_PROCESS_ATTACH 일 경우에 Hook procedure 에서 사용할 Message 를 등록하는 코드를 작성한다.

 

BOOL APIENTRY DllMain( HANDLE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved

                                       )

{

        if( ul_reason_for_call == DLL_PROCESS_ATTACH )

        {

               hDll = (HINSTANCE) hModule;   // dll module

               ::DisableThreadLibraryCalls( hDll );

 

               if(gMsg == NULL)

                       gMsg = ::RegisterWindowMessage( "WM_HOOKDLLMSG" );                 

    }

       

    return TRUE;

}

 

- C) Hook Install Code

 

HOOKDLL_API int InjectDll (HWND hWnd)

{

     gHwnd = hWnd;  // get window to hook

     gHook = SetWindowsHookEx (

                  WH_KEYBOARD,   // hook (message) type; others->WH_MOUSE, WH_CBT…

                  HookProc,      // hook procedure

                  hInstance,     // dll instance handle

                  GetWindowThreadProcessId (hWnd, NULL)

);

    

}

 

- D) Hook Uninstall Code

 

HOOKDLL_API void RemoveHook (HWND hWnd)

{

    

     UnHookWindowsHookEx(gHook);

    

}

 

- E) Hook Procedure Code

: Hook Procedure 로서, 기본 Procedure Message 를 처리하기 전에 먼저 호출되는 부분이다. 이곳에서 Hook 메시지를 찾아내어 기존 호출 방법을 변경한다.

 

#define pCW ((CWPSTRUCT*)lParam)

HOOKDLL_API LRESULT CALLBACK HookProc (

int nCode,

     WPARAM wParam,

     LPARAM lParam

)

{

If ((pCW->message == WM_HOOKEX) && pCW->lParam) 

        {

              

               // change dll owner (process)’s procedure

               OldProc = (WNDPROC) SetWindowLong(

gHwnd, GWL_WNDPROC, (LONG)NewProc

);

              

        }

return (CallNextHookEx(gHook,nCode,wParam,lParam));

}

 

 

 

 

- F) New Procedure Code

: 기존 Procedure 를 실행하기에 앞서 실행되는 부분이다. 이 부분은 DLL Injection 되는 영역 상에서 동작한다.  

 

LRESULT CALLBACK NewProc(

HWND hwnd,     

UINT uMsg,

WPARAM wParam, 

LPARAM lParam  

)

{

       

        Switch (uMsg)

{

Case WM_LBUTTONDOWN:

        MessageBox(NULL, “[Injected Code] Left Button”, “MSG”, MB_OK”);

        break;

        }

       

        // call original window procedure

        return CallWindowProc(OldProc,hwnd,uMsg,wParam,lParam);

}

 

 

4. Method3: VirtualAllocEx & CreateRemoteThread

- VirtualAllocEx, CreateRemoteThread

 

 

LPVOID VirtualAllocEx(

  HANDLE hProcess,  // process within which to allocate memory

  LPVOID lpAddress, // desired starting address of allocation

  DWORD dwSize,      // size, in bytes, of region to allocate

  DWORD flAllocationType, // type of allocation

  DWORD flProtect   // type of access protection

);

 

 

HANDLE CreateRemoteThread(

  HANDLE hProcess,        // handle to process to create thread in

  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security attributes

  DWORD dwStackSize,      // initial thread stack size, in bytes

  LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function

  LPVOID lpParameter,     // argument for new thread

  DWORD dwCreationFlags,  // creation flags

  LPDWORD lpThreadId      // pointer to returned thread identifier

);

 

 

이미 존재하는 프로세스 상에 thread 를 외부에서 생성하여, 이 쓰레드가 DLL 코드를 실행하도록 동작하는 방법이다. 앞서 Method1 에서 사용한 DLL 코드를 새로 생성한 thread 에서 load 하기 위해 LoadLibrary 를 실행한 뒤 필요한 작업을 수행하면 된다.

 

 

사용자 삽입 이미지



- A) Open Process (in App)

: OpenProcess 함수를 사용하여 기존 프로세스를 open 한다.(정확한 의미로는 기존 Process handle 을 가져온다)

 

int WINAPI WinMain(

HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow

)

{

       

        // get remote process’s id

GetWindowThreadProcessId (hWnd, &dwProcessId);

if (GetCurrentProcessId() == dwProcessId)

               return;

        // open remote process

        HANDLE hProcess = OpenProcess(         PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|

PROCESS_VM_WRITE|PROCESS_VM_READ, FALSE, dwProcessId

);            

// inject dll

// reject dll

}

 

- B) Dll Injection Code (in App)

: remote thread 를 생성하고 LoadLibrary 를 호출한 뒤, thread 에서 dll 코드가 종료될 때까지 기다린다. Dll 코드는 remote thread, 즉 외부 Process 영역에서 동작하며, 필요한 작업을 한 뒤 return 되는데, return 되고 난 뒤에는 만들어 놓은 thread 를 종료하면 된다.

 

int InjectDll( HANDLE hProcess )

{

       

        // 1: get kernel address, this will be used to launch LoadLibrary

HMODULE hKernel32 = GetModuleHandle("Kernel32");

       

        // 2: get full path of dll

        Char szDllPath[MAX_PATH] = {255, 0};

        if(!GetModuleFileName(hInst, szDllPath, MAX_PATH))

               return 0;

        strcpy(strstr(szDllPath,".EXE"), ".dll");

       

        // 3: allocate dll length in the remote thread

        pLibRemote = VirtualAlloc(hProcess,

NULL, sizeof(szDllPath),

MEM_COMMIT, PAGE_READWRITE

);

        // 4: write dll code to the remote thread

        WriteProcessMemory(hProcess, pLibRemote, (void*)szDllPath,

               sizeof(szDllPath), NULL

               );

 

        // 5: call DLL code in the remote thread (LoadLibrary)

        hThread = CreateRemoteThread(hProcess, NULL, 0,

        (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, “LoadLibraryA”)

        , pLibRemote, 0, NULL

);

WaitForSingleObject(hThread, INFINITE); // wait! while DLL code execute

       

// 6: get loaded module

GetExitCodeThread(hThread, &hLibModule);

 

// Close handle, and free dll

CloseHandle(hThread);

VirtualFreeEx(hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE);

       

}

 

- C) Dll Rejection Code (in App)

: remote thread 를 생성하고 FreeLibrary 를 실행하여 DLL unload 한다.

 

void RejectDll (HANDLE hProcess)

{

       

        hThread = CreateRemoteThread(hProcess, NULL, 0,

        (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, “FreeLibrary”)

        , pLibRemote, 0, NULL

);

WaitForSingleObject(hThread, INFINITE); // wait! while DLL code execute

       

// Close handle, and free dll

CloseHandle(hThread);

}

 

 

 

 

 

5. Method4: CreateRemoteThread & WriteProcessMemory

- CreateRemoteThread, WriteProcessMemory

 

HANDLE CreateRemoteThread(

  HANDLE hProcess,        // handle to process to create thread in

  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security attributes

  DWORD dwStackSize,      // initial thread stack size, in bytes

  LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function

  LPVOID lpParameter,     // argument for new thread

  DWORD dwCreationFlags,  // creation flags

  LPDWORD lpThreadId      // pointer to returned thread identifier

);

 

BOOL WriteProcessMemory(

  HANDLE hProcess,             // handle to process whose memory is written to

  LPVOID lpBaseAddress,        // address to start writing to

  LPVOID lpBuffer,             // pointer to buffer to write data to

  DWORD nSize,                 // number of bytes to write

  LPDWORD lpNumberOfBytesWritten   // actual number of bytes written

);

 

사용자 삽입 이미지



프로세스의 address 영역에 thread 를 외부에서 생성하여, 이 쓰레드가 DLL 코드를 실행하도록 동작하는 방법이다. 이 방법은 Method3 에서 사용한 방법과 비슷한 구조를 가지지만, 앞서의 경우와는 달리 DLL 을 외부 프로세스가 Load 하도록 동작하는 것이 아니라, DLL 에서 사용한 코드 자체를 외부 프로세스의 address 영역에다 Write 하고 이를 호출하여 코드를 실행하는 방법이다.

 

 - A) WinMain

: OpenProcess 함수를 사용하여 기존 프로세스를 open 한다.(정확한 의미로는 기존 Process handle 을 가져온다)

int WINAPI WinMain(

HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow

)

{

       

        // get remote process’s id

GetWindowThreadProcessId (hWnd, &dwProcessId);

 

// inject dll

bRet = InjectCode(hWnd);

MessageBox(NULL, “Code Injected. Press OK to reject!”,

                “Injection Sample”, MB_OK

               );

// reject dll

bRet = RejectCode(hWnd);

}

 

 

- B) INJDATA

: 실제 Injection 을 할 때 사용할 구조체이다. 이 구조체를 remot 프로세스의 address 영역에 write 하여 사용할 것이고, 여기에 저장된 내용은 Dispatch , 사용할 프로시저나 함수의 포인터, 내부 변수등을 저장한다.

 

 

typedef struct __INJDATA {

        // pointer to the function used within injected code

        SETWINDOWLONG  fnSetWindowLong;

        CALLWINDOWPROC fnCallWindowProc;     

        //

        HWND hwnd;

        WNDPROC fnNewProc;

        WNDPROC fnOldProc;    

} INJDATA, *PINJDATA;

 

- C) Remote Code (Inject, Reject)

: Remote thread 영역에서 사용할 코드

DWORD WINAPI InjectFunc (INJDATA *pData)

{

        // change old procedure to the new one

        pData->fnOldProc = (WNDPROC) pData->fnSetWindowLong(

               pData->hwnd, GWL_WNDPROC, (long)pData->fnNewProc

               );     

        return (pData->fnOldProc != NULL);

}

 

DWORD WINAPI EjectFunc (INJDATA *pData)

{

        // restore to the old procedure

        return (pData->fnSetWindowLong(pData->hwnd, GWL_WNDPROC,

               (long)pData->fnOldProc) != NULL);

}

 

- D) InjectCode

: 실제 Injection 에 사용될 코드

 

int InjectCode(HWND hWnd)

{

       

        // 1: get user module address, this will be used with dispatch function

HMODULE hKernel32 = GetModuleHandle("user32");

       

        // 2: open remote process

        hProcess = OpenProcess(               PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|

PROCESS_VM_WRITE|PROCESS_VM_READ, FALSE, dwProcessId

                );

        // 3: allocate space for remote procedure(function) and data

        pDataRemote = (BYTE*) VirtualAllocEx(hProcess, 0, size,

               MEM_COMMIT, PAGE_EXECUTE_READWRITE

               );

        pNewProcRemote = pDataRemote + sizeof(INJDATA);

       

        // 4: write pNewProcRemote to the process memory (address space)

        WriteProcessMemory(hProcess, pNewProcRemote,

               &NewProc, cbNewProc, &dwNumBytesXferred

               );     

 

        // 5: initialize INJDATA

        injData.fnSetWindowLong = (SETWINDOWLONG)

               GetProcAddress(hUser32, “SetWindowLongA”);

        injData.fnCallWndProc   =  (CALLWNDPROC)

               GetProcAddress(hUser32, “CallWndProcA”);

        injData.fnNewProc = (WNDPROC) (pNewProcRemote);

 

        // 6: write injData to the process memory           

        WriteProcessMemory(hProcess, pDataRemote,

               &DataLocal, sizeof(INJDATA), &dwNumBytesXferred

               );

       

        // 7: allocate space for remote function (InjectFunc)

        pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbInjectFunc,

               MEM_COMMIT, PAGE_EXECUTE_READWRITE
               );

       

        // 8: write function to the remote thread   

        WriteProcessMemory( hProcess, pCodeRemote,

               &InjectFunc, cbInjectFunc, &dwNumBytesXferred

               );

       

        //  9: call function in the remote thread (pCodeRemote)

        hThread = CreateRemoteThread(hProcess, NULL, 0,

        (LPTHREAD_START_ROUTINE) pCodeRemote, pDataRemote,

                0 , &dwThreadId);

 

WaitForSingleObject(hThread, INFINITE); // wait! while DLL code execute

       

// 10: get loaded module

GetExitCodeThread(hThread, &hLibModule);

 

// Close handle, and free dll

CloseHandle(hThread);

VirtualFreeEx(hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE);

       

}

 

- D) RejectCode

: 실제 Injection 에 사용될 코드

 

int RejectCode(HWND hWnd)

{

       

        // 1: open remote process

        hProcess = OpenProcess(               PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|

PROCESS_VM_WRITE|PROCESS_VM_READ, FALSE, dwProcessId

                );

       

        // 2: allocate space for remote function (RejectFunc)

        pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbRejectFunc,

               MEM_COMMIT, PAGE_EXECUTE_READWRITE
               );

       

        // 3: write function to the remote thread   

        WriteProcessMemory( hProcess, pCodeRemote,

               &RejectFunc, cbRejectFunc, &dwNumBytesXferred

               );

       

        //  4: call function in the remote thread (pCodeRemote)

        hThread = CreateRemoteThread(hProcess, NULL, 0,

        (LPTHREAD_START_ROUTINE) pCodeRemote, pDataRemote,

                0 , &dwThreadId);

 

WaitForSingleObject(hThread, INFINITE); // wait! while DLL code execute

       

// 5: get loaded module

GetExitCodeThread(hThread, &hLibModule);

 

// Close handle, and free dll

CloseHandle(hThread);

VirtualFreeEx(hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE);

       

}

Posted by skensita
Programming/MFC2008. 11. 14. 10:45
배포전에 Dependency를 이용 꼭 필요한 파일의 목록을 확인한다.(8버젼 부터는 안먹히기도 하나 그래도 꼭 확인한다)
VC에 있는 Dependency툴은 단일파일로 실행되고 한번만 실행해주면
윈도우 탐색기 컨텍스트 메뉴에 자동으로 View Dependencies를 추가해준다.

VC 6 이하(~ VS 6)
 MSVCRT, MFC42 등의 MFC버젼과 관련된 DLL과 추가로 사용한 DLL을 같이 배포한다.
 98이후부터는 운영체제에 포함되어 있다.
참고로 http://activex.microsoft.com/controls/vc/mfc42.cab로 관련 DLL을 다운받을 수 있으며
ActivX 배포시 inf파일에 추가해서 자동으로 설치되게 만들 수도 있다.

VC 7(VS 2003)
MSVCR71, MFC71
이 DLL은 최신 운영체제라고 해서 더는 기본 내장을 해 주지않기 때문에 응용 프로그램이 알아서 자기 디렉터리나 윈도우 시스템 디렉터리에다 구비해야 한다.

VC 8이후(VS 2005~)
side by side asembly라는 기술이 도입되어 windows 디렉토리에 밑에 WinSxS(Windows Side-by-Side) 폴더에 추가 작업을 해야한다.
Dependency를 사용해서 관련 DLL을 같이 배포해도 제대로 실행된다는 보장을 할 수 없다.
관련 DLL들은 C:\Program Files\Microsoft Visual Studio 8\VC\redist\x86\Microsoft.VC80.CRT(이하 2005기준)에서 얻을수 있으며
MFC를 안쓰고 release버전만 배포한다면 Microsoft.VC80.CRT.manifest, msvcp80.dll, msvcr80.dll만 배포해도 된다.
(msvcm80.dll은 매니지드C++용 crt라 native를 쓰는 경우는 배포하지 않아도 된다.)

배포방법으로는
1. 재배포 패키지를 이용한다. 링크에서 다운받아 설치해도 되며 다른 배포방법도 설명하고 있다.

설치 디렉토리에서 얻기
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages\vcredist_x86

Microsoft Visual C++ 2005 SP1 재배포 가능 패키지(x86)
http://www.microsoft.com/downloads/details.aspx?familyid=200B2FD9-AE1A-4A14-984D-389C36F85647&displaylang=ko

Microsoft Visual C++ 2008 재배포 가능 패키지(x86)
http://www.microsoft.com/downloads/details.aspx?FamilyID=9b2da534-3e03-4391-8a4d-074b9f2bc1bf&DisplayLang=ko

2. 정적 라이브러리를 사용한다(포함) -> CRT(/MT), MFC, ATL 등
실행파일이 커지긴 하나 제일 간단하다. MFC 라이브러리를 사용시는 MFC라이브러리를 정적으로 포함하면 CRT도 자동으로 /MT로 변경된다. 고로 어디서든 기존보다 더 간단하다. 귀찮아서 ActiveX를 이렇게 배포한적있음 ㅡ.ㅡ;

용량은 기본 MFC 다이얼로그 프로젝트가 아무것도 추가안하고 52k정도에서 308k 정도로 커진다.


3. 인스톨쉴드(Install Shield), 설치 프로젝트를 이용한다.

4. .Net Framework를 설치한다.
managed 로 컴파일했다면 .Net Framework는 필수이다.

-> 추가! MFC,ATL을 사용안한 프로젝트에 한해서만 먹힙니다. 프레임웍만 깔면 WinSxS에 VC80.CRT관련만 설치되네요.

    VC90도 똑같습니다. 재배포 패키지를 설치하셔야 MFC,ATL관련이 깔립니다.

위 방법 외에도 몇가지가 더 있으며 재배포 패키지 링크에 연결된 설명들과 아래 링크들을 참조하기 바란다.
http://www.codeproject.com/cpp/vcredists_x86.asp(codeproject에 소개된 방법)
http://www.serious-code.net/moin.cgi/RedistributingVisualCppRunTimeLibrary

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

재배포 패키지를 이용한 OCX(ActiveX) 배포 (2005 ~)
Cab 파일들어갈 파일 내용은 다음과 같다.
자신이 만든 ocx파일
inf 파일
재배포패키지

INF파일의 내용은 다음과 같다.

[Setup Hooks]
hook1=hook1

[hook1]
run=%EXTRACT_DIR%\vcredist_x86.exe

[version]
signature="$CHICAGO$
AdvancedINF=2.0

[Add.Code]
mytest.ocx=mytest.ocx
vcredist_x86.exe=vcredist_x86.exe

[vcredist_x86.exe]
file-win32-x86=thiscab
DestDir=11
RegisterServer=no

[mytest.ocx]
file-win32-x86=thiscab
DestDir=11
FileVersion=1,0,0,3
clsid={1DF754CB-D85F-490B-9011-BDCB0BFDC430} <= 이건 아시져?
RegisterServer=yes

inf파일의 [Add.Code]는 나열된 순서의 반대로 설치가 되는 점을 유의하며 위와같이 하면
vc2005 재배포 패키지가 실행된후 맨 마지막으로 ocx가 설치된다.


출처 - DEVPIA slipy1님이 쓰신글

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

Visual Studio 6.0 + vmware + windbg 개발환경 설정 정리 문서
Posted by skensita
Programming/Win32 API2008. 10. 9. 15:11
[API] Native API의 개념과 용도, 활용 예(Native API를 이용해서 컴퓨터 전원을 꺼버리자!!)

 안녕하세요. 오랜만에 글을 올리는 수학쟁이입니다. ^^

 오랜만에 글을 쓰는것이니 만큼 팁보다는 개념적인 수준의 글을 올려보겠습니다.

 이번 강좌에서는 Native API라는 놈들의 정체좀 밝혀보겠습니다. ^^..

 API는 크게 두 종류로 나뉘어져있습니다. (명칭만 다르지 구조는 같다고 보시면 됩니다.)
 보통 API는 Win32 API Native API 두 종류 로 나뉘어져있는데요.

 우리가 흔히 접할 수 있는 API는 Win32 API 입니다.

 *** Native API라는 것은 대체 무엇일까요?

 우선 Native API를 알기 전에 API 라는 녀석에 대해서 제대로 파헤쳐봅시다.

 API 라는 놈은 Application Programming Interface의 약자로 운영체제(OS)에서 소프트웨어에게 제공하는 기능 정도로 이해하시면 됩니다.

 이 API라는 놈은 내부적으로 DLL이라는 확장자를 가진 파일(동적 연결 라이브러리; Dynamic Link Library)에 함수 형태로 들어있습니다(* 유식한 말로 Export라고 합니다.).

 *** API는 몇개일까?

 그냥 API라고 하면 종류가 다양한데(Windows API, Java API, 네이버 OpenAPI 등등...) 이 여러 API 중 비베에서 함수 형태로 호출이 가능한 Windows API의 개수만 해도 어마어마합니다. 게다가 프로그래머가 이 윈도우 API를 직접 만들 수 있습니다.(Visual C++ 등의 C 컴파일러를 이용하여)

 그렇다면 프로그래머가 만들지 않은 순수하게 제공되는 'Win32 API'는 몇개일까? Native API만 빼도 대략 어림잡아 자그마치 6,000~7,000개 정도로 예상하고 있습니다. (allapi라는 사이트에서만 집필된 api가 5,000개 정도 되는데 이 수치로는 어림도 없죠.)

 *** API의 종류를 보려면?

 
 API는 아까도 언급했듯이 종류가 한두개가 아닐 뿐더러 사용되는 목적도 제각기 각각 다릅니다. (예: FindWindow 라는 API
는 특정 창의 핸들을 찾는데 사용되어지고, OpenProcess 라는 API는 프로세스의 핸들을 구할 때(접근할때)사용되는 함수이며, DDraw.dll라는 dll의 api들은 DirectDraw에서 사용되는 API입니다.) 게다가 그 수가 약 수천개에 이릅니다. 이를 다 외워서 쓰는것은 불가능하며, 다른 문서나 프로그램을 이용해서 찾아보아야합니다. 그 방법은 아래에 짤막하게 써놓겠습니다.

 Visual Basic에서는 API 텍스트 뷰어라는 뷰어를 제공합니다. 단 이 뷰어는 미리 넣어둔 API밖에 사용하질 못합니다.

 그리고 양도 적고 버그도 있습니다. 그래서 대부분의 VB 프로그래머들은 ApiViewer 2004라는 툴을 사용하거나 직접

 선언문을 만들어서 사용합니다. (ApiViewer 2004 다운로드 게시글 바로가기)

 VB의 API 선언문은 아래와 같은 구조이니까 함수 인자와 dll, API 함수 본명만 안다면 작성할 수 있겠죠.

 (함수 본명이나 인자들은 구글 검색이나 MSDN을 이용하시면 쉽게 찾을 수 있습니다. 물론 C언어 기준으로한 선언문이지만요... DLL에 export된 똑같은 함수니 C에서만 사용 가능한것이 아니라 충분히 VB에서도 쓸수있습니다.)


 [Public/Private] Declare [Sub/Function] 함수 이름 Lib "DLL이름" Alias "함수 원래 이름" ( _

     [ByVal/ByRef] 인자1 As [인자형식], _

     [ByVal/ByRef] 인자2 As [인자형식], _

     [ByVal/ByRef] 인자3 As [인자형식], _

     ...

     [ByVal/ByRef] 인자n As [인자형식] _

 ) [As 반환값형식]

 - 선언문은 필요없고 이름이라도 좋으니 이름이라도 보고싶으시다면, VS6에서 제공하는 툴인 Dependency Walker를 이용해보실 수 있겠습니다.

위치: 시작 메뉴 - Microsoft VIsual Basic 6.0 - Microsoft Visual Studio 6.0 도구들 - Depends

사용자 삽입 이미지

 *** 그렇다면 Native API라는 녀석은 무엇이냐?

 - Windows 9x 시절로 돌아가보면, 9x 시절에는 블루스크린이 유난히 많았습니다. 심지어 빌게이츠 회장이 98 시연중 장치 드라이버 검색 도중에 블루스크린(BSOD; Blue Screen Of Death)가 뜬것만 봐도 알수가 있죠. (http://kr.youtube.com/watch?v=RgriTO8UHvs)

 이는 윈도우 9x의 동작 체계가 매우 불안정했음을 보여주는 단적인 예입니다. 그런데 어째서 그렇게 불안정할까요?
 그리고 NT계열의 OS(NTx, 2000, XP, ...)들은 블루스크린 보기가 왜이렇게 힘들까요? 그 이유은 간단합니다.

 9x는 16비트와의 연계를 할 수 있는데 (그래서 고전 게임의 대다수가 동작할 수 있었죠.), XP와 달리 운영체제 레벨(Ring0)에서 바로 실행해버려서 16비트 프로그램과 32비트 OS 모듈이 충돌할 수가 있었습니다. 충돌되면 그게 블루스크린이 뜨는것이죠.

 Windows NT 계열의 경우 16비트 프로그램을 실행하면 직접 메모리에 올려놓고 실행하는것이 아닌, WOW(Windows-On-Windows)라는 체계에서(wowexec.exe) 16비트 메모리를 에뮬레이팅하고 명령어를 에뮬레이션해서 위험한 명령어를 제외하고 실행시킵니다. 그래서 도스 프로그램의 일부는 XP에서 호환되지 않는것입니다.

 그런데 이 XP가 갑자기 16비트를 에뮬레이트하게 된 내부 요인은 무엇일까요? 이것은 NT에서부터 Intel CPU(R) 및 그의 호환기종(AMD 등)만 지원하게 된 이유와도 관련이 깊습니다. 9x까지는 대부분의 CPU를 지원했으나, NT에서는 인텔 CPU만을 수용하기 시작했습니다. 이 이유중의 하나는 인텔 CPU의 링 보호 체계(Ring Protection System)를 활용하여 운영체제를 보호하는 코드를 추가하기 시작해서 인데요. 이 링 보호 체계는 간단합니다. CPU의 권한을 네등급으로 분류하고(Ring 0 ~ Ring 3) 각 링의 권한에서 실행가능한 명령어를 제한하는것입니다. 가장 좋은 권한이 Ring0이고 가장 낮은 권한이 Ring3이죠. 현재의 WinNT 계열의 OS에서는 프로그램이 실행되면 Ring3이 실행되고, 운영체제(커널)은 RIng0에서 실행됩니다. [ 보통 Ring3은 유저 모드라고 부르고 Ring 0은 커널 모드라고 부릅니다. RIng1-2는 디바이스를 위해 예약되었지만, 현재 대부분의 OS에서는 사용하지 않는 계층입니다. ]

 어떤분은 이렇게도 말하기도 합니다. " 그러면 Ring 3 프로그램에서 CreateFile()을 이용해서 파일을 읽거나 쓸수도 있는데, 그렇게 되면 결과적으로 하드디스크에 접근하게 되는데 이건 어떻게 된거냐 "

 이 질문에 대한 답도 간단합니다. Ring3 권한에서는 '하드웨어 포트 입/출력', '몇몇 일부 레지스터 접근 제한(CRx,DRx,IDTR,EFLAGS...)', '인터럽트 발생', '물리 메모리 접근', '커널 메모리(0x80000000~0xFFFFFFFF) 접근' 등이 제한됩니다. 하드디스크에 직접적으로 데이터를 읽거나 쓰기 위해서는 하드디스크에 해당하는 포트(Hardware I/O Port)에 데이터를 입출력하여 바이너리 레벨에서 데이터를 읽거나 쓸 수 있는데요. 문제는 Ring3에서는 포트 입출력 권한이 없기 때문에 Ring0으로의 권한 이행이 필요합니다. 이 떄 Ring3 API에서 Ring0으로의 권한 이행의 중간 매개 API가 'Native API'입니다.

 CreateFile()이 호출되면 내부적으로 여러 함수들이 호출되는데, 호출되는 과정을 요약하여 그림으로 나타내면 아래와 같습니다.

사용자 삽입 이미지

 (그림에서 XXX()는 생략하였음을 의미합니다. 즉 Ob,Hal,Fs,Iop로 시작하는 내부함수들을 호출하는것을 의미)

 (참고: Ob, Hal, Fs, Iop 등은 각각 Object, HAL(Hardware Abstract Layer), File System, Input/Output [private] 의 약자입니다. 이렇듯 Zw*를 제외한 Native APi들은 모두 의미있는 prefix(접두어)들을 가지고 있습니다.)

 상당히 복잡합니다만... VB에서 Native API라고 하면 위의 ZwCreateFile() 부분일겁니다.

 * Native API는 NTSTATUS라는 이상한 값(?)들로 오류 코드를 반환합니다.
 
함수 반환값 자체가 오류 코드죠. Win32 API는 GetLastError()함수나 Err.LastDllError로 얻어야하지만요.
 
Native API는 성공 했을 때가 STATUS_SUCCESS (0)의 값 (혹은 양수의 Long 값)을 가집니다.
* 여기서 알 수 있으시다시피 Win32 API는 Native API를 더욱 안전하게 쓸 수 있도록 가죽만 씌운것에 불과합니다.

 *** Native API는 활용할 수 없나?

* VB에서도 Native API를 활용할 수 있습니다. 유저 모드에서만 호출 가능한 Native API는 ntdll.dll에 커널 모드에서만 호출이 가능한 API는 ntoskrnl.exe에 들어있기 때문이죠.

 예를 들어 직접적으로 Native API를 호출하여 컴퓨터의 파워를 꺼지게 할 수 있습니다. 파워를 끄는 API는 없지만 Native API에서는 이 기능을 지원하고 있습니다.

 윈도우 종료 API로는 ExitWindows가 있고 이 API는 내부적으로 ZwShutdownSystem이라는 함수를 호출합니다.

 이 함수를 직접 호출하면 간단하게 파워를 종료시킬 수 있습니다.

 * Windows 98에서도 지원하도록 하기 위해 호출에 실패하면 Windows 98의 krnl386.exe::ExitKernel을 호출하도록 지시하게 만들었습니다.

Private Declare Function RtlAdjustPrivilege Lib "ntdll.dll" _
     (ByVal Privilege As Long, _
     ByVal bEnablePrivilege As Long, _
     ByVal IsThreadPrivilege As Long, _
     ByRef PreviousValue As Long) As Long

Private Declare Function ZwShutdownSystem Lib "ntdll.dll" _
    (ByVal Action As Long) As Long

Private Declare Function ExitWindowsEx Lib "user32.dll" _
    (ByVal uFlags As Long, ByVal dwReturnCode As Long) As Long

Private Declare Sub ExitKernelWin9x Lib "krnl386.exe" Alias "EXITKERNEL" ()

Private Const ShutdownPowerOff As Long = 2&
Private Const EWX_FORCE As Long = 4
Private Const EWX_POWEROFF As Long = &H8
Private Const EWX_SHUTDOWN As Long = 1

Private Sub PowerOff()
    On Error Resume Next
    RtlAdjustPrivilege 19, 1, 0, 0&
    If ZwShutdownSystem(ShutdownPowerOff) Then
        If ExitWindowsEx(EWX_POWEROFF Or EWX_FORCE, 0&) = 0 Then
            ExitKernelWin9x
            ExitWindowsEx EWX_SHUTDOWN, 0&
        End If
    End If
End Sub

Posted by skensita