반응형

Microsoft에서는 프로세스간의 주고 받는 메세지를 후킹할 수 있는 함수를 제공한다.
바 로 SetWindowsHookEx()함수이다. 이 함수를 통해 DLL을 특정 프로세스나 모든 프로세스에 Injection할 수 있다.그리고 User-mode(ring 3)에서 동작하는 Keylogger들 대부분이 이 함수를 사용하여 구현된다.(SetWindowsHookEx()함수를 통해 후킹(Hooking)을 할 경우 전역 후킹이 간단하므로 많이 사용한다.)

먼저 SetWindowsHookEx()함수의 원형부터 알아보자.
(MSDN에 있는 내용이다.)

HHOOK SetWindowsHookEx(
    int idHook,
    HOOKPROC lpfn,
    HINSTANCE hMod,
    DWORD dwThreadId
);

첫번째 파라메터인 idHook 변수는 후킹 하고자 하는 메세지의 ID라고 할 수 있다. WH_GETMESSAGE, WH_KEYBOARD 등 다양한 메세지가 있다.(MSDN 참조)
idHook 파라메터에 지정된 값의 이벤트(메세지)가 발생할 경우 훅 프로시저(lpfb 파라메터에 설정된)가 동작하게 된다. 훅 프로시저는 Injection할 DLL(앞으로 Injected.dll이라 부르겠다)에 지정되어 있어야한다.

/* Injected.dll */
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
 /* 원하는 코드 삽입 */
 return CallNextHookEx(0, nCode, wParam, lParam);
};

마 지막에 CallNextHookEx()함수를 호출한 이유는 훅 체인에 있는 다른 훅 프로시저가 해당 메세지를 사용할 수도 있기때문이다. 예를들면 키보드메세지를 후킹하여 Keylogger를 만들었다고 하면 해당 메세지를 후킹 후 로깅을 하고 CallNextHookEx()함수를 통해 처리를 하지 않고 그냥 끝내 버린다면 들통나버릴것이다.

두번째 파라메터는 lpfn이다. 이 파라메터에는 Injected.dll에 지정된  훅 프로시저의 주소값을 지정한다.만약 IdHook에 WH_KEYBOARD를 지정하여 후킹 할경우 해당 훅 프로시저는 KeyboardProc이다. 주소 값을 지정해줘야하는데 하드코딩하여 지정해 줄 수 있지만 LoadLibrary()함수와 GetProcAddress를 이용하여 Injected.dll의 훅 프로시저의 주소를 구할 수 있다.

/* Injector.c */
HMODULE hDll;
unsigned long KeyboardProcAddr;

hDll = LoadLibrary("injected.dll");
KeyboardProcAddr = GetProcAddress(hDll, "KeyboardProc");

다음으로 세번째 파라메터인 hMod를 보자. hMod는 DLL의 핸들(Handle)을 지정해야한다.여기에선 Injected.dll의 핸들을 지정해주면 된다.
이미 Injector.c에서 LoadLibrary("Injected.dll"); 통해 핸들을 얻었으므로 hDll변수를 사용하면 된다.

네 번째 파라메터 dwThreadId는 후킹할 프로세스 즉 Injected.dll을 삽입할 스레드(Thread)의 ID이다. PID라고 하며 해당 파라메터에 0을 지정하면 모든 스레드에 훅(Hook)을 한다는 의미로 전역 훅을 할 수 있다. 특정 스레드에 Injection을 하려고 한다면 "작업관리자"를 통해 PID를 얻어 하드코딩하거나 아래와 같은 방법으로 PID를 얻어 사용할 수 있다.

/* 전역 훅 */
SetWindowsHookEx(WH_KEYBOARD, KeyboardProcAddr, hDll, 0)

/* 지역 훅*/
SetWindowsHookEx(WH_KEYBOARD, KeyboardProcAddr, hDll, GetCurrentThreadID());

dwThreadId 값을 얻기 위한 함수는 많다.

반응형

Static Linked Library, 정적 링크 라이브러리

컴파일시 실행파일에 라이브러리의 함수코드가 추가되어 작성되기 때문에 실행파일이 커지는 단점이 있으나 별도의 DLL이 필요없다.

VC++6에서는 New Projects에 Win32 Static Library로 작성하고 2005에서는 Win32 콘솔 응용프로그램에서 정적라이브러리(미리 컴파일된 헤더 해제)를 선택하여 작성한다.
명령줄에서 작성하려면 cl.exe /c /EHsc 파일명.cpp(/c 옵션은 링크없이 컴파일) 로 컴파일 하여 obj 파일 생성후 lib.exe ~.obj 으로 ~.lib 파일을 생성한다.

만들어진 LIB(~.lib파일)과 ~.h(header파일)을 작성하고자 하는 Project 생성후라이브러리는 Project->Setting->Library modules에서 ~.lib를 추가하고 헤더는 원하는 코드에 추가하여 빌드한다.

Dynamic Linked Libarary(DLL), 동적 링크 라이브러리

우리가 흔히 말하는 DLLs 이다. 컴파일시 실행파일에 라이브러리의 함수코드를 넣지 않고 DLL 이라는 파일에 따로 두어 모듈화를 통해 관리와 테스트가 용이하며 공유를 통해 코드의 낭비를 줄이고 다국어버전등의 리소스교체를 용이하게 한다.

New Projects에서 Win32 Dynamic-link library로 작성한다. 2005에서는 win32 응용프로그램작성에서 DLL 을 선택하면 된다.

Header파일을 공통적으로 사용하기 위한 예시

#ifdef _USRDLL
#define DLLFunction  __declspec(dllexport)
#elseif
#define DLLFunction  __declspec(dllimport)
#endif

#ifdef __cplusplus
extern “C” {
#endif

DLLFunction void somefunc();
#ifdef __cplusplus
}
#endif


~.DEF 파일 
예전에는 ~.def 파일을 통해 dll을 export 하였지만 VC++에서는 __declspec(dllexport)를 통해서 dll 의 함수들을 export한다. 반대로 import시에는 __declspec(dllimport)를 사용한다.

LIBRARY              "TESTDLL.DLL"
DESCRIPTION 'SIMPLEDLL Windows Dynamic Link Library'

EXPORTS
  somefunc   @1


Implicit Linkage, 암시적 링크
LoadLibrary를 사용하지 않는다. 따라서 프로그램이 실행될때 dll이 로드된다.
그러기 위해서는 Static Library와 같이 ~.lib파일과 ~.h 파일이 필요하고 실행시는 같은 폴더 또는 시스템폴더(dll을 찾는 정책에 따라)에 dll 파일이 존재하여야 한다. 없을시 찾을수 없다면 에러가 발생한다.
dll을 생성하고 나면 생성된 폴더에 lib 파일도 같이 생성되는데 Static Library와는 달리 함수의 코드가 없다. 다만 함수형이 정의되어 있을뿐이다. 실행파일의 implict section에서 실행시 로드되어야 할 dll을 추가하여 나중에 프로그램이 실행되면 dll이 로드된다.

Explicit Linkage,  명시적 링크
  LoadLibrary로 명시적으로 dll을 로드한다. GetProcAddress로 사용하고자 하는 함수의 주소를 얻어서 사용한다. dll이 load되는 시점은 프로그램이 실행되는 순간이 아니고 LoadLibrary가 호출되는 시점이다.

Delay Linkage, 지연 링크
빌드시 옵션에 /delayload:dllname 으로 작성한다.
로드되는 시점은 프로그램 실행시가 아니라 함수가 사용되는 시점이 된다.
/delay 옵션도 참고할것

확장DLL
MFC 클래스에서 파생된 재사용 가능한 클래스를 구현한 DLL. 따라서 MFC로 만든 응용 프로그램에서만 확장 DLL이 사용되어 질수 있다.
이 DLL로부터 익스포트할 클래스에 대해서 여러분이 해줄일은 클래스 선언문에 AFX_EXT_CLASS란 매크로만 붙여준다.

예시

Class AFX_EXT_CLASS CDllDialog : public CDialog
{
...
}

반응형

DLL - 함수 호출시 __declspec(dllimport)의 사용

Level of Difficulty 1 2 3

DLL을 통해서 API를 제공하기 위해서 DLL(PE)의 Export Table에 함수를 나타내도록 __declspec(dllexport)지시자를 사용합니다.
반면에 함수를 호출하는 쪽에서는 특별히 __declspec(dllimport)으로 정의된 함수원형을 사용하지 않아도 DLL에서 제공된 함수를 사용 할 수 있습니다.


결론 부터 말씀드리면 DLL에서 제공되는 함수를 사용할 때에는 함수선언 앞에 __declspec(dllimport) 지시자를 사용할 것을 강력히 권합니다.

대부분 DLL의 API는 헤더에서 다음과 같이 선언 되어있습니다.

// API Export/Import Header

#ifdef CALLEE_EXPORTS
#define CALLEE_API __declspec(dllexport)
#else
#define CALLEE_API __declspec(dllimport)
#endif

CALLEE_API DWORD __stdcall no_no_stdcall(DWORD); // WINAPI
CALLEE_API DWORD __cdecl no_no_cdecl(DWORD); // CDECL
CALLEE_API DWORD __fastcall no_no_fastcall(DWORD);
위 헤더의 핵심은 동일한 헤더를 공유해서 사용하게 되는 것입니다.
(특히 ThirdParty 벤더에서 제공되는 API모듈이라면 더욱더 그렇겠죠)

즉, Preprocessor definitions에 CALLEE_EXPORTS가 선언되어 있지 않다면 헤더는 함수를 __declspec(dllimport)형식으로 정의하게 되겠죠.
(위의 API 제공하는 벤더는 프로젝트 설정에 CALLEE_EXPORTS를 정의해서 컴파일하고 있을 것입니다.)

위의 예는 가장 확실하게 __declspec(dllimport)를 선언해서 사용할 수 있는 경우이고 헤더가 중복해서 관리해야하는 어려움도 필요없는 최적의 케이스 입니다.

하지만 내부 프로젝트 모듈이건 다른 벤더의 모듈이던 이렇게 사용되지 않고 다음과 같이 import용으로 별도로 정의되어서 사용되는 경우가 많이 있습니다.

// API Import Header

DWORD __stdcall no_no_stdcall(DWORD); // WINAPI
DWORD __cdecl no_no_cdecl(DWORD); // CDECL
DWORD __fastcall no_no_fastcall(DWORD);
이 경우는 헤더 관리의 복잡성을 떠나서 별로 좋지 않은 케이스 입니다.
(최소한 다른 DLL에 선언되어 있는 함수를 호출하는 경우에...)

DWORD __stdcall no_no_stdcall(DWORD);
__declspec(dllimport) DWORD __stdcall no_no_stdcall(DWORD);
DLL을 static link 할 경우에 위의 두가지 경우 모두 정상적으로 수행됩니다.
그렇다면 차이점이 무엇일까요?


DWORD __stdcall no_no_stdcall(DWORD);
위와 같이 선언된 상태에서 링커는 DLL에서 생성된 빈 Lib에서 가르키고 있는 Stub을 직접 호출하도록 합니다. 왜냐하면 컴파일러는 no_no_stdcall() 이라는 함수가 내부에 선언된 함수인지 다른 모듈(DLL)에 선언되어 있는 함수인지를 구별 할 수 없기 때문입니다.

그렇다면 이 경우 실제 해당 DLL의 함수가 어떻게 호출 될까요?
DLL생성시 함께 생성되는 빈 Lib의 Stub내부에는 실제 모듈(DLL)의 함수로 점프할 수 있는 코드가 컴파일 타임에 이미 생성되서 Stub안에 존재하기 때문에 가능한 것입니다.
즉 해당 Stub을 내부 함수처럼 호출하면 그곳에서 실제 모듈(DLL)의 함수를 가르키는 IAT(Import Address Table)를 참조해서 실제 API가 점프하게 되는 코드가 수행되는 것입니다.


정리해서 말씀드리면, DLL의 함수를 호출할 경우에 아래와 같은 두가지 형태의 코드가 생성될 수 있습니다.

// 1. 함수원형 앞에 __declspec(dllimport) 지시자를 사용할 경우
0xXXXXXXXX : CALL DWORD PTR [0x56780000]; // CALL DWORD PTR[_imp__MyFunc];


// 2. 지시자 없이 그냥 사용할 경우
0xXXXXXXXX : CALL 0x12340000;
0x12340000 : JMP DWORD PTR[0x56780000];


(0x5678000 : 실제함수주소를 가르키고있는 IAT의 주소)
위에서 설명드렸듯이 당연히 1번의 경우 DLL 함수호출시 훨씬 효과적인 것을 알 수 있습니다.
2번의 경우에는 JMP를 위한 추가적인 5Byte의 코드가 더 필요하게 되는 경우이므로 비효율적이겠죠.

__declspec(dllimport)지시자를 사용함으로서 1번의 코드형태로 DLL함수가 호출 되는 것을 알았습니다.
좀 더 설명드리면 컴파일러는 __declspec(dllimport) 지시자를 보았을 경우에 실제함수 MyFunc을 참조하지 않고 __imp__MyFunc을 참조하는 코드를 만들어 놓습니다.
그후에 링커는 __imp_ Prefix를 보고 실제 DLL에 존재하는 함수를 직접 호출하는 것임을 알게 되는 것입니다.

즉, 컴파일리가 링커에게 DLL에 존재하는 함수를 호출한다는 것을 알려주기 위해 __imp_ Prefix를 함수앞에 추가해 주는 것입니다.


: error LNK2001: unresolved external symbol __imp__MyFunc

: fatal error LNK1120: 1 unresolved externals
위의 에러를 보면 실제 함수앞에 __imp__ Prefix가 존재하는 것을 알 수 있습니다.

그런데 이 경우에 링크에러가 발생했는데 링크에러가 발생하지 않으려면 빈 Lib안에 __imp__MyFunc에 대한 Stub이 존재하고 있어야 겠죠.
DLL생성시 만들어진 Lib의 내용을 보면 MyFunc, __imp__MyFunc에대한 두가지 Stub이 모두 존재하고 있음을 볼 수 있습니다.

----------------------------------------------------------------------------------
// 확장DLL사용시 참고 사항

AFX_EXT_CLASS는  요렇게 정의되어 있습니다..


#ifdef _AFXEXT

    #define AFX_EXT_CLASS    __declspec(dllexport)

#else

    #define AFX_EXT_CALSS    __declspec(dllimport)

#endif // _AFXEXT


이 선언은 Extended DLL에서 export, 다른 프로젝트에서는 import로 해석됩니다...



그래서 결론만 말하면...........


#ifdef _MYEXT

    #define MY_EXT_CLASS    __declspec(dllexport)

#else

    #define MY_EXT_CLASS    __declspec(dllimport)

#endif


이렇게 헤더파일 처음에 정의하고,


class AFX_EXT_CALSS AAA

{

};



class MY_EXT_CLASS AAA

{

};


이렇게 선언합니다.......


그리고 DLL의 프로젝트 Setting에서 _MYEXT를 선언합니다.

(Project Menu의 Setting 다이얼로그에서 C++ tab의 Preprocessor

definitions에 _MYEXT를 추가합니다.)



이렇게 하면 class AAA는 DLL에서는 export, 다른 곳에서는 import가

됩니다.....

반응형

__declspec(extended-decl-modifier-seq) : 데이터나 모듈의 저장형태를 지정.

 

align(#)

    자주 사용되는 데이터를 특정 프로세서의 캐시라인 크기로 정렬해 캐시성능을 높인다. #값은

    1부터 8192 (바이트) 까지 2의 거듭제곱 수 즉 2, 4, 8, 16, 32, 64.

    Visual C++은 기본적으로 데이터 크기에 따라 경계정렬을 한다. 가령 4바이트 정수는 4바이트

    경계, 8바이트 더블은 8바이트 경계에 정렬되며, 클래스나 구조체의 데이터들은 현재 패킹 설정

    (#pragma pack(n) 또는 /Zp 옵션)에 따라 클래스나 구조체 내에 최소크기로 정렬된다.

    구조체, 공용체, 클래스, 변수에 대해 사용할 수 있지만, 스택변수에는 사용불가.

 

    #define CACHE_LINE      32

    #define CACHE_ALIGN    __declspec(align(CACHE_LINE))

    struct CACHE_ALIGN S1

    {

        int a, b, c, d;                // 이 타입의 모든 인스턴스는 32바이트 경계에서 시작해야하며,

    };                                    // sizeof(struct S1)는 32. 뒤따르는 16바이트는 채우기용

   

    __declspec(align(8)) struct S2

    {

        int a, b, c, d;               // sizeof(struct S2)는 16

    };

 

    struct S3

    {

        int a;                          // 4바이트 뒤를 잇는 28 바이트가 채우기용으로 쓰이며,

        struct S1 s1;              // s1은 오프셋 32에서 시작. sizeof(struct S3)는 64

    };

 

    // 배열의 시작주소는 32바이트로 정렬되지만, 각 배열멤버는 그렇지 않다.

    CACHE_ALIGN int array[128];

    // 배열의 각 멤버를 정렬하는 예

    typedef CACHE_ALIGN struct { int a; } S4;

    S4 array[10];

 

    TLS(Thread Local Storage : 정적변수가 스레드마다 다른 값을 갖도록 윈도우에서 제공

    하는 메모리 영역)에서 데이터 정렬

    __declspec(thread) 속성으로 만들어지고, 이미지에서 .tls 섹션에 놓인 TLS는 일반적인 정적

    데이터와 똑같이 정렬된다. 운영체제는 .tls 섹션 크기만큼 데이터를 할당하고 .tls 섹션 정렬

    속성을 고려해 TLS 데이터를 만든다.

 

    아래는 TLS에 정렬된 데이터를 놓는 다양한 방법들이다.

    __declspec(thread) __declspec(align(32)) int a;    // TLS에 정렬된 정수 놓기

 

    // 정렬된 구조체를 정의하고 구조체 타입의 변수를 TLS에 놓기

    __declspec(thread) __declspec(align(32)) struct F1 { int a; int b; } a;

 

    이터 패킹과 함께 정렬하는 방식

    /Zp 옵션과 #pragma pack(n)은 구조체와 공용체 멤버 데이터를 패킹하는 효과가 있다.

    /Zp 옵션과 __declspec(align(#))을 같이 사용한 예와 각 멤버의 오프셋

 

    struct S{                                // 변수        /Zp1    /Zp2    /Zp4    /Zp8

        char a;                               //   a            0          0         0         0

        short b;                              //   b           1          2         2         2

        double c;                            //   c            3          4         4         4

        CACHE_ALIGN double d;    //   d           32        32       32       32

        char e;                               //   e           40        40       40        40

        double f;                            //    f           41        42       44        48

    };                                           //sizeof(S)  64        64       64        64

 

    따라서 객체의 오프셋은 객체가 __declspec(align(#)) 속성을 갖지 않으면, 이전 객체의

    오프셋과 현재 패킹 설정에 따라 결정되며, 그렇지 않으면 이전 객체의 오프셋과 객체의

    __declspec(align(#)) 값에 따라 결정된다.



allocate(segname)

    데이터가 할당될 데이터 세그먼트 지정. 가능한 segname pragma는

    code_seg, const_seg, data_seg, init_seg, section

 

    ㄱ. #pragma code_seg( ["섹션이름"[, "섹션클래스"]] )

    함수가 할당될 기본 코드섹션 지정. 클래스와 섹션이름은 선택적이며, 섹션이름이 없는 경우

    컴파일 시작시 값으로 재설정된다

 

    ㄴ. #pragma const_seg( ["섹션이름"[, "섹션클래스"]] )

    상수데이터의 기본 섹션 지정. 모든 상수데이터를 하나의 읽기전용 섹션에 넣을 때 사용

    #pragma const_seg("MY_DATA")    // 이후의 모든 상수데이터는 MY_DATA에 놓여짐

    const_seg pragma로 할당된 데이터는 어떤 위치정보도 유지하지 않는다.

    "섹션클래스"는 Visual C+ 2.0 이전 버전과의 호환을 위해 포함되며, 이제는 무시된다

 

    ㄷ. #pragma data_seg( ["섹션이름"[, "섹션클래스"]] )

    초기화된 데이터의 기본 섹션 지정

    #pragma data_seg("MY_DATA")    // 이후의 모든 데이터는 MY_DATA에 놓여짐

    data_seg pragma로 할당된 데이터는 어떤 위치정보도 유지하지 않는다.

    "섹션클래스"는 Visual C++ 2.0 이전 버전과의 호환을 위해 포함되며, 이제는 무시된다

 

    ㄹ. #pragma init_seg( {compiler | lib | user | "섹션이름"[, 함수이름]} )

    시작코드가 실행되는 순서에 영향을 주는 키워드나 코드섹션 지정. 전역 정적 객체의 초기화는

    실행코드를 포함할 수 있으므로, 언제 객체가 생성되는지를 정의하는 키워드를 지정해야한다.

    특히 이는 초기화가 필요한 라이브러리 또는 DLL에서 init_seg pragma 사용시 중요하다.

    compiler

             Microsoft C 런타임 라이브러리 초기화를 위해 예약됨. 이 그룹의 객체가 먼저 생성됨

    lib

             타사의 클래스 라이브러리 초기화. 이 그룹의 객체는 compiler 그룹 바로 뒤에 생성됨

    user

             그 외. 이 그룹의 객체는 마지막에 생성됨

    섹션이름

             초기화 섹션을 명시적으로 지정. 사용자지정된 섹션이름의 객체는 암묵적으로 생성되지

             않지만 그 주소는 명명된 섹션에 놓여진다.

             섹션이름은 pragma 이후의 모듈에 선언된 전역 객체를 생성할 도우미 함수에 대한

             포인터를 포함한다.

    함수이름

             프로그램 종료시 atexit 대신에 호출될 함수 지정. 또한 이 도우미 함수는 전역객체용

             파괴자에 대한 포인터를 갖고 atexit 또한 호출한다. 다음처럼 함수 식별자를 지정하면,

             int myexit( void (__cdecl *pf) (void) )

             C 런타임 라이브러리인 atexit 대신 사용자 함수가 호출된다. 이로써 객체를 파괴할 준비

             가 됐을 때 호출되어야 할 파괴자 목록을 만들 수 있다.

 

    초기화를 연기해야 한다면(DLL의 경우에), 섹션이름을 명시적으로 지정할 수 있다.

    atexit 대체 식별자는 인용부호가 없으며, 여전히 사용자 객체는 XXX_seg pragma로 정의된

    섹션에 놓일 수 있다.

    모듈에 선언된 객체는 C 런타임에 의해 자동으로 초기화되지 않으므로, 직접 해주어야 한다.

    #include <stdio.h>

    // init_seg pragma는 허용되지 않은 섹션이름을 사용하므로 C4075 경고(초기자가 허용되지

    // 않은 영역에 놓인다)를 발생시키는데 이를 무시

    #pragma warning(disable : 4075)

 

    typedef void (__cdecl *PF)(void);

    int    cxpf = 0;     // 호출해야 할 파괴자 수

    PF    pfx[200];    // 파괴자에 대한 포인터. 오버플로우에 주의!

 

    int myexit(PF pf){

        pfx[cxpf++] = pf;

        return 0;

    }

 

    struct A{

        A(){ puts("A()"); }

        ~A(){ puts("~A()"; }

    }

 

    // pragma init_seg 이전이므로, 생성자와 파괴자는 CRT 시작코드에서 호출

    A aaaa;

 

    // 여기서 순서가 중요하며, 섹션이름은 8자 이하다.

    // $ 이전에 동일한 이름의 섹션은 하나의 섹션으로 합쳐진다. 합쳐지는 순서는 $ 이후 문자를

    // 정렬함으로써 결정된다.

    // InitSegStart와 InitSegEnd는 경계를 설정하는데 사용되며, 이로써 초기화를 위해 호출할

    // 실제 함수를 찾을 수 있다.

 

    #pragma data_seg(".mine$a")

    PF InitSegStart = (PF)1;

    #pragma data_seg(".mine$z")

    PF InitSegEnd = (PF)1;

    #pragma data_seg()

 

    // 0값 비교는 중요하다.

    // 지금부터, 각 섹션은 256바이트다. 각 섹션이 통합될 때, 각각은 0으로 채워진다.

    // 섹션이 256바이트인것은 보장할 수 없지만, 0으로 채워지는 건 보장할 수 있다.

 

    void InitializeObjects(){

        PF *x = &InitSegStart;

        for(++x; x<&InitSegEnd; ++x){

            if(*x) (*x)();

        }

    }

 

    void DestroyObjects(){

        while(cxpf>0){

            --cxpf;

            (pfx[cxpf])();

        }

    }

 

    #pragma init_seg(".mine$m", myexit)

 

    // 지금부터 생성자와 파괴자를 호출한다

    A bbbb;

    A cccc;

 

    int main(){

        InitializeObjects();

 

        // 여기서 잡다한 작업을 한다

 

        DestroyObjects();

        return 0;

    }

dllimport, dllexport

    DLL에서(로) 함수, 데이터, 객체를 가져오는(내보내는) 속성. 함수를 dllexport로 선언하면,

    적어도 노출된 함수와 관련된 .DEF (module-definition) 파일이 필요없어진다.

    또한 dllexport__export 키워드를 대체한다.

 

    __declspec( dllimport ) int i;

    __declspec( dllexport ) void func();

 

    좀 더 보기좋게 다음과 같이 매크로를 쓸 수도 있겠다.

 

    #define DllImport __declspec( dllimport )

    #define DllExport __declspec( dllexport )

 

    DLL 인터페이스에 포함된 모든 선언은 선언을 내포하고 있는 dllimport 또는 정의를 내포하고

    있는 dllexport이며, externdllexport를 같이 사용해 선언을 강제화할 수도 있다.

 

    DllImport int func(){ return 1; }    // 에러 : dllimport는 정의할 수 없다

    DllImport int i = 10;                       // 요것도 정의이므로 에러

    DllExport int i = 10;                       // 요건 OK

 

    extern DllImport int k;                   // 둘다 OK

    DllImport int j;

 

    static __declspec( dllimport ) int l;          // 에러. extern으로 선언되지 않음

    void func()

    {

        static __declspec( dllimport ) int s;      // 에러. extern으로 선언되지 않음

        __declspec( dllimport ) int m;              // OK. 이것은 선언

        __declspec( dllexport ) int n;               // 에러. 지역범위에서 외부정의를 포함

        extern __declspec( dllimport ) int i;     // OK. 이것은 선언

        extern __declspec( dllexport ) int k;    // OK. extern은 선언을 포함

        __declspec( dllexport ) int x = 5;         // 에러. 지역범위에서 외부정의를 포함

    }

 

    라인함수로 선언하기

 

    dllexport 함수를 인라인으로 선언하면, 모듈이 그 함수를 참조하던말던 함수는 항상 초기화

    되고 익스포트된다. 함수는 다른 프로그램에 의해 임포트된다고 가정한다.

    dllimport 함수를 인라인으로 선언하면, 그 함수는 확장될 수 있지만 (/Ob가 지정돼 있을 때)

    절대 초기화되지는 않는다. 특히 인라인 임포트 함수의 주소를 가지면, DLL 내의 함수 주소가

    리턴된다. 이는 인라인이 아닌 임포트 함수의 주소를 가져오는 것과 같은 방식이다.

    이 규칙은 클래스 안에서 정의된 인라인 함수에 적용된다. 게다가, 인라인 함수 내 정적 지역

    데이터와 문자열은 마치 같은 프로그램 (즉, DLL 인터페이스가 없는 실행 파일) 안에 있는 것

    처럼 DLL과 클라이언트 사이에 동일성을 유지한다.

    임포트된 인라인 함수는 주의깊게 제공해야한다. 가령 DLL을 업데이트할 때, 클라이언트가

    바뀐 버전의 DLL을 사용할 거라고 가정하지 않는다. 적절한 버전의 DLL을 로딩하려면, DLL

    클라이언트 또한 재빌드해야한다.

 

    반 규칙과 제한사항

 

    ■ 함수나 객체를 dllimportdllexport 없이 선언하면 이는 DLL 인터페이스 일부로 간주되

    지 않으므로, 그들의 정의는 선언한 모듈이나 같은 프로그램 안의 다른 모듈에 있어야한다. 함수

    나 객체를 DLL 인터페이스로 만들려면 다른 모듈에서 그 정의를 dllexport로 선언해야하며, 그

    렇지 않을 경우 링커에러가 발생한다.

    함수나 객체를 dllexport로 선언하면, 그 정의는 같은 프로그램 모듈에 있어야하며, 그렇지 않

    을 경우 링커에러가 발생한다.

 

    ■ 프로그램 내 단일 모듈이 같은 함수나 객체에 대해 dllimport와 dllexport 2개의 선언을 가지

    면, dllexport 속성이 dllimport 속성보다 우선한다. 하지만, 컴파일러 경고는 발생한다.

 

    __declspec( dllimport ) int i;

    __declspec( dllexport ) int i;    // 경고; 불일치; dllexport가 우선한다

 

    ■ C++에서는 전역 포인터 데이터 또는 정적 지역 포인터 데이터를 dllimport 데이터 객체의

    주소로 초기화할 수 있다. 하지만 C에서는 에러다. 또한, 정적 지역 포인터 함수를

    dllimport 함수 주소로 초기화할 수 있다. C에서는 함수 주소가 아닌 DLL 임포트 청크 (함수에

    대한 제어를 전송하는 코드 조각) 의 주소로 설정한다.

 

    __declspec( dllimport ) void func1( void );

    __declspec( dllimport ) int i;

 

    int *pi = &i;                                       // C에서는 에러

    static void ( *pf )( void ) = &func1;    // C에서는 청크 주소, C++에선 함수 주소

 

    void func2()

    {

        static int *pi = &i;                              // C에서는 에러

        static void ( *pf )( void ) = &func1;    // C에서는 청크 주소, C++에선 함수 주소

    }

 

    하지만 dllexport 객체 선언을 가진 프로그램은 정의도 포함해야하므로, 전역 또는 지역 정적

    포인터 함수를 dllexport 함수의 주소로 초기화할 수 있다. 비슷하게, 전역 또는 지역 정적 포인

    터 데이터를 dllexport 데이터 객체의 주소로 초기화할 수도 있다. 예를 들어 다음 코드는 C와

    C++에서 모두 에러가 발생하지 않는다:

 

    __declspec( dllexport ) void func1( void );

    __declspec( dllexport ) int i;

 

    int *pi = &i;

    static void ( *pf )( void ) = &func1;

 

    void func2()

    {

        static int *pi = &i;

        static void ( *pf )( void ) = &func1;

    }

 

    래스에서 사용하기

 

    dllimportdllexport로 클래스를 선언하면, 하나의 클래스 전체에 적용된다. 즉 모든 멤버함

    수와 정적 데이터가 익스포트된다. 이렇게 익스포트 된 클래스를 익스포터블 클래스라 한다.

 

    #define  DllExport  __declspec( dllexport )

    class DllExport C

    {

        int i;

        virtual int func( void ){ return 1; }

    };

 

    참고 익스포터블 클래스의 멤버는 dllimportdllexport를 명시적으로 쓸 수 없다.

 

    dllexport 클래스는 모든 멤버함수와 정적 데이터멤버가 익스포트되므로, 프로그램 내에 정의

    를 포함해야한다. 그렇지않으면 링커에러가 발생한다. 단 순수가상함수의 경우 예외지만, 가상

    클래스 파괴자는 기본클래스 파괴자에 의해 항상 호출되므로, 순수가상파괴자는 항상 정의해야

    한다.

    이런 규칙들은 익스포터블 클래스가 아니여도 적용되므로, 클래스타입의 데이터나 클래스를 리

    턴하는 함수를 익스포트하려면, 클래스를 익스포트해야한다.

 

    dllimport 클래스는 모든 멤버함수와 정적 데이터멤버가 임포트된다. 비클래스 타입의

    dllimport/dllexport와는 달리, 정적 데이터멤버는 dllimport 클래스가 정의된 프로그램 내에

    정의할 수 없다.

 

    익스포터블 클래스의 모든 기본 클래스는 익스포트될 수 있어야한다. 아니면, 컴파일러 경고가

    발생한다.

이로써 dllexport 클래스는 dllimport 클래스에서 상속받을 수 있고, dllimport 클래스는 dllexport 클래스에서 상속받을 수 있다

반응형

Win32 API 주요 함수

AdjustWindowRect : BOOL AdjustWindowRect(LPRECT lpRect, DWORD dwStyle, BOOL bMenu);
작업 영역의 크기는 윈도우 크기에서 타이틀 바와 경계선, 메뉴, 스크롤 바 등의 영역을 제외한 영역이다. 일반적으로 MoveWindow, SetWindorPos 등의 함수로 윈도우 크기는 원하는대로 바꿀 수 있지만 작업 영역의 크기를 원하는대로 설정하는 방법은 없다. 작업 영역을 특정한 크기대로 설정하려면 이 함수로 원하는 윈도우 크기를 먼저 계산하고 MoveWindow 등의 함수로 윈도우 크기를 변경해야 한다.
이 함수는 윈도우의 스타일(타이틀 바의 유무, 타이틀 바의 높이, 경계선의 두께)와 메뉴의 존재 여부 등을 고려하여 작업 영역의 크기가 lpRect가 될 수 있는 윈도우 크기를 구해 lpRect로 리턴해 준다. 단 이 함수는 메뉴 바가 두 줄로 출력되어 있는 경우와 스크롤 바의 존재에 대해서는 정확한 계산을 해 주지 않는다. 만약 스크롤 바를 가지고 있다면 구해진 영역 크기에 스크롤바의 높이와 폭을 더해 주어야 한다

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

BeginPaint : HDC BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint);
윈도우즈 환경에서 화면이나 프린터로 출력을 하려면 DC를 먼저 구해야 한다. DC를 구하는 일반적인 방법은 두 가지가 있는데 GetDC와 ReleaseDC를 사용하는 방법이 있고 BeginPaint와 EndPaint를 사용하는 방법이 있다. BeginPaint와 EndPaint는 짝을 이루어 사용되며 반드시 WM_PAINT 메시지 내부에서만 사용해야 한다.

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

BitBlt :
BOOL BitBlt(HDC hDC,int X,int Y,int nWidth,int nHeignt, HDC hSrcDC,int XSrc,int ySrc,DWORD dwROP);
하나의 DC에 있는 비트맵을 다른 DC로 복사하는 비트맵 전송함수이다. 이때 두 DC는 호환되어야 하나 만약 색상 포맷이 호환되지 않을 경우 BitBlt는 복사원의 색상 포맷을 복사처의 포멧에 맞게 변경한다. 비트맵을 화면에 출력하기 위해서는 우선 CreateCompatibleDC함수를 사용하여 메모리 DC를 만들어야 하며 SelectObject 함수를 사용하여 메모리 DC에 출력하고자 하는 비트맵을 선택한 후 BitBlt로 실제 화면 DC에 전송한다. 이때 비트맵은 원본 그대로 복사가 되지만 ROP코드에 따라 배경과 함께 논리 연산되어 변형될 수는 있다. 복사원의 비트맵은 복사처의 맴핑모드에 따라 크기가 커지거나 작아지기도 한다. 모든 장치가 BitBlt를 지원하는 것은 아니므로 GetDeviceCaps를 사용하여 BitBlt를 쓸 수 있는 장치인가를 확인해 보아야 한다

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

CheckDlgButton : BOOL CheckDlgButton(HWND hDlg, int nIDButton, UINT uCheck);
체크 버튼이나 라디오 버튼 등 체크 상태를 가지는 버튼의 체크 상태를 변경한다. 이때 해당 컨트롤로 BM_SETCHECK 메시지를 보낸다.

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

CheckRadioButton :
BOOL CheckRadioButton( HWND hDlg, int nIDFirstButton, int nIDLastButton, int nIDCheckButton );
대화상자에 배치된 라디오 버튼 그룹 중 하나의 라디오 버튼을 체크한다. 두번째 인수와 세번째 인수는 라디오 버튼의 그룹의 지정하되 이 두 ID 사이에 속한 라디오 버튼들을 같은 그룹으로 인식한다. 이 함수는 네번째 라디오 버튼을 체크하되 그룹에 속한 다른 모든 버튼의 체크는 해제한다. 라디오 버튼의 체크 상태를 변경하기 위해 각 라디오 버튼으로 BM_SETCHECK 메시지를 보낸다.

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

ClientToScreen : BOOL ClientToScreen(HWND hWnd, LPPOINT lpPoint);
hWnd의 작업 영역 원점을 기준으로 하는 좌표 lpPoint를 전체 화면을 기준으로 하는 좌표로 변환한다. hWnd윈도우의 작업 영역 원점의 화면 좌표가 cx, cy일 때 lpPoint는 lpPoint.x + cx, lpPoint + cy로 변환된다. 작업 영역의 좌표를 받아 GetCurosrPos, MoveWindow 등과 같이 화면 좌표를 요구하는 함수로 좌표를 전달하기 위해 이 함수를 사용한다.

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

CloseHandle : BOOL CloseHandle(HANDLE hObject);
열려진 핸들을 닫는다. 대상이 되는 핸들은 파일, 파일 맵핑, 콘솔 입력, 콘솔 버퍼, 소켓, 프로세스, 스레드, 뮤텍스, 이벤트, 세마포어 등이다. Win32 환경에서 핸들로 표현되는 대부분의 커널 오브젝트를 닫는다. 그러나 아이콘, 윈도우, 펜, 브러시 등의 유저 오브젝트, GDI 오브젝트는 이 함수로 닫을 수 없다.
이 함수는 핸들의 사용 카운트를 1 감소시키고 오브젝트를 계속 유지할 것인가를 결정한다. 오브젝트를 가리키는 마지막 핸들이 닫히면 오브젝트를 제거한다. 단, 스레드는 이 함수로 핸들을 닫는다고 해서 스레드가 파괴되는 것은 아니다.

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

CloseWindow : BOOL CloseWindow( HWND hWnd );
hWnd윈도우를 최소화하여 아이콘 상태로 만든다. 단순히 최소화시킬 뿐이며 윈도우를 파괴하는 것은 아니다. 윈도우는 아이콘 영역으로 이동하며 아이콘 아래에 윈도우의 타이틀이 출력된다.

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

CopyFile : BOOL CopyFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, BOOL bFailIfExists);
파일을 복사하여 새로운 파일을 만든다. DOS의 copy 명령과 사용하는 방법이 동일하다. CopFile("a","b", FALSE);는 a파일을 복사하여 b 파일을 만들되 이미 b 파일이 있으면 덮어쓴다. 파일의 속성은 복사되나, 보안 속성은 복사되지 않는다.
이 함수는 원본 파일을 읽어 새 파일을 완전히 작성한 후에 리턴한다. 만약 파일 복사중에 경과를 보여주고 싶다면 CopyFileEx 함수를 사용해야 한다. 복사중에 데이터를 변경하거나 검사하고 싶다면 이 함수를 사용할 수 없으며 ReadFile, WriteFile 함수로 직접 파일의 데이터를 일정량만큼 복사해 주어야 한다.

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

CreateCompatibleDC : HDC CreateCompatibleDC(HDC hdc);
인수로 주어진 hdc와 호환되는 메모리 DC를 생성한다. 여기서 호환된다는 뜻은 색상 포맷이 같다는 뜻이며 색상 포맷이 같은 DC끼리는 비트맵을 전송할 수 있다.
화면 DC는 메모리 상에 존재하는 그리기 표면이다. 실제 화면 DC와 마찬가지로 모든 GDI 출력 함수를 사용할 수 있으므로 프로그램 내부에서 미리 그리기를 할 때 메모리 DC를 사용한다. 메모리 DC로 보내지는 출력은 메모리 DC에 선택되어 있는 비트맵의 표면으로 출력된다.
최초 메모리 DC가 생성되면 그리기 표면은 1픽셀짜리 흑백 비트맵을 가지며 비트맵을 선택해 주면 이 비트맵의 높이와 폭, 색상 포맷을 가지는 그리기 표면이 만들어진다. 따라서 메모리 DC에 미리 그리기를 하고자할 때는 먼저 CreateCompatibleBitmap 함수로 생성한 원하는 크기의 비트맵을 먼저 선택해 주어야 한다. 또는 미리 읽어놓은 그림을 출력할고자 할 때는 LoadBitmap으로 읽은 비트맵을 메모리 DC에 선택해 준 후 BitBlt 등의 함수로 화면 DC로 전송한다.
다음 코드는 비트맵을 화면으로 출력하는 가장 일반적인 코드이다. 화면 DC와 호환되는 메모리 DC를 생성한 후 비트맵을 메모리 DC에 선택하고 BitBlt로 화면 DC로 전송한다. 화면 DC는 비트맵을 선택할 수 없기 때문에 반드시 메모리 DC에 비트맵을 선택한 후 원하는 부분을 화면 DC로 전송해야 한다.

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

CreateDialog :
HWND CreateDialog(HINSTANCE hInstance, LPCTSTR lpTemplate, HWND hWndParent, DLGPROC lpDialogFunc);
lpTemplate 인수가 지정하는 템플리트로 대화상자를 생성하며 대화상자 프로시저로 WM_INITDIALOG메시지를 보내 초기화를 하도록 한다. 대화상자 템플리트가 WS_VISIBLE 스타일을 가지고 있으면 대화상자를 화면으로 출력하나 이 스타일이 없으면 ShowWindow 함수를 호출하기 전에는 대화상자가 보이지 않는다. 이 함수는 대화상자 생성 후 곧바로 대화상자 핸들을 리턴한다.
주로 모델리스 대화상자를 생성할 때 이 함수를 사용한다. 이 함수를 호출하기 전에 반드시 이 대화상자가 이미 생성되어 있는지를 점검해 보아야 한다. 메인 윈도우는 이 함수로 모델리스 대화상자를 생성한 후 메시지로 상호 작용을 하며 대화상자가 필요없을 때 DestroyWindow 함수로 대화상자를 파괴힌다.

/////////////////////////////////////////////////////////////////////////////////////////////////////
CreateFile : HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES pSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
이 함수는 파일을 생성하는 가장 기본적인 함수이다. 그러나 이름과는 달리 파일을 생성하는 것뿐만 아니라 기존의 파일을 열 수도 있으며 파일 외에 다음과 같은 오브젝트를 생성하거나 열 수도 있다.

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

CreateFont : HFONT CreateFont( int nHeight, int nWidth, int nEscapement, int nOrientation, int fnWeight, DWORD fdwItalic, DWORD fdwUnderline, DWORD fdwStrikeOut, DWORD fdwCharSet, DWORD fdwOutputPrecision, DWORD fdwClipPrecision, DWORD fdwQuality, DWORD fdwPitchAndFamily, LPCTSTR lpszFace );
CreateFont는 인수가 지정하는 특성에 가장 일치하는 논리 폰트를 생성하며 이 함수로 생성한 논리 폰트는 SelectObject 함수에 의해 DC에 선택된다. 논리 폰트는 응용 프로그램이 사용하고자 하는 폰트에 대한 정의일 뿐이며 실제로 시스템에 존재하는 물리 폰트와는 다르다. GDI는 논리 폰트의 정보를 참조하여 시스템에 존재하는 물리 폰트 중 논리 폰트의 특성에 가장 근접 폰트를 선택해 준다. 논리 폰트에 가장 근접하는 물리 폰트를 찾는 과정을 폰트 맵핑이라고 하며 그 알고리즘을 폰트 매퍼라고 한다. CreateFont 함수는 인수가 지정하는 특성대로 논리 폰트를 만들 뿐이며 폰트 매퍼에 의해 물리 폰트를 선택하는 것은 SelectObject 함수가 한다.
폰트 매퍼가 폰트를 선택할 때 가장 우선적으로 고려하는 요소는 문자셋, 패치와 패밀리 그리고 타입 페이스이다. 따라서 정확한 결과를 얻기 위해서는 최소한 이 세값을 정확하게 요구해야 한다. 만약 "BaboFont"라는 이름을 가지는 폰트를 선택하고 싶다고 해서 타입 페이스만 "BaboFont"라고 요구하고 나머지 정보를 아무렇게나 요구할 경우 이 폰트가 설치되지 않은 시스템에서는 엉뚱한 폰트가 선택될 수도 있다. 문자셋, 피치, 패밀리 정보를 정확하게 요구했으면 설사 이 폰트가 없어도 최대한 비슷한 폰트를 찾아 선택할 수 있다.

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

CreateFontIndirect : HFONT CreateFontIndirect(CONST LOGFONT *lplf);
LOGFONT 구조체가 지정하는 특성의 논리 폰트를 생성한다. 이 구조체의 멤버는 CreateFont 함수의 인수와 정확하게 일치하므로 자세한 사항은 CreateFont 함수의 인수 설명을 참조하기 바란다. 단 이 함수는 폰트의 특성 지정에 구조체를 사용하므로 구조체 배열을 미리 만들어 놓거나 저장할 수 있다는 장점이 있으며 비슷한 특성을 가지는 일련의 폰트를 반복적으로 만들 수 있다.

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

CreateHatchBrush : HBRUSH CreateHatchBrush( int fnStyle, COLORREF clrref);
브러시는 GDI가 도형의 안쪽을 채우기 위해 사용하는 오브젝트이다. 이 함수는 무늬와 색상이 있는 브러시를 생성한다.사용하고 난 후에는 반드시 DeleteObject 함수로 브러시를 삭제해 주어야 한다.

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

CreatePen : HPEN CreatePen(int fnPenStyle, int nWidth, COLORREF crColor);
펜은 GDI가 선을 그릴 때 사용하는 오브젝트이며 DC에 선택된 펜의 속성대로 선이 그어진다. 디폴트 펜은 굵기 1의 검정색 실선이나 펜을 만들어 DC로 전송하면 만들어진 펜대로 선이 그어진다. 다 사용하고 난 후에는 DeleteObject 함수로 펜을 삭제해야 한다.

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

CreateSolidBrush : HBRUSH CreateSolidBrush( COLORREF crColor);
브러시는 GDI가 도형의 안쪽을 채우기 위해 사용하는 오브젝트이다. DC가 처음 만들어지면 디폴트로 흰색의 브러시가 선택되어 있으나 브러시를 만들어 선택해 주면 원하는 색상으로 도형의 내부를 채색할 수 있다. 사용하고 난 후에는 반드시 DeleteObject 함수로 브러시를 삭제해 주어야 한다.

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

CreateWindow : HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HANDLE hInstance, LPVOID lpParam);
윈도우 클래스와 이 함수의 인수 정보를 바탕으로 하여 윈도우를 생성한다. RegisterClass 함수로 직접 윈도우 클래스를 등록하여 메인 윈도우를 만들 수도 있으며 또는 이미 등록된 컨트롤을 생성할 수도 있다.
이 함수는 윈도우 생성 후 WM_CRETATE. WM_GETMINMAXINFO, WM_NCCREATE 메시지를 해당 윈도우로 차례대로 보내주며 WS_VISIBLE 스타일이 지정되어 있을 경우 윈도우를 보여주고 활성화시키기 위한 모든 동작을 하며 관련 메시지를 보내준다.

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

CreateWindowEx : HWND CreateWindowEx(DWORD dwExStyle, LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HANDLE hInstance, LPVOID lpParam);
윈도우를 생성하는 기능은 CreateWindow 함수와 동일하되 확장 스타일을 지정하는 dwExStyle 멤버가 있다는 점만 다르다. CreateWindow 함수는 dwExStyle이 0인 매크로 함수로 정의되어 있다.

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

DefWindowProc : LRESULT DefWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
이 함수는 윈도우 프로시저가 처리하지 않은 메시지의 디폴트 처리를 한다. WndProc은 원하는 메시지를 처리하고 자신이 처리하지 않은 메시지는 이 함수에게 전달하여 디폴트 처리를 하도록 해 주어야 한다.

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

DeleteObject : BOOL DeleteObject( HGDIOBJ hObject);
GDI오브젝트를 삭제하고 오브젝트가 사용하던 시스템 리소스를 해제한다. CreatePen, CreateSolidBrush 등의 함수로 만들어진 GDI 오브젝트는 반드시 삭제해 주어야 한다. DC에 선택되어 있는 오브젝트는 삭제할 수 없으므로 삭제하기 전에 반드시 같은 타입의 다른 오브젝트를 선택해 주어야 한다. 패턴 브러시가 삭제될 때 비트맵은 삭제되지 않으므로 직접 삭제해 주어야 한다.

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

DestroyWindow : BOOL DestroyWindow( HWND hWnd );
hWnd 윈도우를 파괴한다. 단, 이 함수로 다른 스레드에서 생성한 윈도우를 파괴할 수는 없다. 이때 이 함수는 다음과 같은 일련의 동작을 수행한다.
1.파괴되는 윈도우에게 WM_DESTROY, WM_NCDESTROY 메시지를 보내준다. 윈도우 프로시저는 이 메시지를 받았을 때 윈도우에서 할당한 자원을 해제하는 등의 종료 처리를 한다. hWnd가 메인 윈도우이면 PostQuitMessage 함수를 호출하여 프로세스를 종료하도록 해야 한다.
2.키보드 포커스를 제거한다.
3.윈도우의 메뉴를 파괴한다.
4.스레드의 메시지 큐를 비운다.
5.타이머를 파괴한다.
6.클립보드의 소유권을 해제한다.
7.파괴되는 윈도우의 차일드와 소유된 윈도우들을 차례대로 파괴한 후 hWnd 윈도우를 파괴한다.

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

DialogBox : int DialogBox( HINSTANCE hInstance, LPCTSTR lpTemplate, HWND hWndParent, DLGPROC lpDialogFunc );
이 함수는 모달 대화상자를 실행한다. lpTemplate가 지정하는 대화상자를 생성하여 화면으로 출력하며(WS_VISIBLE 스타일이 없어도 화면에 보인다) 소유자 윈도우를 사용 금지시키고 lpDialogFunc 인수가 지정하는 메시지 처리 함수로 메시지를 보내주어 대화상자를 실행한다. 모달 대화상자는 EndDialog가 호출될 때까지 실행을 계속하며 이 함수는 대화상자가 완전히 종료되어야 리턴한다.

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

DispatchMessage : LONG DispatchMessage(CONST MSG *lpmsg);
이 함수는 GetMessage가 읽은 메시지를 이 메시지를 처리할 윈도우로 보낸다. 단, WM_TIMER 메시지의 lParam이 NULL이 아닐 경우, 즉 콜백 함수가 지정되어 있을 경우는 윈도우 프로시저로 메시지를 보내는 대신 lParam이 저장하는 콜백 함수를 곧바로 호출한다. 이 함수는 윈도우 프로시저가 메시지를 완전히 처리하기 전에는 리턴하지 않는다. 다음은 일반적인 메시지 루프이다.

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

DrawText : int DrawText(HDC hDC, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT uFormat);
문자열을 사각 영역 내부에 출력하며 사각 영역 외부로는 출력을 하지 않는다. 단, DT_NOCLIP 플레그가 설정된 경우는 예외적으로 사각 영역 바깥으로도 출력을 할 수 있다. DT_SINGLELINE 플레그가 설정된 경우를 제외하고 문자열이 여러줄로 구성된 것으로 간주한다. TextOut 함수에 비해 여러 줄을 한꺼번에 출력할 수 있다.
출력에 사용할 폰트와 문자열의 색상, 배경 색상은 TextOut 함수와 마찬가지로 DC에 선택된 글꼴과 색상 설정을 따른다. 그러나 SetTextAlign으로 정렬 상태를 변경할 수는 없으며 Escapement, Orientation이 0이 아닌 글꼴, 즉 기울어진 글꼴은 출력할 수 없다.

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

Ellipse : BOOL Ellipse(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
주어진 사각형에 내접하는 타원을 그린다. 타원의 원주는 현재 DC에 선택된 펜으로 그려지며 타원의 내부는 현재 DC에 선택된 브러시로 채워진다.

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

EndDialog : BOOL EndDialog(HWND hDlg,int nResult);
DialogBox등의 함수로 생성한 대화상자를 종료한다. 모달 대화상자는 반드시 이 함수로 종료해야 하며 또한 이 함수는 대화상자 종료 이외의 목적으로 사용해서는 안된다. 이 함수는 대화상자 프로시저에서 언제든지 호출할 수 있으며 심지어 대화상자 초기화 중인 WM_INITDIALOG에서도 호출할 수 있다. 만약 초기화중에 일정한 조건에 의해 대화상자를 종료하기 위해 이 함수를 호출할 경우 대화상자는 화면에 보이기 전에 파괴된다.
이 함수는 호출 즉시 대화상자를 파괴하지 않고 대화 상자 파괴를 지시하는 플레그만 설정한다. 시스템은 다음 메시지를 읽을 때 이 플레그의 설정상태를 보고 대화상자를 종료한다. 따라서 이 함수 호출 다음에 있는 명령문은 모두 제대로 실행된다.

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

EndPaint : BOOL EndPaint(HWND hWnd, CONST PAINTSTRUCT *lpPaint);
WM_PAINT 함수 내에서 그리기를 종료하기 위해 사용된다. BeginPaint와 항상 짝을 이루어 사용되며 BeginPaint가 캐럿을 숨겼을 경우 캐럿을 다시 화면으로 출력해 주는 역할을 한다.

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

FillRect : int FillRect(HDC hDC, CONST RECT *lprc, HBRUSH hbr);
lprc 사각 영역을 hbr 브러시로 칠한다. 경계선은 그리지 않으며 내부만 칠하기 때문에 특정 영역을 단색으로 칠할 때 편리하다. 사각 영역의 왼쪽과 위쪽은 채색 영역에 포함되지만 오른쪽과 아래쪽은 맵핑모드에 상관없이 포함되지 않는다. 예를 들어 (10,10)-(100,100) 영역을 칠하면 실제로 칠해지는 영역을 (10,10)-(99,99)까지이다.
브러시는 CreateSolidBrush, CreateHatchBrush 등으로 만든 커스텀 브러시를 쓸 수도 있고 GetStockObject 함수로 구한 스톡 브러시를 쓸 수도 있다. 또는 시스템 색상에 1을 더하여 사용하는 것도 가능하다. 브러시의 핸들을 인수로 전달하며 DC에 먼저 브러시를 선택해 놓을 필요가 없기 때문에 다른 GDI 함수에 비해 필요한 코드가 훨씬 더 짧다는 것이 특징이다. 경계선을 그리지 않으므로 주로 특정 영역을 완전히 채울 때 많이 사용하며 특히 배경색과 동일한 브러시를 사용하면 이미 출력된 내용을 지울 수 있다.

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

FindWindow : HWND FindWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName);
윈도우 클래스와 캡션으로 윈도우를 검색하여 핸들을 얻는다. 윈도우간의 상호작용을 하기 위해서는 윈도우의 핸들이 필요한데 이 함수로 원하는 윈도우의 핸들을 조사할 수 있다. 윈도우 클래스 또는 윈도우 캡션 둘 중 하나의 조건으로 검색할 수 있다. 차일드 윈도우는 검색할 수 없으며 탑 레벨 윈도우만 검색 대상이 된다. 윈도우 클래스와 캡션 문자열은 대소문자를 가리지는 않지만 부분 문자열을 검색할 수는 없다. 따라서 캡션이 가변적으로 변하는 윈도우를 검색하는 목적으로는 이 함수를 사용할 수 없으며 우연히 같은 캡션을 가지는 윈도우가 있을 경우 잘못된 검색을 할 위험도 있다.
두 프로그램이 상호 작용을 하고자 할 때 미리 캡션을 정해 놓고 정해진 캡션으로 상대방을 찾는 용도로 주로 많이 사용된다. 이 경우 윈도우의 캡션은 항상 고정되어 있어야 한다는 제약이 있다.

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

GetBkColor : COLORREF GetBkColor(HDC hdc);
현재 DC에 설정되어 있는 배경 색상을 조사한다. 배경 색상은 문자열 출력시 문자의 획 뒤쪽에 출력되는 색상이다.

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

GetBkMode : int GetBkMode(HDC hdc);
DC에 설정되어 있는 혼합 모드를 조사한다. 혼합 모드란 출력되는 문자열의 배경을 어떻게 처리할 것인가를 지정하는 값이며 불투명 모드, 투명 모드 두 가지가 있다. 혼합 모드에 대한 자세한 사항과 예제는 SetBkMode 함수를 참조하기 바란다. 디폴트 혼합 모드는 OPAQUE이다.

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

GetClassLong : DWORD GetClassLong( HWND hWnd, int nIndex);
윈도우 클래스는 WNDCLASS(EX) 구조체에 의해 속성들이 지정되며 RegisterClass(Ex) 함수에 의해 이 속성들이 등록된다. 일단 등록된 클래스의 속성을 조사하고자 할 때 이 함수를 사용하며 SetClassLong 함수로 속성값을 변경할 수도 있다. 속성값을 대입할 때는 SetClassLong 함수를 사용할 수 있지만 기존에 설정되어 있는 값을 편집하고자 할 경우는 GetClassLong 함수로 값을 조사하여 변경한 후 SetClassLong 함수로 다시 지정하면 된다. 예를 들어 윈도우 클래스의 스타일에 다른 스타일을 추가로 지정하고자 할 때는 먼저 값을 조사한 후 스타일에 원하는 추가 스타일을 OR 연산자로 지정해야 한다.

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

GetClientRect : BOOL GetClientRect(HWND hWnd, LPRECT lpRect);
윈도우의 작업영역 크기를 계산해 준다. 크기만 계산해 주기 때문에 좌상단(left, top)값은 항상 0,0이며 우하단 좌표(right, bottom)가 곧 윈도우의 크기를 나타낸다. 작업영역이란 윈도우의 타이틀바, 스크롤 바, 경계선, 메뉴 등을 제외한 영역이며 윈도우가 그리기를 하는 대상 영역이다.

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

GetDC : HDC GetDC( HWND hWnd);
윈도우즈 환경에서 화면이나 프린터로 출력을 하려면 DC를 먼저 구해야 한다. DC를 구하는 일반적인 방법은 두 가지가 있는데 WM_PAINT 메시지내에서 DC를 얻을 때는 BeginPaint, EndPaint 함수쌍을 사용하며 이 메시지 이외의 코드에서 DC를 얻을 때는 GetDC, ReleaseDC 함수쌍을 사용한다. GetDC의 인수로 DC를 구하고자 하는 핸들을 주면 이 윈도우의 DC를 구해준다. 윈도우의 클래스 스타일에 따라 윈도우 DC와 클래스 DC가 구해지는 경우도 있으나 대부분의 경우 작업 영역에 그리기를 할 수 있는 커먼 DC가 리턴된다.

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

GetDeviceCaps : int GetDeviceCaps(HDC hdc, int nIndex);
DC가 참조하는 장치의 여러 가지 속성이나 능력을 조사한다. 이 값들은 장치에 대해 그리기나 기타 각종 설정 변경 등에 참고 정보로 사용된다. 예를 들어 플로터는 벡터의 직선이나 곡선을 그릴 수는 있지만 비트맵은 출력할 수 없으며 흑백 프린터는 색상을 표현하지 못한다. 이런 장치의 특성이나 능력을 이 함수로 반드시 먼저 조사한 후 가능한 기능일 때만 사용해야 한다.
이 함수로 조사할 수 있는 값의 종류가 많고 의미가 어려운 것도 있으므로 정확하게 의미를 파악한 후 사용해야 한다. 이 함수로 조사한 값을 부적절하게 사용할 경우 엉뚱한 동작을 할 수도 있다. 다른 방식으로는 조사하기 힘든 정보를 의외로 쉽게 구할 수도 있으므로 이 함수의 인덱스들은 한번씩 눈여겨 봐두는 것이 좋다

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

GetDlgItem : HWND GetDlgItem(HWND hDlg,int nIDDlgItem);
대화상자내에서의 컨트롤은 ID로 구분된다. 컨트롤의 ID는 CreateWindow 함수의 hMenu인수로 지정하거나 리소스 편집기의 ID란에 정수로 지정한다.
같은 대화상자에 속한 컨트롤들은 중복된 ID를 가지지 않으며 ID는 컨트롤의 유일한 식별자 역할을 한다. 컨트롤의 ID를 알고 있으면 GetDlgItemInt(Text) 등의 함수로 컨트롤의 값을 읽거나 쓸 수 있으며 통지 메시지에서 어떤 컨트롤로부터 통지 메시지가 전달되었는지를 알 수 있다.
또한 대화상자내의 컨트롤들은 각각이 독립된 차일드 윈도우이므로 고유의 윈도우 핸들을 가진다. 컨트롤의 ID로부터 윈도우 핸들을 구하고자 할 때 이 함수를 사용한다. 일단 윈도우 핸들을 구하면 SetWindowText, ShowWindow, MoveWindow 등 윈도우를 대상으로 하는 모든 함수를 사용할 수 있다.
이 함수는 주로 대화상자에 속한 차일드의 윈도우 핸들을 구할 때 사용하지만 일반 윈도우에도 적용할 수 있다. 일반 윈도우에서도 차일드 컨트롤이 있고 이 컨트롤이 ID를 가지고 있으면 GetDlgItem 함수로 차일드 컨트롤의 ID를 구할 수 있다.

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

GetDlgItemInt : UINT GetDlgItemInt( HWND hDlg, int nIDDlgItem, BOOL *lpTranslated, BOOL bSigned );
이 함수는 WM_GETTEXT 메시지로 대화상자 컨트롤의 텍스트를 읽어 정수형으로 변환해 리턴해 준다. 만약 읽는 텍스트가 INT_MAX 또는 UINT_MAX 범위보다 더 클 경우 이 함수는 0을 리턴한다.

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

GetDlgItemText : UINT GetDlgItemText( HWND hDlg, int nIDDlgItem, LPTSTR lpString, int nMaxCount );
WM_GETTEXT 메시지를 컨트롤로 보내 컨트롤의 텍스트를 읽어 lpString 버퍼에 채워준다. 이때 컨트롤은 버튼, 에디트, 스태틱 등의 텍스트 표현이 가능한 컨트롤이어야 한다. 만약 버퍼 길이(nMaxCount)보다 문자열이 더 길면 문자열은 잘려진다.

/////////////////////////////////////////////////////////////////////////////////////////////////////
GetFocus : HWND GetFocus(VOID);
현재 스레드의 윈도우중 포커스를 가진 윈도우, 즉 키보드 관련 메시지를 받을 윈도우의 핸들을 조사한다. 다른 스레드의 윈도우가 포커스를 가지고 있으면 NULL이 리턴된다.

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

GetLocalTime : VOID GetLocalTime(LPSYSTEMTIME lpSystemTime);
로컬 시간을 조사해 준다. 로컬 시간이란 시스템이 유지하는 시스템 시간(UTC)에서 현재 컴퓨터가 실행되고 있는 시간대와 일광절약 설정을 계산하여 변환한 시간이다. 대한민국의 로컬 시간은 UTC 시간보다 9시간 더 빠르므로 시스템 시간에서 9시간만큼 더해주어야 로컬 시간이 구해진다. 일반적으로 현지 시간이라고 하면 이 함수로 구해지는 로컬 시간을 의미한다.

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

GetMapMode : int GetMapMode(HDC hdc);
SetMapMode로 설정한 DC의 맵핑 모드를 조사해 준다.

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

GetMessage : BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
호출한 스레드에서 메시지를 꺼내 첫번째 인수로 전달된 lpMsg구조체에 채워준다. 특정 윈도우(그 차일드도 포함된다)로 보내지는 메시지나 PostThreadMessage 함수에 의해 스레드로 보내진 메시지를 조사할 수 있으며 특정한 범위에 있는 메시지만 조사할 수도 있다. 이 함수는 다른 스레드나 다른 프로세스의 메시지는 조사하지 못하며 오직 호출 스레드의 메시지만 조사할 수 있다. 다음은 전형적인 메시지 루프의 예이다.
while(GetMessage(&Message,0,0,0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
return Message.wParam;
전체 루프는 while문으로 싸여져 있어 GetMessage가 WM_QUIT를 조사할 때까지 반복된다. GetMessage는 이 스레드에 속한 모든 윈도우에 대해 모든 메시지를 조사하여 Message구조체에 채워준다. 이 메시지는 DispatchMessage 함수에 의해 해당 윈도우의 윈도우 프로시저로 보내진다.
GetMessage 함수는 스레드 메시지 큐에서 메시지를 읽은 후 이 메시지를 큐에서 제거한다. 단 예외적으로 WM_PAINT 메시지는 이 메시지가 처리된 후에 메시지 큐에서 제거된다.
GetMessage 함수는 메시지 큐에 대기중인 메시지가 없을 경우 메시지가 전달될 때까지 무한히 대기한다.

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

GetObject : int GetObject(HGDIOBJ hgdiobj, int cbBuffer, LPVOID lpvObject);
GDI 오브젝트에 대한 정보를 구한다. 오브젝트 타입에 따라 적절한 구조체를 선언하고 그 구조체의 포인터를 lpvObject 인수로 전달해 주면 구조체에 조사된 정보를 채워 준다. 이 정보들은 일반적으로 오브젝트를 생성할 때 지정한 정보와 동일하다. 생성 정보를 가지고 있지 않거나 임의의 오브젝트에 대해 동작하는 범용적인 함수를 작성할 때는 이 함수로 오브젝트의 정보를 실행중에 조사해야 한다.
인수로 전달되는 임의의 비트맵에 대해 동작해야 하므로 GetObject 함수로 이 비트맵 오브젝트의 크기를 실행중에 조사하도록 하였다. BITMAP 구조체를 선언하고 GetObject로 정보를 조사한 후 bmWidth, bmHeight 멤버를 읽어 비트맵 크기를 구하였다. 따라서 이 함수는 어떤 크기의 비트맵이라도 임의의 위치에 출력할 수 있다.

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

GetOpenFileName : BOOL GetOpenFileName(LPOPENFILENAME lpofn);
파일 입출력을 하기 전에 대상 파일을 입력받기 위해 이 함수를 호출한다. 이 함수는 lpofn 구조체의 설정대로 파일 열기 공통 대화상자를 열어 주며 사용자가 선택한 파일의 완전 경로를 구조체로 다시 리턴해 준다. 함수를 호출하기 전에 먼저 구조체에 초기값을 적절하게 설정해 주어야 하며 리턴값과 구조체의 결과값으로 사용자가 선택한 파일명을 구할 수 있다. 이렇게 입력받은 파일명은 이후 CreateFile 등의 함수로 열어서 입출력에 사용한다.
OPENFILENAME 구조체를 선언하고 이 구조체의 모든 멤버를 0으로 초기화해 준 후 필수적으로 필요한 멤버에만 값을 대입해 주었다. lpstrFile 멤버에는 사용자가 선택한 파일명을 리턴받기 위한 충분한 길이의 버퍼 포인터를 반드시 대입해 주어야 함을 유의하도록 하자. 사용자가 파일을 선택했으면 lpstrFile 멤버를 읽어 어떤 파일을 선택했는지 메시지 박스로 보여준다. 이 멤버로 리턴되는 경로는 CreateFile 함수에 곧바로 사용할 수 있다.

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

GetPixel : COLORREF GetPixel(HDC hdc, int nXPos, int nYPos);
hdc의 한 점에 출력되어 있는 색상을 조사한다. 이 함수는 자주 사용되지는 않지만 복잡한 영역에 대한 좌표 점검을 할 때 색상으로 특정 좌표의 영역을 조사하는 용도로 종종 사용된다. 화면이나 메모리 DC등은 이 함수를 지원하지만 프린터나 플로터 등은 이 함수를 지원하지 못한다.

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

GetROP2 : int GetROP2(HDC hdc);
DC에 설정되어 있는 ROP값을 조사한다. ROP값은 이미 그려져 있는 값과 그려지는 값과의 관계를 정의하는 값이며 SetROP2 함수에 의해 설정한다. 디폴트 ROP값은 R2_COPYPEN이다.

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

GetScrollInfo : BOOL GetScrollInfo(HWND hwnd, int fnBar, LPSCROLLINFO lpsi);
스크롤 바위 위치, 범위, 페이지 크기, 트래킹중의 위치 등을 조사한다. 이 함수를 사용하면 WM_THUMBTRACK 메시지 등에서 스크롤 바의 32비트 범위값을 조사할 수 있다.

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

GetStockObject : HGDIOBJ GetStockObject( int fnObject);
스톡 오브젝트(Stock Object)는 운영체제가 기본적으로 제공해주는 GDI 오브젝트이다. 생성할 필요가 없으며 이 함수로 핸들을 구해 언제든지 사용할 수 있고 파괴시켜 주지 않아도 된다. 다음 코드는 흰색 스톡 브러시를 구해 윈도우 클래스의 배경 브러시로 지정한다.
WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
이 윈도우는 배경을 지울 필요가 있을 때 희색 브러시를 사용하므로 배경은 항상 흰색으로 채색된다

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

GetSubMenu : HMENU GetSubMenu(HMENU hMenu, int nPos);
메뉴 바에서 하위 메뉴를 구한다. 이 함수로 메뉴 바에 있는 하위 메뉴 하나에 대한 핸들을 구한 후 팝업 메뉴로 출력할 수 있다. 이 함수에 대한 사용예는 TrackPopupMenu 함수를 참조하기 바란다

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

GetSystemDirectory : UINT GetSystemDirectory(LPTSTR lpBuffer, UINT uSize);
윈도우즈 시스템 디렉토리의 경로를 구한다. lpBuffer는 경로를 대입받을 수 있는 충분한 길이를 제공해야 하는데 일반적으로 MAX_PATH길이면 충분하다. 조사된 경로는 백슬레쉬 문자를 포함하지 않는다. 시스템 디렉토리는 운영체제 동작에 필수적으로 필요한 DLL, OCX, DRV 등의 중요한 파일을 가진다.
시스템 폴더는 윈도우즈 디렉토리 아래에 있으며 일반적으로 95/98계열은 System이라는 이름을 가지며 NT/2000계열은 System32라는 이름을 가진다. 그러나 다음 버전의 운영체제에서는 이 디렉토리의 이름이 바뀔 수도 있으므로 시스템 디렉토리의 경로가 필요한 경우는 반드시 이 함수로 그 경로를 조사하여 사용해야 한다. 예를 들어 시스템 폴더에 DLL을 복사하는 설치 프로그램의 경우 이 폴더의 경로를 정확하게 구해 복사해야 모든 프로그램이 DLL을 제대로 공유할 수 있다. 다음 코드는 Src 경로에 있는 MyProgram.dll을 윈도우즈 시스템 폴더로 복사한다.
GetSystemDirectory(Dest,MAX_PATH);
strcat(Dest,"\\MyProgram.dll");
CopyFile(Src,Dest,FALSE);
일반적으로 설치 프로그램이 이 함수를 많이 사용하는데 마찬가지 이유로 설치 해제 프로그램도 삭제할 프로그램의 정확한 경로를 조사하기 위해 이 함수를 사용해야 한다.

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

GetSystemMetrics : int GetSystemMetrics( int nIndex );
시스템 설정 정보를 조사한다. nIndex가 지정하는 정보를 조사해 주며 각 값을 해석하는 방법은 nIndex에 따라 다르다. 시스템의 설정값은 실행중에도 항상 변경될 수 있으므로 이 함수로 필요한 설정 정보를 그때 그때 조사해서 사용해야 한다. 예를 들어 마우스가 설치되어 있지 않은 시스템에서 실행을 거부한다거나 할 때 이 함수로 마우스 설치 여부를 조사할 수 있다. 이 함수는 값을 조사할 수만 있으며 변경할 수는 없다.

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

GetTextAlign : UINT GetTextAlign(HDC hdc);
텍스트 정렬 방식을 조사한다. 정렬 방식에 대해서는 SetTextAlign 함수의 예제를 참고하기 바란다.

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

GetTextColor : COLORREF GetTextColor(HDC hdc);
DC에 설정되어 있는 텍스트의 전경색을 조사한다. 전경색은 출력되는 문자의 색상을 지정하며 디폴트 전경색은 검정색이다. 전경색은 SetTextColor 함수로 변경할 수 있으며 이 함수는 현재 설정된 전경색을 조사한다.

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

GetTextMetrics : BOOL GetTextMetrics(HDC hdc, LPTEXTMETRIC lptm);
DC에 현재 선택되어 있는 폰트의 여러 가지 정보를 조사한다. 주로 폰트의 크기에 대한 정보를 얻을 수 있다리턴되는 정보는 다음과 같이 선언되어 있는 구조체이다.

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

GetWindowLong : LONG GetWindowLong( HWND hWnd, int nIndex);
CreateWindow(Ex) 함수로 윈도우를 생성할 때 지정한 윈도우의 속성을 조사한다. 일단 생성된 윈도우의 속성을 조사하고자 할 때 이 함수를 사용하며 SetWindowLong 함수를 사용하면 속성을 변경할 수도 있다. 단순히 속성을 변경할 때는 SetWindowLong 함수를 사용하지만 기존의 속성값을 편집하고자 할 경우는 이 함수로 먼저 속성값을 읽어야 한다. 예를 들어 윈도우의 스타일에 다른 스타일을 추가로 지정하고자 할 때는 먼저 값을 조사한 후 스타일에 원하는 추가 스타일을 OR 연산자로 지정해야 한다.윈도우의 속성을 변경하는 예제는 SetWindowLong 함수를 참조하기 바란다.

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

GetWindowRect : BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
윈도우의 현재 위치와 크기를 구해준다. (left, top)은 윈도우의 현재 좌상단 위치를 나타내는데 이 좌표는 전체 화면을 기준으로 한 좌표이다. (right, bottom)은 윈도우의 우하단 위치를 나타내며 역시 전체 화면을 기준으로 한 좌표이다. 윈도우의 현재 크기(폭과 높이)를 구하고 싶으면 right-left, bottom-top을 계산하면 된다.

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

GetWindowsDirectory : UINT GetWindowsDirectory(LPTSTR lpBuffer, UINT uSize);
운영체제가 설치되어 있는 경로를 구한다. lpBuffer는 경로를 대입받을 수 있는 충분한 길이를 제공해야 하는데 일반적으로 MAX_PATH길이면 충분하다. 조사된 경로는 백슬레쉬 문자를 포함하지 않으나 단, 윈도우즈가 루트 디렉토리에 설치된 겨우는 백슬레쉬 문자를 가질 수도 있다.
운영체제가 설치되는 폴더는 일반적으로 95/98 계열은 C:\Windows이며 NT/2000 계열은 C:\Winnt이나 설치시의 상황에 따라 또는 사용자의 특별한 지정이 있을 경우는 다른 폴더가 될 수도 있다. 따라서 반드시 이 함수로 설치된 경로를 조사한 후 사용해야 한다. 이 폴더에는 초기화 파일(INI)과 도움말 파일, 스크린 세이버 등의 주요 파일이 복사되어 있다. 윈도우즈 디렉토리에 설치되어 있는 Some.scr 이라는 스크린 세이버의 완전 경로를 조사하고 싶다면 다음 코드를 작성한다.
TCHAR ScrPath[MAX_PATH];
GetWindowsDirectory(ScrPath,MAX_PATH);
lstrcat(ScrPath,"\\Some.scr");
윈도우즈 디렉토리에 있는 파일이라고 해서 c:\Windows\Some.scr이라고 함부로 가정해서는 안된다.

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

GetWindowText : int GetWindowText( HWND hWnd, LPTSTR lpString, int nMaxCount);
윈도우의 캡션을 조사한다.

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

InvalidateRect : BOOL InvalidateRect(HWND hWnd, CONST RECT *lpRect, BOOL bErase);
운영체제는 윈도우의 작업 영역중 일부에 무효 영역이 있으면 WM_PAINT 메시지를 보내 다시 그리도록 한다. 프로그램 내부에서 작업 영역을 변경한 경우 이 함수로 변경된 부분을 무효화해 주어야 WM_PAINT 메시지가 발생하며 화면이 다시 그려진다. 배경을 지우고 그려야 할 때는 bErase를 TRUE로 설정해 주어야 이전에 출력되어 있던 내용이 삭제되고 다시 그려진다. 그리기 속도를 최대한 빠르게 하려면 lpRect에 무효화할 최소한의 영역을 지정하여 꼭 필요한 부분만 다시 그리도록 해 주어야 한다.

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

IsDlgButtonChecked : BOOL IsDialogMessage(HWND hDlg, LPMSG lpMsg);
lpMsg의 메시지가 hDlg 대화상자를 위한 메시지인지를 검사하고 만약 그렇다면 이 메시지를 대화상자 프로시저로 보내 처리하도록 한다. 예를 들어 Tab키나 커서 이동키를 누를 경우 컨트롤간의 포커스 이동이나 라디오 그룹내에서의 포커스 이동을 처리한다. 이 함수 내부에서 메시지의 변환 전송을 모두 처리하므로 이 메시지가 TRUE를 리턴하면, 즉 이 함수에 의해 메시지가 처리되었으면 이 메시지는 더 이상 DispatchMessage 함수로 전달되지 않아도 상관없다.
주로 모델리스 대화상자를 위해 이 함수를 사용하지만 컨트롤을 가지고 있는 모든 윈도우에 이 함수를 사용할 수 있다. 이 함수에 대한 예제 코드는 CreateDialog 함수를 참조한다.

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

IsIconic : BOOL IsIconic( HWND hWnd );
hWnd 윈도우가 최소화 상태, 즉 아이콘 상태인지를 조사한다. 최소화된 상태에서는 특별한 다른 처리를 하고자 할 때 이 함수로 윈도우의 현재 상태를 조사할 수 있다. 예를 들어 최소화 상태일 때 아이콘을 직접 그리고 싶으면 WM_PAINT에서 이 함수를 호출하여 현재 상태를 조사하고 작업 영역 대신 아이콘으로 출력을 내보낸다.

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

IsWindow : BOOL IsWindow(HWND hWnd);
인수로 주어진 hWnd가 유효한 윈도우 핸들인지 조사한다. 이 윈도우가 존재하는지, 생성되어 있는지 검사하고자 할 때 이 함수를 사용한다. CreateDialog의 예제를 보면 모델리스 대화상자의 존재 여부를 조사할 때 이 함수를 사용하는 코드를 볼 수 있다.

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

IsWindowEnabled : BOOL IsWindowEnabled( HWND hWnd );
hWnd 윈도우가 사용 가능한 상태인지 조사한다. 사용 가능한 윈도우만 사용자로부터 입력을 받아들일 수 있다. 그러나 사용 금지된 윈도우도 내부적인 메시지는 처리한다. 버튼, 에디트 등의 컨트롤이 입력을 받을 수 있는 상태인지를 조사할 때 이 함수가 사용된다.

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

IsWindowVisible : BOOL IsWindowVisible( HWND hWnd );
윈도우가 보이는 상태인지 숨겨진 상태인지를 조사한다. 윈도우가 화면에 보이려면 WS_VISIBLE 스타일을 가지고 있어야 하며 또한 부모 윈도우가 WS_VISIBLE 스타일을 가지고 있어야 한다. 보인다는 뜻은 이 스타일이 설정되어 있다는 뜻일 뿐 실제로 화면에 나타나 있다는 뜻은 아니다. 이 함수가 TRUE를 리턴하더라도 다른 윈도우에 완전히 가려져 있을 경우 화면에 보이지 않을 수도 있으며 폭이나 높이가 0일 경우도 이 함수는 TRUE를 리턴하지만 화면에서는 숨겨져 있을 수 있다. 이 함수는 다만 윈도우의 WS_VISIBLE 스타일이 설정되어 있는지만을 검사할 뿐이다.

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

IsZoomed : BOOL IsZoomed( HWND hWnd );
hWnd 윈도우가 최대화되어 있는 상태인지를 조사한다. 최대화된 상태에서는 특별한 다른 처리를 하고자 할 때 이 함수로 윈도우의 현재 상태를 조사할 수 있다.

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

KillTimer : BOOL KillTimer(HWND hWnd, UINT_PTR uIDEvent);
SetTimer에 의해 설치된 타이머를 해제한다. 메시지 큐에 이미 포스팅된 WM_TIMER 메시지가 있을 경우 이 함수는 메시지를 제거하지 않는다.

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

LineTo : BOOL LineTo(HDC hdc, int nXEnd, int nYEnd);
현재 위치에서 (nXEnd, nYEnd) 끝점까지 선을 긋는다. 이때 끝점은 선에서 제외된다. 선을 긋고 난 후 현재 위치를 끝점으로 옮겨주므로 LineTo 함수를 계속 호출하면 선을 이어서 그릴 수 있다. 현재 위치는 DC에 보관되어 있으며 MoveToEx 함수로 변경할 수 있다.
그려지는 선은 DC에 선택되어 있는 펜으로 그려지므로 선의 모양을 변경하려면 펜을 생성한 DC에 선택해 주어야 한다.

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

LoadAccelerator : HACCEL LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName);
응용 프로그램의 리소스에 정의된 액셀러레이터 테이블을 읽어온다. 액셀러레이터 테이블은 응용 프로그램이 사용하는 단축키의 목록을 가지는 리소스이다. 이 함수로 읽어온 액셀러레이터 테이블은 메시지 루프에서 TranslateAccelerator 함수에 의해 해석되어 WM_COMMAND 메시지로 변환된다. 이 함수로 읽어온 액셀러레이터 테이블은 응용 프로그램이 종료될 때 자동으로 파괴되므로 직접 파괴해주지 않아도 된다.

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

LoadBitmap : HBITMAP LoadBitmap(HINSTANCE hInstance, LPCTSTR lpBitmapName);
비트맵 리소스를 읽어온다. 리소스에 정의된 비트맵은 장치에 독립적인 DIB 포맷으로 저장되어 있으나 이 함수로 읽혀질 때 현재 화면 모드와 호환되는 DDB로 변환된다. 따라서 이 함수로 읽은 비트맵은 호환 DC에 곧바로 선택할 수 있으며 BitBlt 함수로 출력할 수 있다. 비트맵 출력 예제는 BitBlt 함수를 참조한다. 비트맵을 다 사용하고 난 후에는 반드시 DeleteObject 함수로 삭제해 주어야 한다.

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

LoadCursor : HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
표준 커서 또는 응용 프로그램의 리소스에 정의되어 있는 커서를 읽어온다. 단, 이미 커서가 로드되어 있을 때는 다시 읽지 않고 읽어 놓은 커서의 핸들을 구해 준다. 만약 lpCursorName 인수가 가리키는 리소스가 커서가 아닌 다른 리소스이더라도 이 함수는 NULL을 리턴하지 않으므로 반드시 커서 리소스만 지정하도록 주의해야 한다. 하나의 커서 리소스에 여러 가지 색상의 커서가 있을 경우 현재 화면 설정에 가장 적합한 커서를 읽어준다

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

LoadIcon : HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);
표준 아이콘 또는 응용 프로그램의 리소스에 정의되어 있는 아이콘을 읽어온다. 단, 이미 아이콘이 로드되어 있을 때는 다시 읽지 않고 읽어 놓은 아이콘 핸들을 구해 준다. 아이콘 리소스 하나에 여러 포맷의 아이콘이 존재할 수 있는데 이 함수는 현재 화면 설정에 가장 적합한 아이콘을 읽어온다. 아이콘 리소스는 흑백일 수도 있고 색상을 가지고 있을 수도 있다.
이 함수는 현재 시스템 메트릭스가 정의하는 SM_CXICON, SM_CYICON 크기의 아이콘만 읽을 수 있는데 이 크기는 통상 32*32이다. 16*16크기의 작은 아이콘이나 기타 표준 아이콘 크기와 다른 크기의 아이콘을 읽을 때는 LoadImage 함수를 사용해야 한다.

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

LoadMenu : HMENU LoadMenu(HINSTANCE hInstance, LPCTSTR lpMenuName);
메뉴 리소스를 읽어온다. 메뉴는 통상 리소스로 작성되며 링크시 실행 파일에 합쳐진다. 윈도우에 메뉴를 붙이는 방법은 여러 가지가 있는데 가장 흔하게 사용되는 방법은 윈도우 클래스의 lpszMenuName 멤버에 사용할 메뉴 리소스를 지정해 주는 것이다.
WndClass.lpszMenuName=MAKEINTRESOURCE(IDR_MENU1);
lpszMenuName 멤버에 메뉴 리소스를 대입해 주면 이후부터 이 윈도우 클래스로부터 생성되는 모든 윈도우는 이 메뉴 리소스를 사용한다. 두번째 방법은 CreateWindow의 hMenu 멤버에 사용할 메뉴 핸들을 대입해 주는 것이다. 만약 윈도우 클래스에도 메뉴가 지정되어 있고 CreateWindow에도 메뉴 핸들이 지정되어 있으면 CreateWindow의 메뉴 지정이 우선이므로 윈도우 클래스에 있는 메뉴 대신 다른 메뉴를 사용하려면 CreateWindow에 메뉴 핸들을 지정하면 된다. 하지만 통상적으로 한 윈도우 클래스로부터 하나의 메인 윈도우를 만드므로 첫번째 방법이 훨씬 더 많이 사용된다.
메뉴를 사용하는 세번째 방법은 실행중에 메뉴를 읽어와 윈도우에 붙이는 것이며 이때 LoadMenu 함수가 사용된다. LoadMenu로 읽어온 메뉴 핸들을 SetMenu 함수로 윈도우에 붙여주면 된다. 단, 이때 LoadMenu로 읽은 메뉴 핸들은 반드시 DestroyMenu 함수로 해제해 주어야 한다.

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

LoadString : int LoadString(HINSTANCE hInstance, UINT uID, LPTSTR lpBuffer, int nBufferMax);
리소스에서 문자열을 읽어 지정한 버퍼에 채워준다. 문자열의 끝에 널 종료 문자를 덧붙여 주므로 이 함수로 읽은 문자열은 곧바로 사용할 수 있다. 프로그램 사용법이나 안내문, 에러 메시지 등의 문자열은 코드에서 바로 정의해서 쓰지 말고 가급적이면 문자열 리소스로 정의해서 사용하는 것이 일괄적으로 수정하기에 편리하다.

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

MessageBeep : BOOL MessageBeep(UINT uType);
지정한 사운드를 연주한다. 이 함수는 사운드를 큐에 넣은 후 즉각 리턴하며 사운드는 비동기적으로 연주되므로 곧바로 다른 작업을 할 수 있다. 만약 지정한 사운드를 연주할 수 없으면 시스템 디폴트 비프음을 내며 시스템 디폴트 음도 낼 수 없으면 스피커로 표준 비프음을 낸다. 사용자에게 사운드로 주의를 주고자 할 때 이 함수를 사용하며 또한 디버깅용으로도 자주 사용된다.

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

MessageBox : int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
메시지 박스는 응용 프로그램이 사용자와 대화할 수 있는 가장 간단한 방법이다. 짧은 메시지와 함께 MB_OK 플래그로 간단하게 전달 사항만 전달하는 것이 보편적이다.

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

MoveFile : BOOL MoveFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName);
파일이나 디렉토리를 다른 위치로 옮긴다. 파일의 경우 같은 디렉토리의 다른 이름으로 변경할 수도 있으며 디렉토리는 포함된 모든 파일과 서브 디렉토리를 같이 이동시킨다. 위치를 옮기는 것은 원래 파일을 지우고 새 위치에 파일을 생성하는 것과 동일하지만 같은 드라이브내에서는 디스크 할당 표만 수정하고 실제 데이터는 이동하지 않아도 되므로 CopyFile 함수를 사용하는 것보다 이 함수를 사용하는 것이 훨씬 더 빠르다.

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

MoveFileEx : BOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags);
MoveFile과 마찬가지로 파일이나 디렉토리를 다른 위치로 옮기되 세부적인 동작에 대해 몇가지 플래그를 지정할 수 있다는 점이 다르다. 특히 재부팅할 때 실제로 파일을 옮겨 주는 기능은 설치 프로그램이 이미 사용중인 DLL을 교체하는 용도로 자주 사용된다. 이 함수는 다음 부팅시 이동할 파일을 레지스트리의 다음 위치에 기록해 놓는다.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations
이 값은 복수개의 널 종료 문자열로 구성되는 REG_MULTI_SZ 타입으로 되어 있으며 이동 대상이 되는 파일 목록을 가진다. 재부팅할 때 AUTOCHK 후 이 레지스트리에 기록된 대로 파일을 이동시키는데 이 때는 페이징 파일도 생성되기 전이므로 페이징 파일까지도 삭제할 수 있다.
단 재부팅 시 파일 이동은 NT/2000 이상에서만 지원되며 95/98에서는 이 기능을 사용할 수 없다. 대신 WinInit.ini 파일에 이동할 파일이나 디렉토리를 적어주면 되다. 이 파일의 [rename]섹션에 "대상=원본" 형식으로 엔트리를 작성해 놓으면 파일명이 변경된다. 단, 이 동작은 운영체제가 보호 모드로 들어가기 전에 이루어지므로 파일명은 반드시 짧은 이름으로 작성해야 한다. 파일명을 변경한 후 시스템은 WinInit.ini를 WinInit.bak로 변경하여 이 파일을 무효화시킨다.
재부팅시 파일을 교체하는 방법이 운영체제 버전에 따라 상이하므로 GetVersionEx 함수로 운영체제의 버전을 판변한 후 적절한 방법을 사용해야 한다.

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

MoveToEx : BOOL MoveToEx(HDC hdc, int X, int Y, LPPOINT lpPoint);
DC에는 현재 좌표가 보관되어 있으며 현재 좌표는 LineTo, PolyLineTo 등의 함수가 선의 시작점으로 사용한다. MoveToEx 함수는 현재 좌표를 (X,Y)좌표로 이동시켜 준다. 현재 위치는 LineTo, PolyLine등의 함수에 의해서도 변경된다.
이 함수의 16비트 버전은 MoveTo였으나 이 함수는 Win32에서 제거되었다. Win32에서 좌표값이 32비트로 확장됨으로써 리턴값 하나로 이전 좌표를 리턴할 수가 없게 됨에 따라 lpPoint로 이전 좌표를 조사하도록 변경되었다. 이 함수에 대한 예제는 LineTo 함수의 예제를 참고하기 바란다.

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

MoveWindow : BOOL MoveWindow(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint);
윈도우의 위치와 크기를 변경하는 가장 일반적인 함수이다. X,Y 인수로 윈도우의 위치를 지정하며 nWidth, nHeight 인수로 윈도우의 폭과 높이를 지정하므로 이 함수로 위치와 크기를 한꺼번에 변경할 수 있다. 단 위치만 변경하거나 크기만 변경하고자 할 경우는 GetWindowRect 등의 함수로 영역을 먼저 구한 후 원하는 값만 변경하거나 아니면 SetWindowPos 함수를 사용해야 한다.
이 함수는 일반적으로 부모 윈도우내에서 차일드 컨트롤을 정렬하기 위한 용도로 많이 사용하며 최상위 윈도우의 크기나 위치를 변경할 때는 이 함수 대신 SetWindowPos 함수를 사용하는 것이 바람직하다.

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

OutputDebugString : VOID OutputDebugString(LPCTSTR lpOutputString);
문자열을 디버거로 출력한다. 이 함수는 순수한 디버깅용의 함수이며 디버깅중에 프로그램의 실행 흐름과 상태를 쉽게 확인하기 위해 사용한다. 디버거(보통 비주얼 C++)가 없을 경우 시스템 디버거로 문자열을 보내며 시스템 디버거도 없을 경우 이 함수는 아무 일도 하지 않는다. 프로그램 실행중에 변수값이나 제어의 흐름을 확인하고자 할 때 메시지 박스 대신 이 함수를 사용한다.

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

PatBlt : BOOL PatBlt(HDC hdc, int nXLeft, int nYLeft, int nWidth, int nHeight, DWORD dwRop);
지정한 사각 영역을 채색하되 현재 DC에 선택되어 있는 브러시와 화면의 색상을 논리 연산한다. 논리 연산의 종류에 따라 두 색상을 다양하게 혼합할 수 있다. 비트맵 브러시가 선택되어 있다면 이 함수로 비트맵을 출력할 수도 있다. 모든 장치가 이 함수를 다 지원하는 것은 아니므로 GetDeviceCaps 함수의 RC_BITBLT 인덱스로 이 함수가 지원되는지를 먼저 조사한 후 사용해야 한다.

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

PeekMessage : BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg);
GetMessage 함수와 마찬가지로 메시지 큐에서 메시지를 읽는다. 메시지의 범위를 줄 수 있는 기능도 GetMessage와 동일하다. 그러나 이 함수는 GetMessage와는 달리 읽은 메시지를 무조건 제거하지 않으며 큐가 비어 있을 경우 대기하지 않고 곧바로 리턴한다는 점이 다르다. 따라서 이 함수는 메시지를 읽지 않고 단순히 메시지가 있는지 확인만 할 수 있으며 이런 특성은 백그라운드 작업에 적절하다.

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

PostMessage : LRESULT PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
메시지를 해당 윈도우의 메시지 큐에 붙이며 즉시 리턴한다. 이렇게 붙여진 메시지는 메시지 루프의 GetMessage 함수에 의해 꺼내져서 DiepatchMessage 함수에 의해 윈도우 프로시저로 보내지며 최종적으로 윈도우 프로시저에 의해 처리된다.
SendMessage와는 달리 메시지를 큐에 붙인 후 곧바로 리턴하므로 이 메시지는 곧바로 처리되지 않으며 메시지를 붙인 스레드는 곧바로 다른 작업을 할 수 있다.
메시지를 붙이는 시점과 메시지를 처리하는 시점이 비동기적이기 때문에 PostMessage의 wParam, lParam으로 포인터를 전달하는 것은 의미가 없다. 메시지를 붙일 때 존재하던 값이 메시지를 처리하는 시점에서는 사라질 수 있기 때문이다. 특히 WM_COPYDATA 메시지는 임시적인 데이터를 전역 공유 메모리에 생성한 후 전달하는데 이 메시지는 절대로 PostMessage로 붙일 수 없으며 SendMessage로만 보내야 한다.
PostMessage 호출의 아주 특수한 경우로 첫번째 인수가 NULL일 수도 있는데 이 경우는 특정 윈도우에게 메시지를 붙이는 것이 아니라 응용 프로그램 전반에 걸친 작업 지시를 보내는 경우이다. 대상 윈도우가 없기 때문에 이렇게 붙여진 메시지는 윈도우 프로시저까지 전달되지 않으며 메시지 루프에서 직접 처리해야 한다. 이때의 메시지 루프는 다음과 같이 작성한다.

while(GetMessage(&Message,0,0,0)) {
if (Message.message==WM_SOME) {
// 직접 처리
} else {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
}
GetMessage로 읽은 메시지가 대상 윈도우가 없는 스레드 전반에 걸친 메시지이므로 DispatchMessage를 호출하기 전에 while루프 내부에서 직접 처리해 주어야 한다.

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

PostQuitMessage : VOID PostQuitMessage(int nExitCode);
스레드 메시지 큐에 WM_QUIT 메시지를 붙이고 즉시 리턴한다. WM_QUIT 메시지를 큐에 붙임으로써 시스템에게 이 스레드가 종료될 것이라는 것을 미리 알려준다. 메시지 루프는 보통 WM_QUIT 메시지를 받으면 종료하도록 되어 있으므로 이 함수를 호출하면 메시지 루프가 종료된다. 특히 이 함수를 호출하는 스레드가 주 스레드일 경우는 주 스레드의 메시지 루프가 종료됨으로써 프로세스가 종료된다.
단, 이 함수는 메시지를 큐에 붙인 후 즉시 리턴하므로 호출 즉시 프로세스가 종료되는 것은 아니다. 즉 PostQuitMessage 호출 후 다른 처리를 계속할 수 있으며 이미 메시지 큐에 들어와 있는 모든 메시지가 처리된 후에 WM_QUIT 메시지가 읽혀져야 종료된다. 반면 ExitProcess 함수는 즉시 프로세스를 종료하기 때문에 이 함수 아래에 작성된 코드는 실행되지 않는다.
통상 이 함수는 메인 윈도우의 WM_DESTROY 메시지 처리 코드에 작성되어 메인 윈도우가 파괴되면 응용 프로그램을 종료하는 역할을 한다. 차일드 윈도우가 WM_DESTROY에서 이 메시지를 호출해서는 안된다. 그래서 DefWindowProc은 WM_DESTROY 메시지를 디폴트 처리할 때 PostQuitMessage를 호출하지 않도록 되어 있다.

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

ReadFile : BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
파일로부터 데이터를 읽는다. 주로 동기적인 입력에 사용하므로 데이터를 완전히 읽기 전에는 리턴하지 않는다. 비동기 입력에 사용할 경우는 즉시 리턴한다. 파일 포인터 위치에서부터 데이터를 읽으며 다 읽은 후 실제 읽은 바이트 수만큼 파일 포인터를 이동시켜 준다. 단, 비동기 입출력중일 때는 응용 프로그램이 파일 포인터를 직접 이동시켜 주어야 한다.

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

Rectangle : BOOL Rectangle(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
지정한 사각형을 그린다. 사각형의 변은는 현재 DC에 선택된 펜으로 그려지며 내부는 현재 DC에 선택된 브러시로 채워진다. 터보C의 Rectangle 함수와는 달리 내부를 채우므로 도스 프로그래밍을 해 본 사람은 주의해야 한다. 내부를 채우지 않으려면 NULL_BRUSH 스톡 오브젝트를 선택한 후 사각형을 그려야 한다. Ellipse의 예제를 참고하기 바란다.

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

RegisterClass : ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
윈도우 클래스를 등록한다. 윈도우 클래스는 생성될 윈도우의 여러 가지 특성을 가지는 구조체이며 CreateWindow 함수로 윈도우를 생성하기 전에 윈도우 클래스가 반드시 등록되어 있어야 한다. WNDCLASS 구조체를 선언한 후 이 구조체에 원하는 속성을 설정하고 RegisterClass 함수로 윈도우 클래스를 등록한다. 다음 코드는 가장 일반적인 윈도우 클래스 등록 코드이다.

WNDCLASS WndClass;


WndClass.cbClsExtra=0;
WndClass.cbWndExtra=0;
WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
WndClass.hInstance=hInstance;
WndClass.lpfnWndProc=(WNDPROC)WndProc;
WndClass.lpszClassName=lpszClass;
WndClass.lpszMenuName=NULL;
WndClass.style=CS_HREDRAW | CS_VREDRAW;
RegisterClass(&WndClass);

hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
NULL,(HMENU)NULL,hInstance,NULL);

이 함수로 등록한 윈도우 클래스는 별도의 작은 아이콘을 지정할 수 없으며 hIcon 멤버가 지정하는 아이콘을 축소하여 작은 아이콘으로 대신 사용한다. 작은 아이콘을 따로 지정하려면 RegisterClassEx 함수를 사용해야 한다.

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

RegisterClassEx : ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx);
윈도우 클래스를 등록한다. RegisterClass 함수와 유사하되 이 함수가 사용하는 WNDCLASSEX 구조체는 작은 아이콘을 따로 등록할 수 있다는 점만 다르다.

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

ReleaseCapture : BOOL ReleaseCapture(VOID);
마우스 캡처를 푼다. 마우스를 캡처한 윈도우는 커서의 위치에 상관없이 모든 마우스 메시지를 받는데 이 함수로 캡처를 풀면 이 상태가 종료되며 마우스 메시지는 커서 아래쪽에 있는 윈도우로 전달된다. SetCapture로 마우스를 캡처한 윈도우는 필요한 동작을 완료한 후 반드시 이 함수를 호출하여 캡처를 풀어 주어야 한다예제는 SetCapture 함수를 참조하기 바란다.

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

ReleaseDC : int ReleaseDC(HWND hWnd, HDC hDC);
GetDC함수로 구한 커먼 DC의 핸들은 반드시 이 함수로 해제해 주어야 한다. 그러나 클래스 DC나 프라이비트 DC는 해제해 주지 않았도 된다. GetWindowDC로 구한 DC는 반드시 이 함수로 해제해 주어야 한다. 이 함수는 GetDC를 호출한 스레드 내에서 호출되어야 하며 다른 스레드에서 호출할 수 없다. GetDC 함수의 예제를 참고하기 바란다.

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

ScreenToClient : BOOL ScreenToClient(HWND hWnd, LPPOINT lpPoint);
화면의 원점을 기준으로 하는 좌표 lpPoint를 hWnd의 작업 영역을 기준으로 하는 좌표로 변환한다. hWnd윈도우의 작업 영역 원점의 화면 좌표가 cx, cy일 때 lpPoint는 lpPoint.x - cx, lpPoint - cy로 변환된다. GetCursorPos, MoveWindow, GetWindowRect 등과 같이 화면 좌표를 리턴하는 함수로부터 작업 영역의 좌표로 변환하고자 할 때 이 함수를 사용한다.

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

ScrollWindow : BOOL ScrollWindow(HWND hWnd, int XAmount, int YAmount, CONST RECT *lpRect, CONST RECT *lpClipRect);
윈도우를 스크롤시킨다. 일반적으로 이 함수는 WM_HSCROLL, WM_SCROLL 등의 스크롤 관련 메시지를 받았을 때 호출되며 스크롤 바로 스크롤 된 양만큼 작업 영역을 이동시키는 역할을 한다. 스크롤된 양만큼 작업 영역을 BitBlt 함수로 고속 복사하며 새로 드러나는 부분은 무효화시켜 WM_PAINT에서 다시 그리도록 해 준다.
만약 스크롤 영역에 캐럿이 있다면 스크롤하기 전에 캐럿을 숨기며 스크롤이 끝난 후 캐럿을 복구시켜준다. 캐럿의 위치는 자동으로 조정된다. 작업 영역의 일부부만을 스크롤시키고자 할 때는 lpRect에 스크롤 영역을 지정할 수 있으며 이때 화면의 깨짐을 방지하기 위해 클리핑 영역을 지정해 주는 것이 좋다.

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

SelectObject : HGDIOBJ SelectObject( HDC hdc,HGDIOBJ hgdiobj);
GDI는 그래픽 출력에 사용되는 도구이며 펜, 브러시, 비트맵, 리전, 패스, 팔레트, 폰트 등등이 있다. 그리기에 사용할 GDI 오브젝트를 변경하고자 할 때 이 오브젝트를 만든 후 이 함수로 DC에 선택해 주어야 한다. 다음은 브러시를 생성하여 DC에 선택하는 예이다.

HBRUSH MyBrush,OldBrush;
MyBrush=CreateSolidBrush(RGB(255,255,0));
OldBrush=(HBRUSH)SelectObject(hdc,MyBrush);
// 브러시를 사용한다.
SelectObject(hdc,OldBrush);
DeleteObject(MyBrush);

CreateSolidBrush로 MyBrush 브러시를 만든 후 SelectObject 함수로 이 브러시를 DC에 선택해 준다. 이 때 SelectObject가 리턴하는 이전 브러시의 핸들은 복구를 위해 OldBrush 등의 변수에 저장해 두어야 한다. 브러시를 선택한 후 모든 그리기 함수는 MyBrush로 면을 채색한다. 브러시를 사용한 후에는 반드시 DeleteObject 함수로 삭제해 주어야 하되 그전에 DC에 선택되어 있는 브러시를 선택해제해 주어야 한다. DC에 선택된 브러시는 삭제할 수 없기 때문이다. 그래서 OldBrush를 다시 선택해 주어 MyBrush를 해제하도록 하였다. 이 코드는 다음과 같이 한줄로 작성할 수도 있다.

DeleteObject(SelectObject(hdc,OldBrush));

SelectObject 함수가 이전에 선택되어 있던 같은 타입의 GDI 오브젝트를 리턴해 주기 때문에 리턴되는 브러시를 DeleteObject 함수로 삭제하였다.

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

SendMessage : LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
메시지를 윈도우에게 보낸다. 해당 윈도우의 윈도우 프로시저를 호출하여 이 메시지가 완전히 처리되기 전에는 리턴하지 않는다. 같은 스레드에 속한 윈도우에게 메시지를 보낼 때는 마치 서브루틴을 호출하는 것과 동일하다. 예를 들어 메인 윈도우가 차일드 윈도우인 리스트 박스에게 LB_ADDSTRING이나 LB_GETCOUNT 등의 메시지를 보내면 리스트 박스는 해당 동작을 수행하는 서브루틴을 호출하고 이 동작이 완료될 때까지 SendMessage는 리턴하지 않는다.
다른 스레드에 속한 윈도우에게 메시지를 보낼 때는 스레드 스위칭이 발생하며 메시지를 받는 스레드가 메시지를 읽는 코드를 실행중이어야 한다. 만약 메시지를 받는 스레드가 메시지 처리에 오랜 시간을 소모한다면 SendMessage를 호출한 스레드는 이 함수가 리턴할 때까지 블록 상태로 남아있게 된다.리스트 박스로 문자열을 추가할 때는 LB_ADDSTRING 메시지를 보내주면 된다. 이와 같이 부모 윈도우가 차일드에게 명령을 내리거나 상태를 조사하는 가장 기본적인 방법은 SendMessage로 메시지를 보내는 것이다. 각 차일드별로 보낼 수 있는 메시지의 종류가 다양하다.
SendMessage 함수는 보낸 메시지가 완전히 처리되기 전에는 리턴하지 않는 블록 특성을 가지고 있다. 특히 이런 특성은 다른 스레드간에 메시지를 주고 받을 때 자주 목격되는데 이 문제를 해결하는 방법에 대해서는 InSendMessage 함수를 참고하기 바란다.
WM_COPYDATA 등의 특정 메시지는 반드시 SendMessage 함수로만 보내야 하며 PostMessage를 쓸 수 없는 것도 있다.

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

SetBkColor : COLORREF SetBkColor(HDC hdc, COLORREF crColor);
TextOut나 ExtTextOut 함수로 출력되는 문자열은 DC에 설정되어 있는 배경색에 출력된다. 디폴트 DC의 배경색은 흰색이므로 아무 지정없이 문자열을 출력하면 흰 바탕에 문자열이 출력되나 이 함수로 배경색을 변경하면 출력되는 문자열의 배경색을 변경할 수 있다. SetTextColor 함수의 예제를 참고하기 바란다.

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

SetBkMode : int SetBkMode( HDC hdc, int iBkMode );
DC의 배경 혼합 모드(Mix Mode)를 설정한다. 혼합 모드란 새로 출력되는 문자열의 배경을 어떻게 처리할 것인가를 지정하는데 SetBkColor에 의해 설정된 배경색 또는 디폴트 배경색인 흰색으로 배경을 출력하는 OPAQUE와 문자열의 획 사이를 투명하게 처리하는 TRANSPARENT 두가지 방법이 있다. 디폴트 혼합 모드는 불투명 모드인 OPAQUE이므로 문자열의 배경 색상이 출력되나 이 모드를 변경하면 투명한 문자열을 출력할 수 있다.

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

SetCapture : HWND SetCapture(HWND hWnd);
마우스 버튼의 누름, 이동, 뗌 등의 마우스 메시지는 보통 커서 바로 아래쪽에 있는 윈도우로 전달된다. 이는 지극히 정상적이며 상식적이나 가끔 커서가 영역밖을 벗어나도 계속적으로 마우스 메시지를 받아야 하는 경우도 있다. 이런 경우는 마우스 커서를 캡처해야 한다. SetCapture 함수는 hWnd 윈도우가 마우스 커서를 캡처하도록 하며 이렇게 되면 커서가 윈도우의 영역밖을 벗어나더라도 계속해서 마우스 메시지를 보내준다. 이 상태는 ReleaseCapture 함수로 캡처를 풀기 전까지 계속된다. 주로 드래그 동작을 할 때 캡처가 필요하다현재 스레드에 속한 윈도우만 캡처를 할 수 있으며 한번에 하나의 윈도우만 마우스를 캡처할 수 있다. 마우스를 캡처한 윈도우는 모든 마우스 메시지를 전달받는데 단 예외적으로 다른 스레드에 속한 윈도우를 누를 경우는 캡처 여부에 상관없이 커서 아래쪽의 윈도우로 메시지가 전달되며 이 경우 해당 윈도우는 포그라운드 상태가 된다. 마우스 버튼 누름은 작업의 전환을 의미하므로 캡처 여부에 상관없이 해당 윈도우로 전달된다. 그러나 보통 마우스 버튼을 누른 상태에서 커서를 캡처하기 때문에 이런 경우는 극히 드물다.
포그라운드 윈도우가 마우스를 캡처하는 것이 보통이지만 백그라운드 윈도우도 마우스를 캡처할 수 있다. 이 경우 커서가 백그라운드 윈도우의 보이는 부분에 있을 때만 마우스 메시지가 전달된다. 캡처 상태에서는 메뉴의 핫키, 액셀러레이터 등은 동작하지 않는다.

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

SetClassLong : DWORD SetClassLong( HWND hWnd, int nIndex, LONG dwNewLong);
윈도우 클래스는 WNDCLASS(EX) 구조체에 의해 속성들이 지정되며 RegisterClass(Ex) 함수에 의해 이 속성들이 등록된다. 일단 등록된 클래스의 속성을 변경하고자 할 때 SetClassLong 함수가 사용되는데 WNDCLASS 구조체의 멤버 중 어떤 값을 변경할 것인가를 nIndex 인수로 지정해 주고 dwNewLong 인수로 새로운 속성값을 지정해 주면 된다. 단 윈도우 클래스의 정보 중 이름은 변경할 수 없다.
윈도우 클래스의 속성 중 어떤 것을 변경하는가에 따라 효과는 다양하게 나타난다. 배경 브러시를 변경할 수도 있고 커서 모양을 바꿀 수도 있고 윈도우 프로시저를 변경할 수도 있다. 이어지는 예제로 이 함수의 사용예를 살펴보도록 하자.

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

SetCursor : HCURSOR SetCursor(HCURSOR hCursor);
마우스의 현재 위치를 알려주는 커서를 hCursor로 변경하되 만약 이미 hCursor가 설정되어 있다면 아무 일도 하지 않는다. 즉, 같은 커서를 두번 설정할 때는 불필요한 커서 변경을 하지 않는다. 여러벌의 커서를 준비해 두고 현재 상태에 따라 다른 모양의 커서를 사용하고자 할 경우 이 함수로 커서를 변경하면 된다.

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

SetDlgItemInt : BOOL SetDlgItemInt( HWND hDlg, int nIDDlgItem, UINT uValue, BOOL bSigned );
대화상자의 컨트롤에 정수값을 대입한다. 이때 컨트롤은 버튼, 에디트, 스태틱 등의 텍스트 표현이 가능한 컨트롤이어야 한다. 이 함수는 정수를 문자열로 변환한 후 WM_SETTEXT 메시지를 컨트롤로 보내 텍스트를 설정한다. 대입한 텍스트는 컨트롤의 적당한 위치에 나타난다. 다음 코드는 hDlg 대화상자의 IDC_EDIT1 컨트롤에 1234라는 정수를 대입한다.
SetDlgItemInt(hDlg,IDC_EDIT1,1234,FALSE);
이 함수는 내부적으로 정수를 문자열로 바꾼 후 문자열을 컨트롤에 대입하므로 다음 호출문과 동일하다.
itoa(1234,buf,10);
SetDlgItemText(hDlg,IDC_EDIT1,buf);
그러나 이렇게 직접 정수값을 문자열로 바꾼 후 대입할 경우 정수값을 16진수로 대입하거나 천단위로 콤마를 삽입하는 등의 추가 작업을 할 수 있다는 이점이 있다.

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

SetDlgItemText : BOOL SetDlgItemText( HWND hDlg, int nIDDlgItem, LPCTSTR lpString );
대화상자의 컨트롤에 텍스트를 대입한다. 이때 컨트롤은 버튼, 에디트, 스태틱 등의 텍스트 표현이 가능한 컨트롤이어야 한다. 이 함수는 WM_SETTEXT 메시지를 컨트롤로 보내 텍스트를 설정한다. 대입한 텍스트는 컨트롤의 적당한 위치에 나타난다. 다음 코드는 hDlg 대화상자의 IDC_EDIT1 에디트 컨트롤에 "테스트"라는 문자열을 대입한다.
SetDlgItemText(hDlg, IDC_EDIT1, "테스트");
이 함수 호출문은 다음 함수 호출문과 동일하다. 컨트롤의 윈도우 핸들을 구한 후 SetWindowText 함수로 텍스트를 대입할 수도 있고 WM_SETTEXT 메시지를 사용할 수도 있다.
SetWindowText(GetDlgItem(hDlg,IDC_EDIT1),"테스트");
SendMessage(GetDlgItem(hDlg,IDC_EDIT1),WM_SETTEXT,0,(LPARAM)"테스트");
이 두 함수는 GetDlgItem 함수로 윈도우 핸들을 먼저 구한 후 텍스트를 대입하지만 SetDlgItemText 함수는 컨트롤의 ID로 텍스트를 대입할 수 있기 때문에 훨씬 더 편리하다.

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

SetFocus : HWND SetFocus(HWND hWnd);
포커스란 입력 촛점이며 포커스를 가진 윈도우에게 키보드 메시지가 전달된다. 한번에 하나의 윈도우만 포커스를 가질 수 있으며 보통 사용자가 컨트롤을 선택함으로써 포커스를 이동시키지만 프로그램이 이 함수로 포커스를 강제로 이동시킬 수도 있다.
이 함수는 hWnd 윈도우로 포커스를 강제로 이동시키는데 이 윈도우는 반드시 같은 스레드에 속해 있어야 한다. 포커스를 변경하기 전에 이전에 포커스를 가지고 있던 윈도우로 WM_KILLFOCUS 메시지를 보내주며 새로 퐄서를 받는 윈도우로 WM_SETFOCUS 메시지를 보내준다. 새로 포커스를 받는 윈도우와 그 부모 윈도우는 활성화된다. 보통 WM_CREATE에서 원하는 컨트롤에 포커스를 설정하기 위해 이 함수를 사용한다. 대화상자의 경우 WM_INITDIALOG 메시지를 받았을 때 이 함수를 호출하여 포커스를 이동시키는데 이 경우 반드시 FALSE를 리턴해 주어야 한다.

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

SetMapMode : int SetMapMode(HDC hdc, int fnMapMode);
윈도우즈에서 사용되는 좌표는 논리 좌표와 물리 좌표 두가지가 있다.

■ 논리 좌표 : 윈도우즈 내부에서 사용되는 좌표를 말한다. TextOut (100,100,...)에서 지정한 (100,100)이 곧 논리 좌표이며 논리 좌표의 실제 위치는 경우에 따라 달라진다. 그래픽 함수들이 사용하는 모든 좌표는 논리 좌표이며 좀 더 현실적으로 얘기한다면 DC핸들을 인수로 받아들이는 모든 함수는 논리 좌표를 사용한다■ 물리 좌표 : 실제 화면에 출력되는 좌표이며 픽셀 단위를 사용한다. 모니터의 물리적인 픽셀 단위를 사용하므로 물리 좌표 (100,100)은 그 위치가 정해져 있다. 윈도우를 관리하는 함수(또는 메시지) 에서 사용하는 좌표는 물리 좌표이다.

맵핑 모드란 주어진 논리 좌표가 화면상의 어디에 해당하는지를 결정하는 변환 방법을 의미한다. 즉 논리 좌표와 물리 좌표의 관계를 정의한다. 디폴트 맵핑 모드는 논리 좌표와 물리 좌표가 일치되어 있는 MM_TEXT이므로 어떠한 좌표 변환도 일어나지 않지만 맵핑 모드를 변경하면 논리 좌표의 실제 화면 위치에 변화를 줄 수 있다.

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

SetMenu : BOOL SetMenu(HWND hWnd, HMENU hMenu);
윈도우의 메뉴를 설정 또는 변경한다. 윈도우에 메뉴가 붙어 있지 않으면 hMenu를 새로 붙이고 이미 메뉴가 붙어 있으면 새로운 메뉴로 대체된다. 이때 이전 메뉴는 자동으로 파괴되지 않으므로 DestroyMenu 함수로 파괴해 주어야 한다. 메뉴가 변경되면 윈도우는 다시 그려진다.

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

SetPixel : COLORREF SetPixel(HDC hdc, int X, int Y, COLORREF crColor);
점을 찍는다. 모든 장치가 이 함수를 지원하 않으므로 GetDeviceCaps 함수로 지원 여부를 조사해 본 후 이 함수를 호출해야 한다.

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

SetROP2 : int SetROP2(HDC hdc, int fnDrawMode);
그리기 모드란 GDI 함수가 화면에 출력을 내보낼 때 화면에 이미 출력되어 있는 그림과 새로 그려지는 그림과의 관계를 정의하는 것이다. AND, OR, XOR 등 비트간의 이진 연상 방법과 NOT 연산의 조합으로 지정된다.그리기 연산은 래스터 디바이스에만 적용되며 벡터 디바이스에는 적용되지 않는다.
이 함수명의 ROP는 Rater OPeration의 약자이며 2는 화면색상과 펜의 색상 2개를 피연산자로 취한다는 뜻이다.

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

SetScrollInfo : int SetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw);
스크롤 바의 범위와 위치를 설정한다. 이 함수로 범위를 지정하면 페이지 크기를 전달할 수 있으므로 비례 스크롤 바를 만들 수 있다. nPage, nPos 값에 대해 유효한 값인지를 점검하므로 엉뚱한 값이 들어가지 않도록 해 준다. 다음 코드는 ScrollWindow 함수의 예제에서 사용한 코드이다.

case WM_SIZE:
si.cbSize=sizeof(SCROLLINFO);
si.fMask=SIF_ALL | SIF_DISABLENOSCROLL;
si.nMin=0;
si.nMax=1000;
si.nPage=HIWORD(lParam);
si.nPos=yPos;
SetScrollInfo(hWnd, SB_VERT, &si, TRUE);

si.nPage=LOWORD(lParam);
si.nPos=xPos;
SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
return 0;

이 윈도우는 1000*1000의 크기를 가지므로 스크롤 범위도 0~1000으로 설정하였으며 페이지 크기는 윈도우의 폭과 높이로 설정하였다.

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

SetScrollPos : int SetScrollPos( HWND hWnd, int nBar, int nPos, BOOL bRedraw );
스크롤 바위 현재 위치, 즉 스크롤 바의 값을 설정한다. 통상 이 함수는 WM_HSCROLL, WM_VSCROLL 등의 스크롤 바 메시지 처리 루틴에서 스크롤 바의 위치값을 변경하기 위해 호출한다. 범위를 지정하는 nPos는 32비트의 정수이나 메시지로 전달되는 스크롤 바 위치는 16비트값이기 때문에 위치값은 16비트로 제한된다. 그러나 메시지의 인수를 참조하지 않고 GetScrollInfo 등의 함수로 스크롤 바의 위치를 직접 조사하면 32비트의 스크롤 위치값을 지정할 수도 있다.

이 함수에 대한 예제는 SetScrollRange 함수의 예제를 참조하기 바란다. 다음 코드는 스크롤 바위 범위를 0~255로 설정하고 초기 위치를 192로 설정한 것이다.

SetScrollRange(hScroll,SB_CTL,0,255,FALSE);
SetScrollPos(hScroll,SB_CTL,192,FALSE);

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

SetScrollRange : BOOL SetScrollRange(HWND hWnd, int nBar, int nMinPos, int nMaxPos, BOOL bRedraw);
표준 스크롤 바 또는 스크롤 바 컨트롤의 범위를 설정한다. 표준 스크롤 바의 디폴트 범위는 0~100까지이며 스크롤 바 컨트롤의 디폴트 범위는 둘 다 0으로 비어 있다. 일반적으로 스크롤 바 생성 직후에 이 함수로 적절한 스크롤 범위를 설정해 주어야 한다. nMaxPos와 nMinPos의 차인 범위는 MAXLONG보다 커서는 안된다.
최소, 최대값이 같을 경우는 스크롤 바가 숨겨지는 효과가 있는데 이런 목적으로 이 함수를 호출하는 것은 바람직하지 않으며 ShowScrollBar 함수를 사용하는 것이 좋다.

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

SetTextAlign : UINT SetTextAlign(HDC hdc, UINT fMode);
TextOut 함수가 지정하는 좌표는 디폴트로 문자열 출력 영역의 좌상단 좌표이다. 이 함수는 문자열의 출력 영역과 출력 좌표와의 관계를 변경함으로써 문자열의 출력 위치에 영향을 준다.

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

SetTextColor : COLORREF SetTextColor(HDC hdc, COLORREF crColor);
TextOut나 ExtTextOut 함수로 출력되는 문자열은 DC에 설정되어 있는 전경색으로 출력된다. 디폴트 DC의 전경색은 검정색이므로 아무 지정없이 문자열을 출력하면 검정색으로 출력되나 이 함수로 전경색을 변경하면 출력되는 문자열의 색상을 변경할 수 있다.

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

SetTimer : WORD SetTimer(HWND hWnd, int nIDEvent,WORD wElapse,FARPROC lpTimerFunc);
타이머를 생성하고 wElapse가 지정하는 간격으로 WM_TIMER메시지를 보낸다. WM_TIMER 메시지를 받는 곳은 lpTimerFunc의 설정에 따라 달라진다. lpTimerFunc가 NULL일 경우는 타이머를 설치한 윈도우의 WndProc으로 보내지며 그렇지 않을 경우는 lpTimerFunc가 지정하는 콜백함수로 보내진다. 타이머 메시지는 응용 프로그램의 메시지 큐에 저장되며 윈도우즈의 다른 프로그램에 의해 시간이 지연될 수도 있으므로 반드시 정확한 간격으로 전달된다는 보장은 없다. 시계, 간단한 에니메이션 등 일정한 주기로 호출되어져야 할 함수가 있을 때 보통 타이머 메시지를 사용한다. 또는 시스템의 속도와는 무관하게 일정한 속도를 유지해야하는 게임등에도 타이머 메시지가 사용된다.
만약 hWnd의 nIDEvent 타이머가 이미 설치되어 있다면 이 함수는 새로운 주기로 타이머를 다시 설치하며 이 경우 타이머는 리셋된다. 타이머의 주기를 바꾸고자 할 경우 같은 ID로 이 함수를 호출해 준다.

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

SetWindowLong : LONG SetWindowLong( HWND hWnd, int nIndex, LONG dwNewLong);
윈도우의 속성은 CreateWindow(Ex) 함수로 윈도우를 생성할 때 지정한다. 일단 윈도우가 만들어진 후에는 이 함수로 윈도우의 속성을 변경할 수 있다. 이때 주로 변경의 대상이 되는 것은 GWL_STYLE 즉 윈도우의 스타일이며 여분 메모리 조작을 위해서도 이 함수가 사용된다. 또한 윈도우 프로시저의 번지를 새로운 함수로 바꿈으로써 윈도우를 서브 클래싱할 수도 있다.
단 이 함수는 같은 스레드에 속한 윈도우의 속성만을 변경할 수 있다. 다른 스레드에서 생성한 윈도우의 속성은 변경할 수 없다. 또한 이 함수로 변경할 수 있는 값들 중 특정 스타일은 시스템에 의해 캐시되므로 변경 즉시 효과가 나타나지 않을 수도 있으며 SetWindowPos 함수로 캐시를 비워주어야 하는 것도 있다. 다음 예제를 통해 이 함수의 사용예를 보도록 하자.

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

SetWindowPos : BOOL SetWindowPos(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags);
이 함수는 윈도우의 위치, 크기, Z순서를 동시에 또는 일부만 변경할 때 사용된다. 예를 들어 크기는 그대로 두고 위치만 변경하고자 한다거나 위치와 크기는 그대로 두고 Z순서만 변경하고자 할 때 사용한다. MoveWindow 함수는 크기와 위치를 항상 같이 변경하지만 이 함수는 SWP_NOSIZE, SWP_NOMOVE 플래그로 위치와 크기를 개별적으로 변경할 수 있다.
또한 이 함수는 Z순서를 변경하기 위한 목적으로, 특히 항상 위(Top Most) 속성을 토글하기 위한 용도로도 많이 사용되는데 두번째 인수에 HWND_(NO)TOPMOST를 줌으로써 이 속성을 토글할 수 있다. 이 함수로 항상 위 속성을 설정하면 이 윈도우에 소유된 윈도우도 항상 위 속성을 같이 가지게 된다. 그러나 이 윈도우를 소유한 윈도우는 영향을 받지 않는다. 반대로 이 함수로 항상 위 속성을 해제하면 이 윈도우에 소유된 윈도우와 이 윈도우를 소유한 윈도우의 항상 위 속성이 모두 해제된다.
일반적으로 항상 위 속성을 가지지 않은 윈도우가 항상 위 속성을 가진 윈도우를 소유할 수는 있지만 반대는 불가능하다. 왜냐하면 소유된 윈도우는 소유한 윈도우보다 Z순서의 위쪽에 있어야 하므로 소유한 윈도우만 항상 위 옵션을 가질 수는 없기 때문이다. 이렇게 되면 항상 위 옵션을 가지는 윈도우의 차일드로 열려 있는 대화상자가 밑으로 숨어 버리는 현상이 발생할 수 있다. SetWindowPos 함수는 이 모든 처리를 다 해 주므로 항상 위 스타일을 토글 할 때는 SetWindowLong으로 SWL_EXSTYLE을 조작하지 말고 반드시 이 함수를 사용해야 한다.

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

SetWindowText : BOOL SetWindowText( HWND hWnd,l LPCTSTR lpString
윈도우나 컨트롤의 캡션을 설정한다. 윈도우의 캡션은 타이틀 바에 나타나며 컨트롤의 캡션은 작업 영역에 나타난다.

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

ShowWindow : BOOL ShowWindow(HWND hWnd, int nCmdShow);
윈도우의 보이기 상태를 지정한다. 보이기 상태(show state)란 보이기/숨기기는 물론이고 최대화/최소화/복구 상태 등 윈도우의 현재 상태를 통칭하는 용어이며 nCmdShow 인수가 지정하는 여러 가지 보이기 상태 중 하나를 선택할 수 있다.
단, 이 함수가 처음 호출될 때는 반드시 WinMain의 인수로 전달된 nCmdShow를 그대로 넘겨 주는 것이 좋다. 쉘은 프로그램을 실행할 때 사용자가 지정해 놓은 등록 정보를 WinMain으로 전달해 주는데 이 설정 상태대로 보여야 하기 때문이다. 만약 이 프로그램이 STARTUPINFO 구조체의 정보대로 생성되었다면 첫번째 ShowWindow 호출에서 nCmdShow 인수 지정은 무시되며 이 구조체가 지정하는 보이기 상태가 적용된다. 두번째 호출부터는 원하는 보이기 상태로 변경할 수 있다.

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

Sleep : VOID Sleep(DWORD dwMilliseconds);
스레드의 실행을 지정한 시간동안 잠시 정지시킨다. 지연 시간이 0일 경우 같은 우선 순위를 가진 다른 스레드에게 실행 시간을 양보하되 그런 스레드가 없으면 즉시 리턴하여 계속 실행한다. INFINITE는 무한 대기하도록 한다. 이 함수는 주로 스레드의 실행 시간을 천천히 수행하도록 함으로써 결과를 분명히 확인하고자 할 때 테스트용으로 종종 사용하며 스레드간의 동기화에도 사용된다.

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

StretchBlt : BOOL StretchBlt(HDC hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, HDC hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, DWORD dwRop);
DC간에 비트맵을 전송하여 복사한다. BitBlt와 동작하는 방식이 유사하나 단 복사원의 크기와 높이를 따로 지정할 수 있기 때문에 확대및 축소 복사할 수 있다. 20*40의 크기를 가지는 비트맵을 40*80영역에 복사하면 이 비트맵은 2배로 확대되며 10*20영역에 복사하면 절반으로 축소된다.

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

TextOut : BOOL TextOut(HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int cbString);
hdc에 문자열을 출력한다. 이때 출력 좌표는 (nXStart, nYStart)이되 이 좌표는 SetTextAlign이 설정한 정렬 상태에 영향을 받는다. 출력할 문자열의 색상은 SetTextColor, SetBkColor, SetBkMode 함수의 영향을 받는다

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

TrackPopupMenu : BOOL TrackPopupMenu(HMENU hMenu, UINT uFlags, int x, int y, int nReserved, HWND hWnd, CONST RECT *prcRect);
화면의 임의 위치에 팝업 메뉴를 출력한다. 팝업 메뉴는 주로 마우스 버튼을 누른 위치에 곧바로 열리므로 선택하기 편리하며 꼭 필요한 항목만을 포함하므로 메인 메뉴에 비해서는 사용하기 쉽다는 장점이 있다. 또한 마우스를 누른 위치나 상황에 따라 다른 메뉴를 보여줄 수도 있으므로 훨신 더 직관적이다.
팝업 메뉴를 만드려면 리소스에 메뉴를 만들고 LoadMenu, GetSubMenu 함수로 메뉴의 핸들을 구한 후 이 함수를 호출하면 된다. (x,y)는 팝업 메뉴가 출력될 좌표이되 팝업 메뉴는 화면상의 어디서나 열릴 수 있으므로 이 좌표는 작업 영역 좌표가 아닌 화면 좌표가 된다. 일반적으로 팝업 메뉴는 WM_CONTEXTMENU 메시지에서 여는데 이 메시지는 lParam으로 화면 좌표를 전달해 주므로 이 좌표에 팝업 메뉴를 열면 무난하다.
uFlags에는 여러가지 옵션의 조합을 줄 수 있는데 플래그는 성격에 따라 몇가지 그룹으로 분류할 수 있다. 다음 플래그들은 팝업 메뉴가 출력될 좌표를 해석하는 방법을 지정한다. 이 플래그들의 조합에 따라 (x,y)좌표를 팝업 메뉴의 어디로 해석할 것인가가 결정된다.
디폴트는 수평으로 좌측, 수직으로 위쪽에 정렬(TPM_LEFTALIGN | TPM_TOPALIGN)되며 이 위치가 가장 이상적이다. 수평으로 왼쪽 정렬된다는 것은 x좌표가 팝업 메뉴의 왼쪽 좌표라는 뜻이며 따라서 마우스 커서의 오른쪽에 팝업 메뉴가 열린다. 다음은 몇가지 플레그의 조합으로 팝업 메뉴의 출력 위치를 변경해 본 것이다.

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

TranslateAccelerator : int TranslateAccelerator(HWND hWnd, HACCEL hAccTable, LPMSG lpMsg);
액셀러레이터 명령을 만든다. 이 함수는 hAccTable을 참조하여 WM_KEYDOWN, WM_SYSKEYDOWN으로부터 WM_COMMAND, WM_SYSCOMMAND 메시지를 만들어 낸다. 눌러진 키가 액셀러레이터 테이블에 정의된 명령일 경우 명령 메시지로 변환하여 메시지 큐에 붙여주며 이 메시지는 다음번 GetMessage나 PeekMessage에 의해 읽혀져 처리되며 이 메시지가 완전히 처리되기 전에는 리턴하지 않는다.

일반적으로 액셀러레이터는 메뉴 항목에 대한 단축키를 제공하기 위해 작성한다. 이 경우 액셀러레이터키가 눌러지면 마치 메뉴가 선택된 것처럼 WM_INITMENU, WM_INITPOPUPMENU 메시지가 전달된다. 단 윈도우가 사용금지되어 있거나 메뉴 항목이 사용금지된 경우, 마우스가 캡처된 경우는 제외된다. WM_COMMAND 메시지는 명령이 액셀러레이터로부터 온 경우 wParam의 상위 워드로 1이 전달되며 메뉴로부터 온 경우 0이 전달되는데 보통 이 구분은 무시하지만 메뉴로부터의 명령과 액셀러레이터로부터의 명령을 구분하려면 HIWORD(wParam)을 참고하도록 한다. 다음은 액셀러레이터가 정의되어 있을 경우의 메시지 루프이다.

while(GetMessage(&Message,0,0,0)) {
if (!TranslateAccelerator(hWnd,hAccel,&Message)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
}

GetMessage로 메시지를 조사한 후 먼저 TranslateAccelerator 함수가 이 메시지를 검사하여 액셀러레이터표에 있는 키보드 입력인지 조사한다. 만약 그렇다면 이 메시지는 WM_COMMAND로 변환되어 메시지 처리 함수로 보내지며 이 경우 TranslateMessage, DispatchMessage 함수는 호출되지 말아야 한다.

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

TranslateMessage : BOOL TranslateMessage(CONST MSG *lpMsg);
조사한 메시지를 문자 메시지로 변환한다. WM_KEYDOWN, WM_KEYUP 조합에 의해 WM_CHAR 메시지를 만들어 내며 WM_SYSKEYDOWN, WM_SYSKEYUP 조합에 의해 WM_SYSCHAR, WM_SYSDEADCHAR 메시지를 만들어낸다. 이때 TranslateMessage 함수는 키보드 드라이버가 제공하는 문자 구성에 따라 문자로 변환되는 키에 대해서만 변환을 하며 나머지 키 입력은 변환하지 않는다. 만들어진 문자 메시지는 메시지 스레드 큐에 붙여지며 다음번 GetMessage나 PeekMessage 함수에서 읽혀진다.
이 함수는 메시지 루프내에서 키보드 메시지를 문자 메시지로 변환하기 위한 목적으로만 사용되며 다른 목적으로 사용해서는 안된다.

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

UpdateWindow : BOOL UpdateWindow(HWND hWnd);
이 함수는 윈도우 프로시저로 WM_PAINT 메시지를 보내 작업영역을 강제로 그리도록 한다. WM_PAINT 메시지는 우선 순위가 늦기 때문에 무효 영역이 있더라도 먼저 처리해야할 다른 메시지가 있으면 즉시 처리되지 않는다. 만약 다른 어떤 메시지보다도 WM_PAINT를 먼저 처리해야 할 필요가 있다면 이 함수를 호출하여 즉시 작업영역을 다시 그리도록 할 수 있다.
이 함수는 메시지 큐를 통하지 않고 윈도우 프로시저로 곧바로 WM_PAINT 메시지를 전달하므로 메시지 대기 순서에 상관없이 즉시 작업영역을 다시 그리도록 한다. 그러나 작업영역에 무효영역이 없으면 이 함수를 호출한다하더라도 WM_PAINT 메시지는 보내지지 않는다. 작업영역을 완전히 다시 즉시 그리려면 InvalidateRect 함수로 작업영역을 무효화한 후 이 함수를 호출하면 된다.

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

WindowProc : LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
윈도우로 전달되는 메시지를 처리하는 메시지 처리 함수이며 보통 윈도우 프로시저(Window Procedure)라고 부른다. 사용자 정의 함수이므로 이름은 정해져 있지 않으나 보통 WndProc 또는 WindowProc이라는 이름을 많이 사용한다. 운영체제는 사용자의 조작과 시스템 내부의 변화가 있을 때 메시지 큐에 메시지를 넣으며 WinMain의 메시지 루프는 메시지 큐에서 메시지를 꺼내 윈도우 프로시저로 전달해 준다.
윈도우 프로시저는 자신에게 전달된 메시지의 의미를 분석하여 응용 프로그램 고유의 처리를 한다. 보통 하나의 프로그램이 복수개의 메시지를 처리하므로 윈도우 프로시저는 일반적으로 메시지별로 고유한 처리를 할 수 있는 switch문으로 구성된다.

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

WriteFile : BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
파일에 데이터를 기록한다. 주로 동기적인 출력에 사용하지만 OVERLAPPED 구조체를 제공하면 비동기 입출력을 할 수도 있다. 파일 포인터가 가리키고 있는 지점에 데이터를 기록하며 기록을 완료한 후 실제 기록한 바이트수만큼 파일 포인터를 이동시켜 준다. 단, 비동기 입출력 중일 때는 응용 프로그램이 파일 포인터를 직접 이동시켜 주어야 한다.
파일뿐만 아니라 파이프, 소켓, 통신 포트, 콘솔 등의 장치로도 데이터를 출력할 수 있다.

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


메시지

WM_ACTIVATE : LOWORD(wParam) : 윈도우가 활성화되었는지 비활성화 되었는지를 표현한다.
어떤 시점에서 활성화 상태의 윈도우는 오직 하나만 존재할 수 있다. 활성화되어 있다는 것은 현재 사용자가 사용하고 있는 윈도우라는 뜻이다. 탑 레벨(오버랩드, 팝업)윈도우만 활성화 될 수 있으며 차일드 윈도우는 활성화될 수 없다. 활성화된 부모 윈도우에 속한 차일드 윈도우는 포커스를 가질 수 있다. 이 메시지는 윈도우의 활성화 상태가 변경될 때 보내지는데 새로 활성화되는 윈도우와 활성 상태를 잃는 윈도우에게 동시에 보내진다. 예를 들어 A윈도우가 활성화된 상태에서 사용자가 B윈도우를 선택했다면 A윈도우에게는 비활성화된다는 메시지가 전달되며 B윈도우에게는 새로 활성화 된다는 메시지가 전달된다.

두 윈도우가 같은 스레드에 소속되어 있으면 즉, 동일한 메시지 큐를 사용하고 있으면 메시지는 동기적으로 전달된다. 비활성화되는 윈도우에게 메시지가 먼저 전달되고 이 메시지가 처리된 후 활성화된 윈도우에게 메시지가 이어서 전달된다. 따라서 비활성화되는 윈도우가 이 메시지에 늦게 응답하면 활성화되는 윈도우도 그만큼 늦게 활성화된다. 두 윈도우가 다른 메시지 큐를 사용하고 있으면 이 메시지는 비동기적으로 전달되므로 비활성화되는 윈도우의 응답 여부에 상관없이 새로 활성화 되는 윈도우는 윈도우는 즉시 사용 가능한 상태가 된다.

DefWindowProc은 최소화되지 않은 윈도우가 활성화될 때 키보드 포커스를 전달한다. 사용자가 마우스로 윈도우를 클릭하여 활성화했다면 WM_MOUSEACTIVATE 메시지가 먼저 전달되고 이 메시지의 응답 결과에 따라 활성화 여부가 결정된다.

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

WM_ACTIVATEAPP : wParam : 이 인수가 TRUE이면 윈도우가 활성화된 것이고 FALSE이면 비활성화된 것이다.
다른 프로세스에 소속된 윈도우로 활성 상태가 이동될 때 이 메시지가 발생한다. 이 메시지는 새로 활성화되는 윈도우와 비활성화되는 윈도우에 동시에 전달된다. 그러나 같은 프로세스내의 윈도우로 포커스가 이동할 때는 이 메시지가 발생하지 않으며 WM_ACTIVATE 메시지가 대신 발생한다. 응용 프로그램 전체의 활성 상태를 프로그래밍할 때만 이 메시지를 사용하며 윈도우 단위의 활성화는 WM_ACTIVATE 메시지를 대신 사용한다.

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

WM_APP : wParam, lParam 모두 의미가 정해져 있지 않다. 응용 프로그램이 의미를 정해서 사용할 수 있다.
WM_APP는 응용 프로그램을 위한 고유의 메시지를 정의하기 위한 상수값이며 이 범위 이후부터 윈도우 클래스의 사용자 정의 메시지를 만들 수 있다. 이 값은 0x8000으로 정의되어 있으며 보통 WM_APP+n으로 사용자 정의 메시지를 정의한다. 이때 n은 1보다 큰 정수이며 사용자 정의 메시지간의 구분을 위해 사용된다. 여러 개의 사용자 정의 메시지가 필요하다면 WM_APP+1, WM_APP+2, WM_APP+3,... 식으로 계속 n을 증가시켜 가며 메시지를 정의할 수 있다. 윈도우즈는 WM_APP이후 0xBFFF까지 사용자 정의 메시지 영역으로 정의하고 있으므로 n은 최대 0x4000까지 가능하다. WM_APP+n을 곧바로 사용할 수도 있으며 자주 사용할 경우 다음과 같이 매크로를 정의하여 별도의 메시지를 만들 수 있다.

#define WM_MYMESSAGE WM_APP+1

이렇게 매크로를 정의해 놓고 이후부터 WM_MYMESSAGE라는 명칭을 대신 사용하면 된다. WM_USER도 사용자 정의 메시지를 정의하는 용도로 사용되지만 표준 컨트롤중에 이미 WM_USER를 사용하는 컨트롤이 있으므로 중복될 위험성이 있다. 반면 WM_APP는 시스템이 전혀 이 영역을 사용하지 않고 있으므로 중복될 위험이 전혀 없으며 응용 프로그램간의 통신에 사용하기에 적합하다. 두 응용 프로그램의 약속에 의해 WM_APP+n 메시지를 정의하여 사용하면 된다.

WM_USER는 윈도우 클래스를 위한 사용자 정의 메시지이며 WM_APP는 응용 프로그램을 위한 사용자 정의 메시지라는 점이 다르다. 그러나 이 구분은 어디까지나 권장 사항일 뿐이지 강제 사항은 아니다. WM_USER를 응용 프로그램간의 통신에 사용하더라도 충돌이 없다는 확신만 있다면 가능하다. 다만 잠재적인 충돌 가능성이 있을 수 있다는 것을 고려할 때 바람직하지는 않다.

예를 들어 이런 상황을 고려해 보자. MyApp에서 Con1이라는 커스텀 컨트롤을 사용하는데 이 컨트롤은 자신에게 변화가 있을 때 WM_USER+1이라는 통지 메시지를 부모 윈도우로 보내도록 되어 있다. 이런 상황에서 MyApp가 자신의 고유 용도로 WM_USER+1을 다시 정의한다면 메시지간의 충돌이 발생하게 된다. 이런 상황을 방지하기 위해 운영체제는 WM_USER를 내부적인 용도로 WM_APP를 응용 프로그램간의 통신에 사용하도록 권장하는 것이다. 하지만 WM_APP도 여전히 충돌이 발생할 가능성이 있으므로 좀 더 안전한 방법으로 메시지를 정의하고자 한다면 RegisterWindowMessage 함수로 문자열 메시지를 등록하여 사용하는 것이 좋다.

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

WM_CHAR : wParam : 입력된 문자 코드이다. 일반적으로 이 코드는 아스키 코드이며 곧바로 문자열 출력에 사용할 수 있다.

lParam : 눌러진 키와 키보드 상태에 대한 여러 가지 정보를 가지는 비트 필드값이며 각 비트별로 다음과 같은 정보가 전달된다. 그러나 WM_CHAR 메시지에서는 이 정보를 사용하지 않는다. 왜냐하면 키 하나와 문자 하나의 대응 방식이 일정하지 않기 때문에 이 정보는 항상 유효하지 않기 때문이다.

키보드로부터 문자키가 입력되었을 때 이 메시지가 보내진다. 여기서 문자키는 화면으로 출력 가능한 문자인 알파벳, 숫자, 기호 등을 의미하며 커서 이동키나, PgUp, PgDn 등의 기능키들은 제외된다. 이 메시지는 TranslateMessage 함수에 의해 생성되어 메시지 큐에 덧붙여진다. 사용자가 키보드를 누를 때 TranslateMessage 함수는 이 키가 현재 키보드에서 대응되는 문자가 있는지를 점검하고 Caps Lock, Shift 키의 상태 등을 참고하여 적절한 WM_CHAR 메시지를 보내준다. 이 메시지를 받으려면 메시지 루프는 반드시 다음과 같이 TranslateMessage 함수를 포함하고 있어야 한다.

while(GetMessage(&Message,0,0,0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}

키보드의 문자키를 눌렀다 뗄 때 WM_KEYDOWN, WM_CHAR, WM_KEYUP 메시지가 순서대로 전달된다. DBCS 문자 코드(유니코드가 아닌 한글)인 경우 이 메시지는 상하위 바이트에 대해 두번 전달되며 이 두 바이트를 합치면 한글 한 문자의 코드가 얻어진다.

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

WM_CLEAR : 인수없음
에디트 컨트롤(또는 콤보 박스의 에디트)로 보내지는 메시지이며 선택 영역을 삭제하도록 한다. 삭제만 될 뿐이며 클립보드로 텍스트가 복사되지는 않는다. 이 메시지를 보내 문자열을 삭제하는 동작은 사용자가 직접 하는 것이 아니므로 실행 취소(EM_UNDO)는 하지 못한다. 에디트 컨트롤이 없는 CBS_DROPDOWNLIST 콤보 박스에는 아무런 효과도 없다.

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

WM_CLOSE : 인수 없음
윈도우가 닫히기 전에 이 메시지가 전달되며 메인 윈도우인 경우는 응용 프로그램이 종료된다는 신호이다. 이 메시지를 처리하지 않고 DefWindowProc으로 보내면 DestroyWindow 함수를 호출하여 윈도우를 파괴하도록 한다. 이 메시지가 전달되었을 때는 아직 윈도우가 파괴된 것이 아니므로 윈도우가 파괴되는 것을 중간에 차단할 수 있다. 미저장 파일이 있거나 프로그램을 종료할 상황이 되지 않을 때 사용자에게 메시지 박스를 통해 종료 사실을 확인시킬 수 있으며 이 메시지를 가로채서 단순히 return하면 DestroyWindow가 호출되지 않도록 할 수 있다.

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

WM_COMMAND : LOWORD(wParam) : 통지 메시지를 보낸 항목의 ID이다. 메뉴의 ID, 액셀러레이터의 ID 또는 컨트롤의 ID가 전달된다.

HIWORD(wParam) : 컨트롤이 이 메시지를 보낼 때는 통지 코드가 전달된다. 통지 코드의 종류는 에디트, 리스트 박스 등의 컨트롤에 따라 다양하다. 메뉴 항목이 선택된 경우 이 값은 0이며 액셀러레이터가 선택된 경우 이 값은 1이다.

lParam : 통지 메시지를 보낸 컨트롤의 윈도우 핸들이 전달된다. 메뉴나 액셀러레이터로부터 이 메시지가 전달된 경우 이 값은 NULL이다.

메뉴, 액셀러레이터를 선택했을 때 이 메시지가 전달되며 차일드 컨트롤이 부모 윈도우로 통지 메시지를 전달할 때도 이 메시지 형태로 전달된다. 각종 컨트롤로부터 값이 전달되며 또한 각 컨트롤은 다양한 통지 메시지를 보내므로 이 메시지는 일반적으로 다음과 같은 이중 switch문으로 작성된다.

switch (LOWORD(wParam)) {
case ID:
switch (HIWORD(wParam))
case code: ........

컨트롤(또는 메뉴 항목의 ID)에 따라 먼저 분기를 하고 통지 메시지 별로 다시 분기를 한다. 메뉴와 액셀러레이터는 보통 같은 명령에 대해 같은 ID로 한쌍이 정의되며 둘 중 어떤 항목을 선택하더라도 프로그램의 동작은 동일하다. 그러나 만약 이 둘을 굳이 구분하려면 HIWORD(wParam)값을 참조하면 된다. 메뉴 항목과 대응되는 액셀러레이터는 윈도우가 최소화되어 있을 때는 사용 금지되지만 메뉴 항목과 무관하게 단독으로 정의된 액셀러레이터는 최소화 상태에서도 전달된다.

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

WM_COPY : 인수없음
에디트 컨트롤(또는 콤보 박스의 에디트)로 보내지는 메시지이며 선택 영역을 복사하도록 한다. 클립보드에는 CF_TEXT 포맷의 문자열이 들어간다. 에디트 컨트롤이 없는 CBS_DROPDOWNLIST 콤보 박스에는 아무런 효과도 없다.

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

WM_CREATE : wParam : 사용되지 않음

lParam : 윈도우 생성 정보인 CREATESTRUCT 구조체의 포인터이다. 이 구조체는 CreateWindow(Ex) 함수의 인수에 대한 정보를 가진다.

CreateWindow(Ex) 함수에 의해 윈도우가 생성될 때 보내진다. 메모리에 윈도우를 생성한 후 화면에 보이기 전에 보내지며 주로 윈도우에 관련된 초기화 작업을 할 때 사용된다. 윈도우 동작을 위한 메모리 할당, 리소스 생성, 차일드 컨트롤 생성, 윈도우 속성 초기화 작업에 이 메시지가 사용된다.

CreateWindow(Ex) 함수는 이 메시지를 완전히 처리한 후에 리턴한다. 만약 이 메시지 처리중에 차일드 윈도우를 생성했다면 각 차일드 윈도우로도 WM_CREATE 메시지가 전달되어 개별적인 초기화를 한다. 인수로 전달되는 LPCREATESTRUCT 구조체는 보통 사용하지 않으며 무시하나 이 구조체의 lParam 멤버는 CreateWindow 함수의 제일 마지막 인수를 전달하며 윈도우로 사용자 정의값을 전달하고자 할 때 사용할 수 있다.

참고:대화상자는 이 메시지 대신 WM_INITDIALOG 메시지를 받는다.

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

WM_CTLCOLORBTN : wParam : 버튼 컨트롤의 DC핸들. 이 DC에 전경색과 배경색 등을 설정한다.

lParam : 버튼 컨트롤의 핸들. 한 윈도우에 버튼이 여러 개 있을 경우 이 핸들값으로 원하는 버튼만 색상을 변경할 수 있다.

시스템이 오너 드로우 버튼을 그리기 전에 이 메시지를 보내 배경색상과 전경색 배경색 등을 질문한다. DefWindowProc은 시스템에 정의된 컨트롤 색상을 리턴하도록 되어 있으므로 이 메시지를 처리하지 않으면 디폴트 색상으로 버튼이 그려진다. 부모 윈도우가 이 메시지를 직접 처리하면 wParam으로 전달되는 DC에 전경색과 배경색을 설정할 수 있으며 배경 브러시 핸들을 리턴함으로써 버튼의 배경 색상을 변경할 수 있다.

배경 브러시를 변경하고자 할 경우 WM_CREATE 등의 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 시스템은 이 메시지에서 리턴한 브러시 핸들로 오너 드로우 버튼의 배경을 채색한다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(WM_DESTROY) 부모 윈도우가 직접 파괴해 주어야 한다.

오너 드로우 버튼에 대해서만 이 메시지가 전달되며 BS_PUSHBUTTON, BS_DEFPUSHBUTTON, BS_PUSHLIKE 스타일의 버튼에 대해서는 이 메시지가 전달되지 않는다. 이 메시지를 받으려면 버튼은 반드시 BS_OWNERDRAW 스타일을 가지고 있어야 한다.


이 메시지는 같은 스레드 내에서만 보내진다

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

WM_CTLCOLORDLG : wParam : 대화상자의 DC 핸들. 이 핸들값으로 전경색과 배경색을 변경한다.

lParam : 대화상자의 핸들.

시스템이 대화상자를 그리기 전에 이 메시지를 보내 배경색상을 질문한다. DefWindowProc은 시스템에 정의된 배경 색상을 리턴하도록 되어 있으나 이 메시지를 대화상자가 직접 처리하면 wParam으로 전달되는 DC에 배경색과 전경색을 설정할 수 있으며 배경 브러시 핸들을 리턴함으로써 대화상자의 배경색을 변경할 수 있다.

배경 브러시를 변경하고자 할 경우 WM_INITDIALOG 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 대화상자는 이 메시지에서 리턴한 브러시 핸들로 배경을 채색하다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(EndDialog 호출 직전)대화상자가 파괴해 주어야 한다.

이 메시지는 같은 스레드 내에서만 보내진다.

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

WM_CTLCOLOREDIT : wParam : 에디트 컨트롤의 DC핸들. 이 DC에 전경색과 배경색 등을 설정한다.

lParam : 에디트 컨트롤의 핸들. 한 윈도우에 에디트가 여러 개 있을 경우 이 핸들값으로 원하는 에디트만 색상을 변경할 수 있다

시스템이 에디트 컨트롤을 그리기 전에 이 메시지를 보내 배경색상과 전경색 배경색 등을 질문한다. DefWindowProc은 시스템에 정의된 컨트롤 색상을 리턴하도록 되어 있으므로 이 메시지를 처리하지 않으면 디폴트 색상으로 에디트가 그려진다. 부모 윈도우가 이 메시지를 직접 처리하면 wParam으로 전달되는 DC에 전경색과 배경색을 설정할 수 있으며 배경 브러시 핸들을 리턴함으로써 에디트의 배경 색상을 변경할 수 있다.

배경 브러시를 변경하고자 할 경우 WM_CREATE 등의 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 시스템은 이 메시지에서 리턴한 브러시 핸들로 에디트 컨트롤의 배경을 채색한다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(WM_DESTROY) 부모 윈도우가 직접 파괴해 주어야 한다.

읽기 전용이나 사용 금지된 에디트 컨트롤은 이 메시지 대신 WM_CTLCOLORSTATIC 메시지가 전달된다. 리치 에디트 컨트롤에 대해서는 이 메시지가 보내지지 않으므로 EM_SETBKCOLOR 메시지로 배경 색상을 바꾼다. 이 메시지를 사용하면 에디트 컨트롤의 폰트를 변경할 수도 있으나 폰트 변경은 통상 WM_SETFONT 메시지를 사용하는 것이 좋다.

이 메시지는 같은 스레드 내에서만 보내진다.

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

WM_CTLCOLORLISTBOX : wParam : 리스트 박스의 DC핸들. 이 DC에 전경색과 배경색 등을 설정한다.

lParam : 리스트 박스의 핸들. 한 윈도우에 여러 개의 리스트 박스가 있을 경우 이 핸들값으로 원하는 리스트 박스만 색상을 변경할 수 있다.

시스템이 리스트 박스 컨트롤을 그리기 전에 이 메시지를 보내 배경색상과 전경색 배경색 등을 질문한다. DefWindowProc은 시스템에 정의된 컨트롤 색상을 리턴하도록 되어 있으므로 이 메시지를 처리하지 않으면 디폴트 색상으로 리스트 박스가 그려지며 리스트 박스의 항목은 디폴트 전경색과 배경색으로 그려진다. 부모 윈도우가 이 메시지를 직접 처리하면 wParam으로 전달되는 DC에 전경색과 배경색을 설정할 수 있으며 배경 브러시 핸들을 리턴함으로써 리스트 박스의 배경 색상을 변경할 수 있다.

배경 브러시를 변경하고자 할 경우 WM_CREATE 등의 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 시스템은 이 메시지에서 리턴한 브러시 핸들로 리스트 박스 컨트롤의 배경을 채색한다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(WM_DESTROY) 부모 윈도우가 직접 파괴해 주어야 한다.

이 메시지는 같은 스레드 내에서만 보내진다.

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

WM_CTLCOLORSCROLLBAR : wParam : 스크롤 바의 DC핸들. 이 DC에 전경색과 배경색 등을 설정한다.

lParam : 스크롤 바의 핸들. 한 윈도우에 여러 개의 스크롤 바가 있을 경우 이 핸들값으로 원하는 스크롤 바만 색상을 변경할 수 있다

시스템이 스크롤 바를 그리기 전에 이 메시지를 보내 배경색상으로 사용할 브러시 핸들을 질문한다. DefWindowProc은 시스템에 정의된 컨트롤 색상을 리턴하도록 되어 있으므로 이 메시지를 처리하지 않으면 디폴트 색상으로 스크를 바가 그려진다. 부모 윈도우가 이 메시지를 직접 처리하여 배경 브러시 핸들을 리턴하면 스크롤 바의 배경 색상을 변경할 수 있다. wParam으로 DC의 핸들이 전달되므로 전경색과 배경색을 변경할 수는 있지만 스크롤 바는 텍스트를 출력하지 않기 때문에 배경 브러시를 바꾸는 것 외에는 별다른 의미가 없다.

배경 브러시를 변경하고자 할 경우 WM_CREATE 등의 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 시스템은 이 메시지에서 리턴한 브러시 핸들로 스크롤 바의 몸체를 그린다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(WM_DESTROY) 부모 윈도우가 직접 파괴해 주어야 한다.

이 메시지는 스크롤 바 컨트롤에게만 보내지며 WM_HSCROLL, WS_VSCROLL 스타일에 의해 윈도우에 부착된 표준 스크롤 바에는 보내지지 않는다. 이 메시지는 같은 스레드 내에서만 보내진다.

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

WM_CTLCOLORSTATIC : wParam : 스태틱 컨트롤의 DC핸들. 이 DC에 전경색과 배경색 등을 설정한다.

lParam : 스태틱 컨트롤의 핸들. 한 윈도우에 스태틱이 여러 개 있을 경우 이 핸들값으로 원하는 스태틱만 색상을 변경할 수 있다.

시스템이 스태틱 컨트롤을 그리기 전에 이 메시지를 보내 배경색상과 전경색 배경색 등을 질문한다. DefWindowProc은 시스템에 정의된 컨트롤 색상을 리턴하도록 되어 있으므로 이 메시지를 처리하지 않으면 디폴트 색상으로 스태틱이 그려진다. 부모 윈도우가 이 메시지를 직접 처리하면 wParam으로 전달되는 DC에 전경색과 배경색을 설정할 수 있으며 배경 브러시 핸들을 리턴함으로써 스태틱의 배경 색상을 변경할 수 있다.

배경 브러시를 변경하고자 할 경우 WM_CREATE 등의 메시지에서 미리 브러시를 만들어 놓고 이 메시지에서 브러시 핸들을 리턴해 주면 된다. 시스템은 이 메시지에서 리턴한 브러시 핸들로 스태틱 컨트롤의 배경을 채색한다. 이 브러시는 시스템이 자동으로 파괴해 주지 않으므로 더 이상 필요가 없어졌을 때(WM_DESTROY) 부모 윈도우가 직접 파괴해 주어야 한다.

읽기 전용 스타일을 가진 에디트 컨트롤과 사용 금지된 에디트도 이 메시지를 대신 받는다. 이 메시지는 같은 스레드 내에서만 보내진다.

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

WM_CUT : 인수 없음
에디트 컨트롤(또는 콤보 박스의 에디트)로 보내지는 메시지이며 선택 영역을 잘라내도록 한다. 잘라낸 문자열은 클립보드에 CF_TEXT포맷으로 들어가며 에디트에서는 삭제된다. 이 메시지를 보내 문자열을 잘라내는 동작은 사용자가 직접 하는 것이 아니므로 실행 취소(EM_UNDO)는 하지 못한다. 에디트 컨트롤이 없는 CBS_DROPDOWNLIST 콤보 박스에는 아무런 효과도 없다.

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

WM_DEADCHAR : wParam : 데드키에 의해 발생한 문자 코드

lParam : 반복 회수, 스캔 코드, 확장키 등에 대한 정보고 비트필드로 전달된다. WM_KEYDOWN의 lParam과 동일하다.

데드키란 단독으로 문자를 구성할 수 없는 키이며 이 키에 의해 발생하는 데드 문자는 다음에 입력되는 문자와 조합되어 하나의 문자를 만든다. 예를 들어 독일어 키보드의 경우 움라이트 키가 먼저 입력된 후 a,o,u 등이 입력되면 a,o,u위에 점 두 개가 찍히는 움라이트 문자가 입력되는데 이때 먼저 입력되는 움라이트가 데드 문자이다. 움라이트 문자를 입력했을 때 다음 메시지가 순서대로 전달된다.

WM_KEYDOWN
WM_DEADCHAR
WM_KEYUP
WM_KEYDOWN
WM_CHAR
WM_KEYUP

이 메시지는 데드키를 누를 때 TranslateMessage 함수에 의해 발생하며 포커스를 가진 윈도우에게 전달된다. 통상 이 메시지는 무시하며 움라우트 입력 결과는 최종적으로 WM_CHAR로 전달되므로 이 메시지만 처리하면 된다. 그러나 데드키 입력 사실을 사용자에게 분명히 알려주고 싶을 때는 이 메시지를 처리하여 다음 입력될 문자와 조합되어 한 문자가 됨을 표시할 수도 있다. 만약 데드키 다음에 입력된 문자가 데드키와 조합되지 못하는 글자일 경우는 데드키와 다음 문자 각각에 대해 두 개의 WM_CHAR 메시지가 전달된다. 독일, 폴란드, 그리스 등의 유럽 계통의 키보드에서 필요한 메시지이며 한국의 키보드에서는 이 메시지가 절대로 발생하지 않는다.

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

WM_DESTROY : 인수 없음
윈도우가 파괴될 때 이 메시지가 전달된다. 사용자가 Alt+F4 또는 닫기 버튼을 누를 경우 WM_CLOSE 메시지가 전달되며 이 메시지를 별도로 처리하지 않으면 DefWindowProc은 DestroyWindow 함수를 호출하여 윈도우를 파괴한다. 또는 프로그램 코드 내부에서 명시적으로 DestroyWindow 함수를 호출할 때도 윈도우가 파괴되는데 이 함수 호출 결과로 WM_DESTROY 메시지가 전달된다.

이 메시지를 받은 윈도우는 윈도우의 종료를 위한 처리를 해야 하는데 예를 들어 열어 놓은 파일을 닫고 할당한 메모리를 해제하는 등의 정리 작업을 한다. WM_CREATE에서의 초기화 처리의 반대 동작이 이 메시지에 작성되는 것이 일반적이며 그외 레지스트리에 미보관 정보를 저장하는 등의 작업을 할 수 있다. 만약 파괴되는 윈도우가 클립보드 체인에 속해 있으면 자신을 클립보드 체인에서 제거해야 한다.

DestroyWindow 함수는 파괴할 윈도우를 화면에서 숨긴 후 이 메시지를 보내므로 이 메시지를 받은 시점에서는 윈도우 자체가 파괴되지 않은 상태이다. 또한 DestroyWindow 함수는 자식 윈도우에게도 이 메시지를 차례대로 보내주는데 부모 윈도우가 먼저 이 메시지를 받고 자식 윈도우에게로 이 메시지가 보내진다. 따라서 부모 윈도우가 이 메시지를 처리하는 동안은 모든 자식 윈도우가 아직 파괴되기 전이므로 자식 윈도우를 프로그래밍할 수 있다.

파괴되는 윈도우가 메인 윈도우일 경우 PostQuitMessage 함수를 반드시 호출하여 프로세스의 메시지 루프를 종료하도록 해야 한다. 만약 이 처리를 생략하면 윈도우만 파괴되고 메시지 루프는 계속 실행중인 상태가 되므로 프로세스가 종료되지 않는다.

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

WM_DRAWITEM : wParam : 이 메시지를 보낸 컨트롤의 ID이다. 메뉴 항목의 경우는 0이 전달된다.

lParam : 그려질 항목에 대한 정보가 담긴 다음 구조체의 포인터이다. 오너는 이 구조체의 내용을 참조하여 컨트롤이나 메뉴 항목을 그린다

오너 드로우 버튼, 리스트 박스, 콤보 박스, 메뉴가 그려져야 할 필요가 있을 때 오너 윈도우에게 이 메시지가 전달된다. 오너는 lParam으로 전달된 컨트롤의 종류와 상태에 따라 컨트롤을 적절히 그려 주어야 할 책임이 있다. 만약 오너 드로우 리스트 박스를 가진 윈도우가 이 메시지를 처리하지 않고 DefWindowProc으로 보내면 포커스 사각형만 그려진다.

lParam의 DRAWITEMSTRUCT에는 항목 그리기에 사용할 DC의 핸들과 출력 영역이 전달되는데 이 정보를 참조하여 그리되 DC는 반드시 원래 상태대로 유지해 주어야 한다. DC에 커스텀 펜이나 브러시를 선택해 사용할 수 있지만 그 이전 객체를 반드시 복구시켜 주어야 한다.

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

WM_DROPFILES : wParam : 드롭된 파일에 대한 정보를 가지는 HDROP 내부 구조체의 포인터이다. 이 구조체로부터 드래그된 파일의 목록을 얻을 수 있다.

lParam : 사용되지 않는다.

DragAcceptFiles 함수로 파일을 드롭받겠다고 등록한 윈도우로 파일이 드롭될 때 이 메시지가 보내진다. wParam으로 전달된 HDROP으로부터 DragQueryFile 함수를 호출하면 드롭된 파일의 목록을 얻을 수 있다. 응용 프로그램은 이 메시지를 받았을 때 드롭된 파일로 원하는 작업을 하게 된다. 예를 들어 파일을 열거나 검색, 삭제 등을 할 수 있다.

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

WM_ENDSESSION : wParam : 셧다운되고 있는 중이면 TRUE이며 그렇지 않으면 FALSE이다. 이 값은 WM_QUERYENDSESSION 메시지의 리턴값과 같으며 TRUE이면 운영체제가 곧 종료된다는 뜻이다.

lParam : 로그오프를 하는 것인지 시스템 셧다운을 하는 것인지를 나타낸다. 이 값이 0이면 시스템을 완전히 종료하는 것이며 ENDSESSION_LOGOFF 플래그가 설정되어 있으면 로그오프만 하는 것이다. 이 값은 비트 필드이므로 반드시 & 연산자로 플래그의 존재 유무를 점검해야 한다.

if (lParam & ENDSESSION_LOGOFF) {
// 로그오프 처리
} else {
// 셧다운 처리
}

운영체제는 종료되기 전에 실행중인 모든 프로그램에게 WM_QUERYENDSESSION 메시지를 보내 종료 허가를 받는다. 각 프로그램이 종료를 허가하면 WM_ENDSESSION 메시지를 보내 운영체제가 종료된다는 사실을 알려준다. 즉 이 메시지를 받았을 때는 이미 셧다운이 결정된 상태이며 더 이상 운영체제 종료를 거부할 수 없다.

응용 프로그램은 이 메시지를 받았을 때 미저장 문서, 설정 상태의 저장 등 필요한 동작을 해야 한다. 그러나 DestroyWindow로 메인 윈도우를 파괴하거나 PostQuitMessage 함수로 메시지 루프를 종료하는 등의 처리는 굳이 할 필요가 없다. 왜냐하면 운영체제가 종료되는 특수한 상황이기 때문에 자원 해제를 할 필요가 없기 때문이다. 물론 자기 자신을 완전히 종료한다고 해서 시스템 종료에 문제가 생기는 것은 아니지만 셧다운 속도가 느려지게 된다. 어차피 전원이 꺼지는 상황이므로 RAM에 남아 있는 윈도우, 프로세스는 그대로 방치해도 상관이 없다.

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

WM_ERASEBKGND : wParam : 배경 채색에 사용될 DC의 핸들

lParam : 사용되지 않음

윈도우 크기 변경되었거나, 다른 윈도우에 가려진 부분이 드러났다거나 할 때 배경을 지우기 위해 이 메시지가 보내진다. WM_PAINT에서 작업 영역에 출력을 하기 전에 먼저 전에 그려져 있던 내용을 지워야 한다. 이 메시지를 처리하지 않을 경우 DefWindowProc은 윈도우 클래스에 정의된 배경 브러시로 작업 영역을 지운다. 그래서 별도의 처리를 하지 않더라도 윈도우를 새로 그릴 때는 항상 배경 브러시로 작업 영역을 지운 후 WM_PAINT에서 출력을 내보내게 된다.

만약 윈도우 클래스의 배경 브러시가 NULL이면 DefWindowProc은 아무것도 하지 않으며 따라서 배경은 지워지지 않는다. 이 경우 응용 프로그램이 직접 이 메시지를 처리하여 배경을 지워 주어야 한다. 별도의 브러시를 만들어 배경을 지울 수도 있고 비트맵이나 작도 함수로 커스텀 배경을 만들 수도 있다

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

WM_FONTCHANGE : wParam, lParam : 사용되지 않음
시스템의 폰트 구성이 변경되면 모든 탑 레벨 윈도우에게 이 메시지가 전달된다. 새로운 폰트가 설치되었거나 또는 기존의 폰트가 삭제되었을 때 이 메시지가 발생한다. 폰트 목록을 유지하고 있는 응용 프로그램은 이 메시지를 받았을 때 폰트 목록을 다시 조사해야 한다. 그렇지 않으면 새로 설치된 추가 폰트를 인식하지 못하거나 이미 삭제된 폰트를 사용할 위험이 있다.

시스템에 설치되어 있는 폰트 목록을 구하기 위해서는 EnumFontFamilies 함수로 폰트 열거를 해야 한다. 폰트 열거는 시스템의 모든 폰트 정보를 조사해야 하므로 다소 시간이 걸리며 따라서 이 작업은 응용 프로그램이 시작될 때 한번만 하며 그 결과를 전역 배열에 저장해 두고 계속 사용하게 된다. 다만 시스템의 폰트 구성이 실행중에 변경되었다면 다시 열거를 해야 하는데 그 시점이 바로 WM_FONTCHANGE 메시지를 받았을 때이다. 폰트 목록을 사용하는 프로그램은 이 메시지를 반드시 처리해야 한다.

응용 프로그램이 AddFontResource, RemoveFontResource 함수로 폰트를 추가 설치했다면 모든 탑 레벨 윈도우에게 이 메시지를 보내 주어 폰트 목록이 변경되었음을 알려 주어야 한다.

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

WM_GETDLGCODE : wParam : 사용되지 않음

lParam : 전달된 메시지인 MSG 구조체의 포인터이며 컨트롤은 전달된 메시지에 따라 다른 결과를 리턴할 수도 있다. 시스템이 단순한 질문을 하는 중에는 NULL이 전달된다.

이 메시지는 대화상자내의 컨트롤들에게 어떤 종류의 입력을 원하는지 질문하기 위해 보내진다. 대화상자에서 사용자의 모든 키 입력은 대화상자가 먼저 받으며 포커스를 가진 컨트롤이 원할 경우만 컨트롤에게 전달된다. 만약 컨트롤이 별다른 입력을 처리하지 않겠다고 응답하면 대화상자는 Tab, Enter, Esc, 커서 이동키 등에 대해 디폴트 처리를 한다. Tab키는 컨트롤간의 포커스 이동을 하며 Enter키는 디폴트 버턴을 누르는 것과 같아진다.

컨트롤들은 자신의 필요에 따라 이 메시지에 응답하여 어떤 입력을 원한다는 것을 대화상자에게 알려 주어야 한다. 예를 들어 ES_WANTRETURN 스타일을 가지는 에디트 컨트롤은 Enter 키 입력을 받아들여 개행해야 하며 이 경우 Enter키는 디폴트 버튼을 누르지 않게 된다. 컨트롤은 자신의 동작과 스타일, 그리고 lParam으로 전달된 메시지를 보고 원하는 키 입력에 대해 응답해야 한다. DefWindowProc은 이 메시지에 대해 항상 0을 리턴하도록 되어 있으므로 커스텀 컨트롤이 이 메시지를 처리하지 않으면 대화상자가 처리하는 키 입력은 받을 수 없다. 대화상자에서 사용될 컨트롤 또는 표준 컨트롤을 서브 클래싱할 때는 이 메시지에 대해 적절히 응답해야 한다.

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

WM_GETFONT : wParam, lParam : 사용되지 않음
컨트롤에 설정된 폰트를 조사한다. 즉 컨트롤이 어떤 폰트로 텍스트를 출력하고 있는지 조사한다.

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

WM_GETMINMAXINFO : wParam : 사용되지 않음

lParam : 윈도우의 최소, 최대 크기를 지정하는 MINMAXINFO 구조체의 포인터이다. 이 구조체값을 변경하면 최소, 최대 크기를 변경할 수 있다.

이 메시지는 윈도우의 크기나 위치가 변경되기 직전에 윈도우에게 보내진다. 응용 프로그램이 이 메시지의 등답하여 lParam으로 전달되는 MINMAXINFO 구조체를 변경하면 윈도우의 크기나 위치는 이 구조체의 값에 영향을 받게 된다. 이 메시지를 처리하지 않으면 윈도우의 크기는 자유롭게 조정할 수 있으나 이 메시지에서 최소, 최대 크기를 변경하면 그 범위내에서만 윈도우 크기 조정이 가능하다.

만약 작업 영역이 너무 좁으면 차일드 컨트롤을 배치하는데 문제가 있거나 텍스트 배치가 어려워진다면 이 메시지를 처리하여 최소 크기를 일정 폭으로 제한할 수 있다. MINMAXINFO 구조체의 멤버 전체를 다 수정할 필요는 없으면 원하는 멤버만 수정할 수 있다.

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

WM_GETTEXT : wParam : 복사될 TCHAR형 문자의 최대 길이이며 널 종료 문자도 포함해야 한다.

lParam : 문자열을 복사할 버퍼의 주소

이 메시지는 윈도우의 텍스트를 조사하기 위해 사용된다. 이 메시지를 처리하지 않고 DefWindowProc으로 보내면 윈도우 텍스트를 lParam이 지정하는 버퍼에 복사하고 복사된 문자 수를 리턴해 준다. 보통 컨트롤의 캡션을 구하기 위해 이 메시지를 보내는데 어떤 문자열이 윈도우의 캡션이 되는가는 컨트롤에 따라 다르다.

가장 일반적인 예는 에디트 컨트롤인데 편집중인 문자열이 조사된다. 리치 에디트 컨트롤도 이 메시지로 문자열을 조사할 수 있지만 길이가 64K를 넘을 경우는 EM_STREAMOUT 메시지나 EM_GETSELTEXT 메시지를 대신 사용해야 한다. 에디트에 편집중인 문자열은 길이가 길 수 있기 때문에 고정된 길이의 버퍼를 사용하는 것은 위험하며 반드시 WM_GETTEXTLENGTH 메시지를 보내 텍스트의 길이를 조사한 후 조사된 길이만큼의 버퍼를 할당해서 사용하는 것이 좋다.

콤보 박스의 경우 콤보 박스의 에디트 또는 스태틱 컨트롤의 텍스트가 조사되며 버튼의 경우 버튼의 이름 문자열이 조사된다. 일반 윈도우는 타이틀 바에 있는 캡션이 조사된다. 리스트 박스 항목의 텍스트는 이 메시지로 얻을 수 없으며 LB_GETTEXT 메시지를 사용해야 한다.

문자열 스타일의 스태틱 컨트롤은 캡션이 조사되지만 아이콘 스타일인 경우는 아이콘의 핸들이 lParam의 첫 4바이트에 조사된다. 단, 아이콘 설정을 위해 WM_SETTEXT 메시지를 사용한 경우에만 해당되며 2000/XP에서는 문자열 스타일이 아닌 스태틱은 무조건 0이 리턴된다. 2000이전 버전의 윈도우즈에서는 이 메시지를 사용하여 비 문자열 스태틱 컨트롤의 ID를 구할 수 있었으나 2000이상에서는 반드시 GetWindowLong 함수로 컨트롤의 ID를 구해야 한다.

보통 메시지를 직접 보내는 경우는 드물며 GetWindowText 함수를 많이 사용하는데 이 함수는 지정한 윈도우로 WM_GETTEXT 메시지를 보내 텍스트를 조사한다. 단, GetWindowText 함수는 다른 프로세스의 윈도우 텍스트트는 조사하지 못하므로 이때는 WM_GETTEXT 메시지를 사용해야 한다.

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

WM_GETTEXTLENGTH : 인수없음
윈도우의 텍스트 길이를 조사한다. 윈도우의 종류에 따라 윈도우 텍스트 종류는 달라지는데 에디트는 편집중인 문자열, 버튼은 이름, 일반 윈도우는 타이틀 바의 캡션이 윈도우 텍스트가 된다. 이 메시지를 보내서 조사한 텍스트 길이는 텍스트 조사를 위한 버퍼 할당에 사용된다. 에디트의 편집중인 문자열은 길이가 길 수 있으므로 고정 길이의 버퍼를 사용해서는 안되며 반드시 이 함수로 길이를 조사한 후 동적 할당한 버퍼를 사용해야 한다.

윈도우가 ANSI 문자와 유니코드 문자를 혼용하고 있을 경우 이 함수가 조사해 주는 텍스트 길이는 때로는 실제 문자열 길이보다 좀 더 클 수도 있다. 그러나 이 경우도 충분한 길이를 조사해 주므로 이 메시지로 조사한 길이만큼 버퍼를 할당하면 항상 안전하다.

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

WM_HSCROLL : LOWORD(wParam) : 사용자의 스크롤 요구를 전달하며 스크롤 바의 눌러진 위치값이 전달된다. 다음 값 중 하나가 전달된다.

HIWORD(wParam) : SB_THUMBPOSITION, SB_THUMBTRACK 메시지의 경우 스크롤 바의 현재 위치가 전달된다. 다른 메시지에서는 사용되지 않는다. 이 값은 16비트이나 스크롤 바는 32비트 범위를 스크롤 할 수 있는데 32비트의 스크롤 값을 얻고 싶을 경우 이 인수를 직접 사용하는 대신 GetScrollInfo 함수로 값을 직접 조사해 사용해야 한다. 만약 스크롤 범위가 음수를 가질 수 있다면 이 값을 int형으로 캐스팅한 후 읽어야 한다.

lParam : 스크롤 바 컨트롤로부터 이 메시지가 전달되었을 경우 스크롤 바 컨트롤의 윈도우 핸들이 전달된다표준 스크롤 바인 경우 이 인수는 NULL이다.

윈도우의 아래쪽에 부착되는 표준 수평 스크롤 바, 또는 SBS_HORZ 스타일을 가지는 수평 스크롤 바 컨트롤이 부모 윈도우로 스크롤 메시지를 보낼 때 이 메시지가 전달된다. 다른 컨트롤은 자신의 변화를 WM_COMMAND로 전달하지만 스크롤 바는 WM_COMMAND 대신 WM_HSCROLL, WM_VSCROLL 메시지를 보낸다. 또한 이 메시지는 트랙 바 컨트롤에 의해 사용되기도 한다.

윈도우는 이 메시지를 받았을 때 스크롤 바의 위치를 갱신해 주어야 하며 화면 스크롤 처리(또는 내부적인 값의 변경)를 해 주어야 한다. 사용자가 썸을 직접 드래그할 때 SB_THUMBTRACK 메시지가 발생하며 드래그를 종료할 때 SB_THUMBPOSITION 메시지가 발생하는데 이 두 메시지 중 하나만 처리해 주면 되며 둘 다 처리할 필요는 없다. 이때 HIWORD(wParam)으로 전달되는 값은 16비트 범위이므로 65535이상의 스크롤 위치값은 전달되지 않으므로 GetScrollInfo 함수로 직접 위치를 구해야 한다.

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

WM_ICONERASEBKGND : wParam : 아이콘 그리기에 사용할 DC 핸들

lParam : 사용되지 않음

이 메시지는 NT 3.51 이전 버전에만 적용되며 95이상에서는 사용되지 않는다. 16비트 윈도우즈에서는 아이콘이 최소화되면 작업 표시줄로 내려가지 않고 바탕 화면에 아이콘이 배치되었으며 이때 최소화된 아이콘에도 출력을 내 보낼 수 있었다. 이 메시지는 최소돠된 아이콘을 그를 때 보내지는 메시지이며 여기서 아이콘을 직접 그릴 수 있다. 윈도우 클래스의 아이콘이 정의되어 있어야만 이 메시지가 전달된다. 95이후에는 이 메시지를 처리할 필요가 없다.

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

WM_INITDIALOG : wParam : 키보드 포커스를 받을 컨트롤의 핸들. 통상 이 컨트롤은 탭 순서가 가장 빠르고 보이며 사용 금지 상태가 아니고 WS_TABSTOP 스타일을 가진 첫번째 컨트롤이다. 이 메시지에서 TRUE를 리턴하면 wParam으로 전달된 컨트롤에 포커스가 맞추어진다.

lParam : 대화상자 초기화 정보가 전달된다. DialogBox 함수로 대화상자를 호출한 경우는 0이 전달되며 DialogBoxParam, CreateDialogParam 등의 함수로 대화상자를 호출했을 때만 전달된다. 대화상자로 전달되는 인수값에 해당하며 이 인수에 따라 대화상자의 모양이나 동작을 다르게 정의할 수 있다. 프로퍼티 시트의 경우 lParam은 PROPSHEETPAGE 구조체의 포인터가 전달된다.

이 메시지는 대화상자가 메모리에 만들어지고 화면에 보이기 직전에 보내진다. 그래서 대화상자내의 모든 컨트롤을 참조할 수 있으며 아직 대화상자가 보이기 전이므로 컨트롤의 재배치, 생성, 삭제, 속성 변경 등을 자유롭게 할 수 있다. 오버랩드 윈도우의 WM_CREATE에 해당하는 함수이며 대화상자가 가장 먼저 받는 메시지이므로 주로 대화상자 초기화에 이 메시지가 사용된다.

대화상자에 속한 컨트롤을 초기화하는 것이 일반적이며 기타 대화상자 동작에 필요한 환경 설정, 메모리 할당, 대화상자 위치 및 속성 변경 등의 작업을 할 수 있다. 만약 초기화중에 실패를 하게 되면 이 메시지를 처리하는 동안에도 EndDialog를 호출하여 대화상자를 즉시 종료하는 것이 가능하다. WM_CREATE와는 달리 리턴값으로 에러 여부를 리턴할 수 없으며 EndDialog 함수로 에러 코드를 리턴해야 한다.

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

WM_INITMENU : wParam : 초기화될 메뉴의 핸들
사용자가 메뉴 바의 메뉴를 클릭하거나 메뉴 키를 눌러 메뉴가 열리기 직전에 이 메시지가 보내진다. 응용 프로그램은 이 메시지를 받았을 때 메뉴 항목에 대한 초기화나 수정을 한다. 선택된 메뉴 항목에 체크 표시를 하거나 사용 금지된 메뉴 항목을 Disable시킬 수 있으며 추가로 더 필요한 메뉴 항목을 AppendMenu 등의 함수로 만들 수 있다.

이 메시지는 메뉴가 활성화될 때 딱 한번만 보내지며 메뉴 바의 팝업 메뉴를 옮겨 다녀도 추가적인 메시지는 발생하지 않는다. 메뉴 항목에 대한 정보는 별도로 제공하지 않으므로 직접 구해서 사용해야 한다.

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

WM_INITMENUPOPUP : wParam : 드롭다운 메뉴 또는 서브 메뉴의 핸들

LOWORD(lParam) : 드롭다운 메뉴나 서브 메뉴를 연 항목의 위치값

HIWORD(lParam) : 드롭다운 메뉴가 윈도우 메뉴이면 TRUE가 되며 그렇지 않으면 FALSE가 된다.

드롭 다운 메뉴나 서브 메뉴가 열리기 직전에 보내진다. 이 메시지를 받았을 때는 아직 메뉴가 화면에 출력되기 전이므로 응용 프로그램은 메뉴를 수정할 수 있다. 각각의 팝업 메뉴에 대해 이 메시지가 전달되므로 전체 메뉴를 수정하지 않고도 원하는 서브 메뉴만 수정하고 싶을 때 이 메시지를 사용한다.

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

WM_KEYDOWN : wParam : 가상 키코드값이며 어떤 키가 눌러졌는지를 나타낸다. 가상 키코드는 키보드의 종류에 독립적인 키 코드값이다.

lParam : 눌러진 키와 키보드 상태에 대한 여러 가지 정보를 가지는 비트 필드값이며 각 비트별로 다음과 같은 정보가 전달된다.

키보드 포커스를 가진 윈도우에서 키보드를 누를 때 이 메시지가 전달된다. 단, Alt키와 함께 키를 눌렀을 때는 이 메시지 대신 WM_SYSKEYDOWN 메시지가 전달된다. wParam으로 눌러진 키에 대한 정보가 전달되며 lParam으로 반복 회수, 스캔코드 등의 추가 정보가 전달된다. 특정 키 입력시 어떤 작업을 하려면 이 메시지를 사용한다. 단, Ctrl+C, Ctrl+T 등의 조합키는 이 메시지에서 처리하는 것보다는 액셀러레이터를 사용하는 것이 더 좋다.

F10키가 눌러지면 DefWindowProc은 내부 플레그만 세트해 놓으며 이때 WM_KEYDOWN 메시지는 발생하지 않는다. 다음번에 F10키에 대해 WM_KEYUP 메시지를 받았을 때 내부 플레그가 세트되어 있으면 이 메시지를 WM_SYSCOMMAND의 SC_KEYMENU를 보내주어 메인 메뉴를 열도록 해준다.

키보드를 뗄 때는 WM_KEYUP 메시지가 전달되는데 키보드는 자동 반복 기능을 가지고 있기 때문에 WM_KEYDOWN이 여러번 발생하고 WM_KEYUP이 한번만 발생할 수도 있다. 이때 각 WM_KEYDOWN에서 이 키가 처음 눌러진 것인지 반복적으로 계속 눌러진 것인지는 lParam의 비트 30을 보면 알 수 있다.

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

WM_KEYUP : wParam : 떨어진 키를 나타내는 가상 키코드 값이다.

lParam : 눌러진 키와 키보드 상태에 대한 여러 가지 정보를 가지는 비트 필드값이다.

눌러진 키가 떨어질 때 이 메시지가 발생한다. 키보드를 계속 누르고 있다가 뗀 경우 반복 기능에 의해 여러번의 WM_KEYDOWN이 발생하므로 반드시 이 메시지가 WM_KEYDOWN과 일대일로 대응되는 것은 아니다. 만약 떨어진 키가 F10이고 WM_KEYDOWN에서 내부 플래그를 설정해 놓았으면 이 메시지는 WM_SYSCOMMAND의 SC_KEYMENU 메시지를 대신 보내 메인 메뉴를 열도록 한다.

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

WM_KILLFOCUS : wParam : 새로 키보드 포커스를 얻은 윈도우의 핸들이 전달된다. 포커스를 얻은 윈도우가 없으면 NULL이다.

키보드 포커스를 잃은 직후에 이 메시지가 전달된다. 이 메시지를 받았을 때는 이미 키보드 포커서가 이동 완료된 후이다. 주로 캐럿 처리를 위해 이 메시지를 프로그래밍하는데 이 메시지를 받았을 때 캐럿을 파괴한다. 이 메시지를 받았을 때 출력 함수나 활성화 상태를 변경하는 함수를 호출해서는 안된다.

키보드 포커스는 키보드 입력을 받을 수 있는 상태를 가리키며 한번에 하나의 윈도우만 포커스를 가질 수 있다. 포커스가 이동될 때는 포커스를 잃는 윈도우에게 WM_KILLFOCUS 메시지가 먼저 전달되며 이어서 포커스를 얻는 윈도우에게 WM_SETFOCUS 메시지가 전달된다.

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

WM_LBUTTONDBLCLK : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다.

작업 영역 내부에서 마우스 왼쪽 버튼을 더블클릭할 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡처되어 있으면 캡처한 윈도우로 메시지가 전달되며 그렇지 않으면 마우스 커서 아래의 윈도우로 전달된다. 이 메시지를 받기 위해서는 윈도우 클래스가 반드시 CS_DBLCLKS 스타일을 가져야 한다. 그렇지 않으면 단순히 마우스 누름 메시지만 두번 발생한다. 또한 두 마우스 클릭의 시간 간격은 시스템에 정의되어 있는 더블클릭 시간 간격내에 발생해야만 더블클릭으로 인정된다.

마우스 더블클릭시 WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK, WM_LBUTTONUP 네개의 메시지가 일련으로 발생한다. 두번째 WM_LBUTTONDOWN 메시지가 더블클릭으로 변경된다.

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

WM_LBUTTONDOWN : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다.

작업 영역 내부에서 마우스 왼쪽 버튼을 누를 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡처되어 있으면 캡처한 윈도우로 메시지가 전달되며 그렇지 않으면 마우스 커서 아래의 윈도우로 전달된다. 모든 메시지 중에 가장 쉽게 받을 수 있는 메시지이므로 실습용이나 간단한 테스트용으로 많이 사용된다.

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

WM_LBUTTONUP : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다. 대부분의 경우 좌표는 양수값이지만 캡처된 특수한 상황에서는 음수일 수도 있는데 이 경우 반드시 (int)형으로 다시 한번 더 캐스팅해 주어야 부호를 제대로 얻을 수 있다.

마우스 왼쪽 버튼을 놓을 때 이 메시지가 큐에 붙여진다. WM_LBUTTONDOWN후에 연속적으로 이 메시지가 발생하는 것이 보통이나 마우스가 캡처되어 있지 않을 때 작업영역 밖에서 마우스 버튼을 놓으면 이 메시지가 전달되지 않을 수도 있다.

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

WM_MENUSELECT : LOWORD(wParam) : 선택된 메뉴 항목의 ID가 저달된다. 드롭다운 메뉴나 서브 메뉴의 경우 메인 메뉴에서의 서브 메뉴 인덱스가 전달되며 lParam은 메인 메뉴의 핸들이 전달된다. 이 인수로 GetSubMenu를 호출하면 열려진 서브 메뉴의 핸들을 얻을 수 있다.

HIWORD(wParam) : 메뉴 플래그값이며 다음 중 하나가 된다. 이 값이 0xFFFF이고 lParam이 NULL이면 시스템이 메뉴를 닫은 것이다.

lParam : 클릭된 메뉴의 핸들

사용자가 메뉴 항목을 선택할 때 메뉴의 소유자에게 보내진다. 이때 선택이란 메뉴 항목을 클릭한 것을 의미하는 것이 아니며 메뉴 항목 위로 마우스 커서나 반전 막대가 움직이고 있다는 뜻이다. 각각의 항목을 선택할 때마다 이 메시지가 전달되므로 개별 항목에 대한 처리가 필요할 때 이 메시지를 이용한다. 예를 들어 각 메뉴 항목에 대한 도움말을 보여 주고 싶다면 이 메시지를 받았을 때 선택된 메뉴 항목에 따른 도움말을 상태란 등에 출력한다

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

WM_MOUSEACTIVATE : wParam : 활성화된 탑 레벨 부모 윈도우의 핸들이다.

LOWORD(lParam) : 마우스를 클릭한 위치인 히트 테스트값이다. 작업 영역을 클릭했으면 HTCLIENT값이 전달된다.

HIWORD(lParam) : 이 메시지를 유발시킨 마우스 메시지의 ID가 전달된다. 이 메시지의 리턴값에 따라 마우스 메시지는 큐에 붙여지거나 버려진다.

사용자가 비활성화된 윈도우에서 마우스 버튼을 누를 때 이 메시지가 전달된다. 이미 활성화되어 있은 윈도우에서 마우스를 누를 때는 이 메시지가 전달되지 않는다. 윈도우는 이 메시지를 받았을 때 자신을 활성화할 것인지, 전달된 마우스 메시지는 어떻게 처리할 것인지 결정해야 한다. 이때 wParam과 lParam값을 읽어 사용자가 어떤 마우스 버튼을 화면의 어느 부분에서 눌렀는지를 조사할 수 있다. DefWindowProc으로 이 메시지를 보낼 경우 이 메시지는 부모 윈도우에게 전달되어 부모 윈도우가 차일드 윈도우의 활성화 여부를 결정한다.

부모 윈도우가 차일드를 활성화시킨다면 MA_NOACTIVATE(ANDEAT)를 리턴하여 시스템이 더 이상 이 메시지를 처리하지 않도록 해야 한다.

이 메시지에 의해 윈도우가 활성화되면 WM_ACTIVATE(APP), WM_NCACTIVATE, WM_SETFOCUS 메시지가 연속적으로 전달된다. 이 메시지는 마우스 버튼 누름 동작에 대해 포커스를 어떻게 처리할 것인가를 결정하기 위해 전달되는 것이다. 포커스가 이동된 후에 발생하는 것이 아니므로 포커스 변화에 따른 처리는 이 메시지에서 하지 않는 것이 옳다.

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

WM_MOUSEMOVE : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다. 대부분의 경우 좌표는 양수값이지만 캡처된 특수한 상황에서는 음수일 수도 있는데 이 경우 반드시 (int)형으로 다시 한번 더 캐스팅해 주어야 부호를 제대로 얻을 수 있다.

마우스가 움직일 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡처되어 있으면 캡처한 윈도우로 이 메시지가 전달되며 그렇지 않을 경우 커서 아래쪽에 있는 윈도우가 이 메시지를 받는다. 마우스가 계속 작업 영역 위에서 움직이고 있으면 이 메시지는 반복적으로 계속 전달된다.

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

WM_MOVE : wParam : 사용되지 않는다.

LOWORD(lParam) : 윈도우의 X좌표.

HIWORD(lParam) : 윈도우의 Y 좌표. 이 좌표는 오버랩드, 팝업 윈도우의 경우 화면 좌표이며 차일드 윈도우는 부모 윈도우의 작업 영역을 기준으로 한 좌표이다.

윈도우의 위치가 변경될 때마다 이 메시지가 보내진다. 일반적으로 윈도우의 위치 변경에 따른 처리는 하지 않기 때문에 이 메시지는 잘 사용되지 않지만 위치에 따라 윈도우의 모양이나 동작에 차이가 있거나 특별한 처리가 필요하다면 이 메시지를 사용한다. 이 메시지는 윈도우의 위치가 완전히 옮겨진 후에 보내지므로 인수로 전달되는 좌표는 이동 후의 좌표이다.

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

WM_NCACTIVATE : wParam : 이 값이 TRUE이면 활성화된 것이고 FALSE이면 비활성화된 것이다.

비작업 영역이 활성화 또는 비활성화되어 변경되어야 할 필요가 있을 때 보내진다. 이 메시지는 보통 응용 프로그램이 처리하지 않으며 DefWindowProc으로 보내준다. 이 메시지의 디폴트 처리는 wParam에 따라 타이틀 바를 활성/비활성화하도록 되어 있다. 그래서 활성화된 타이틀 바는 파란색으로 그려지며 비활성화되면 회색으로 그려진다. 만약 타이틀 바의 활성 여부를 다르게 프로그래밍하고 싶다면 이 메시지를 처리하도록 한다.

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

WM_NCCREATE : wParam : 사용되지 않음

lParam : CreateWindow(Ex) 함수의 마지막 인수로 지정한 CREATESTRUCT 구조체의 포인터이며 윈도우 생성에 필요한 추가 정보이다.

CreateWindow(Ex) 함수에 의해 윈도우가 만들어질 때 보내진다. 비작업 영역이 만들어진다는 의미를 가지고 있으며 WM_CREATE보다 먼저 이 메시지가 보내진다. 윈도우가 만들어질 때 가장 먼저 보내지는 메시지이며 윈도우가 제일 먼저 받는 메시지이기도 하다. 그러나 이 메시지는 일반적으로 사용되지 않으며 초기화를 할 필요가 있을 때는 통상 WM_CREATE 메시지가 대신 사용된다.

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

WM_NCDESTROY : 없음
비작업 영역이 파괴될 때 보내진다. 윈도우와 그 차일드들이 먼저 파괴된 후에 비작업 영역이 파괴되므로 이 메시지는 윈도우가 가장 마지막으로 받는 메시지이다. WM_DESTROY보다 뒤에 발생되며 이 메시지를 받았을 때는 모든 차일드가 이미 파괴된 후이다. 반면 WM_DESTROY 메시지는 차일드가 아직 파괴되기 전이다. 종료 처리가 필요할 경우는 일반적으로 WM_DESTROY 메시지에 코드를 작성하므로 이 메시지는 실용적인 가치가 거의 없는 셈이며 처리하는 경우가 극히 드물다.

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

WM_NCHITTEST : wParam : 사용되지 않는다.

lParam : 화면상의 커서 좌표가 전달된다. 하위 워드에 x좌표, 상위 워드에 y좌표가 전달된다

마우스를 이동하거나 버튼을 누르거나 놓을 때마다 이 메시지가 발생한다. 이 메시지는 커서가 있는 위치가 윈도우의 어디쯤인지를 윈도우에게 질문을 하며 운영체제는 이 메시지의 리턴값에 따라 마우스를 처리한다. 예를 들어 이 메시지가 HTBOTTOM을 리턴하면 아래쪽 경계선에 커서가 있는 것으로 판단하며 이 상태에서 마우스를 드래그하면 윈도우의 수직 크기를 변경한다.

DefWindowProc은 커서가 있는 위치를 정확하게 계산하여 적절한 위치값을 리턴해 준다. 이 메시지를 처리하여 리턴값을 조작하면 운영체제의 커서 관리 동작을 변경할 수 있다. 예를 들어 작업 영역에 커서가 있을 때 HTCLIENT 대신 HTCAPTION을 리턴해 주면 작업 영역을 드래그하여 윈도우의 위치를 변경한다. 이런 조작을 하려면 현재 위치를 먼저 조사해야 하므로 DefWindowProc을 먼저 호출한 후 현재 커서 위치를 파악하고 그 결과로부터 리턴값을 조작해야 한다.

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

WM_NEXTDLGCTL : wParam : 다음 포커스를 옮길 컨트롤을 지정한다. lParam이 TRUE일 경우 포커스를 받을 컨트롤의 핸들값을 지정하며 lParam이 FALSE일 경우 이전, 또는 다음 컨트롤을 지정한다. wParam이 0이면 다음 컨트롤로 포커스가 이동되며 0이외의 값이면 이전 컨트롤로 포커스를 이동한다. 포커스를 받을 컨트롤은 WS_TABSTOP 스타일을 가지고 있어야 한다.

lParam : 포커스를 이동시킬 방법을 지정한다. 이 값이 TRUE이면 wParam은 포커스를 받을 윈도우 핸들을 가리키며 FALSE이면 wParam값에 따라 이전/다음 컨트롤로 포커스를 옮긴다.

대화상자 컨트롤의 포커스를 이동시킨다. 어떤 컨트롤이 다음 포커스를 받을 것인가는 wParam와 lParam값에 따라 달라진다. 만약 포커스를 받을 컨트롤의 핸들을 알고 있다면 lParam에 TRUE를 주고 wParam에 컨트롤의 핸들값을 전달하면 된다. 현재 포커스를 가진 컨트롤의 이전 또는 다음 컨트롤로 이동하려면 lParam에 FALSE를 주고 wParam으로 이전(0이외) 또는 다음(0)을 지정한다.

이 메시지는 SetFocus로 단순히 입력 포커스를 옮기는 것보다 훨씬 더 많은 일을 한다. 새로 포커스를 받은 컨트롤이 디폴트 버튼일 경우 경계선을 두껍게 바꾸고 에디트 컨트롤이면 텍스트를 선택해 준다. 다른 작업을 하던 중에 포커스를 변경하고자 한다면 SendMessage 함수로 이 메시지를 보내서는 안되며 반드시 PostMessage 함수로 메시지를 붙여야 한다.

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

WM_NULL : wParam, lParam : 사용되지 않는다.

아무런 동작도 하지 않는 빈 메시지이다. DefWindowProc은 이 메시지를 받았을 때 아무 동작도 하지 않으며 곧바로 리턴한다. 이 메시지가 필요한 경우는 다음 두 경우이다.

1.응용 프로그램이 메시지에 잘 반응하는지 테스트한다. SendMessageTimeOut 함수로 WM_NULL을 보내 보고 곧바로 리턴되는지 검사함으로써 응용 프로그램이 동작중인지 아닌지를 확인할 수 있다. WM_NULL은 아무 동작도 하지 않는 메시지이므로 보내는 즉시 리턴해야 정상적이다. 만약 이 메시지를 보냈는데 리턴되지 않는다면 해당 응용 프로그램은 아주 바쁜 상태이거나 죽은 것으로 판단할 수 있다.

2.WH_GETMESSAGE 훅을 설치한 프로그램이 메시지를 무효화할 때 수신된 메시지를 WM_NULL로 변경함으로써 해당 메시지를 무시하도록 한다. 예를 들어 마우스 이동 메시지를 무시하도록 하고 싶다면 WM_MOUSEMOVE를 받았을 때 이 메시지를 WM_NULL로 바꾸면 된다.

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

WM_PAINT : wParam : 그리기에 사용할 DC 핸들이 전달되며 이 값이 NULL일 경우 디폴트 DC에 그려야 한다. 이 인수로 전달되는 DC는 일부 공통 컨트롤에 의해 사용될 뿐이며 일반적인 목적으로는 사용하지 않는 것이 안전하다. 이 DC 핸들보다는 BeginPaint 함수가 리턴하는 DC 핸들을 사용하는 것이 좋다.

윈도우의 작업 영역중 일부가 무효화되어 있을 때 시스템이 이 메시지를 큐에 넣어준다. 다음과 같은 경우에 무효 영역이 설정되며 이때마다 WM_PAINT메시지가 전달된다.

① 윈도우가 처음 생성되었을 때
② 윈도우의 위치가 이동되었을 때
③ 윈도우의 크기가 변경되었을 때. 최대, 최소화되었을 때
④ 다른 윈도우에 가려져 있다가 드러날 때
⑤ 스크롤 될 때

또는 응용 프로그램 내부에서 InvalidateRect 함수 호출에 의해 강제로 무효 영역을 설정할 수 있다. 윈도우는 이 메시지를 받았을 때 작업 영역 전체 또는 무효화된 부분을 다시 그려야 한다. 운영체제는 윈도우의 작업영역을 복구해 주지 않는 대신에 무효화될 때 이 메시지를 보내 줌으로써 윈도우에게 다시 그려야 할 시점을 알려 주기만 한다. 따라서 윈도우는 자신의 작업영역에 출력할 수 있는 모든 자료를 완벽하게 저장해 두어야 한다.

WM_PAINT 메시지는 모든 메시지 중에 우선 순위가 가장 늦다. GetMessage 함수는 메시지 큐에 WM_PAINT가 있더라도 다른 메시지가 대기중이면 이 메시지를 먼저 처리한다. WM_PAINT 메시지는 큐에 대기중인 다른 메시지가 없을 때, 그리고 무효 영역이 존재할 때만 윈도우 프로시저로 보내진다. 또한 WM_PAINT 메시지는 한번에 하나만 메시지 큐에 들어갈 수 있다. 만약 무효영역이 생겼을 때 WM_PAINT 메시지가 이미 메시지 큐에 있으면 기존의 무효영역과 새 무효영역의 합집합으로 새로운 무효영역이 설정된다.

윈도우가 이 메시지를 처리하지 않으면 이 메시지는 DefWindowProc 함수가 처리한다. 이 함수는 무효영역을 모두 유효화화기만 하며 다시 그리기는 하지 않는다. 만약 비작업 영역도 그려져야 한다면 WM_NCPAINT 메시지를 전달하며 또한 배경을 지워야 한다면 WM_ERASEBKGND 메시지를 전달한다.

WM_PAINT 메시지에서 그리기를 할 때는 BeginPaint 함수와 EndPaint 함수를 사용해야 한다. 이 두 함수는 WM_PAINT내에서만 사용되며 다시 그려야 할 영역에 대한 정확한 좌표를 조사하며 무효영역을 유효화하고 캐럿을 숨기거나 배경을 지우는 등의 꼭 필요한 동작을 한다.

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

WM_PASTE : 인수 없음
에디트 컨트롤(또는 콤보 박스의 에디트)로 보내지는 메시지이며 현재 캐럿 위치에 클립보드의 텍스트를 붙이도록 한다. 클립보드가 CF_TEXT 포맷의 데이터를 가지고 있을 때만 붙여지고 그렇지 않으면 이 메시지는 아무 동작도 하지 않는다. 에디트 컨트롤이 없는 CBS_DROPDOWNLIST 콤보 박스에는 아무런 효과도 없다.

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

WM_QUERYENDSESSION : wParam : 사용되지 않는다.

lParam : 로그오프를 하는 것인지 시스템 셧다운을 하는 것인지를 나타낸다. 이 값이 0이면 시스템을 완전히 종료하는 것이며 ENDSESSION_LOGOFF 플래그가 설정되어 있으면 로그오프만 하는 것이다. 이 값은 비트 필드이므로 반드시 & 연산자로 플래그의 존재 유무를 점검해야 한다.

if (lParam & ENDSESSION_LOGOFF) {
// 로그오프 처리
} else {
// 셧다운 처리
}

사용자가 운영체제를 종료 또는 로그 오프하고자 할 때, 또는 응용 프로그램이 ExitWindows 함수로 운영체제를 종료하고자 할 때(단, EXW_FORCE 플래그가 없어야 한다)모든 응용 프로그램으로 이 메시지가 보내진다. 이 메시지는 실행중인 각 프로그램에게 "지금 셧다운할건데 해도 괜찮습니까?" 라고 질문하는 것과 같다. 응용 프로그램은 이 메시지를 받았을 때 자신의 내부 상태를 보고 운영체제의 종료를 허가하거나 거부할 수 있다. 만약 한 프로그램이라도 0을 리턴하면 운영체제의 종료는 취소된다.

미저장 문서가 있다거나 또는 네트웍 연결을 함부로 끊을 수 없는 상황이거나 아니면 절대로 종료해서는 안되는 중요한 서버 프로그램인 경우는 이 메시지를 받았을 때 셧다운을 거부하는 것이 가능하다. 만약 TRUE를 리턴하여 종료를 허가하게 되면 운영체제를 종료한다는 WM_ENDSESSION 메시지가 전달된다. DefWindowProc은 이 메시지를 받았을 때 TRUE를 리턴하도록 되어 있으므로 이 메시지를 특별히 처리하지 않으면 운영체제의 종료를 항상 허가하는 것이 된다.

운영체제는 셧다운전에 실행중인 모든 프로그램에게 이 메시지를 보내 셧다운 허가를 받은 후 모든 프로그램이 허가할 때만 개별 프로그램을 종료시킨 후 셧다운한다. 이때 개별 프로그램의 종료 시점은 운영체제 버전에 따라 다르다. 95/98 계열은 모든 프로그램에게 셧다운 허가를 받은 후 각 프로그램을 종료하지만 NT/2000 계열은 개별 윈도우에게 허가를 받은 후 각각의 프로그램을 순서대로 종료시킨다. 그래서 만약 A,B,C 세개의 프로그램이 있고 B가 허가를 거부할 때 95/98계열은 모든 프로그램이 종료되지 않지만 NT/2000 계열은 일단 A가 먼저 종료되고 B의 거부를 받았을 때 셧다운을 중지한다.

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

WM_QUIT : wParam : 종료 코드이며 PostQuitMessage의 인수가 전달된다. 이 종료 코드는 응용 프로그램을 실행시킨 프로세스가 받으나 보통 무시한다.

응용 프로그램을 종료하라는 신호이다. PostQuitMessage 함수 호출에 의해 발생하며 GetMessage 함수가 0을 리턴하도록 함으로써 메시지 루프를 종료시키는 역할을 한다. GetMessage 함수는 WM_QUIT 이외의 모든 메시지에 대해 0이 아닌 값을 리턴하므로 계속 루프를 돌지만 WM_QUIT에 대해서만 0을 리턴한다. 그래서 메시지 루프는 통상 다음과 같이 작성된다.

while(GetMessage(&Message,0,0,0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
return (int)Message.wParam;

GetMessage 함수가 0이 아닌 값을 리턴하는 동안 무한히 이 루프를 도는데 단 WM_QUIT가 전달될 때는 while문이 종료되며 따라서 WinMain이 종료된다. 메인 윈도우의 WM_DESTROY에서는 반드시 PostQuitMessage 함수를 호출하여 메시지 루프가 종료될 수 있도록 해 주어야 한다. 그렇지 않으면 메인 윈도우는 파괴되었으나 프로세스는 계속 실행중인 상태가 된다.

PeekMessage 함수는 WM_QUIT 메시지와 상관없이 메시지 큐에 메시지가 있는지만 리턴하므로 메시지 루프를 구성할 때 따로 WM_QUIT 메시지를 점검해야 한다.

for (;;) {
if (PeekMessage(&Message,NULL,0,0,PM_REMOVE)) {
if (Message.message==WM_QUIT)
break;
TranslateMessage(&Message);
DispatchMessage(&Message);
}
else {
// 백그라운드 작업
}
}

조사한 메시지가 WM_QUIT이면 메시지 루프를 탈출하는 별도의 코드가 필요하다.

WM_QUIT는 윈도우에게 전달되는 메시지가 아니므로 윈도우 프로시저는 이 메시지를 받을 수 없다. 윈도우 프로시저까지 전달되기 전에 메시지 루프에서 이 메시지를 차단하여 루프를 탈출하게 된다.

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

WM_RBUTTONDOWN : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다.

작업 영역 내부에서 마우스 오른쪽 버튼을 누를 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡처되어 있으면 캡처한 윈도우로 메시지가 전달되며 그렇지 않으면 마우스 커서 아래의 윈도우로 전달된다. 이 메시지를 받았을 때 컨텍스트 메뉴를 열 수도 있으나 이 메시지는 현재 커서 위치를 작업영역 좌표로 전달해 주므로 불편하다. 이 메시지보다는 커서 위치를 화면 좌표로 전달해 주는 WM_COMTEXTMENU 메시지를 사용하는 것이 더 유리하다.

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

WM_RBUTTONUP : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플레그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 X, Y좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다. 대부분의 경우 좌표는 양수값이지만 캡처된 특수한 상황에서는 음수일 수도 있는데 이 경우 반드시 (int)형으로 다시 한번 더 캐스팅해 주어야 부호를 제대로 얻을 수 있다.

마우스 오른쪽 버튼을 놓을 때 이 메시지가 큐에 붙여진다. WM_RBUTTONDOWN후에 연속적으로 이 메시지가 발생하는 것이 보통이나 마우스가 캡처되어 있지 않을 때 작업영역 밖에서 마우스 버튼을 놓으면 이 메시지가 전달되지 않을 수도 있다.

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

WM_SETCURSOR : wParam : 커서 위치의 윈도우 핸들이다. 차일드 위에 커서가 있을 수도 있으므로 이 메시지를 받은 윈도우 핸들과는 다르다.

LOWORD(lParam) : 히트 테스트 코드이며 커서가 윈도우의 어디쯤에 있는지 알려 준다. 이 값에 대해서는 WM_NCHITTEST 메시지를 참조하기 바란다.

HIWORD(lParam) : 이 메시지를 보낼 때의 마우스 메시지 ID, 메뉴가 열려있는 상태이면 0이다.

커서가 윈도우 영역에서 이동될 때마다 이 메시지가 보내지며 새 위치에서 커서를 어떤 모양으로 바꿀 것인가를 질문한다. 단, 커서가 캡처되어 있을 때는 이 메시지가 보내지지 않는다. DefWindowProc은 이 메시지를 받았을 때 직접 처리하기 전에 부모 윈도우에게 이 메시지를 먼저 보내 처리하도록 한다. 그래서 차일드의 커서 모양은 부모 윈도우가 우선적으로 변경할 수 있는 기회를 준다. 부모 윈도우가 이 윈도우를 처리한 후 TRUE를 리턴하면 더 이상의 커서 관련 처리를 하지 않는다. 그렇지 않을 경우 DefWindowProc은 디폴트 처리한다.

디폴트로 커서는 작업 영역에 있을 때 윈도우 클래스에 등록된 커서로 변경되며 경계선이나 타이틀 바 등의 비작업 영역에 있을 때는 크기조절 모양이나 화살표 모양의 커서로 변경된다. 이 방식대로 커서를 처리하려면 WM_SETCURSOR를 처리하지 않고 DefWindowProc으로 보내주기만 하면 된다. 만약 커서를 조건에 따라 다른 모양으로 바꾸고 싶다면 이 메시지를 받았을 때 좌표, 커서 위치의 컨트롤 등을 참고하여 적절히 커서를 변경하고 TRUE를 리턴하면 된다.

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

WM_SETFOCUS : wParam : 키보드 포커스를 잃은 윈도우의 핸들이 전달된다. 포커스를 잃은 윈도우가 없으면 NULL이다.

이 함수는 키보드 포커스가 이동될 때 발생한다. 포커스를 가진다는 말은 키보드 입력을 받을 수 있다는 뜻이며 한번에 오직 하나의 윈도우만 포커스를 가질 수 있다. 포커스는 윈도우의 활성화 상태 변경에 따라 이동되는데 새로 활성화된 윈도우나 그 차일드가 포커스를 가진다. 또는 SetFocus 함수에 의해 명시적으로 포커스 이동이 발생할 수도 있다.

이 메시지는 윈도우가 키보드 포커스를 얻은 후에 전달된다. 즉, 이 메시지를 받았을 때는 이미 키보드 포커스가 이동 완료된 후이다. 만약 포커스를 다른 윈도우에게 양보하고자 한다면 이 메시지를 받자 마자 SetFocus 함수를 호출하여 다른 윈도우(주로 차일드 중 하나)에게 포커스를 건네 주어야 한다. 캐럿 처리를 위해 이 메시지를 프로그래밍하는데 이 메시지를 받았을 때 캐럿을 보여주면 된다.

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

WM_SETFONT : wParam : 폰트의 핸들. NULL일 경우 시스템 폰트를 사용한다.

lParam : 폰트를 변경한 후 컨트롤을 다시 그릴 것인지 아닌지를 지정한다. 이 값이 TRUE이면 컨트롤은 자기 자신을 다시 그린다. WM_CREATE나 WM_INITDIALOG에서 폰트를 변경한다면 굳이 다시 그릴 필요가 없으므로 이 인수는 FALSE로 준다.

컨트롤의 폰트를 변경한다. 표준 컨트롤 및 공통 컨트롤은 기본적으로 시스템 폰트로 텍스트를 출력하며 대화상자에 있는 컨트롤의 경우 대화상자의 폰트와 같은 폰트를 사용한다. 실행중에 컨트롤의 글꼴을 변경하고자 한다면 폰트 오브젝트를 먼저 만든 후 이 메시지로 폰트의 핸들을 전달하면 된다.

컨트롤의 폰트를 변경하는 가장 좋은 때는 대화상자가 WM_INITDIALOG 메시지를 받았을 때, 또는 오버랩드 윈도우의 경우 WM_CREATE 메시지를 받았을 때이다. 즉, 컨트롤이 생성된 직후, 화면에 보이기 직전에 폰트를 바꾸어야 화면 떨림도 없고 다시 그릴 필요도 없어 가장 좋다. 물론 실행중에라도 폰트를 자유롭게 변경할 수 있지만 폰트 변경에 의해 컨트롤이 다시 그려져야 하므로 보기에 좋지 않다. 컨트롤의 텍스트 출력에 사용할 폰트는 컨트롤의 부모가 생성해야 하며 다 사용하고 난 후에 직접 파괴해 주어야 한다.

실행중에 컨트롤의 폰트를 바꾼다고 해서 컨트롤의 크기가 바뀌는 것은 아니다. 따라서 폰트를 변경할 필요가 있는 컨트롤은 미리 변경될 폰트의 크기에 맞는 충분한 크기로 생성할 필요가 있다. 그렇지 않으면 컨트롤의 텍스트가 컨트롤 바깥으로 삐져 나와 잘릴 수가 있다.

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

WM_SETTEXT : lParam : 설정하고자 하는 텍스트이며 널 종료 문자열이다.
윈도우의 텍스트를 변경한다. 윈도우의 종류에 따라 윈도우 텍스트 종류는 달라지는데 에디트는 편집중인 문자열, 버튼은 이름, 일반 윈도우는 타이틀 바의 캡션이 윈도우 텍스트가 된다. 콤보 박스로 이 메시지를 보낸 경우 콤보의 에디트 컨트롤 내용만 바뀔 뿐 리스트 박스의 선택 상태가 변경되는 것은 아니다. 콤보의 선택 항목을 변경하려면 CB_SELECTSTRING 메시지를 보내 주어야 한다.

같은 프로세스에 속한 윈도우는 이 메시지를 보내는 대신 SetWindowText 함수를 대신 사용할 수도 있다. 다른 프로세스에 속한 윈도우의 텍스트를 변경할 때는 WM_SETTEXT 메시지를 보내야 한다.

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

WM_SHOWWINDOW : wParam : TRUE이면 윈도우가 보여지는 것이고 FALSE이면 윈도우가 숨겨지는 것이다.

lParam : 이 메시지가 전달된 이유이다. 0이면 ShowWindow 함수에 의해 이 메시지가 전달된 것이며 아니면 다음 값 중 하나가 된다.

윈도우의 보임 상태가 변경되기 직전에 보내진다. 즉 숨겨져 있던 윈도우가 보이게 되었거나 보이던 윈도우가 숨겨지게 되었을 때 이 메시지가 보내진다. ShowWindow나 ShowOwnedPopups 함수에 의해 명시적으로 보임 상태가 변경될 때는 물론이고 다른 윈도우의 상태 변화에 의해 보임 상태가 변경될 때도 이 메시지가 보내진다. WS_VISIBLE 스타일을 가진 윈도우가 생성될 때는 새로 만들어진 윈도우가 보이게 되므로 이 메시지가 보내진다.

보임 상태의 변화에 따라 어떤 처리를 하고 싶다면 이 메시지에서 처리하면 된다. 예를 들어 윈도우가 주기적으로 애니메이션을 재생하고 있다면 보이지 않는 상태에서는 애니메이션을 잠시 중지할 수 있다.

오버랩드 윈도우가 WS_MAXIMIZE 또는 WS_MINIMIZE 스타일을 가질 때, 즉 생성되자 마자 최대, 최소화될 때는 이 메시지가 발생하지 않으면 ShowWindow 함수를 SW_SHOWNORMAL 플래그로 호출할 때도 이 메시지는 발생하지 않는다.

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

WM_SIZE : wParam : 윈도우의 크기가 변경된 이유와 유형값을 가진다. 다음 중 하나의 값이 전달된다.

lParam : 윈도우의 작업 영역 크기이다. LOWORD(lParam)이 윈도우의 폭이며 HIWORD(lParam)이 윈도우의 높이이다. 윈도우의 폭과 높이는 32비트값으로 전달되지만 이 메시지로 전달되는 폭과 높이는 16비트 크기를 가진다.

윈도우의 크기가 변경될 때 이 메시지가 보내진다. 사용자가 윈도우의 경계선을 드래그해서 직접 크기를 변경할 때는 물론이고 프로그램 내부에서 MoveWindow, SetWindowPos 등의 함수로 윈도우의 크기를 변경할 때도 이 메시지가 전달된다. 윈도우의 크기에 따라 작업영역의 출력 내용이 달라지거나 차일드 윈도우를 재배치해야 할 경우 이 메시지에서 처리한다

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

WM_SYSCHAR : wParam : 윈도우 메뉴 케의 문자 코드

lParam : 눌러진 키와 키보드 상태에 대한 여러 가지 정보를 가지는 비트 필드값이다.

WM_SYSKEYDOWN 메시지가 TranslateMessage 함수에 의해 문자 코드로 번역될 때 포커스를 가진 윈도우에게 전달된다.

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

WM_SYSCOMMAND : wParam : 시스템 명령의 종류가 전달되며 사용자가 시스템 메뉴에서 어떤 항목을 선택했는지를 알 수 있다. 이 값의 하위 4비트는 시스템이 내부적으로 사용하는 값이므로 명령의 종류를 알고 싶으면 wParam을 0xFFF0와 AND연산해야 한다. 연산한 결과는 다음값 중의 하나가 된다.

lParam : 마우스로 윈도우 메뉴를 선택한 경우 커서의 좌표가 전달된다. 하위 워드에는 수평 좌표, 상위 워드에는 수직 좌표가 전달되는데 이 좌표는 화면 기준 좌표이다. 액셀러레이터에 의해 명령이 선택되었으면 -1이 되며 니모닉에 의해 선택되었으면 0이 된다.

시스템 메뉴에 있는 메뉴 항목을 선택하면 WM_COMMAND 메시지 대신 이 메시지가 전달된다. 시스템 메뉴를 직접 선택하는 동작 외에도 타이틀 바에 있는 최대, 최소, 닫기 버튼 등의 명령들도 이 메시지를 발생시킨다. 시스템 메뉴에 있는 명령들은 윈도우를 관리하기 위한 기본적인 명령이므로 응용 프로그램은 이 메시지를 직접 처리하지 않고 보통 DefWindowProc으로 그냥 보내 준다.

DefWindowProc은 wParam값에 따라 시스템에 미리 정의되어 있는 동작을 수행한다. 예를 들어 SC_MINIMIZE 시스템 명령이 전달되었으면 윈도우를 최소화하고 SC_CLOSE 명령이 전달되었으면 윈도우를 닫는다. 응용 프로그램이 직접 이 시스템 명령을 프로그래밍 하고 싶다면 이 메시지를 처리하며 자신이 처리한 시스템 명령은 DefWindowProc으로 보내지 말아야 한다. 그외의 시스템 명령은 모두 DefWindowProc으로 보내 주어 디폴트 처리를 하도록 해야 한다.

시스템 메뉴에는 이동, 최소화, 최대화, 크기 조정, 닫기 등의 표준 윈도우 관리 명령들만 들어 있다. GetSystemMenu, AppendMenu 등의 메뉴 관련 명령을 사용하면 시스템 메뉴에도 응용 프로그램 고유의 메뉴 항목을 추가할 수 있다. 이렇게 만들어진 메뉴 항목을 선택할 때는 WM_COMMAND 대신 WM_SYSCOMMAND 메시지가 대신 전달되므로 반드시 이 메시지를 처리해야 한다. 이 경우 직접 추가한 메뉴 항목 외의 시스템 명령은 모두 DefWindowProc으로 전달해 주어야 한다.

응용 프로그램이 시스템 명령을 직접 실행할 필요가 있다면 wParam에 원하는 시스템 명령을 대입하고 DefWindowProc으로 WM_SYSCOMMAND를 보내 준다. 예를 들어 윈도우를 닫고 싶으면 SendMessage(hWnd, WM_SYSCOMMAND, SC_CLOSE, 0); 명령을 보내 주면 된다.

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

WM_SYSDEADCHAR : wParam : 데드키에 의해 발생한 문자 코드

lParam : 반복 회수, 스캔 코드, 확장키 등에 대한 정보고 비트필드로 전달된다. WM_KEYDOWN의 lParam과 동일하다.

Alt키와 데드키를 함께 누를 때 이 메시지가 발생한다. 데드키에 대한 자세한 사항에 대해서는 WM_DEADCHAR 메시지를 참조하기 바란다. 통상 이 메시지는 처리하지 않는다.

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

WM_SYSKEYDOWN : wParam : 눌러진 가상 키 코드

lParam : 키보드의 상태에 대한 여러 가지 정보가 비트별로 전달된다.

F10키를 누르거나 Alt키와 함꼐 다른 키를 같이 누르면 포커스를 가진 윈도우에게 이 메시지가 전달된다. 포커를 가진 윈도우가 없을 경우 활성화된 윈도우로 전달되는데 이때는 lParam의 29번 비트인 컨텍스트 코드가 0이 된다. 컨텍스트 코드가 0인 경우 이 메시지는 TranslateAccelerator 함수로 전달될 수 있는데 포커스를 가지지 않은 활성화된 윈도우도 액셀러레이터를 처리할 수 있도록 한다.

키보드의 반복 입력 기능으로 인해 WM_SYSKEYUP이 오기 전에 복수 개의 WM_SYSKEYDOWN 메시지가 발생할 수도 있다. 반복 기능에 의해 전달된 메시지인지 처음 눌러진 키인지를 구분하려면 lParam의 비트 30을 점검해 본다.

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

WM_SYSKEYUP : wParam : 놓이진 키의 가상 키 코드

lParam : 키보드의 상태에 대한 여러 가지 정보가 비트별로 전달된다.

Alt키와 함께 눌러진 키가 놓아질 때 포커스를 가진 윈도우에게 발생한다. 키보드 포커스를 가진 윈도우가 없을 경우는 활성화된 윈도우로 이 메시지가 전달되는데 이때 lParam의 29번 비트인 컨텍스트 코드가 0이 된다. 컨텍스트 코드가 0인 경우 이 메시지는 TranslateAccelerator 함수로 전달될 수 있는데 포커스를 가지지 않은 활성화된 윈도우도 액셀러레이터를 처리할 수 있도록 한다.

DefWindowProc은 Alt키나 F10키가 놓아질 때 탑 레벨 윈도우에게 WM_SYSCOMMAND 메시지를 보내며 이때 wParam은 SC_KEYMENU가 된다.

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

WM_TIMECHANGE : 없음. wParam, lParam 모두 사용되지 않음
시스템 시간이 변경되면 이 메시지가 보내진다. 만약 응용 프로그램이 시스템 시간을 사용하고 있다면 이 메시지를 받았을 때 시간을 갱신해야 한다. 예를 들어 상태란에 현재 시간을 보여주고 있다면 이 메시지를 받는 즉시 상태란을 갱신할 필요가 있다.

SetSystemTime, SetLocalTime 등의 함수로 시스템 시간을 변경하는 프로그램은 시간 변경 후 모든 탑레벨 윈도우에게 이 메시지를 보내 시간이 변경되었음을 알려야 한다. SendMessage(HWND_TOPMOST, WM_TIMECHANGE,0,0); 함수를 호출하면 된다.

2000/XP는 시스템 시간이 변경될 경우 운영체제가 시간 변경을 각 프로그램에게 알려 주므로 응용 프로그램이 이 메시지를 보낼 필요가 없다. 그러나 95/98/ME와 NT 4.0이전 버전에서는 이 메시지를 반드시 보내주어야 한다.

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

WM_TIMER : wParam : 타이머의 ID가 전달된다. 이 ID는 SetTimer의 두번째 인수로 지정한 값이다.

lParam : 콜백 함수가 있을 경우 콜백 함수의 번지가 전달된다

SetTimer 함수로 타이머를 설치했을 경우 지정한 시간 간격으로 이 메시지가 반복적으로 큐에 붙여진다. 주기적으로 어떤 작업을 반복해야 한다면 타이머를 설치하고 이 메시지에서 작업을 처리하도록 한다. 두 개 이상의 타이머가 설치되어 있을 경우 각각의 타이머는 정해진 시간 간격으로 이 메시지를 큐에 붙이며 WM_TIMER에서는 wParam으로 어떤 타이머에 의해 이 메시지가 발생했는지 조사한다.

타이머 콜백 함수를 지정했을 경우는 이 메시지를 처리할 필요가 없으며 시스템이 콜백 함수를 주기적으로 호출해 준다.

이 메시지는 다른 메시지들에 비해 우선순위가 낮게 설정되어 있기 때문에 먼저 처리해야 할 메시지가 있을 경우 곧바로 윈도우 프로시저로 보내지지 않을 수 있다. 따라서 정확한 시간에 이 메시지가 전달되지 않는 경우도 있으므로 정확도를 요하는 작업에는 이 메시지를 사용하지 않는 것이 좋다.

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

WM_UNDO : 없음

에디트 컨트롤에게 최후의 편집 동작을 취소하라는 명령을 보낸다. 즉 마지막으로 삽입한 문자열을 삭제하거나 마지막으로 삭제한 문자열을 다시 삽입하도록 한다. 에디트 컨트롤은 1회의 실행 취소만 지원하므로 바로 직전에 한 편집만 취소할 수 있다. 리치 에디트 컨트롤은 이 메시지를 지원하지 않는다.

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

WM_USER : wParam, lParam 모두 의미가 정해져 있지 않다. 응용 프로그램이 의미를 정해서 사용할 수 있다.

WM_USER는 한 윈도우 클래스를 위한 고유의 메시지를 정의하기 위한 상수값이며 이 범위 이후부터 윈도우 클래스의 사용자 정의 메시지를 만들 수 있다. 이 값은 0x400으로 정의되어 있으며 보통 WM_USER+n으로 사용자 정의 메시지를 정의한다. 이때 n은 1보다 큰 정수이며 사용자 정의 메시지간의 구분을 위해 사용된다. 여러 개의 사용자 정의 메시지가 필요하다면 WM_USER+1, WM_USER+2, WM_USER+3,... 식으로 계속 n을 증가시켜 가며 메시지를 정의할 수 있다. 윈도우즈는 WM_USER이후 0x8000까지 사용자 정의 메시지 영역으로 정의하고 있으므로 n은 최대 0x7c00까지 가능하다. WM_USER+n을 곧바로 사용할 수도 있으며 자주 사용할 경우 다음과 같이 매크로를 정의하여 별도의 메시지를 만들 수 있다.

#define WM_MYMESSAGE WM_USER+1

이렇게 매크로를 정의해 놓고 이후부터 WM_MYMESSAGE라는 명칭을 대신 사용하면 된다. 표준 컨트롤 중 일부는 자신만의 사용자 정의 메시지를 정의하여 사용하고 있다. 따라서 WM_USER+n은 가급적이면 한 윈도우 클래스내에서만 정의하여 사용해야 하며 응용 프로그램간의 통신에는 사용하지 않는 것이 바람직하다. 표준 컨트롤을 서브클래싱했을 경우 함부로 WM_USER+n을 사용하면 표준 컨트롤 고유의 메시지와 충돌이 발생할 수 있다

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

WM_VSCROLL : LOWORD(wParam) : 사용자의 스크롤 요구를 전달하며 스크롤 바의 눌러진 위치값이 전달된다. 다음 값 중 하나가 전달된다.

HIWORD(wParam) : SB_THUMBPOSITION, SB_THUMBTRACK 메시지의 경우 스크롤 바의 현재 위치가 전달된다. 다른 메시지에서는 사용되지 않는다. 이 값은 16비트이나 스크롤 바는 32비트 범위를 스크롤 할 수 있는데 32비트의 스크롤 값을 얻고 싶을 경우 이 인수를 직접 사용하는 대신 GetScrollInfo 함수로 값을 직접 조사해 사용해야 한다. 만약 스크롤 범위가 음수를 가질 수 있다면 이 값을 int형으로 캐스팅한 후 읽어야 한다.

lParam : 스크롤 바 컨트롤로부터 이 메시지가 전달되었을 경우 스크롤 바 컨트롤의 윈도우 핸들이 전달된다표준 스크롤 바인 경우 이 인수는 NULL이다.

윈도우의 오른쪽에 부착되는 표준 수직 스크롤 바, 또는 SBS_VERT 스타일을 가지는 수직 스크롤 바 컨트롤이 부모 윈도우로 스크롤 메시지를 보낼 때 이 메시지가 전달된다. 다른 컨트롤은 자신의 변화를 WM_COMMAND로 전달하지만 스크롤 바는 WM_COMMAND 대신 WM_HSCROLL, WM_VSCROLL 메시지를 보낸다. 또한 이 메시지는 트랙 바 컨트롤에 의해 사용되기도 한다.

윈도우는 이 메시지를 받았을 때 스크롤 바의 위치를 갱신해 주어야 하며 화면 스크롤 처리(또는 내부적인 값의 변경)를 해 주어야 한다. 사용자가 썸을 직접 드래그할 때 SB_THUMBTRACK 메시지가 발생하며 드래그를 종료할 때 SB_THUMBPOSITION 메시지가 발생하는데 이 두 메시지 중 하나만 처리해 주면 되며 둘 다 처리할 필요는 없다. 이때 HIWORD(wParam)으로 전달되는 값은 16비트 범위이므로 65535이상의 스크롤 위치값은 전달되지 않으므로 GetScrollInfo 함수로 직접 위치를 구해야 한다.

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

WM_WINIINICHANGE : wParam : 사용되지 않는다.

lParam : 변경된 시스템 설정의 이름 문자열이며 레지스트리의 키 이름이나 Win.ini 의 섹션 이름이 전달된다. 그러나 레지스트리 키일 경우 전체 경로가 아니라 단순히 키의 이름만 전달되며 설정을 변경하는 프로그램이 이 인수에 정확하게 값을 대입해 주지 않기 때문에 이 인수로부터 어떤 설정이 변경되었는지 정확하게 알아내기는 어렵다. 따라서 이 메시지를 받았을 때 응용 프로그램이 참조하고 있는 모든 설정값을 다시 조사해야 한다.

이 메시지는 하위 호환성을 위해서만 제공되므로 Win32 응용 프로그램은 이 메시지 대신 WM_SETTINGCHANGE 메시지를 대신 사용해야 한다. 이 두 메시지는 이름만 다른 같은 메시지이다.

#if(WINVER >= 0x0400)
#define WM_SETTINGCHANGE WM_WININICHANGE
#endif /* WINVER >= 0x0400 */

Win.ini 파일은 운영체제의 중요한 설정 상태를 저장하는 정보 파일이다. 이 파일이 변경되었다는 것은 곧 시스템 설정에 중요한 변화가 있었다는 뜻이며 이때 WM_WININICHANGE 메시지가 모든 탑 레벨 윈도우에게 보내진다. Win.ini는 16비트 윈도우즈에서는 파일 형태로 존재하며 95이후부터 다음 레지스트리 키애 연결되어 있다.

HKEY_LOCAL_MACHINE\Software\Microsoft\ Windows NT\CurrentVersion\IniFileMapping

Win.ini를 직접 편집하는 것뿐만 아니라 이 레지스트리 키를 편집하는 것도 동일하게 시스템 설정을 변경시킨다. 만약 Win.ini를 편집하여 시스템 설정을 변경하였다면 SendMessage 함수의 첫번째 인수로 HWND_BROADCAST를 주어 모든 탑 레벨 윈도우에게 이 메시지를 전달해야 한다.

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

반응형

** 간단정리 ****

후킹이란?


다른 프로그램(프로세스), 쓰레드가 메시지를 받기 전에 가로채는 기술


IPC란?

프로그램끼리 메시지나, 데이타를 주고받는 기능


후킹예제는 F9를 눌렀을때 비프음 내기


1. 쓰레드 후킹은 쉽지만, 프로세스 후킹은 DLL을 통해서 해야한다.(꼭 그렇게 해야된다고 생각)


먼저 DLL만들기.

DLL이란? 프로그램 실행중 동적으로 연결 되는 라이브러리로 만든 파일

즉~ 실행파일은 이 함수가 없기때문에 DLL파일에서 참조를 해와서 수행

DLL 파일이 없으면 이 프로그램도 작동 안함



2. Visaul C++ 실행 --> File --> New --> Projects탭 --> Win32 Dynamic-Link Library 체크 --> Project name "Kartdll"

--> OK --> An empty DLL project --> Finish --> OK


3. File --> New --> C++ Source File --> File "Kart" --> OK


4. 소스


#include <windows.h>

#pragma data_seg(".kbdata")  // 시작


HINSTANCE hModule=NULL; // 시작과 끝 사이에 객체를 선언

HHOOK hKeyHook=NULL;

HWND hWndBeeper=NULL;


#pragma data_seg()

#pragma comment (linker, "/SECTION:.kbdata,RWS") //끝


// 키보드 입력이 먹혔을때 F9키이면 WM_USER+1 이라는 메세지를 보낸다

// 즉 프로그램에 WM_USER+1 메세지를 받으면 비프음이 나게 한다


LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wParam, LPARAM lParam)

{

   if (nCode>=0) {

       if(wParam==VK_F9)

           SendMessage(hWndBeeper,WM_USER+1,wParam,lParam);

   }


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

}

// 훅을 설치하는 함수

extern "C" __declspec(dllexport) void InstallHook(HWND hWnd)

{

   hWndBeeper=hWnd;

   hKeyHook=SetWindowsHookEx(WH_KEYBOARD,KeyHookProc,hModule,NULL);

}


// 훅을 제거하는 함수

extern "C" __declspec(dllexport) void UninstallHook()

{

   UnhookWindowsHookEx(hKeyHook);

}


// Dll 메인함수

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes)

{

   switch (fdwReason) {

   case DLL_PROCESS_ATTACH:

      hModule=hInst;

      break;

   case DLL_PROCESS_DETACH:

      break;

   }

   return TRUE;

}


5. 컴파일


6. 이 상에서 만든 dll을 참조하는 프로그램 작성


7. Visaul C++ 실행 --> File --> New --> Projects탭 --> Win32 Application 체크 --> Project name "KartRider" --> OK


8.File --> New --> C++ Source File --> File "Kart" --> OK


9.kartdll 프로젝트에 Debug 폴더의 kartdll.dll, kartdll.lib 프로젝트 kartrider 폴더에 복사


10. 메뉴에서 Project --> Settings --> Link탭 --> Object/library modules: kartdll.lib


11. KartRider 소스


#include <windows.h>

#include <mmsystem.h>  // 비프 소리


// dll에서 함수를 참조할때 이런식으로 써주면 되요

extern "C" __declspec(dllimport) void InstallHook(HWND hWnd); 

extern "C" __declspec(dllimport) void UninstallHook();


LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

LPSTR lpszClass="간단한 후킹";


int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

          ,LPSTR lpszCmdParam,int nCmdShow)

{

    HWND hWnd;

    MSG Message;

    WNDCLASS WndClass;

    WndClass.cbClsExtra=0;

    WndClass.cbWndExtra=0;

    WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);

    WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

    WndClass.hIcon=LoadIcon(NULL,NULL);

    WndClass.hInstance=hInstance;

    WndClass.lpfnWndProc=(WNDPROC)WndProc;

    WndClass.lpszClassName=lpszClass;

    WndClass.lpszMenuName=NULL;

    WndClass.style=CS_HREDRAW | CS_VREDRAW;

    RegisterClass(&WndClass);

    hWnd=CreateWindow(lpszClass,lpszClass,WS_CAPTION | WS_SYSMENU,

        100,100,405,200,

        NULL,(HMENU)NULL,hInstance,NULL);

    ShowWindow(hWnd,nCmdShow);

    while(GetMessage(&Message,0,0,0)) {

        TranslateMessage(&Message);

        DispatchMessage(&Message);

    }

    return Message.wParam;

}


LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

    switch(iMessage)

    {

    case WM_DESTROY:

        UninstallHook();

        PostQuitMessage(0);

        return 0;

    case WM_CREATE :

        InstallHook(hWnd); 

        return 0;

    case WM_USER+1: // F9를 누르면 F9를 눌렀다는 메세지가 Dll을 먼저 거친다음 WM_USER+1

                               // 메시지를 보낸다

        MessageBeep(NULL);

        return 0;

    }

    return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}


*************************************************************************************

카트라이더 프로그램에 VK_F5와 VK_RETURN 메세지를 보내준다

실전 예제


#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

HINSTANCE g_hInst;

LPSTR lpszClass="카트라이더 매크로";

HANDLE hTimer;

HWND hWnd2; // 카트라이더 윈도우 핸들을 위해...

int time;

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

          ,LPSTR lpszCmdParam,int nCmdShow)

{

    HWND hWnd;

    MSG Message;

    WNDCLASS WndClass;

    g_hInst=hInstance;

   

    WndClass.cbClsExtra=0;

    WndClass.cbWndExtra=0;

    WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);

    WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

    WndClass.hIcon=LoadIcon(NULL,NULL);

    WndClass.hInstance=hInstance;

    WndClass.lpfnWndProc=(WNDPROC)WndProc;

    WndClass.lpszClassName=lpszClass;

    WndClass.lpszMenuName=NULL;

    WndClass.style=CS_HREDRAW | CS_VREDRAW;

    RegisterClass(&WndClass);

    hWnd=CreateWindow(lpszClass,lpszClass,WS_CAPTION | WS_SYSMENU,

        100,100,405,200,

        NULL,(HMENU)NULL,hInstance,NULL);

    ShowWindow(hWnd,nCmdShow);

   

    while(GetMessage(&Message,0,0,0)) {

        TranslateMessage(&Message);

        DispatchMessage(&Message);

    }

    return Message.wParam;

}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

    switch(iMessage)

    {

    case WM_DESTROY:

        KillTimer(hWnd, 1);

        PostQuitMessage(0);

        return 0;

    case WM_CREATE :

        time=0;

        hWnd2 = FindWindow(NULL, "KartRider Client"); // 카트라이더 윈도우 찾아서 hWnd2에 대입

        // 카트라이더 윈도우 : "KartRider Client"

        if(hWnd2 == NULL)

            MessageBox(hWnd, "카트라이더가 켜진 상태에서 프로그램 실행시켜 주세요",NULL,NULL);

        else

            hTimer=(HANDLE) SetTimer(hWnd, 1, 1000, NULL);

        return 0;

    case WM_TIMER :

        time++;

        if(time==1) {

            SendMessage(hWnd2, WM_KEYDOWN, VK_RETURN, NULL); 

                // hWnd2 카트라이더 윈도우 핸들에 메세지 전달

            SendMessage(hWnd2, WM_KEYUP, VK_RETURN, NULL); 

        }

        if(time==2) {

            SendMessage(hWnd2, WM_KEYDOWN,VK_F5,NULL);

            SendMessage(hWnd2, WM_KEYUP,VK_F5,NULL);

        }

        if(time==3)

            time=0;

        return 0;

    }

    return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}


//Point/////////////////////////////////////////////////////////////////////////

hWnd2 = FindWindow(NULL, "KartRider Client");

SendMessage(hWnd2, WM_KEYDOWN, VK_RETURN, NULL);

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


카트라이더 윈도우 핸들을 얻은다음 그핸들에 메세지를 보낸다

카트라이더 키고 프로그램 실행하면 자동으로 래디

후킹 : F9키 눌렀을때 매크로 시작 F10키 눌렀을때 중지 기능

F9키 누르면 타이머 작동, F10키 누르면 타이머 Kill

****************************************************************************************


아래와 같은 형태의 콜백 함수는 away기능을 구현하기 위한 후킹에 별로 효과적이지 않습니다.


LRESULT CALLBACK KeyboardHook (int nCode, WORD wParam, DWORD lParam )

{

    if(nCode>=0)

        lastTime = CTime::GetCurrentTime(); // lastTime이 공유변수입니다.


    return (int)CallNextHookEx(kbHook, nCode, wParam, lParam);

}


LRESULT CALLBACK MouseHook (int nCode, WORD wParam, DWORD lParam )

{

    if(nCode>=0)

        lastTime = CTime::GetCurrentTime();


    return (int)CallNextHookEx(kbHook, nCode, wParam, lParam);

}



그 이유는 다이얼로그 박스가 하나 생성되고 소멸될 때마다 마우스 쪽 훅체인으로 HTCLIENT 메시지가 날아가기 때문입니다.

키보드는 문제가 없습니다.


따라서 마우스 쪽 콜백함수는 다음과 같이 작성하는 것이 낳습니다.


LRESULT CALLBACK MouseHook (int nCode, WORD wParam, DWORD lParam )

{

    if(nCode>=0)

    {    UINT a=((MOUSEHOOKSTRUCT*)lParam)->wHitTestCode;

        if(a!=HTCLIENT)

        {    lastTime = CTime::GetCurrentTime();

        }

    }

    return (int)CallNextHookEx(msHook, nCode, wParam, lParam);

}


MOUSEHOOKSTRUCT와 wHitTestCode에 대한 것은 MSDN을 참조하시길...



위와같이하면 아웃룩의 정기적인 메일 체크기능이나 다른 몇몇 프로그램들이 내부적으로 다이얼로그 박스를 생성하고 없애더라도 거기에 영향을 받지 않고 away(자리비움) 기능을 제대로 사용할 수 있습니다.


****************************************************************************************


Win32 Global API Hook - 1 Win32 API 후킹의 기본


자신이 만약 어느정도의 레벨을 가진 윈도우즈 프로그래머라면 이런 생각을 한번쯤 해보았을 것이다.


"만약 Windows API를 후킹할 수 있다면 재미있는 것을 많이 해볼 수 있을텐데..."


그리고 의욕과 시간이 있었다면 아마 도전해본 사람도 꽤 있었을것이다. 그러나 실제로 이것을 성공한 사람은 그리 많지 않았을것으로 안다. 여러분이 만약 어플리케이션 레벨에서만 프로그래밍했다면 이것은 불가능해보였을지도 모른다. 시스템 레벨 프로그래머라면 이것이 제법 까다롭고 다루기 힘든 주제라는것을 알았을 것이다. 자 여기서 필자는 많은 사람들이 궁금해하는 이 비밀스런 작업을 하나하나 풀어가려고 한다. 그리고 의외로 간단한 곳에 해답이 있었음을 이 강좌가 끝날때쯤 알게될 것이다.


자, 서론을 접고 본론으로 들어가자. 우리가 하려는 일은 다음과 같다.


Win32 API를 후킹해서 내가 원하는 작업을 수행하거나, 작업의 흐름을 원하는대로 제어할 수 있다.


단지 이것이다. 무슨 설명이 더 필요한가? 실제로 우리가 하려는 것은 이것이 전부이다. 예를 들어서 쉽게 말하자면 CreateProcess() 라는 API를 후킹하면 내가 원하지않는 프로그램의 실행을 막을수도 있고, 윈속함수인 send()나 recv()를 후킹하면 나가고 들어오는 패킷을 훔쳐보거나 조작할 수 있다. 느낌이 팍 오지 않는가? 느낌이 오지않는 사람은 아마도 아직 이 강좌를 들을만한 수준이 아니거나 해커(순수한 의미의)의 기질이 없는 사람일 수도 있다.


일단 오늘은 첫날이니 그동안 많은 사람들이 제시했던 Windows API 후킹방법에 대해서 먼저 얘기해보자.


1. exe 파일헤더의 import descriptor table을 변경하는 방법


가장 쉬운 방법이 되겠다. 물론 쉬운만큼 문제점이 적지 않다. 일단 Win32 응용프로그램은 PE(Portable Executable)이라는 형식으로 바이너리화 되어있다. 실행가능한 바이너리 파일구조에는 POSIX, COFF, PE 형식등등이 있는데, 윈도는 PE형식을 사용한다. 따라서 PE형식을 알면 윈도실행파일구조를 분석할 수 있겠다. 실제 윈도실행파일은 크게 헤더, 리소스, 임포트테이블, 익스포트테이블, 데이터, 코드 등등의 영역으로 나누어지는데 여기서 임포트테이블이 이름 그대로 임포트된 라이브러리의 함수에 관한 정보가 담겨졌있다. 따라서 정적으로 링크된 모든 함수는 이곳에서 볼수 있게된다. 그러면 이부분의 임포트된 함수의 주소들을 내가 만든 후킹함수주소로 바꿔치기해주면 간단할 것이다. 아래에 리스트된 코드를 보자.


// 포인터 변환 및 연산 매크로

#ifndef MakePtr

#define MakePtr(cast, ptr, addValue) (cast)((DWORD)(ptr)+(DWORD)(addValue))

#endif // MakePtr


PIMAGE_IMPORT_DESCRIPTOR GetImportDescriptor(HMODULE hMod,        // 모듈

                                        LPCTSTR pszModName)// 모듈이름

{

    TRACE("[FIND IMPORT DESCRIPTOR] \n");


    // 매개변수 유효성 검사

    ASSERT(!IsBadReadPtr(hMod, sizeof(IMAGE_DOS_HEADER)));

    ASSERT(NULL != pszModName);


    // DOS 헤더

    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)hMod;

    if(IMAGE_DOS_SIGNATURE/*0x5A4D*/ == pDosHdr->e_magic)

    {

        // NT 헤더

        PIMAGE_NT_HEADERS pNtHdr = MakePtr(PIMAGE_NT_HEADERS,

            pDosHdr, pDosHdr->e_lfanew);

        if(!IsBadReadPtr(pNtHdr, sizeof(IMAGE_NT_HEADERS))

            && IMAGE_NT_SIGNATURE/*0x00004550*/ == pNtHdr->Signature)

        {

            // image descriptor

            PIMAGE_IMPORT_DESCRIPTOR pImpDesc =

                MakePtr(PIMAGE_IMPORT_DESCRIPTOR,

                    pDosHdr,

                    pNtHdr->OptionalHeader.

                    DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].

                    VirtualAddress);

            if(NULL != pImpDesc)

            {

                while(NULL != pImpDesc->Name)

                {

                    PSTR pszName = MakePtr(PSTR, pDosHdr, (DWORD)pImpDesc->Name);

                    TRACE(" %s ", pszName);


                    if(stricmp(pszName, pszModName) == 0)

                    {

                        // 찾았다 !!

                        TRACE(": Found It !! \n");


                        return pImpDesc;

                    }


                    TRACE("\n");

                    pImpDesc++;

                }

            }

        }

    }


    return NULL;

}


PROC WINAPI HookImportFunction(HMODULE hMod,// Hooking 할 모듈

                     PSTR pszModName,        // Hooking 함수가 위치한 모듈이름

                     PSTR pszFuncName,        // Hooking 할 함수

                     PROC pfnNewProc)        // Hooking 함수

{

    // 매개변수 유효성 검사

    ASSERT(!IsBadReadPtr(hMod, sizeof(IMAGE_DOS_HEADER)));

    ASSERT(NULL != pszModName);

    ASSERT(NULL != pszFuncName);

    ASSERT(NULL != pfnNewProc);

    ASSERT(!IsBadCodePtr(pfnNewProc));


    // 반환값 (원래 함수)

    PROC pfnOrgProc = NULL;


    // Win9x이고 2GB 이상의 시스템 DLL 이라면

    // 조용히 사라져야 한다.

    if(1 != __GetOsType() && 0x80000000 < (DWORD)hMod)

    {

        return NULL;

    }


    // import descriptor 를 얻는다.

    PIMAGE_IMPORT_DESCRIPTOR pImpDesc = GetImportDescriptor(hMod, pszModName);

    if(NULL == pImpDesc)

        return NULL;


    // 원본 thunk

    PIMAGE_THUNK_DATA pOrgThunk = MakePtr(PIMAGE_THUNK_DATA,

        hMod, pImpDesc->OriginalFirstThunk);


    // 실제 thunk (for Hooking)

    PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA,

        hMod, pImpDesc->FirstThunk);


    // 루프를 돌면서 Hooking 할 함수를 찾는다.

    TRACE("[FIND IMPORT FUNCTION] : %s \n", pszModName);

    while(NULL != pOrgThunk->u1.Function)

    {

        // 이름으로 import된 함수만 검색

        if(IMAGE_ORDINAL_FLAG !=

            (pOrgThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG))

        {

            // import된 함수 이름

            PIMAGE_IMPORT_BY_NAME pByName =

                MakePtr(PIMAGE_IMPORT_BY_NAME,

                hMod, pOrgThunk->u1.AddressOfData);


            // 이름이 NULL로 시작되면 넘어간다.

            if(0 == pByName->Name[0])

                continue;


            TRACE(" %s ", (PSTR)pByName->Name);


            if(pszFuncName[0] == pByName->Name[0]

                && 0 == stricmp(pszFuncName, (PSTR)pByName->Name))

            {

                // 찾았다 !!

                TRACE(": Found It !! \n");


                MEMORY_BASIC_INFORMATION mbi;

                VirtualQuery(pRealThunk, &mbi, sizeof(mbi));


                // 가상 메모리의 보호속성 변경

                if(!VirtualProtect(mbi.BaseAddress, mbi.RegionSize,

                    PAGE_READWRITE, &mbi.Protect))

                {

                    // VirtualProtect() fail !!

                    ASSERT(0);

                    return NULL;

                }


                // 원래 함수(반환값) 저장

                pfnOrgProc = (PROC)pRealThunk->u1.Function;


                // 새로운 함수로 덮어쓴다.

                TRACE("** old function : 0x%08X \n", (DWORD)pRealThunk->u1.Function);

                pRealThunk->u1.Function =

                    (DWORD)pfnNewProc;

                TRACE("** new function : 0x%08X \n", (DWORD)pRealThunk->u1.Function);


                // 가상 메모리의 보호속성 원래대로 되돌림

                DWORD dwTmp;

                VERIFY(VirtualProtect(mbi.BaseAddress, mbi.RegionSize,

                    mbi.Protect, &dwTmp));


                // 세상을 다 가져라 !!

                TRACE("** Hook OK !! What a wonderful world !! \n");

                return pfnOrgProc;

            }


            TRACE("\n");

        }


        pOrgThunk++;

        pRealThunk++;

    }


    return NULL;

}


GetImportDescriptor()함수는 모듈의 임포트디스크립터를 얻어내는 함수이고, HookImportFunction()함수는 실제로 임포트된 함수를 후크하는 함수이다. 이 두 함수를 이용해서 MessageBox API를 후킹해보자.


// MessageBox API 원형

typedef int (WINAPI *PROC_MESSAGEBOX)(HWND, PSTR, PSTR, UINT);


PROC_MESSAGEBOX pfnOrgMessageBox = NULL;


// MessageBox를 대체할 훅함수

int WINAPI MyMessageBoxA(HWND hWnd, PSTR pszText, PSTR pszTitle, UINT uType)

{

    ASSERT(NULL != pfnOrgMessageBox);


    return pfnOrgMessageBox(NULL, "Hooked MessageBox !!", "Hooked !!", MB_ICONINFORMATION);

}


// Main 프로세스

int APIENTRY WinMain(HINSTANCE hInstance,

                     HINSTANCE hPrevInstance,

                     LPSTR     lpCmdLine,

                     int       nCmdShow)

{

     // TODO: Place code here.


    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);


    // hook !!

    pfnOrgMessageBox = (PROC_MESSAGEBOX)HookImportFunction(

        GetModuleHandle(NULL), "user32.dll", "MessageBoxA", (PROC)MyMessageBoxA);

    if(NULL == pfnOrgMessageBox)

        return 0;


    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);


    // unhook !!

    pfnOrgMessageBox = (PROC_MESSAGEBOX)HookImportFunction(

        GetModuleHandle(NULL), "user32.dll", "MessageBoxA", (PROC)pfnOrgMessageBox);

    if(NULL == pfnOrgMessageBox)

        return 0;

    TRACE("replace orginal function \n");


    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);


    return 0;

}


MyMessageBoxA 라는 함수를 보면 타이틀에 Hooked !! 라는 캡션을 가진 Hooked MessageBox !! 메시지를 출력하는 메시지 박스를 보여주는 함수이다. 후킹이 성공적으로 이루어지면 MessageBox는 무조건 위와같은 형태로 보여지게 될것이다. MessageBoxA의 A 문자는 ANSI 문자열 버전으로서 Windows 9x 계열에서 주로 사용되며 1바이트 문자를 사용한다. 반대로 W가 붙은 API는 Wide Charactor로서 2바이트 문자를 사용하는 유니코드 사용 API이다. 실제로 Win9x 계열에서는 생략할경우 A문자 버전의 API가 호출되도록 재지정되어 있으며, NT4/W2K 계열에서는 내부적으로는 W문자 버전의 API가 호출된다. 문자열을 인자로 사용하는 대부분의 API가 이와같이 두가지 버전으로 나뉘어지며 만약 훅함수를 설치한다면 정확한 버전을 사용해야 할것이다.


실행시켜보면 실제로 중간의 MessageBox(NULL, "Default MessageBox", "Windows 98",

MB_ICONINFORMATION); 함수는 우리가 지정한 후킹함수로 대체되어 Hooked ... 라는 메시지 박스가 출력될것이다.


자, 원리를 알고 실행이 제대로 되는것을 확인했다면 문제점에 대해서 알아보자.

위의 방법으로는 현재 내 프로그램만이 후킹될수 있다. 이유인즉슨 임포트 디스크립터 테이블이라는것이 프로그램마다 가지고 있는것이기 때문에 당연히 내 프로그램만이 적용되어진다. 그렇다면 다른 프로그램들도 모조리 임포트 디스크립터 테이블을 변경하면 될것이 아닌가? 라고 생각하는 사람이 있을지 모르겠지만, 그건 불가능하다. 왜냐하면 Win32 응용프로그램들은 각각 독립적인 주소공간에서 실행되기때문에 기본적으로 서로 다른 프로그램의 영역을 침범할수 없다. 그러면 어떻게 다른 응용프로그램(정확히 말하면 프로세스)의 주소공간을 볼수있을까? 이 문제에 관해서는 다음 강좌에서 다루어 보도록 하자.


끝으로...오늘은 Win32 API 후킹에 대한 맛보기였다. 사실 오늘 제시한 방법으로는 우리가 생각하는 것들을 하기에는 턱없이 모자란다. 그렇지만 모든것은 순서가 있듯이 가장 기본적인 방법론부터 제시해보았다. 사실 오늘 강좌에도 따라오는 부수적인 내용은 상당히 방대하다. 일단 Windows 실행파일구조인 PE구조에 대한 이해와 Windows 프로세스간 메모리 관리 또한 매우 중요한 내용이다. 지면상(또는 시간상 ^^) 여기서 다 다룰수는 없지만 참고할만한 서적을 소개하면 PE구조에 관해서는 MSJ나 마이크로소프트웨어 잡지를 검색하면 찾을 수 있을것이다. 실제로 PEDUMP 같은 유틸리티를 작성해본다면 더없이 좋을것이다. Win32 메모리관리에 관해서는 Jeffry Richter의 Advanced Windows 라는 책을 추천한다. 좀 오래된 책인데 필자가 공부할때는 가장 훌륭했다고 여겨지는 책이다. 아마 다른 시스템 프로그래밍을 다루는 책에서도 자료를 얻을 수 있을것이다. 또한 위 소스코드의 모체가된 John Robbins의 Debugging Applications라는 책을 참고하는 것도 좋을 것이다.


노파심에서 사족을 달자면, 혹시 C/C++와 Windows 시스템과 메카니즘에 익숙하지 않다면 과감히 강좌를 보는것을 포기하길 권유한다. 적어도 위의 소스코드정도는 쉽게 이해할 수 있어야 할것이다. 또한 필자의 사정상 빨라야 일주일에 두번정도 강좌를 올릴수 있을것 같다. 그래서 전체적인 커리를 다음과 같이 제시하니 필요한 부분은 미리미리 공부한다면 빨리 따라올 수 있을것이다.


1. Win32 API 후킹의 기본

2. 다른 프로세스의 주소공간으로 들어가자 !!

3. Win32 어셈블리 프로그래밍

4. Win9x 디바이스 드라이버(VxD) 모델

5. 기계어 프로그래밍 - Shell Code 작성

6. Win9x Global API Hooking

7. WinNT/2000 디바이스 드라이버 모델

8. WinNT/2000 Global API Hooking


앞으로 전개될 강좌의 주요 테마이다. 하나같이 만만하지 않은 주제들로 이루어져있으며 한두회의 강좌로는 턱없이 부족한 주제도 대부분일 것이다. 그러나 차근차근 따라오다보면 어느새 자신의 실력이 부쩍 향상되어 있음을 피부로 느낄것이다. 이것은 필자가 이름을 걸고 맹세할 수 있다. 실제로 디바이스드라이버 프로그래밍과 어셈블리 프로그래밍에 대한 경험이 있거나, 얼마전까지 유행하던 해킹기법인 Stack Overflow의 익스플로잇과 쉘코드를 직접 제작할 수 있는 능력이 있다면 강좌를 따라오기가 한결 수월할 것이다.




/*===================================

Win32 Global API Hooking - 쉬어가기

===================================*/


안녕하세요? API 후킹강좌를 진행하고 있는 성상훈입니다.

사실 처음에 강좌를 진행할때는 이정도로 많은 분들이 관심을 가질거라고는 생각을 못했었습니다. 어쨋든 의외로 많은 분들이 관심과 응원을 보내주시니 몸둘바를 모르겠군요. 전체 강좌가 초반을 지나서 중반부로 가고 있는데요. 개별적인 질문들에 일일히 답변하기가 여의치않아, 몇가지 질문메일에 대한 답변과 강좌의 진행방향에 대해서 알려드리고자 잠시 몇자 적어봅니다.


일단 참고문서나 서적등을 추천해달라는 말씀이 많았습니다. 아래에 참고서적등을 나열하오니, 말그대로 참고하시기 바랍니다. (참고서적은 순전히 저의 주관적인 판단에 따른것입니다)


1) Advanced Windows NT, Jeffrey Richter

: 설명이 필요없는 Windows 시스템프로그래밍의 바이블, 시스템프로그래밍의 페촐드라고 보면됩니다. 어플리케이션 레벨에서의 시스템프로그래밍을 주로 다루고 있습니다.


2) Debugging Applications, John Robbins

: MSJ의 유명한 컬럼 "Bugslayer"의 저자이자 Numega Software의 시스템 엔지니어인 John Robbins의 명저, 주제는 디버깅이지만 상당한 수준의 시스템프로그래밍 지식을 얻을 수 있으며, 실무에서도 바로 적용할수 있는 기술과 정보가 아주 잘 소개되어있습니다.


3) 파괴의 광학,    김성우

: 지난 강좌에서도 잠시 소개한적이 있습니다. 월간 마이크로소프트에서 연재한 시스템해킹 컬럼을 서적으로 출간, 윈도 환경에서의 해킹과 보안을 주제로 한 상당히 흥미로운 내용으로 구성되어있습니다.


4) 매크로 어셈블러 기초, 황희융

: 뭐 설명이 필요없는 어셈블리 필독서, 도스용 8비트 어셈블리부터 32비트 어셈블리까지 포괄적으로 다루고 있습니다. 일단 꽂아놓고 필요할때마다 참고하시면 좋겠죠?


5) System Programming for Windows 95, Walter Oney

: Win9x의 디바이스드라이버인 VxD를 다루는 서적으로, VxD 바이블로 불리워질 정도로 유명한 책입니다. 번역서는 없지만 대우중공업 자동화 연구팀의 홍진철님의 번역문서를 인터넷에서 구할수 있습니다.


6) Programming Microsoft Windows Driver Model, Walter Oney

: 5번책, System Programming for Windows 95로 유명한 Walter Oney의 WDM 개발서적입니다. 전편의 인기와 저자의 명성으로 짐작할수 있듯이 체계적인 설명이 잘되어 있는 NT/2000용 디바이스드라이버 모델인 WDM의 교과서라고 할 수 있습니다.


7) Inside Microsoft Windows 2000, David A. Solomon & Mark E. Russinovich

: Windows 2000 시스템과 내부구조에 대해서 포괄적으로 다루는 Win2K 시스템프로그래머의 필독서라고 할 수 있습니다.


실제로 필자도 위의 모든책들을 모두 다 읽어보지는 못했고 이것외에도 좋은 책들이 많이 있는것으로 알고 있습니다만 아직은 번역서나 국내저서보다는 원서가 많은 형편입니다. 물론 바이블이라고 불릴만한 대표적인 책들은 대부분 번역서가 나와있는 상태이므로 영어가 딸리는 저같은 분(^^)들은 번역서와 원서를 둘다 구입하셔서 번역서로 도저히 이해안되는 부분은 직접 원서를 참고하는 방식으로 공부하시면 효과적일것 같습니다.(물론 경제사정이 되신다면... 만약 시스템 프로그래밍이 처음이시라면 각 분야의 대표적인 서적 한권을 여러번 읽는 방법이 좋을것 같습니다.(분명히 처음읽을때와 두번째 읽을때가 다를것입니다.) 저렇게 많은 자료들을 모두 숙지할 필요까지는 없더라도(실제로 실무에서 더 많은것을 배운다고 하더군요.) 필요한 부분이 있을때에는 찾아볼수라도 있으려면 기본적인 개념에 대해서는 숙지해야만 합니다. 또한 서적외에도 인터넷이나 전문잡지등을 참고하는것도 하나의 방법이라고 볼 수 있겠죠.


사족을 덧붙이면, 저또한 장치와 연결되는 순수한 디바이스드라이버의 개발쪽보다는 시스템 어플리케이션과 운영체제를 이해하기 위한 방법으로 디바이스드라이버를 다루어왔습니다. 이글을 읽고계시는 대부분의 개발자 여러분 또한 하드웨어와 운영체제를 연결하기 위한 순수한 디바이스드라이버보다는 일반 어플리케이션으로 불가능한 작업을 위해서 디바이스드라이버를 배우시려는 분들이 대부분일거라고 생각되는군요. 따라서 강좌의 진행도 그쪽을 중심으로 진행해 나가겠습니다.


또 한가지 디바이스드라이버를 처음하시는 분이라면 VxD로 대표되는 Win9x계열의 디바이스드라이버보다는 WDM이나 NT 커널드라이버 쪽에 비중을 두고 공부를 하시는것이 좋으실 겁니다. (이유는 웬지 다 아시죠? 이제 9x의 시대는 저물고 있으니까요...)


참고서적 소개는 이쯤하고 메일이나 쪽지 주셨던 분들의 질문에 대해서 몇가지 언급할만한것들을 다루어보겠습니다.

(음... DB 핸들링이나 윈도컨트롤 프로그래밍을 질문하신 분도 계시지만 죄송하게도 본 강좌와 무관하다고 판단되어서 답변을 드리지 못함을 이해해주시기 바랍니다.)


1. WH_CALLWNDPROC 훅을 통한 프로세스 영역 침투하기


김상희(potpry)님의 의견중, WH_CALLWNDPROC 훅을 사용한 프로세스 주소공간 칩입하기에 관한 글이 있었는데요. 역시 지적하셨던것처럼 몇가지 문제점을 가지고 있는 관계로 논외로 하려고 했습니다만, 의견을 주신분도 계시니 잠깐 다루어보도록 하겠습니다. (의견주신 김상희님께 다시한번 감사의 말씀을 전하며...)


일단 WH_CALLWNDPROC 훅은 잘 아시는대로 윈도프로시저를 가로채기위한 훅입니다. 따라서 지정된 훅프로시저가 현재 프로세스에 매핑되지 않았다면 시스템은 자동적으로 훅프로시저를 가지고 있는 dll을 프로세스에 매핑시킵니다. (dll을 로드시킨다는 말이죠.) 이 순간 당연히 DLL은 분명 ATTACH되는 프로세스의 주소공간에 작업한다는 것은 의심할 여지가 없으며, 따라서 이 부분에서 우리가 원하는 프로세스 영역에 관한 어떠한 작업도 가능하다는 것입니다. 실제로 Matt Pietrek이 MSJ 컬럼에서 Api Hijacking 이란 이름으로 선보였던 방법입니다. 그러나 이미 언급했다시피 이 방법은 몇가지 문제점을 가지고 있습니다.


먼저, 윈도우프로시저를 가지지 않는 프로그램일 경우, 적용할 수 없다는 점과 명시적 링킹(explicit link)과 링크된 DLL에서 또다시 임포트된 함수의 처리에 관한 문제입니다. 윈도프로시저를 가로채는 훅이니 만큼 윈도프로시저가 없는 프로그램에서는 사용할수 없다는 것은 반론의 여지가 없을텐데, 두번째문제는 좀 복잡합니다. 실제로 PE구조를 분석해보셨다면 임포트된 함수의 유일한 절대주소를 호출하는 방식으로 API호출이 이루어지지 않는다는 것을 아셨을것입니다. 각 프로세스마다 jmp + API 함수주소의 내용으로 되어있는 임포트테이블을 따로 관리하며, 실제 프로그램은 API로 직접 call 하는 것이 아니라 테이블의 구성요소를 call하는 형태로 변환됩니다. 그런데 문제는 그러한 임포트된 모듈에서 또 다시 임포트된 모듈의 함수를 후킹하는 경우입니다. 쉽게 설명하면 USER32.DLL은 내부적으로 KERNEL32.DLL을 임포트하고 있는데 이런경우 Win9x 사용자모드에서는 시스템 DLL의 임포트테이블을 수정할 수 없으므로 사실상 후킹이 불가능해집니다. 또한 API내부에서 API를 호출하는 경우에도 마찬가지의 이유로 적용할 수  없는 또하나의 예가 되겠습니다. 어쨋든 임포트테이블을 가지고 전역 API 후킹을 구현하기에는 여러가지 어려움이 많으므로 실제로 우리는 이러한 방법을 사용하지 않을 예정입니다.


2. 왜 9x와 NT/2K에서의 API 후킹 접근방식이 달라야 하나?


아주 성격이 급하신분이 질문하신 모양입니다.(^^) 실제 구현할때 자세하게 설명드리겠지만 왜 9x와 NT/2K계열에서 전역 API 후킹이 다르게 작성되어야 하는지 간단하게 설명해드리겠습니다. 가장 커다란 차이는 시스템에서 공유하는 가상메모리 영역(보통 0x80000000, 2GB 이상)을 각각의 프로세스가 어떻게 접근하느냐의 차이점입니다. 쉬운예로 2번째 강좌에서 실습해보았던 공유메모리영역이 9x에서는 2GB이상의 영역에 위치하지만 NT/2K에서는 2GB 안쪽에 위치하는 것을 볼 수 있습니다. 따라서 NT/2K에서는 그 주소공간이 주소만으로 다른 프로세스에서 유효하게 재사용되어질 수 없다는 것이 문제입니다. 이것은 공유메모리뿐 아니라 실제 시스템에서 공유되는 시스템 DLL의 모듈핸들(다른말로 베이스어드레스)이나 함수주소등도 마찬가지입니다. 따라서 9x와 는 다른 방식으로 접근해야만 하며 난이도 또한 9x보다 난해하고 복잡합니다. 또한 시스템 DLL의 내용을 수정하려면 RING0로 불리는 운영체제와 동일한 특권레벨을 가져야 하는데 9x에서는 사용자레벨(RING3)에서 편법적으로 RING0를 획득할수 있지만(보통 Win9x의 뒷문(?)이라고도 합니다.) NT/2K에서는 디바이스드라이버를 통하지않고 RING0를 권한을 얻을수 있는 방법은 없는것으로 알려져 있습니다. CIH등의 무시무시한(?) 바이러스가 WinNT/2K에서 무용지물인 이유 또한 바로 여기에 있습니다. 만약에 NT/2K 환경에서 사용자레벨(RING3)에서 RING0를 획득하는 방법을 알고계시다면 바이러스같은것 만들지말고 좋은쪽으로 활용하시길 바랍니다. (필자에게도 알려주시면 고맙구요... ^^) 어쨋든 NT/2K는 9x에 비해 상당히 안정적인 운영체제이며 그 커다란 이유중에 하나가 시스템이 공유하는 가상 메모리영역을 사용자레벨에서 접근하는것을 원칙적으로 금지하고 있습니다. 이 부분에 대한 더 자세한 설명은 후일 NT/2K 디바이스드라이버를 다룰때 다시한번 다루도록 하겠습니다.


3. 서버와 인증을 통해 수행되는 프로그램들의 인증루틴을 건너뛰면서 인증없이 작업을 수행할 수 있나?


프로그램마다 다르겠지만 인증루틴이 단순한 검사루틴(함수같은)처럼 구성되어 있다면 얼마든지 크래킹이 가능합니다만 지난강좌에서도 말씀드렸듯이 안티디버깅 코드로 역어셈블을 방해한다면 크래킹하기가 쉽지는 않겠죠. 그밖에도 크래킹을 막는 기법이나 바이러스 감염을 막는 방법은 몇가지가 더 존재하지만 특별한 경우가 아닌이상 적용하지 않는것으로 알고 있습니다. 본강좌는 크랙강좌가 아니므로 더이상의 설명은 드리지 않겠습니다.


끝으로...

다음 강좌에서는 예고한대로 Win9x 디바이스드라이버 모델인 VxD에 대해서 다루어보겠습니다. 아마도 매우 타이트한 진행이 되어야 할텐데 이론보다는 실제구현에 초점을 두고 진행할 예정입니다. 자세한 내용은 위에서 말씀드린 서적등을 참고하시는것이 좋을것 같네요.


****************************************************************************************

Win32 Global API Hook - 2 다른 프로세스의 주소공간으로 !! (1)


 지난 강좌의 내용이 어땠는지 모르겠군요. 첨부터 반말로 써서 혹시 기분이 상하셨을까봐, 이번 강좌부터는 처음에 간단한 인사를 드리고 시작하겠습니다. ^^ 생각보다 많은 사람들이 보신것 같은데요. 실제로 코딩을 해가면서 디버깅까지 해보셨다면 별로 어려울게 없었을거라고 믿습니다.

 자 이제 이번 강좌부터는 약간의 레벨업이 필요할듯 한데요. 이번 강좌에서 다루는 내용은 SOFTICE, BOUNDS CHECKER 등의 디버깅툴로 유명한 NUMEGA SOFTWARE의 시스템 엔지니어인 Matt Pietrek의 아이디어에서 빌어온 것임을 밝히며, API HOOKING의 원리를 이해할수 있을것으로 생각됩니다. 또 기운이 빠지는 얘기일지도 모르겠지만 이번 강좌의 내용으로도 우리가 원하는것(첫 강좌에서 밝혔죠?)을 완벽하게 이룰수는 없다는점입니다. 그렇지만 이 내용을 모르고는 다음 강좌로 넘어갈수는 없다는 판단에 두회에 걸쳐 강좌를 진행하도록 하겠습니다. 그럼, 담배한대 피우고 가보기로 하죠 ^^



1. CPU


 시스템프로그래밍에서의 CPU의 중요성은 말할필요가 없을것이다. CPU 아키텍쳐는 무엇보다도 중요하고 기본적인 내용이지만 이 강좌에서 그것을 다룰수는 없다. 자세한것은 각자 자료를 찾거나 책을 보면서 익히도록하고 우리는 여기서 CPU가 프로그램을 어떻게 실행하는지에 관해서만 언급하고자 한다. 운영체제가 실행파일을 메모리에 로드하게 되면 정해진규칙에 따라 실행시작주소(보통 엔트리포인트라고 말한다.)를 찾고 그곳으로 제어를 넘긴다. (여기서 제어를 넘긴다는 말은 IP(인스트럭션 포인터)가 지정된곳으로 세팅된다는 말과 같다.) 그렇게 되면 CPU는 그곳에서부터 정해진 바이트씩 읽어오면서 그것을 해석하며 실행해나간다. 이러한 작업을 두고 명령어가 패치된다고 말한다. 당연히 CPU가 해석하는 명령어는 기계어코드이며 어셈블리로 1:1 대응시킬수 있다. 이것만 기억하고 넘어가자, CPU는 기계어로 된 명령어를 해석해서 순차적으로 실행해 나간다. 혹시 OOP 프로그래밍이나 이벤트드리븐방식(Windows같은)의 프로그래밍에만 익숙하다면, 프로그래밍 방법론은 변했어도 CPU가 명령어를 처리하는 그순간은 그 옛날 도스시절이나 지금이나 별반 다를게 없다는것을 명심하기 바란다.


2. Win32의 메모리 관리


 지난강좌에서도 잠깐 언급한적이 있지만, 시스템프로그래밍에서 메모리관리는 빠질수 없는 주제이다. 대부분의 CPU와 운영체제가 보호모드를 지원하는 최근에 와서는 더더욱 중요한 주제가 되었고 우리도 당연히 짚고 넘어가야 하겠다. 지면과 시간의 한계로 메모리 관리의 많은 부분을 다룰수 없는것을 안타깝게 생각한다. 역시 이부분도 우리가 다루려는 핵심만 짚고 넘어가야 할듯하다. CPU는 메모리에 있는 데이터를 다룬다. CPU가 디스크를 엑세스한다? 말도 안되는 얘기다. CPU는 모든것이 메모리라고 생각하면서 작업한다.

(물론 이말에 대해서 반론의 여지가 있는 사람도 있겠지만, 일단은 이렇게 생각하고 강좌를 진행하는 것이 이해가 빠를것 같다.)

 CPU의 실행모드에는 리얼모드와 보호모드의 두가지가 있는데, 리얼모드는 본 강좌와 무관하니 언급하지 않기로 하고, 보호모드에 대해서만 얘기해보자. 메모리관리에서 웬 보호모드냐 하겠지만, 말하고자 하는것은 보호모드의 특징중의 하나인 가상메모리 메카니즘을 말하려고 함이다. 가상메모리라고 하는것은 말그대로 진짜메모리가 아니다. 단언하건데 여러분이 디바이스드라이버나 하드웨어를 제작하는 사람이 아니라면 여러분이 지금까지 알고 있었던, 사용해왔던 메모리는 모두 가상메모리였을것이다. 보호모드에서 가상메모리는 연속된다는 의미로 선형메모리(leaner 또는 flat memory)라고도 한다. (앞으로 선형메모리와 가상메모리는 같은 의미로 사용하겠다.) 가상메모리로 인해서 실제로 시스템에 장착된 메모리보다 큰 메모리를 우리는 사용할수 있었던 것이다. 이런 얘기는 들었을것이다. Windows는 모두 4기가바이트의 메모리를 사용할 수 있는데, 그중 응용프로그램이 사용하는 영역은 0-2기가바이트이며, 그 이상은 운영체제가 사용한다. 이것 역시 가상메모리이며, 윈도 운영체제는 가상메모리를 페이징메카니즘이란 방법으로 관리한다.

 그럼 페이징메카니즘이란 도대체 무얼 말하는것인가? 페이지는 시스템이 인식하는 가상메모리의 단위이다. 페이지는 CPU와 운영체제에 따라 그 크기가 다양한데 보통 Intel CPU의 윈도운영체제일 경우 보통 4K의 크기를 갖는다. 가상메모리는 페이지 단위로 스왑되거나 맵된다. 여러분이 단지 한바이트의 메모리만 할당한다하더라도 시스템은 하나의 페이지를 준비한다. 또 하나의 중요한 사실은 하나의 페이지는 연속된다는 것이다. 이말은 하나 이상의 페이지는 실제로는 연속되지 않을수도 있다는 말이다. 보호모드에서의 가상메모리는 리얼모드와 달리 산술적인 연산으로 물리주소로 연결되지 않으며, 페이지디렉토리와 페이지테이블이라는 자료구조를 통해서 실제메모리(물리메모리)로 연결된다. 실제로 가상메모리상에서는 연속되는 메모리영역이라 하더라도 실제 물리메모리상에서는 연속되지 않을수도 있다.

 이러한 개념은 매우 중요한데, 예를 들면 이러하다. 내가 만약 1메가바이트의 메모리를 할당했다고 하더라도 실제로 그 메모리는 물리주소상에서 연속된다고 보장할수는 없다. 가상메모리가 3페이지에 걸쳐서 존재한다면 그에 따르는 물리주소도 3페이지만큼이 존재하지만 그들의 위치는 우리가 예상하는대로 배치되지 않을수 있다는것이다.

3. 프로세스와 메모리


 내가 만든 A라는 프로그램이 있다고 치자. 그놈이 어떤 메모리를 할당했는데 그것의 시작주소가

0x70000000 이었다고 가정하자. 그런데 B라는 프로그램도 메모리를 할당하는데 그것의 시작주소 또한 0x70000000 이었다고 한다면, A, B 두 프로세스가 가지고 있는 이 두개의 메모리 영역은 과연 실제로는 어디에 존재할까? 주소는 같지만 실제로 둘은 전혀다른 메모리이다. 둘다 같은 가상주소값을 가지고 있지만 둘은 엄연히 물리주소상에서는 다른 곳을 가리키고 있다. 이것이 어떻게 가능할까? 시스템은 현재프로세스가 변할때(태스크 스위칭이 일어날때)마다 페이지디렉토리의 내용을 갱신한다. 윗부분에서 가상주소는 페이지디렉토리와 페이지테이블를 통해 물리주소와 연결된다고 했다. 따라서 이들이 변한다는것은 실제 가상주소가 가리키는 실제주소(물리주소)가 변한다는 말과 같다. 이러한 원리로 프로세스 A와 B는 서로의 공간을 전혀 알수가 없으며 이로 인해서 운영체제가 더욱 견고해지는 것이다.

 그렇다면 만약 10메가 바이트의 메모리를 사용하는 프로세스 10개가 동시에 동작한다면 100메가 바이트의 물리메모리가 필요할까? 반드시 그렇지는 않다. 왜냐면 위에서 말한 페이지 스왑이라는 기법을 운영체제가 지원하기 때문이다. 운영체제는 어떠한 페이지가 현재 필요하지 않다고 판단되면 그것을 디스크에 기록한후, 물리메모리에서 해제한다. 그러다가 그 페이지가 다시 필요한 시점에서 디스크에 보관된 페이지를 다시 물리메모리로 로드한다. 이러한 일련의 작업들로써 응용프로그램들은 현재 시스템에 장착된 메모리보다 더 큰 메모리를 사용할수 있는것이다. 우리가 자주보는 시스템 오류중 하나인 페이지 폴트(page fault)는 바로 이러한 페이징에 오류가 생겼을때 발생하는데 대표적인 경우는 현재 물리메모리가 할당되어지지 않은(디스크 스왑된) 페이지를 마치 메모리에 존재하는 페이지처럼 접근하려고 할때이다. 시스템은 기본적으로 이러한 에러를 예외핸들러를 설치해서 복구하게된다.

그렇다면 이제 프로세스간에 주소공간을 공유할수 있는가라는 문제에 대해 생각해보자. 계속 이론만 늘어놓았으니 이번에는 간단한 예제를 통해서 확인해보도록 하자. 아래에 리스트된 코드를 보자.


PVOID p1 = malloc(16);

if(NULL == p1)

    return -1;

memset(p1, "A", 16);

*((char*)p1 + 15) = "\0";


HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,

    PAGE_READWRITE|SEC_RESERVE, 0, 16, NULL);

PVOID pMap = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 16);

PVOID p2 = VirtualAlloc(pMap, 16, MEM_COMMIT, PAGE_READWRITE);

if(NULL == p1)

    return -1;

memset(p2, "B", 16);

*((char*)p2 + 15) = "\0";


printf("p1:0x%08x \n", p1);

printf("dump:%s \n", p1);

printf("p2:0x%08x \n", p2);

printf("dump:%s \n", p2);


printf("press any key... \n");

getch();


if(NULL != p1)

    free(p1);

if(NULL != p2)

    VirtualFree(p2, 16, MEM_DECOMMIT);

if(NULL != pMap)

    UnmapViewOfFile(pMap);

if(NULL != hMap)

    CloseHandle(hMap);


간단히 설명하면 일단 malloc()으로 16바이트만큼 메모리를 할당한다음 문자 "A"로 채운다음 그 주소와 내용을 화면에 출력한다. 그런다음 메모리맵파일을 생성한후 문자 "B"로 채운다음 그 주소와 내용을 화면에 출력한다. 그리고 잠시 사용자 키입력을 기다린후, 입력이 들어오면 메모리를 해제하고 프로그램을 종료한다. 메모리맵파일에 관한 자세한 설명은 MSDN을 참고하길 바라며, 일단 컴파일한후 실행시켜보자.


p1:0x00780eb0

dump:AAAAAAAAAAAAAAA

p2:0x85536000

dump:BBBBBBBBBBBBBBB

press any key...


필자의 시스템에서는 위와같이 출력되고 사용자 입력을 기다리는 상태가 되었다. 키입력을 하게되면 메모리를 해제하고 프로그램을 종료하게 되므로 일단 저상태로 내버려 두고 p1과 p2의 주소만 잘 적어두고 두번째 프로그램을 작성하자.


printf("dump:%s \n", 0x00780eb0);

printf("dump:%s \n", 0x85536000);


먼저 작성한 프로그램의 p1, p2의 주소를 출력하는 코드이다. 혹시 그냥 Cop & Paste하는 사람이 있을까봐 얘기하는데, 하드코딩된 주소는 당연히 앞서 작성한 프로그램에서 출력된 주소를 적어주어야 할것이다. 자, 두번째 프로그램을 컴파일한후 실행시켜보자.


dump:emTest2.exe

dump:BBBBBBBBBBBBBBB


어떤가? 확실히 이해가 되는가? 결론부터 말하자면 malloc으로 할당한 메모리의 주소는 다른 프로세스에서는 쓸모없는 무효한 주소가 된다. 그러나 메모리맵파일로 할당한 메모리의 주소는 다른 프로세스의 주소공간에서도 여전히 유효한것을 볼수 있다. (혹시 운이 없는 사람은 두번째 프로그램을 실행시키다가 시스템이 죽거나 블루스크린을 만났었을지도 모른다. 아마 대부분 그러지 않았을거라 확신하지만... ^^) 여기서 중요한것은 바로 다른 프로세스간에도 유효한 메모리와 무효한 메모리의 주소이다. 주소는 시스템마다 약간씩 차이가 있었겠지만 분명한것은 malloc으로 할당한 메모리의 주소는 0x80000000보다 작았을것이고, 메모리맵파일로 할당한 메모리주소는 분명히 0x80000000보다 큰 주소로 할당되었을것이다. 0x80000000은 10진수로 2147483648, 즉 정확히 2기가바이트이다. 우리는 이제 주소만 보고도 이것이 프로세스 전용메모리인지 아니면 시스템에서 공유되는 메모리인지를 구별할 수 있을것이다.


4. 끝으로


 필자의 생각으로 이번 강좌는 여러분들에게 정말로 지루하고 재미없는 강좌였을것이다. 대부분 이론적인 내용뿐이니 말이다. 하지만 이번에 다룬내용은 모두 우리가 앞으로 해야할 작업의 기초가 되는 내용들이니 지루하더라도 꼭 이해하고 넘어가기 바란다. CPU와 메모리, 프로세스에 관한 내용은 이것말고도 굉장히 중요한 내용들을 포함하고 있으니 다른자료나 참고서적을 통해서라도 꼭 살펴보길 바란다. 참고서적을 추천해달라는 분이 계셨는데 지금은 시스템프로그래밍에 관한 책이 여러권 나와있지만 필자가 살펴본 바로는 1강에서도 언급한 Jeffrey Ritcher의 Advanced Windows라는 책이 볼만할것이다. 한글번역본도 있으니 꼭 구해다가 한번씩 읽어보기 바란다. 이책은 어플리케이션 프로그래머를 위한 시스템프로그래밍 서적이지만 워낙 유명한 책이니 책장에 꽂아놓는것만으로도 의미가 있을듯 싶다. 그밖에 역시 1강에서 언급한, NUMEGA SOFTWARE의 시스템 엔지니어인 John Robbins의 Debugging Applications 라는 책 또한 참고할 내용이 많다.

나중에 NUMEGA SOFTWARE의 제품인 SOFTICE라는 디버거를 사용하게 될터인데(그때보면 알게되겠지만 정말 엄청난 프로그램이라 하지않을수 없다. 필자는 SOFTICE가 없는 시스템프로그래밍 디버깅을 생각할수조차 없을 정도이다.) 이책은 디버깅을 위한 책이라고 할수있지만 시스템에 관한 내용도 다루어지며, 아마 이책을 읽고나면 그럴듯한 디버거를 하나 만들수 있을것이다. 더 깊은 내용을 다루는 서적이나 자료를 알고싶다면 추후에 디바이스드라이버 강좌를 진행할때 소개하기로 하자.


5. 다음 강좌에서는...


 다음 강좌는 "제2강 다른 프로세스의 주소공간으로 !!" 두번째 시간이다. 원래 이번 강좌에서 다룰예정이었던 Debugging API와 그것을 이용한 간단한 디버거를 작성해보고, 다른 프로세스 공간에 우리의 모듈을 삽입하는 방법을 알아보자. 사실 강좌를 진행하다보니 욕심이 지나쳐 예상보다 진도가 늦어진 것같다. 애초 2강을 두번에 걸쳐서 진행하려 했으니 다음 강좌는 좀 타이트한 진행이 될것 같다. 그렇지만 이번 강좌보다는 덜 지루한 내용으로 채워질 예정이니 너무 걱정마시길...


****************************************************************************************

Win32 Global API Hook - 2 다른 프로세스의 주소공간으로 !! (2)


 지난시간에 이어서 다른 프로세스의 주소공간을 넘나들수 있는 방법에 대해서 알아보도록 하죠. 지난시간에 말씀드렸듯이 오늘 내용은 NUMEGA SOFTWARE의 시스템 엔지니어인 Matt Pietrek의 아이디어에서 빌어온것이지만 Advanced Windows의 저자 Jeffrey Ritcher나 Debugging Applications의 저자 John Robbinson 등이 인용했었고, 국내의 모 프로그래밍 잡지의 시스템 해킹칼럼에서도 다루어 진적이 있었던 내용입니다. API 후킹의 실질적인 기초를 다루는 내용이기도 하지요. 그럼 강좌나갑니다.


 자 그럼 우리는 무얼하려고 했는지 생각해보자. 이번 강좌의 내용이 다른 프로세스의 주소공간을 우리가 사용해보자는 것이었다. 어떻게 하면 다른 프로세스의 주소공간으로 들어갈 수 있을까? 아니 그것보다 다른 프로세스의 주소공간을 넘나드는 것이 어떤게 있을지 생각해보자. 쉽게 떠오르고 가장 대표적인것이 바로 디버거일것이다. 여러분이 Visual C++ 등의 개발툴로 디버깅을 하는것을 한번 생각해보자. 디버거와 내가 만든 프로그램은 엄연히 다른 프로세스이다. 그치만 디버거는 여러분이 만든 프로그램(디버기, 디버깅을 당하는 프로세스를 말한다.)의 변수의 값을 추적하거나 변경할 수 있다. 디버거는 컴파일러와 더불어 시스템프로그래밍을 익히는 가장 좋은예제가 될것이지만, 다들 아시다시피 제대로된 디버거를 제작하는것은 컴파일러만큼이나 복잡하고 어려운 작업이다. 음... 필자는 국내에서 제작된 상용 컴파일러와 디버거가 하나도 없다는것이 좀 아쉬운데(예전에 씨앗이라는 C언어 비슷한 한글언어가 있었긴 하지만, DOS용이었던것으로 기억된다.) 물론 우리나라의 개발자들의 능력이 못미쳐서라기보다는 일단 개발하는데 필요한 시간과 노력에 비해 그 사업성에서 결코 낙관적이지 않을것이라는 이유일것이다. 음... 어쨋든 얘기가 잠시 삼천포로 빠졌는데, 다시 디버거 얘기를 해보자. 어쨋든 디버거는 확실히 다른 프로세스의 주소공간을 넘나들며 실행된다는것은 의심의 여지가 없는것 같다.

 그렇다면?, 우리도 그와 비슷한 작업을 할 수 있지 않을까? Win32 API는 다른 프로세스의 주소공간을 엑세스할수 있는 API세트를 제공한다. 이것이 바로 Debugging API 이다. 아마 처음 들어보는 사람도 많을 것이다. 백문이 불여일견, 지금 당장 MSDN을 열고 "WriteProcessMemory"라고 타이핑해보자. 그러면 WriteProcessMemory함수에 대한 내용이 표시될것이다. (WriteProcessMemory는 대표적인 디버깅 API로 이름그대로 프로세스 메모리를 쓰는(WRITE)하는 함수이다.) Win32에서는 프로세스간의 주소공간이 철저히 보호된다더니만 그렇지도 않네? 이거 너무 싱겁잖아? 뭐 이렇게 생각하는 분들도 있을지 모르겠다. 그렇지만 운영체제가 어디 그리 엉성하게 제작되었겠는가? Debugging API는 말그대로 Debugging을 위해서, 또는 디버거를 제작하기 위한 목적으로 생겨난것이므로 우리같이 엉뚱한 목적을 위해 사용하려는데에는 많은 제한을 두고 있다. 쉽게 말하면 저러한 종류의 API를 사용할수 있는 경우는 매우 제한적이라는 것이다. 실례로 WriteProcessMemory등으로 다른 프로세스의 주소공간을 접근하려면 프로세스를 디버깅모드로 실행시키지 않는한 매우 까다로운 절차를 거쳐야 하며, 그나마 운영체제에 따른 지원여부또한 불투명하다. (운영체제, 정확히 말하면 상당수의 시스템 API가 Win9x에서 지원되지 않거나, 제한적으로 적용된다는 말이다.)

 참고로 언급하면 지금까지의 예제들은 모두 Win9x에서 동작한다. WinNT/2000에서는 정상적인 동작을 보장할 수 없다. 실제로 지난번 예제또한 WinNT/2000에서는 필자가 말한대로 동작하지 않았을것이다. NT계열(2000, XP까지)의 운영체제와 9x계열의 운영체제는 보기에는 비슷해보이고 대부분의 어플리케이션이 호환되는듯 동작하지만, 내부적인 많은 차이점을 지니고 있다. 결론적으로 말하면 API 후킹에 관한 아이디어는 9x나 NT계열이나 별반 다를게 없지만 적용하는 방법에 있어서는 차이를 가진다. 잘라말하면 9x가 NT계열보다 쉽다. 이유는 9x는 호환성을 위해 DOS와 Windows 3.x의 내부구조를 상당부분 포함하고 있기때문이다. 결국 호환성때문에 운영체제의 안정성과 합리적인 구조를 포기할수밖에 없었던 것인데, 9x가 NT계열보다 불안정한 이유도 여기에 있다. 일단 구체적인 두 운영체제의 차이점은 나중에 디바이스드라이버 강좌에서 자세히 알아보기로하고 우린 9x에서 작업한다고 가정하고 강좌를 진행하도록 하자.

 우리가 다른 프로세스의 주소공간에서 작업할 수 있는 가장 효과적인 방법이 무엇이 있을까? 당연히 DLL을 이용하는 것이다. 다들 아시다시피 DLL은 DLL을 로드한 프로세스의 주소공간에 매핑되며 얼마든지 프로세스의 자원을 사용할 수도 있다. 그럼 우리는 원하는 작업을 수행하는 DLL을 제작한다음, 그녀석을 원하는 프로세스에 주입시키면 될것이다.

 그런데, 내가 제작한 프로그램이 아닌녀석에게 어떻게 내가 원하는 DLL을 주입할수 있을까? 지난번 강좌에서 구체적으로 프로그램을 어떻게 실행시키는지를 잠깐 언급한적이 있다. 결국 컴파일러는 바이너리파일을 만들어내며, CPU는 그 파일에서 지정된 코드를 찾아 순차적으로 실행해나간다고 했다. 그렇다면 우리가 그 실행코드를 원하는 코드로 덮어써버린다면? 당연히 내가 덮어쓴 코드가 실행될 것이다. 실행파일이 실행되는것은 실제로 주기억장치(오랜만에 들어본다. ^^ 메모리를 말하는것이다.)에 로드된후에 실행된다고 했다. 그렇다면 실행파일이 로드된 지점을 찾아 위에서 말한 Debugging API로 원하는 코드영역을 내가 원하는 코드로 바꿔치기한후, 실행시키면 될것이다. 자, 아이디어는 매우 간단하다. 그럼 실제로 다음과 같은 과정을 거쳐 구현에 들어가보자.

1. 프로세스의 실행코드를 알아낸다.

 : Debugging API를 사용해서 특정프로세스의 실행코드위치를 알아낼 수 있다. 먼저 언급한대로 우리는 9x에서 우리가 프로세스를 출발시키는 경우에만 적용하도록하자.

2. 원하는 실행코드를 제작한다.

 : 아마 상당수의 어플리케이션 프로그래머(대부분 VC++, VB, Delphi 등으로 작업할 것이다.)가 이해하기 힘들어하는 부분일것이다. 왜냐면 어셈블리, 정확히 말하면 기계어코드를 작성해야 하기때문이다. 지금은 어셈블리로 프로그래밍하는 사람이 거의 없겠지만 어셈블리를 아는 프로그래머와 그렇지 않은 프로그래머는 분명한 차이가 있다. 반드시 어셈블리를 이용해서 프로그래밍하지 않더라도 어셈블리를 알면 디버깅과 시스템에 대한 이해가 분명해질것이다. 이번 강좌에서는 매우 간단한 어셈블리 코드만을 사용하지만, 자신이 프로그래밍을 천직으로 알고 있다면(이 강좌를 보는 대부분의 사람들이 그럴것이라고 필자는 믿고 싶다.) 반드시 어셈블리를 공부하길 바란다. (어셈블리에 관해서는 다음 강좌 "Win32 어셈블리 프로그래밍" 다시 다루도록 하자.)

3. 실행코드를 덮어쓴다.

 : 원하는 기능을 수행하는 코드를 원래의 코드에 덮어쓴다.

4. 필요한 기능이 실행되었다면 코드를 복원한다.

 : 3에서 덮어써진 코드를 복원/실행 한다.

전체적인 작업의 흐름은 이렇다. 자, 그럼 잠시 머리속을 정리한후, 실제 코드를 구현해보도록 하자.


그럼 먼저 간단한 마루타가 될 Win32 응용프로그램을 제작한다. 일단은 그냥 윈도에서 제공되는 노트패드를 그냥 가져다 써도 무방할 것이지만 나중에 API 후킹을 테스트하려면 하나 만들어두는것도 좋을것이다. 간단하게 VC++에서 Win32 Application - Hello World 프로그램을 선택해서 AppWizard를 통해 만들어도 상관없다. 실행해보면 그냥 달랑 메인윈도가 뜨고, 클라이언트 영역에 "Hello, World"라고 출력될것이다.


그 다음은 프로세스 몰래 주입할 DLL을 하나 제작한다. 일단 다른 프로세스에 제대로 로드되었는가를 확인하기 위해, 아래와 같이 프로세스에 붙을때와 떨어질때 메시지 박스를 츨력해주는 간단한 DLL을 만들어보자.


BOOL APIENTRY DllMain(HANDLE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved)

{

    switch(ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

        MessageBox(NULL, "DLL_PROCESS_ATTACH", "TestDll", MB_OK);

        break;

    case DLL_PROCESS_DETACH:

        MessageBox(NULL, "DLL_PROCESS_DETACH", "TestDll", MB_OK);

        break;

    }


    return TRUE;

}


일단 여기까지 별 무리없을 것이다. 그러면 프로세스에 DLL을 주입하는 모듈을 제작해보자. 앞서서 프로세스를 디버깅모드로 실행시켜야 한다고 말했다. 아래와 같이 프로세스를 디버깅 모드로 실행한다.


CreateProcess(NULL,

    (LPSTR)szCmdLine,

    0,

    0,

    FALSE,

    DEBUG_ONLY_THIS_PROCESS,

    0,

    0,

    &StartupInfo,

    &ProcessInfo);


단지 CreateProcess의 6번째 인자로 DEBUG_ONLY_THIS_PROCESS를 준것외에 특별한 것은 없다. 그러면 szCmdLine으로 실행된 프로세스는 내 프로세스의 디버기(전에 언급했다. 디버깅을 당하는 프로세스라는 의미이다.)가 된다. 그러면 디버기는 디버거(바로 내 프로세스가 된다.)에게 디버깅 이벤트를 발생시킨다. 우리는 적절한 이벤트를 핸들링함으로써 원하는 작업을 수행할 수 있을것이다. 아래의 코드를 보자.


DEBUG_EVENT event;

DWORD dwContinueStatus;


while(1)

{

    // 디버그 이벤트가 발생할때까지 대기

    WaitForDebugEvent(&event, INFINITE);


    dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;


    if(CREATE_PROCESS_DEBUG_EVENT == event.dwDebugEventCode)

    {

        // 디버그 프로세스 생성 이벤트

        TRACE("CREATE_PROCESS_DEBUG_EVENT fired !!\n");


        // 디버그 프로세스 정보

        m_ProcessDebugInfo = event.u.CreateProcessInfo;

    }

    else if(EXCEPTION_DEBUG_EVENT == event.dwDebugEventCode)

    {

        // 디버그 예외 이벤트

        TRACE("EXCEPTION_DEBUG_EVENT fired !!\n");

        HandleException(&event, &dwContinueStatus);

    }

    else if(EXIT_PROCESS_DEBUG_EVENT == event.dwDebugEventCode)

    {

        // 디버그 프로세스 종료 이벤트

        TRACE("EXIT_PROCESS_DEBUG_EVENT fired !!\n");

        return;

    }


    // 디버그 프로세스로 제어를 넘김.

    ContinueDebugEvent(event.dwProcessId, event.dwThreadId, dwContinueStatus);

}


 EXIT_PROCESS_DEBUG_EVENT 이벤트가 들어올때까지 무한루프를 수행하는 것을 알수 있다.

EXIT_PROCESS_DEBUG_EVENT 이벤트는 디버그 프로세스(== 디버기)가 종료될때 발생한다. 우리가  처리해야하는 이벤트는 이것 외에 CREATE_PROCESS_DEBUG_EVENT 와 EXCEPTION_DEBUG_

EVENT 이벤트인데 이름 그대로 디버기가 생성될때와 디버기에서 예외가 발생할때 디버거에서 발생한다. 우리는 여기서 프로세스 정보를 백업하고, 코드를 덮어쓰고 복원한다. 실제로 우리가 관심을 가져야 할 곳은 바로 EXCEPTION_DEBUG_EVENT 이벤트인데, 이녀석은 아까 말한대로 디버기가 예외를 일으킬때 디버거로 발생되는 이벤트인데, 예외의 종류는 다들 잘 알다시피 각종 오버플로우, 0으로 나눔, 접근금지 등등이 있는데, 여러분이 디버깅을 할때 사용하는 중단점(브레이크포인트) 또한 예외의 한 종류이다. 중단점은 디버기가 실행되는 순간(CREATE_PROCESS_DEBUG_EVENT 다음으로)에 실행파일 로더에 의해서 한번 발생하며, 당연히 중단점이 설정될 경우에도 발생한다.

그럼 HandleException() 함수의 내용을 보자.


// 브레이크 포인트인가?

if(EXCEPTION_BREAKPOINT

    == pEvent->u.Exception.ExceptionRecord.ExceptionCode)

{

    TRACE("EXCEPTION_BREAKPOINT fired !!\n");


    if(0 == m_uBreakCount)

    {

        // 첫번째 브레이크 포인트

        TRACE("First EXCEPTION_DEBUG_EVENT fired !! - InjectSpyDll() Call !!\n");

        if(!InjectSpyDll())

            TRACE("ERROR : InjectSpyDll() Fail !!\n");

    }

    else if(1 == m_uBreakCount)

    {

        // 두번째 브레이크 포인트

        TRACE("Second EXCEPTION_DEBUG_EVENT fired !! - ReplaceOriginalPagesAndContext() Call !!\n");

        if(!ReplaceOriginalPagesAndContext())

            TRACE("ERROR : ReplaceOriginalPagesAndContext() Fail !!\n");

    }


    m_uBreakCount++;

    *pContinueStatus = DBG_CONTINUE;

}

else

    *pContinueStatus = DBG_EXCEPTION_NOT_HANDLED;


 첫번째 중단점은 디버기가 실행된 직후에 한번 발생한다고 했다. 따라서 우리가 코드를 덮어쓰는 시점이 바로 이부분이 되어야 할것이다. 아직 디버기는 실행되기 전이며, 우리는 디버기의 첫 실행코드에서 우리의 코드를 덮어쓰는 셈이다. 두번째 중단점은 원본코드를 복원하기 위해서, 덮어쓰는 코드에서 지정해주는데, 디버거로 제어를 넘기기 위해 사용된다. 이부분에서 원본코드를 복원해주어야 할 것이다.


 그럼 실제로 실행코드를 덮어쓰는 부분(InjectSpyDll())을 살펴보자.

// LoadLibraryA()의 주소

FARPROC pfnLoadLibrary = GetProcAddress(

    GetModuleHandle("KERNEL32.DLL"), "LoadLibraryA");


// 실행 프로세스의 첫번째 페이지 얻어옴

m_pFirstCodePage = FindFirstCodePage(m_ProcessDebugInfo.hProcess,

    m_ProcessDebugInfo.lpBaseOfImage);


// 실행 스레드 컨텍스트 백업

m_OrgContext.ContextFlags = CONTEXT_CONTROL;

GetThreadContext(m_ProcessDebugInfo.hThread, &m_OrgContext);


BOOL    bRetCode;

DWORD    cBytesMoved;


// 실행 프로세스의 첫번째 페이지 백업

bRetCode = ReadProcessMemory(m_ProcessDebugInfo.hProcess, m_pFirstCodePage,

    m_pOrgCodePage, sizeof(m_pOrgCodePage), &cBytesMoved);

if(!bRetCode || sizeof(m_pOrgCodePage) != cBytesMoved)

    return FALSE;


// 스파이 DLL 을 로드할 루틴을 담은 구조체 제작

PFAKE_LOADLIBRARY_CODE pNewCode = (PFAKE_LOADLIBRARY_CODE)m_pFakeCodePage;


// sub esp, 1000h

pNewCode->instr_SUB = 0xEC81;

pNewCode->operand_SUB_value = PAGE_SIZE; // 페이지크기(4096);


// push <매개변수>

pNewCode->instr_PUSH = 0x68;

pNewCode->operand_PUSH_value = (DWORD)m_pFirstCodePage

    + offsetof(FAKE_LOADLIBRARY_CODE, data_DllName);


// call <함수주소> ; LoadLibraryA() 호출

pNewCode->instr_CALL = 0xE8;

pNewCode->operand_CALL_offset = (DWORD)pfnLoadLibrary

    - (DWORD)m_pFirstCodePage - offsetof(FAKE_LOADLIBRARY_CODE, instr_CALL) - 5;


// 마지막에 브레이크 포인트 삽입

pNewCode->instr_INT_3 = 0xCC;


// 매개변수 (로드될 스파이 DLL)

char pszDll[MAX_PATH];

if(!GetSpyDllName(pszDll, sizeof(pszDll)))

    return FALSE;

strcpy(pNewCode->data_DllName, pszDll);


// 우리의 루틴을 실행프로세스에 Write !!

bRetCode = WriteProcessMemory(m_ProcessDebugInfo.hProcess, m_pFirstCodePage,

    &m_pFakeCodePage, sizeof(m_pFakeCodePage), &cBytesMoved);

if(!bRetCode || sizeof(m_pFakeCodePage) != cBytesMoved)

    return FALSE;


// 실행 포인트(EIP)를 첫번째 페이지로 설정

m_FakeContext = m_OrgContext;

m_FakeContext.Eip = (DWORD)m_pFirstCodePage;


// 실행 스레드 컨텍스트 설정

if(!SetThreadContext(m_ProcessDebugInfo.hThread, &m_FakeContext))

    return FALSE;


return TRUE;


 코드가 좀 긴데, 흐름은 이렇다. 먼저 우리가 수행하기를 원하는 코드는 이렇다. 바로 우리가 앞서 만든 testdll.dll을 디버기로 하여금 로드하게 하는것이다. C코드로 하면 디버기에 아래와 같은 코드를 삽입하는 것이다.


LoadLibrary("testdll.dll");


그럼 이런 작업을 수행할 코드를 제작해보자. 결론부터 말하면 어셈블리로 아래와 같은 코드가 될것이다.

push <"test.dll"의 주소>

call <LoadLibrary의 함수주소>

int 3


 C언어가 파라미터를 패스하는 방법은 스택을 이용하는 것이다. 함수 파라미터 패싱은 레지스터를 이용하는 방법과 스택을 이용하는 방법이 있는데, 일반적인 경우 C언어에서 파라미터는 스택을 통해서 넘겨진다.(함수호출규약에 관해서는 다음 강좌 "Win32 어셈블리 프로그래밍" 자세히 다루도록 하자.) 어쨋든 위와 같은 코드를 통해서 우리가 원하는 DLL이 로드될것이며, 작업이 끝나면 원본코드의 복원을 위해 디버거로 제어를 넘겨야 하는데, 이를 위해 중단점을 지정한다. 중단점은 어셈블리 코드로 인터럽트 3번 즉, int 3 이다.


 그럼 위의 어셈블리코드를 기계어로 변환시켜보자. 그런데 문제가 있다. test.dll이나 LoadLibrary의 함수주소를 어떻게 처리해야 하는것일까? 먼저 LoadLibrary의 함수주소를 찾아보자. GetModuleHandle(), GetProcAddress()로 함수의 주소를 알아내는 것은 간단할 것이다. (적어도 9x에서는) 그런데 이 함수주소를 실제로 CPU가 해석할때는 어떤방식으로 접근하는지를 알아야할것이다. call 명령이나 jmp 명령 등의 실행제어를 변경하는 명령어들은 32비트 환경에서 보통 5바이트의 크기를 갖는데, 이는 명령어코드 1바이트와 이동할 주소 4바이트(32비트 어드레싱이므로)로 이루어진다. 그런데 명령어의 파라미터가 되는 주소는 절대주소가 아니고 상대주소이다. 그러니까 현재 call 명령을 수행하고 돌아올 리턴주소를 실제 이동할 주소에서 뺀 값으로 기계어코드를 생성한다. 예를 들어 call 100 이라는 어셈블리 명령이 위치한 주소가 50이라면 이 명령은 기계어로 변환되면 0xe8(call 명령어)과 100(이동할주소) - 50(현재 명령의 주소) - 5(현재 명령의 크기)로 전개된다는 것이다. 결과적으로 0xe845000000의 코드값으로 변환될 것이다.(물론 일반적으로 위와 같은 주소값은 Win32에서 유효할수 없다.) 그렇다면 실제로 LoadLibararyA의 함수주소가 0xbff77750이고, 현재 실행중인 코드의 주소가 0x00401bc2 라고 한다면 LoadLibraryA를 호출하는 기계어 코드는 다음과 같을 것이다. 0xe8895bb7bf(call bff77750h) (Intel 계열의 CPU에서는 역워드지정방식을 사용한다는것을 기억하자. 0x12345678은 실제 메모리상에는 0x78, 0x56, 0x34, 0x12로 적재된다.)


 자, 함수호출이 어떻게 이루어지는지 알았으니 이제는 파라미터를 어떻게 전달하는가를 알아보자. 위부분에서 설명했듯이 C언어는 일반적으로 스택을 통해서 파라미터를 전달한다고 했다. 스택에 파라미터("testdll.dll")을 푸쉬하고 좀전에 본대로 함수를 호출해주면 되겠는데, 우리가 사용할 파라미터는 정수나 문자같은 단순데이터형이 아니고 포인터형이다. 그렇다면 실제 문자열을 담고 있는 데이터를 우리가 마련해주어야 하며 그것을 어떻게 찾아서 프로그램이 사용하는지 또한 우리가 지정해주어야 할것이다. 컴파일러는 지역변수와 전역변수를 스택과 힙에 각각 공간을 할당해주며, 코드내에서 이들을 찾아서 연결될수 있도록 배려한다. 그러므로 프로그래머는 데이터를 어떻게 접근해야 하는지를 신경쓸 필요가 없게되고, 단순히 데이터의 이름으로만 참조하면 된다. 우리가 만드는 코드 또한 작은 실행파일과 유사한 실행가능한 코드조각이지만 이런 작업들을 해줄 컴파일러따위는 없다. 그러므로 데이터를 접근하는 방법또한 우리가 직접 지정해주어야 할것이다. 일단 우리는 독립적으로 실행될수 있어야 하므로 전역변수가 사용하는 힙을 사용하기에는 무리가 있다. 결국 스택을 이용해야 하는데 컴파일러는 지역변수를 스택에서 관리한다. (다음강좌에서 다시 다루겠지만 일단 스택포인터를 감소시켜서 지역변수를 위한 공간을 할당한다고만 알아두자. 이부분은 어셈블리를 얘기할때 아주 중요한 사항이므로 꼭 알아두도록 하자.) 스택포인터를 감소시키고 그 공간에 문자열을 담은 데이터를 써넣고 그 문자열의 주소를 다시 스택에 푸쉬한다음 LoadLibraryA를 호출한다면 원하는 DLL을 로드할 수 있을것이다.


 전체적인 쉘코드(쉘코드란 말은 원래 유닉스 계열의 운영체제에서 루트의 권한에서 실행되는 프로그램의 코드를 변조해서 루트의 권한으로 쉘을 획득할수 있게 하는 해킹 코드덩어리를 말한다. 최근까지 유행했던 스택오버플로우 해킹기법으로 세상에 알려졌으며, 보통 리턴주소를 덮어쓰는 방법으로 코드를 실행한다.)가 구상되었다면 이제 실제로 이러한 기능을 담은 쉘코드를 위한 자료구조를 아래와 같이 준비한다.

// 구조체를 1바이트씩 packing한다.

#pragma pack(1)


// 실행프로세스에 주사될 실행루틴(LoadLibraryA())

typedef struct _FAKE_LOADLIBRARY_CODE{

    WORD    instr_SUB;

    DWORD    operand_SUB_value;

    BYTE    instr_PUSH;

    DWORD    operand_PUSH_value;

    BYTE    instr_CALL;

    DWORD    operand_CALL_offset;

    BYTE    instr_INT_3;

    char    data_DllName[1];

}FAKE_LOADLIBRARY_CODE, *PFAKE_LOADLIBRARY_CODE;


 일단 1바이트로 구조체를 정렬하도록 지정한후, 구조체의 각 필드를 채워넣는 로직은 위에 리스트된 코드를 참고하길 바라며, 결과적으로 우리가 삽입할 전체적인 어셈블리 구문은 아래처럼 될것이다.


sub esp, 1000h

push <"test.dll"의 주소>

call <LoadLibrary의 함수주소>

int 3


 처음 스택포인터를 0x1000(4096)만큼 감소시킨것은 아까 말했듯이 마치 컴파일러가 지역변수를 위한 공간을 확보하듯이 우리의 데이터를 위한 스택공간을 확보한것이다.

 자!! 어쨋든 쉘코드가 완성되었다면 이녀석을 프로세스의 코드영역에 덮어쓰고 실행시키기만 하면 우리가 원하는 작업(LoadLibraryA("testdll.dll"))을 수행해줄것이다. 그렇다면 어느곳에 덮어쓸것인가? 우리는 프로그램의 제일 처음코드에 우리의 코드를 덮어씀으로써 먼저 우리가 원하는 작업을 수행한후, 원본코드를 복원해서 마치 프로세스는 아무일없었다는듯 실행되게 할 예정이다. 그렇다면 먼저 프로세스의 실행코드의 첫부분을 찾아내야 하는데, 이를 위해서 FindFirstCodePage() 함수를 보도록 하자.

BOOL    bRetCode;

DWORD    cBytesMoved;

DWORD    peHdrOffset, baseOfCode;


// 실행 프로세스의 첫번째 페이지 얻음.

bRetCode = ReadProcessMemory(hProcess, (PBYTE)pProcessBase + offsetof(IMAGE_DOS_HEADER, e_lfanew),

    &peHdrOffset, sizeof(peHdrOffset), &cBytesMoved);

if(!bRetCode || sizeof(peHdrOffset) != cBytesMoved)

    return FALSE;


bRetCode = ReadProcessMemory(hProcess, (PBYTE)pProcessBase + peHdrOffset

    + 4 + IMAGE_SIZEOF_FILE_HEADER

    + offsetof(IMAGE_OPTIONAL_HEADER, BaseOfCode),

    &baseOfCode, sizeof(baseOfCode), &cBytesMoved);

if(!bRetCode || sizeof(baseOfCode) != cBytesMoved)

    return FALSE;


return (LPVOID)((DWORD)pProcessBase + baseOfCode);


 그다지 까다로운것은 없는것 같다. 1강에서 언급했던 PE구조를 이해했다면 별어려움 없이 이해할 수 있을것이다. 프로세스의 베이스어드레스에서 코드의 베이스 어드레스만큼의 오프셋을 더한 값을 반환해준다. 이미 눈치챘겠지만 코드의 베이스 어드레스 또한 상대주소라는 것을 알 수 있을것이다.

 어쨋든 디버기의 현재 스레드의 컨텍스트와 첫번째 코드페이지를 백업한후, 아까 만들어놓은 쉘코드를 디버기의 첫번째 코드페이지에 덮어쓴다. 그다음 현재 스레드의 컨텍스트의 실행포인터를 덮어쓴 첫번째 코드페이지로 복원한다. 스레드 컨텍스트에 대해 생소한 사람이 있을것 같아서 짚고 넘어가면 스레드 컨텍스트는 문맥그대로 현재 스레드의 실행상태를 보관하고 있다. 대부분 CPU 레지스터에 관한 정보인데 그중 우리가 꼭 알고 넘어갈것이 eip(확장 인스트럭션 포인터)이다. 이 레지스터가 가지고 있는 데이터는 다음에 실행할 코드주소를 가지고 있다. 따라서 이 녀석을 수정하게 되면 프로그램의 흐름을 원하는 방향으로 제어할수 있다. VC++ 디버거에서 제공하는 Set Next Statment 명령이 바로 이 eip를 수정하므로써 실행흐름을 원하는 곳으로 점프 또는 리턴시킨다. 못믿겠다면 VC++ 디버거에서 레지스터 윈도우를 오픈한뒤 eip의 값을 수정해보라. 프로그램이 여러분이 eip로 지정한 주소로 점프하는 것을 볼수 있을것이다.(실행흐름을 조작하는것은 시스템프로그래밍 디버깅에서는 아주 흔한경우이지만 어플리케이션 레벨의 디버깅에서는 많이 쓰이지 않는듯 하다. Set Next Statment 는 VC++ 디버거에서도 매우 유용한 기능이므로 꼭 기억해두도록 하자. 그러나 남발하게 되면 스택이 망가지거나 돌이킬수 없는 상황을 초래하기도 하므로 잘 알고 사용해야 할것이다.)


 마지막으로 ReplaceOriginalPagesAndContext() 함수는 덮어썼던 코드페이지와 컨텍스트를 원본대로 복원하는 함수이다.

BOOL    bRetCode;

DWORD    cBytesMoved;


// 첫번째 페이지와 스레드 컨텍스트를 원래대로 되돌림.

bRetCode = WriteProcessMemory(m_ProcessDebugInfo.hProcess, m_pFirstCodePage,

    m_pOrgCodePage, sizeof(m_pOrgCodePage), &cBytesMoved);

if(!bRetCode || sizeof(m_pOrgCodePage) != cBytesMoved)

    return FALSE;


if(!SetThreadContext(m_ProcessDebugInfo.hThread, &m_OrgContext))

    return FALSE;


return TRUE;


 이전에 InjectSpyDll()에서 백업해 두었던 페이지와 컨텍스트를 단순히 되돌림으로써 프로세스(디버기)는 자신이 무슨짓을 했는지도 모른채 정상적으로 실행된다.

<끝으로 !!>


 오늘강좌는 약간 긴 분량의 강좌였던것 같다. 우리는 실행중인 다른 프로세스에 우리의 모듈을 몰래 주입하는 방법을 배웠다. 어셈블리가 생소한분에게는 좀 버거웠던 강좌였을것이다. 이번 강좌는 예외적으로 예제프로그램의 소스코드를 같이 올리도록 하겠다. 같이 올린 소스코드를 분석해보면 알겠지만 몇가지 잡다한 부분은 오늘 설명에서 제외되었지만 그리 어렵지 않거나 별로 중요하지 않은 부분이니 충분히 혼자 이해할수 있을거라고 믿는다.

 원래는 1강에서 강의한 모듈로서 DLL을 제작한 후, 그것을 다른 프로세스에 주입해서 실제로 다른 프로세스의 API 후킹을 보여주려 했지만, 역시나 욕심이 지나쳐 설명이 많아지는 바람에 다루지 못했다. 이부분은 여러분들이 직접 스스로 해보길바란다. 1강에서 다룬 내용은 동일한 프로세스의 주소공간에서는 유효한 API 후킹이었으므로 이것을 DLL로 제작해 오늘 배운방법으로 프로세스에 주입시키면 그 프로세스의 API를 후킹할수 있을것이다.

 그렇지만 오늘 강좌도 모든 경우에 적용할 수 있는 API후킹이라고 하기에는 모자란점이 많다. 일단 임포트테이블을 조작하는 방법에는 한계가 있다는 점을 말해주고 싶다. 또한 어떠한 경우에는 위의 방법이 전혀 통하지 않는 경우도 있다는것을 참고하기 바란다. 그밖에 멀티스레딩에 대한 문제 등, 일반화하기 위해서는 넘어야 할 산이 많다. 그렇지만 오늘 강좌의 내용은 API 후킹의 실질적인 기초가 되었던 강좌임에는 틀림없다. 시간관계상 설명하지 못하거나 또는 빠뜨린것이 있을수 있겠지만 적어도 오늘 내용만큼은 꼭 이해해주길 바란다.

 다음 강좌는 Win32 어셈블리 프로그래밍에 대해서 약 2회에 걸쳐서 강좌를 진행할 예정이다.


****************************************************************************************

Win32 Global API Hook - 3 Win32 어셈블리 프로그래밍 (1)


 오늘부터 진행할 강좌는 실질적인 기초라고 할수 있겠습니다. 그러나 기초가 제대로 잡히면 그 다음의 응용은 아주 간단한것(물론, 거짓말이란거 다 아시죠?)들이니까 기초부터 튼튼히(!!)하자구요. ^^ 그럼, 어느 성인(?)의 말을 인용하면서 강좌나갑니다.


"무릇 프로그래밍이라 하는것은 현자가 우매한 중생들에게 진리를 설파하는것과 같다. 오로지 그 수의 많고 적음에 상관없이 소유와 무소유만을 기대하는 우매한 자들에게 그들만이 이해할수 있는 방법으로 세상을 바르게 살아가는 법을 깨우쳐주는것이다."



1. 어셈블리, 참을수 없는 구조의 단순함 !!


흔히들 어셈블리를 어렵다고들 한다. 그러나 이것은 정확히 말하면 어셈블리로 프로그래밍하기가 어렵다는 의미이다. 왜냐면 어셈블리 자체는 매우 단순하기 때문이다. 어셈블리 자체가 매우 단순하기때문에 그 단순한 기능만으로 요즘 추세에 맞는 복잡한 어플리케이션을 작성하기가 매우 번거롭다는것이다. (물론 최근에 나온 어셈블러들은 고급언어에 버금갈만한(물론 이것도 사실은 아니다. 예전에 비해서는 그렇다는 말이다.) 코딩의 편의를 제공하기는 하지만) 필자가 예전에 처음 프로그래밍을 공부한지 얼마되지 않았을때 어셈블리와 C언어의 차이점을 선배프로그래머가 이렇게 얘기했다. 여기서는 C언어를 얘기하지만 대부분의 고급언어로 이해해도 무난할것이다.


"C언어는 "냉장고에서 사과를 꺼내고 바나나를 넣어두어라!!"라는 식으로 명령한다면, 어셈블러는 "바나나를 들고 냉장고 앞으로 이동한다음, 냉장고 문을 열고, 사과를 꺼낸다음, 그 자리에 바나나를 넣고, 냉장고 문을 닫아라!!" 이런식으로 명령되어져야 한다"


지금 생각하면 정말 명쾌한 설명이 아닐수없다. 실제로 C언어는 "냉장고에서 사과를 꺼내고 바나나를 넣어라"라고만 하면 스스로 냉장고 앞으로 이동해야 한다는것, 냉장고 문을 열어야 한다는것, 사과를 꺼내고, 바나나를 넣어야 하며, 마지막으로 냉장고문을 닫아야 하는것을 이해하지만, 어셈블러는 이러한 일련의 작업을 일일이 지정해주어야 한다는 것이다. 그렇지만 C언어든 어셈블리든 결국 행동하는 패턴은 같을수밖에 없으며, 단지 명령하는 사람의 명령을 어디까지 이해할것인가의 문제인것이다. 결과적으로 C언어가 어셈블리보다는 더욱 똑똑하다는 것이다.


모든것은 상대적인 것일수밖에 없다. 그 옛날 어셈블러도 없이 기계어로 프로그래밍하던 세대에게 어셈블러는 매우 편리한 도구였다. 실제로 DOS용 "LOTUS123"같은 OA프로그램이나, 당시에 엄청난 인기를 끌었던 "페르시아왕자" 1탄 같은 프로그램들이 순전히 어셈블리로 제작되어었다는 것만 보더라도 그 시대에서는 어셈블리가 지금처럼 어렵고 생소한 언어가 아니었을것이다. 단지 우리는 어셈블리보다는 똑똑한 C언어나 파스칼 등의 고급언어에 익숙해져있으니 어셈블리가 생소하고 어려워보이는 것이다. (물론 예전과는 비교도 할수없을 정도로 지금의 프로그램들은 복잡, 다양해지고 있지만) 서두에 언급했듯이 우매한 어셈블리가 세상에서 바르게 살아나갈수 있도록 우리는 조금더 현명해져야 하며, 그들을 이해하며 눈높이를 맞출수있는 열린마음을 가져야할때다.


어셈블리가 별것아니라는 얘기만 했는데 그렇다고 어셈블리가 정말로 별것 아닌것은 아니다. 아마 개발환경이 아무리 편해지더라도 어셈블리가 세상에서 사라지지는 않을것이라는 필자의 생각이다. 그 이유는 다름아닌 CPU가 명령을 처리하는 가장 하위단계는 잘 알다시피 기계어코드이다. 그러나 사람이 기계어만으로 프로그래밍을 하거나, 기계어코드만 가지고 명령을 이해하기는 매우 힘든작업이므로(불가능한것은 아니다. 예전에는 사람이 이런 작업을 하기도 했었다. 또 우리도 그러한 일을 곧 하게될것이다.) 기계어를 사람이 이해하는 형식으로 표현해주어야 하는데 이것이 바로 어셈블리이다.


조금 과장해서 말한다면 어셈블리를 알면 프로그램의 흐름을 분석할 수 있으며, 원한다면 프로그램을 원하는대로 수정할수도 있다. 실제로 디바이스드라이버의 경우, 바이너리를 역어셈블(바이너리파일을 어셈블리코드로 변환하는것)해서 C코드를 얻어내는 작업을 실제로 행하기도 한다. 이러한 작업들을 통틀어서 역공학(reverse engineering)이라고 하는데 실제로 쉐어웨어를 크랙하거나 패치하는 작업들도 어찌보면 간단한 수준의 역공학이라고 부를수 있겠다. 물론 이러한일들이 쉬운일은 아니다. 완벽한 역공학을 위해서는 어셈블리는 물론 바이너리가 생성된 환경(보통 컴파일러를 예로 들수 있겠다.)에 대한 깊은 이해를 필요로 하며, 디버깅 심볼이 제거된 릴리즈 바이너리일 경우, 데이터와 함수주소등을 오로지 단순한 메모리주소로만 사용하게 되며, MFC등의 클래스라이브러리를 이용하는 경우에는 그 구조의 복잡함으로 생성되는 바이너리 또한 더더욱 이해하기 힘든구조가 되어버린다. 엎친데 덮친격으로 어떠한 바이너리들은 역공학을 할수없게하거나 힘들게 만드는 anti-debugging 코드를 삽입하는 경우도 있으니 모든 바이너리를 완벽하게 역공학하기란 정말로 힘든 일이 아닐수 없다.


다시 어셈블리로 돌아와보자. 우리가 이번 강좌에서 배우고자 하는것은 어셈블리로 프로그램을 작성하는 방법이라기보다는 실제로 컴파일러가 어떠한 기계어코드를 만들어내는지와 CPU가 어떻게 프로그램을 해석하고 실행하는지를 이해하기 위한 방법으로 어셈블리를 배우고자 한다. 물론 어셈블리만으로 윈도 어플리케이션을 작성할수는 있다. 그러나 시스템 사양이 높아지고 컴파일러의 성능이 향상됨에 따라 굳이 어셈블리를 통해서 전체 프로젝트를 수행할만한 명분이 점점 사라져가는것 또한 사실이다. 실제로 예전에 많이 사용하던 인라인 어셈블리의 경우에도 자칫하면 수행속도를 떨어뜨리는 결과를 가져오는 경우도 종종 있다.(인라인 어셈블리나 레지스터변수의 사용등이 컴파일러의 최적화기능을 방해하는 경우가 발생할수 있기때문이다.)


실제로 어셈블리로 프로그래밍한다는 것은 어셈블러의 기능을 익히는것이 어셈블리를 익히는것보다 크게 작용한다. 예를 들면 아래와 같다.


; 메시지 루프

@@: ; start of loop

    invoke GetMessage, addr msg, NULL, 0, 0

    or eax, eax

    je @f

    invoke TranslateMessage, addr msg

    invoke DispatchMessage, addr msg

    jmp @b

@@: ; end of loop


필자가 사용하는 어셈블리 템플리트 코드의 일부분으로서, 우리가 API로 윈도프로그램을 작성할때 대부분 공통적으로 적용되는 메시지루프의 로직이다. 대충 훑어보면 C로 작성했을때와 비슷한점을 많이 볼수 있다. 이것은 어셈블러가 고급언어의 특성을 지원하기 위한 의사명령을 제공하기 때문이다. 예를 들면 invoke라는 단어를 보자. 이것은 실제로 기계어로 1:1 대응되는 명령어가 아니다. 확인하고 싶다면 C로 동일한 기능의 프로그램을 작성한다음, 디버거의 디스어셈블리창을 통해서 실제로 기계어(어셈블리)로 변환된 코드를 확인해보자. 분명 invoke라는 명령어는 찾을 수 없을 것이다. 이러한 것들을 통틀어서 의사명령이라고 한다. 실제로 기계어로 변환되지 않지만 어셈블러가 어셈블을 하기위한 환경이나 조건을 제시해주는 것이다.(바로 고급언어의 특성이다.) 그뿐 아니라 @@, @f, @b 등의 라벨들 또한 실제 기계어로 어셈블될때에는 실제 주소로 치환될것이다. 이렇듯 실제로 어셈블리 프로그래밍을 배운다는 것은 단순히 어셈블리(기계어로 치환되는)를 익히는것보다는 어셈블러가 제공하는 기능이나 문법을 배운다는 의미가 크다. 그러나 우리는 어셈블리로 프로그램을 작성하려는 목적이 아니므로 실제로 어셈블리가 어떻게 쓰여지는가를 주로 다루어보도록 한다.


2. CPU를 알면 프로그램이 보인다 !!


또 다시 CPU에 관한 얘기를 해야할때가 왔다. 결국 프로그램을 수행하는 주체는 CPU이므로 CPU를 모르고서는 프로그래밍을 말할 수 없다. 어셈블리나 시스템 프로그래밍을 다루는 많은 책에서 CPU 아키텍쳐에 대한 설명이 많이 있지만, 그 모든것을 여기서 다룰수는 없으므로 자세한것들은 참고서적을 이용하도록 하고, 필자의 생각으로 꼭 짚고 넘어가야할 몇가지만 언급하겠다. CPU를 알기위해서 CPU가 다루는 데이터와 명령을 알아야 할것이다. CPU가 다룰수 있는 데이터와 명령의 종류는 CPU마다 차이가 있고 명령의 종류 또한 한두개가 아니지만, 우리는 Intel x86 계열의 CPU 위주로 설명해나가자. CPU가 처리하는 명령의 종류는 CPU가 업그레이드될때마다 추가되기도 확장되기도 하는데(좋은예로 MMX 코드를 들수있다. MMX 코드는 CPU가 멀티미디어 데이터처리를 빠르게 하기위해서 지원되는 명령어세트이다.) 필요한 명령어들은 조금 있다가 알아보도록 하고, 일단 CPU가 다루는 데이터에 대해 얘기해보자. 지난 강좌에서 CPU는 메모리만을 다룬다고 했었는데, 사실은 메모리와 레지스터라는 기억장소를 데이터로써 다룬다. 레지스터는 CPU 내부의 임시기억장소라고 보면 될것이다. 따라서 CPU가 다루는 가장 빠른 매체라고 보면될것이다. x86 레지스터에는 8개의 범용레지스터와 6개의 세그먼트레지스터, 인스트럭션 포인터 레지스터와 플래그레지스터, 디버그 레지스터등, 여러가지 종류가 있는데 보통 사용자가 다룰수 있는 레지스터의 종류와 그 용도는 다음과 같다.


<레지스터의 종류와 용도>


eax    : 정수 함수의 반환값들을 담는용도로 사용된다.

ebx : 뭐 특별한건 없는것 같다. 그냥 범용으로 사용된다.

ecx : 반복문에서 카운터로 사용된다.

edx : 64비트(large integer) 값들의 상위 32비트를 담는 용도로 사용된다.


esi : 보통 메모리 이동이나 비교시에 원본주소를 담는 용도로 사용된다.

edi : 보통 메모리 이동이나 비교시에 대상(타겟)주소를 담는 용도로 사용된다.


esp : 스택포인터, 스택의 꼭대기(x86은 스택이 아래로 자라므로 실제로는 바닥이 되겠다.)를 가리킨다. 스

택에 데이터가 푸쉬하거나 팝될때마다 증감된다.

ebp : 스택프레임, 프로시저(함수)에 대한 스택프레임을 담는 용도로 사용된다.


cs : 코드세그먼트, 코드영역의 세그먼트를 지닌다. 32비트 어드레싱에서는 별의미가 없다.

ds : 데이터세그먼트, 데이터영역의 세그먼트를 지닌다. 역시 32비트 어드레싱에서는 별의미가 없다.


eip : 인스트럭션 포인터, 실행중인(정확히는 실행되어질) 코드의 주소를 가지고 있다.


이밖에도 몇가지 레지스터가 더 존재하지만 CPU가 내부적으로 사용하거나, 사용자가 건드릴수 없는것들이므로 일단 위에 리스트된 레지스터만이라도 숙지하고 넘어가자. 어셈블리가 처음인 사람에게는 저게 무슨얘긴가 하겠지만, 일단 외워두던가 아님 한쪽에 잘 프린트해서 붙여놓자. 나중에 실제 코드를 보게되면 이해가 될것이므로... 참고로 접두어 e가 붙은 레지스터이름은(eax, esi, eip 등...) extended의 약자로서 32비트 확장레지스터라는 의미이며, 4바이트(32비트)크기를 가지며, 기존의 16비트로 엑세스하려면 e를 뺀 ax 등의 형태로 쓰여지며, 명시적으로 상위/하위 영역을 지정하려면 ah, al의 형태로 사용된다.


3. 구조화의 시작, 서브루틴(함수) !!


서브루틴은 보통 함수(반환값이 있는)와 프로시저(반환값이 없는)로 구분하는데 아시다시피 C언어는 프로시저와 함수의 구별이 없이 무조건 다 함수로 취급한다. 그러므로 C언어에서는 프로시저와 함수는 동일한 의미로 사용한다. 실제로 프로그램의 실행이라는 것이 main함수가 시작됨으로써 시작되며, main함수가 종료됨으로써 프로그램도 종료된다고 알고있다. 이렇듯 프로그램 자체도 하나의 함수로 취급되는것이 일반적이므로 함수가 어떻게 구현되는지에 안다는 것은 프로그램의 흐름을 이해하는데 매우 중요한 의미를 가진다.


함수(또는 프로시저)는 특정한 목적을 위해 구현된 코드덩어리이다. 함수호출은 실제로 프로그램의 지시포인터(인스트럭션 포인터, ip)를  함수가 구현된 코드로 옮겨서 실행한뒤 원래의 호출한쪽의 코드로 되돌아오는 일련의 작업이다. 백문이 불여일견!! 실제로 함수호출이 어떻게 구현되는지 VC++ 디스어셈블러를 통해서 확인해보도록 하자.


간단히 아래와 같은 코드를 작성한뒤 디버거에서 디스어셈블리코드를 확인해보자.


int TestFunc(int a, char b)

{

    return a+1;

}


int main()

{

    int ret;

    ret = TestFunc(3, "a");


    return 0;

}


일단 main()함수내에서 TestFunc()을 호출하는 부분을 살펴보자.


19:       ret = TestFunc(3, "a");

00401098   push        61h

0040109A   push        3

0040109C   call        @ILT+5(TestFunc) (0040100a)

004010A1   add         esp,8

004010A4   mov         dword ptr [ebp-4],eax


어셈블리는 [명령어] [매개변수], ... 의 형식을 갖는데 일단 위에서 나오는 명령어를 살펴보자.

push 명령으로 정수 3과 문자 "a"를 스택에 집어넣는다. 이 작업으로 스택포인터는 8바이트 감소하는데, 스택은 4바이트로 정렬되기때문에 1바이트인 문자를 넣더라도 4바이트가 감소한다. 스택포인터가 감소하는 이유는 스택이 아래로 자라기 때문이다.

call 명령으로 TestFunc 함수로 실행주소를 옮긴다(점프한다). call 명령은 jmp 명령과 더불어 실행흐름(eip)을 변경할수 있는 명령이다.  jmp는 단순히 실행위치를 옮기는데 그치지만, call은 돌아올 주소를 스택에 백업한뒤 실행위치를 옮긴다. 따라서 TestFunc이 호출되는 순간에는 호출되기 전보다 스택포인터는 12만큼 감소되어 있을것이다.(직접 디버거로 확인해보라.)

add 명령으로 esp(스택포인터)를 8바이트만큼 증가시킨다. 스택을 함수를 부르기전으로 맞춰놓는작업을 해주는 것이다. 어라? 그런데 이상하다. 아까 매개변수 8바이트 + 리턴주소 4바이트, 분명 12바이트가 감소되었다고 했었는데? 그 이유는 다음에 나올 함수코드를 보면 이해할수 있다. 함수의 마지막에 리턴주소로 돌아가는 ret 명령을 만나는데 이작업으로 스택에 저장된 리턴주소를 pop해주게 된다. 결국 스택은 정확하게 복원된다.(많은 책에서는 내부적으로 call 명령과 ret 명령이 리턴주소를 저장하기위해 스택을 사용한다고 설명하는데, 실제로 디버거로 따라가보면 이를 명확하게 확인할 수 있다.)


다음코드는 TestFunc 함수의 내부코드이다. 눈으로 쭉 따라가보자.


9:    int TestFunc(int a, char b)

10:   {

00401020   push        ebp            ; (1) 함수 시작코드 !!

00401021   mov         ebp,esp


00401023   sub         esp,40h        ; (2) 지역변수를 위한 공간확보 !!


00401026   push        ebx

00401027   push        esi

00401028   push        edi

00401029   lea         edi,[ebp-40h]

0040102C   mov         ecx,10h

00401031   mov         eax,0CCCCCCCCh

00401036   rep stos    dword ptr [edi]


11:       return a+1;

00401038   mov         eax,dword ptr [ebp+8]    ; (3) 반환값 설정 !!

0040103B   add         eax,1       


12:   }

0040103E   pop         edi

0040103F   pop         esi

00401040   pop         ebx


00401041   mov         esp,ebp        ; (4) 함수 종료코드 !!

00401043   pop         ebp

00401044   ret


일단 현재 TestFunc이 호출된 시점에서의 스택의 상태는 매개변수와 되돌아갈 주소가 담겨져있다. (아까 위에서 매개변수 61h, 3과 call 명령이 리턴주소를 푸쉬했다.) 이것도 한번 확인해보자. 함수의 시작에 중단점을 걸고(함수의 시작에 중단점을 설정하려면 중괄호 열기("{")에다가 중단점을 지정하면 된다.), 레지스터윈도우에서 esp의 값을 복사한뒤 메모리윈도우에서 붙여넣어보자. 그냥 보면 틀림없이 바이트 형태로 보여질테니까 메모리윈도에서 오른쪽 버튼을 눌러서 long hex format(4바이트 형식)을 선택해서 보자. (메모리 형식을 변경하면 갑자기 엉뚱한 곳으로 메모리가 튀는경우가 종종있는데, 이것은 VC++ 디버거의 버그인것 같다. 그럴땐 다시 주소를 지정해주면 된다.)


0012FF24  004010A1  00000003  00000061 

0012FF30  00000000  00000000  7FFDF000

...


필자의 시스템에서는 esp의 값이 0x0012FF24이다. 스택은 아래로 자란다고 했으니 실제 메모리상에는 리턴주소, 3, "a" 순서로 들어있을 것이다. 보아하니 리턴주소는 0x004010a1이고, 정수 3과 "a"(아스키코드 0x61)이 올바르게 들어있는것을 볼수 있을것이다.


자, 이제 매개변수와 리턴주소가 어떤 형식으로 스택에 보관되어 있는지를 알았으니 함수를 시작해보

자. "(1)함수 시작코드"를 보자.


00401020   push        ebp            ; (1) 함수 시작코드 !!

00401021   mov         ebp,esp


일단, ebp의 값을 스택에 백업한다. 그런다음 mov 명령으로 esp의 값을 ebp로 복사(이동)한다. mov 명령은 오른쪽에 있는 값을 왼쪽으로 이동하는 명령이다. 현재 esp는 리턴주소를 가리키고 있으므로 ebp도 리턴주소를 가리키고 있을것이다. 이것이 일반적으로 C언어에서 함수를 기술할때 사용되는 형태의 함수 시작코드이다. 일단 이상태에서 매개변수를 접근하려면 어떻게 해야할까? esp+알파, 또는 ebp+알파의 형태로 매개변수를 접근할수 있을것이다(위에서 확인했던것처럼). 그러나 esp(스택포인터) 함수내부에서 또 다른 함수를 호출한다든가 하는 경우에 얼마든지 변할수 있으므로 함수내부에서는 ebp를 기준으로 매개변수를 접근한다. 자, 잘 이해가 안된다면 일단은 외워서라도 알고 있자. "esp+알파의 형식으로 함수의 매개변수(파라미터)를 접근한다."


중간은 다음에 보도록 하고 "(4)함수의 종료코드"를 먼저 보도록 하자.


00401041   mov         esp,ebp        ; (4) 함수 종료코드 !!

00401043   pop         ebp

00401044   ret


함수의 종료코드는 함수의 시작코드와 반대의 작업을 해준다. esp의 값을 ebp로 복원한뒤, 스택에 백업된 ebp를 복원한다. pop명령은 push와 반대로 스택에 있는 데이터를 꺼내오는 일은 한다. (혹시 헷갈릴까봐 노파심에서 얘기하면 pop ebp는 ebp를 꺼내온다는 의미가 아니고 스택에서 꺼내온 데이터를 ebp에 집어넣는다는 의미이다. 필자는 처음에 이것을 헷갈렸던것 같은데... 다른사람들은 안그렇나?) 마지막으로 ret 명령으로 함수를 호출한 쪽으로 되돌아간다. (이것도 정확히 말하면 함수를 호출한 바로 다음주소로 돌아간다.) 아까 call명령이 리턴주소를 스택에 푸쉬한다음 점프한다고 했는데, ret은 반대로 스택에서 리턴주소를 팝하고 그 주소로 점프한다.


자, 그러면 나머지를 보도록하자. 먼저 "(2)지역변수를 위한 공간확보"를 살펴보자.


00401023   sub         esp,40h        ; (2) 지역변수를 위한 공간확보 !!


sub 명령은 add 명령과 반대의 기능을 수행하는 명령으로 스택포인터(esp)를 40h 만큼 감소시킨다. 이것은 스택에 지역변수를 위한 공간을 할당하는 것으로 위 명령의 실행되는 시점에서의 esp와 esp-40h의 40h만큼의 공간을 지역변수를 위해 사용할것이라는 의미이다. 지난강좌에서 LoadLibrary를 호출할때 사용할 매개변수인 문자열을 위해서 지역변수 공간을 사용했었다. (잘 생각이 안난다면 지난강좌에서 FAKE_LOADLIBRARY_CODE 구조체를 설정하는 InjectSpyDll() 함수를 다시 보도록 하자.) 어쨋든 여기서 또하나 중요한 사실은 지역변수는 ebp-알파의 형태로 접근된다는 사실이다.(역시 잘 이해가 안된다면 외워서라고 알고있자.)


여기서 재미있는 사실하나를 발견하게 된다. 사실 위의 코드는 디버그 버전으로 빌드된 바이너리의 실행코드이다. 실제로 TestFunc()은 지역변수를 사용하지 않는데도 불구하고 지역변수를 위한 공간을 확보하고 그 공간을 온통 0xcc의 값으로 채우는것을 볼수있다. 사실 릴리즈 모드로 빌드하게 되면 TestFunc() 아래처럼 간단한 어셈블리코드로 변환된다.


00401000   mov         eax,dword ptr [esp+4]

00401004   inc         eax

00401005   ret


어쨋든 굳이 지역변수 공간을 잡고 그 안을 0xcc로 채우는이유를 생각해보면 먼저 0xcc의 값에 대해 알아볼 필요가 있다. 0xcc는 어셈블리 코드로 int 3 이다. (이전 강좌에서 본적이 있다. 바로 중단점이다 !!) 초기화되지 않은 지역변수의 값이 0xcccccccc인 것 또한 우연이 아니다. 컴파일러가 디버그 모드일때에 수행할 에러처리를 위해 위와 같은 검사루틴을 삽인한것으로 유추할 수 있다. (일단 저런 메모리 공간으로 뛰어들게 되면 중단점으로 인해서 디버거가 활성화될것이며 이벤트처리기는 적절한 처리를 해줄수 있을것이다.)


마지막으로 "(3)반환값 설정"을 보자.


00401038   mov         eax,dword ptr [ebp+8]    ; (3) 반환값 설정 !!

0040103B   add         eax,1       


앞에서 ebp+알파의 형식으로 함수의 매개변수를 접근한다고 했다. 대괄호("[]") 표시는 C언어의 *(간접지정연산자)와 비슷한 용도로 사용된다. 대괄호안의 값을 포인터로 인식하고 포인터가 가리키는 값을 가져온다. 어쨌든 첫번째 매개변수 정수 a를 eax 레지스터에 싣고, add 명령으로 1 증가시킨다. eax 포인터는 일반적으로 리턴값을 담는용도로 사용된다. 따라서 a+1의 값이 eax에 담기면서 리턴값으로 처리된다.


4. 끝으로


오늘은 어셈블리에 대한 기본적인 설명과 더불어 C언어에서 함수호출이 어떻게 구현되는지를 어셈블리를 통해 상세히 알아보았다. 오늘 강좌에서 가장 중요한 것은 뭐니뭐니 해도 C언어의 함수호출 메카니즘일 것이다. 매개변수와 지역변수가 어떻게 쓰여지며, 함수의 반환값은 또 어떻게 구현되는지. 이것들만큼은 반드시 이해하고 넘어가길 바란다. 필자의 경험으로는 함수호출 메카니즘을 이해하는 가장 좋은 방법은 VC++ 디버거를 이용하는것이다. 디버거의 레지스터윈도, 메모리윈도, 디스어셈블리윈도를 띄어놓고서 한줄한줄(Step-by-Step) 진행하면서 레지스터와 메모리의 값들을 비교하면서 따라가다보면 이해하기가 한결 수월할것이다. 물론 옆에 연습장 가져다 놓구 그림도 그려가면서 말이다.


5. 다음강좌에서는


다음강좌는 Win32 어셈블리 두번째 시간이 될것이다. 다음시간에는 오늘 배운 함수호출 메카니즘을 토대로 각각의 호출규약에 따른 함수들의 형태를 다루며, 조건문과 반복문이 어떻게 어셈블리로 쓰여지는지와 시간이 된다면 Win32 구조화 예외처리(SEH)가 실제로 어떻게 구현되는지를 다뤄보도록 하자. 느끼고 있는지는 모르지만 다음 강좌까지만 마스터한다면 "시스템프로그래밍의 사생아"라 불리는 바이러스 제작에 대해서도 심도있게 다루어볼수 있을것이다. (물론 강좌를 진행할 생각은 아직 없지만 말이다.) 사실 바이러스와 전역 API 후킹은 여러모로 비슷한 점이 많이 있다. 크게 다른것은 사용자의 인증을 받은것인지 그렇지 않은지의 차이가 있을뿐이다. 이쯤되면 귀가 번쩍 트이는 독자분도 여럿될텐데 추천할만한 서적을 하나 소개하고 오늘 강좌를 마치고자 한다.


시스템프로그래밍에 대한 국내서적은 거의 전무하다시피한데 재미있는 책한권을 소개한다. 노파심에서 말씀드리면 이책의 저자나 출판사와 필자는 아무런 상관관계가 없음을 밝힌다. (주)정보게이트 라는 출판사에서 나온 "파괴의 광학"이라는 책이다. 이책의 저자인 김성우님은 월간 마이크로소프트웨어에서 윈도 시스템 해킹을 주제로 강좌를 진행한적이 있는데, 그 강좌의 내용을 모아 한권의 책으로 출간하게 되었다. 주제가 시스템 해킹이고 심지어는 바이러스제작까지 다루고 있지만, 기존의 해킹관련 서적들이 단순한 프로그램 사용법을 소개하는데 그치는 수준인데 비해 이 책의 내용은 프로그래머를 위한 내용을 상당히 알차게 구성하고 있다. 난이도는 상당히 높은 편이지만 본강좌의 내용을 이해할 정도라면 어렵지않게 따라갈수 있을것이다.


****************************************************************************************

 Win32 Global API Hook - 3 Win32 어셈블리 프로그래밍 (2)


1. 프로시저 호출규약 (Procedure Calling Convention)


지난시간에 프로시저(함수)가 어떤 형태로 매개변수와 반환값을 다루고, 지역변수를 관리하는가에 대한 일반적인 메카니즘을 배웠다. 그러면 조금 더 구체적인 내용으로 들어가서 호출규약에 관한 얘기를 해보자. 호출규약(calling convention)이란 매개변수를 함수에 전달하고 함수가 사용한 스택을 어떻게 정리하는지에 대한 구체적인 규칙에 따른 분류이다. 호출규약에는 다음과 같은 것들이 있다.


(1) __cdecl        : C 호출규약

(2) __stdcall     : 표준 호출규약

(3) __fastcall    : 빠른 호출규약

(4) __declspec(naked)    : 벗은(?) 호출규약

(5) this        : this 호출규약


참고로, 이것외에 __pascal 호출규약이 있는데 Win32로 넘어오면서 더이상 지원하지 않는다. 자, 그러면 하나씩 알아보자. 일단 코드를 보면서 눈으로 직접 확인해보자. (진실은 언제나 디버거가 말해주듯이...)


(1) __cdecl    (C 호출규약)


70:       ret = cdecl_Call(3, 4);

0040D588   push        4

0040D58A   push        3

0040D58C   call        @ILT+0(cdecl_Call) (00401005)

0040D591   add         esp,8

0040D594   mov         dword ptr [ebp-4],eax


우리는 이미 지난시간에 많은것(?)을 배웠기에 이정도 어셈블리 코드는 그야말로 껌인것이다. 함수이름은 따로 설명할 필요도 없이 __cdecl로 선언된 C 호출규약 함수이다. 먼저 매개변수가 스택에 푸쉬되는 순서를 보자. 오른쪽 매개변수부터 거꾸로 푸쉬되는 것을 볼 수 있다. 그리고 함수가 호출되고 난 다음, 호출한쪽에서 명시적으로 스택을 정리하는것 또한 확실히 알 수 있겠다.


그럼, 실제 함수의 구현을 보자.


19:   // C 호출규약

20:   int __cdecl cdecl_Call(int a, int b)

21:   {

00401040   push        ebp

00401041   mov         ebp,esp

00401043   sub         esp,40h

00401046   push        ebx

00401047   push        esi

00401048   push        edi

00401049   lea         edi,[ebp-40h]

0040104C   mov         ecx,10h

00401051   mov         eax,0CCCCCCCCh

00401056   rep stos    dword ptr [edi]

22:       return a+b;

00401058   mov         eax,dword ptr [ebp+8]

0040105B   add         eax,dword ptr [ebp+0Ch]

23:   }

0040105E   pop         edi

0040105F   pop         esi

00401060   pop         ebx

00401061   mov         esp,ebp

00401063   pop         ebp

00401064   ret


함수가 열리면서("{") 함수시작코드와 함께 에러처리를 위한 0xCC(int 3)이 지역변수공간에 쫙 깔리는걸 보니 디버그모드인걸 알수 있다. 어쨋든 중요한것은 맨 마지막 ret 문이다. 그냥 반환되는걸 알 수 있다. 왜냐면 아까 함수를 호출한쪽에서 스택을 정리해주기 때문에 호출되는 함수에서는 그냥 리턴만 하면된다.


(2) __stdcall (표준 호출규약)


72:       ret = stdcall_Call(3, 4);

0040D597   push        4

0040D599   push        3

0040D59B   call        @ILT+5(stdcall_Call) (0040100a)

0040D5A0   mov         dword ptr [ebp-4],eax


보아하니 __stdcall 호출규약 또한 매개변수를 오른쪽에서 왼쪽으로 스택에 푸쉬한다. 그런데? 함수호출이 수행되고 난 다음 아까보았던 스택 정리코드(add esp, 8)가 빠져있는걸 볼 수 있다. 이걸로 보아 호출되는 함수내부에서 스택을 정리한다고 미루어 짐작할 수 있다.


25:   // 표준 호출규약

26:   int __stdcall stdcall_Call(int a, int b)

27:   {

00401070   push        ebp

00401071   mov         ebp,esp

00401073   sub         esp,40h

00401076   push        ebx

00401077   push        esi

00401078   push        edi

00401079   lea         edi,[ebp-40h]

0040107C   mov         ecx,10h

00401081   mov         eax,0CCCCCCCCh

00401086   rep stos    dword ptr [edi]

28:       return a+b;

00401088   mov         eax,dword ptr [ebp+8]

0040108B   add         eax,dword ptr [ebp+0Ch]

29:   }

0040108E   pop         edi

0040108F   pop         esi

00401090   pop         ebx

00401091   mov         esp,ebp

00401093   pop         ebp

00401094   ret         8


실제로 함수구현을 보니까 정말로 마지막 반환코드(ret 8) 스택포인터을 8바이트 올리고 리턴하는걸 볼 수 있다. "ret n" 은 n바이트 만큼 esp를 증가한 후에 리턴하는 어셈블리 명령이다. 그럼, 여기서 문제 ret 8 이 수행되고 나면 esp(스택포인터)는 얼마만큼 증가할까? 8바이트라고? 그렇게 간단하면 왜 물어보겠는가. 정답은 12바이트(0xC)이다. 왜냐하면 ret 명령이 스택에서 리턴주소를 꺼내온뒤 그곳으로 점프(jmp)하기 때문이다. (지난시간에 call 명령과 함께 비교하며 설명했다. 기억해두자, call과 ret명령은 내부적으로 스택에 리턴주소를 위한 push, pop을 수행한다.)


(3) __fastcall (빠른 호출규약)


__fastcall 호출규약은 이름 그대로 빠른 호출을 위해서 사용되어진다. 메모리보다 상대적으로 빠른 레지스터를 이용한다고 해서 레지스터 호출규약(Register calling convention)이라고도 한다. 그러나 Win32 사용자모드에서는 원칙적으로 레지스터 호출규약을 사용하지 않는다. 다음강좌인 VxD 강좌때 보게 되겠지만, VxD 시스템 함수들은 레지스터 호출규약을 사용하는 것들이 상당수 존재한다. 그러나 Windows NT/2000 커널모드 드라이버에서는 더이상 레지스터 호출규약을 사용하지 않는데 그 이유는 레지스터는 CPU에 종속적인 저장매체이기때문에 CPU간 호환성을 보장할 수 없기때문이다.


74:       ret = fastcall_Call(3, 4, 5, 6);

0040D5A3   push        6

0040D5A5   push        5

0040D5A7   mov         edx,4

0040D5AC   mov         ecx,3

0040D5B1   call        @ILT+35(fastcall_Call) (00401028)

0040D5B6   mov         dword ptr [ebp-4],eax


호출하는 쪽을 보게 되면, 첫번째와 두번째 매개변수를 ecx, edx 레지스터에 담아서 호출하는것을 볼 수 있다. 그러나 레지스터의 수는 한정되어 있으므로 모든 매개변수를 레지스터에 담을수는 없다. 따라서 처음 두개의 매개변수만 ecx, edx에 담고 나머지는 스택을 통하는데 이때에도 마찬가지로 오른쪽에서 왼쪽으로 스택에 푸쉬하고, __stdcall과 마찬가지로 호출되는 함수쪽에서 스택을 정리한다.


31:   // 빠른 호출규약

32:   int __fastcall fastcall_Call(int a, int b, int c, int d)

33:   {

004010A0   push        ebp

004010A1   mov         ebp,esp

004010A3   sub         esp,48h

004010A6   push        ebx

004010A7   push        esi

004010A8   push        edi

004010A9   push        ecx

004010AA   lea         edi,[ebp-48h]

004010AD   mov         ecx,12h

004010B2   mov         eax,0CCCCCCCCh

004010B7   rep stos    dword ptr [edi]

004010B9   pop         ecx

004010BA   mov         dword ptr [ebp-8],edx

004010BD   mov         dword ptr [ebp-4],ecx

34:       return a+b+c+d;

004010C0   mov         eax,dword ptr [ebp-4]

004010C3   add         eax,dword ptr [ebp-8]

004010C6   add         eax,dword ptr [ebp+8]

004010C9   add         eax,dword ptr [ebp+0Ch]

35:   }

004010CC   pop         edi

004010CD   pop         esi

004010CE   pop         ebx

004010CF   mov         esp,ebp

004010D1   pop         ebp

004010D2   ret         8


함수의 구현부이다. 그런데 뭔가 좀 이상한것이 있다. 빠른 호출을 위해서 레지스터를 통해서 매개변수를 넘겨준다고 했는데, 매개변수로 넘어온 레지스터를 다시 지역변수 공간(ebp-알파)에 담아서 사용하는것을 볼 수 있다. 이건 예제가 디버그모드로 빌드되었기 때문인데 릴리즈로 빌드된 어셈블리 코드는 다음과 같다.


00401020   lea         eax,[ecx+edx]

00401023   mov         edx,dword ptr [esp+4]

00401027   mov         ecx,dword ptr [esp+8]

0040102B   add         eax,edx

0040102D   add         eax,ecx

0040102F   ret         8


lea 명령이 처음나온것 같은데, lea 명령은 아주 자주 쓰이는 명령이니까 잘 알아두자.

lea (Load Effective Address): 오른쪽 피연산자의 주소(메모리)를 왼쪽 피연산자(레지스터)로 전송한다. 보통 C언어 포인터변수를 설정하는 문장에서 사용된다.


처음 두개의 매개변수는 eax와 ecx 레지스터로 취급하고 나머지 레지스터는 스택을 이용하는것을 볼 수 있다.


(3) __declspec(naked) (벗은(?) 호출규약)


해석이 조금 이상한데 의미상으로는 스택프레임을 설정하는 기존의 함수구조를 위한 함수시작코드와 함수종료코드를 제공하지 않는 다는 의미이다. 그러므로 이것을 __cdecl 형태로 구현해서 사용하던, __stdcall 형태로 사용하던 그것은 순전히 사용자 맘이다. naked 호출규약은 엄밀한 의미로는 호출규약이라고 할수 없는데, 왜냐면 이것은 함수의 구현부에서 함수가 구현되는 방법의 문제이지 앞에서의 호출규약처럼 함수의 선언의 문제가 아니기 때문이다. 어쨋든 코드를 보자.


76:       ret = naked_Call(3, 4);

00401159   push        4

0040115B   push        3

0040115D   call        @ILT+25(naked_Call) (0040101e)

00401162   add         esp,8

00401165   mov         dword ptr [ebp-4],eax


매개변수는 위의것과 동일하게 오른쪽에서 왼쪽으로 푸쉬되며, __cdecl 과 마찬가지로 호출한 쪽에서 스택을 복원해줌을 알 수 있다. 이것은 naked 호출규약이라서 그런게 아니고 디폴트 호출이 일어난것이다. 아까 말했듯이 naked 호출규약은 함수구현의 문제이다.


// 벗은(?) 호출규약

__declspec(naked) int naked_Call(int a, int b)

{

    //return a+b;

    __asm

    {

        push    ebp            // 함수 시작코드

        mov        ebp, esp


        mov        eax, a

        add        eax, b


        mov        esp, ebp    // 함수 종료코드

        pop        ebp

        ret

    }

}


디스어셈블된 코드가 아니고 그냥 C 코드를 보였다. 왜냐면 인라인어셈블리를 사용했기 때문이다. 보다시피 함수시작코드와 함수종료코드를 사용자 스스로 구현해주어야 한다는 것을 알 수 있다.


(5) this : this 호출규약


마땅히 붙일 이름이 애매한데 C++에서 this 포인터가 어떻게 처리되는지를 정의해놓은 규약이라고 보면된다. 역시 코드를 보면,


78:       This_Call thiscall;

79:       ret = thiscall.Call(3, 4);

00401168   push        4

0040116A   push        3

0040116C   lea         ecx,[ebp-8]

0040116F   call        @ILT+10(This_Call::Call) (0040100f)

00401174   mov         dword ptr [ebp-4],eax


다른것들은 별로 눈여겨볼것이 없는데, 함수를 call 하기 직전에 무언가(지역변수로 추정되는)를 ecx에 lea 명령으로 로드한뒤 call해주는 것을 볼 수 있다. 여기서 ecx 레지스터가 바로 this 포인터이다. 클래스의 멤버함수를 부를때 C++ 컴파일러는 ecx 레지스터를 클래스 인스턴스 자신을 가리키는 포인터를 담는 용도로 사용한다는 것이다. 따라서 멤버함수는 자기가 언제 어디에서 호출되었다 하더라도 나를 부른 녀석이 어떤 인스턴스인지를 정확히 알 수 있다. (물론 이 ecx 레지스터를 이용해서 재미있는 장난을 해볼수도 있을텐데, 시간이 남는 사람은 한번 이것 저것 해보기 바란다.)



56:   // this 호출규약

57:   class This_Call

58:   {

59:   public:

60:       int Call(int a, int b)

61:       {

004011C0   push        ebp

004011C1   mov         ebp,esp

004011C3   sub         esp,44h

004011C6   push        ebx

004011C7   push        esi

004011C8   push        edi

004011C9   push        ecx

004011CA   lea         edi,[ebp-44h]

004011CD   mov         ecx,11h

004011D2   mov         eax,0CCCCCCCCh

004011D7   rep stos    dword ptr [edi]

004011D9   pop         ecx

004011DA   mov         dword ptr [ebp-4],ecx

62:           return a+b;

004011DD   mov         eax,dword ptr [ebp+8]

004011E0   add         eax,dword ptr [ebp+0Ch]

63:       }

004011E3   pop         edi

004011E4   pop         esi

004011E5   pop         ebx

004011E6   mov         esp,ebp

004011E8   pop         ebp

004011E9   ret         8


구현부는 별로 볼게 없다. 단지 this 포인터라고 알려진 ecx 레지스터를 지역변수 공간으로 로드하는것을 볼수 있는데 이것 또한 디버그 모드이기때문에 생기는 코드일것이라고 생각된다. (궁금하면 직접 확인해보시길...)


자, 모두 5개의 호출규약에 관해서 각각의 호출규약이 실제로 어떤 형태로 구현되는지를 완벽하게 이해했을거라고 믿는다. 몇가지 덧붙이자면 실제로 대다수의 Win32 API는 __stdcall 로 지정되어 있다. 우리가 항상 보는 WinMain() 함수 또한 __stdcall 이다.

마이크로소프트는 추후 확장성을 고려해서 재정의된 데이터타입(WORD, DWORD 등)을 사용하는것을 권장하는데 같은 맥락으로 함수 호출규약또한 재정의된 형태로 사용하는것을 권장한다. 그럼 Win32 SDK 에서 재정의된 호출규약이 어떤것인지 잠깐 알아보자.


CDECL        __cdecl

WINAPI        __stdcall

APIENTRY    __stdcall

CALLBACK    __stdcall

PASCAL        __stdcall

WSAAPI        __stdcall

FASTCALL    __fastcall


CDECL을 제외한 우리가 많이 보던 CALLBACK, WINAPI니 하는 것들은 모두 __stdcall 로 처리되는것을 알 수 있다. (사족을 달자면, 꼭 저렇지만은 않다. 어떤 환경이냐에 따라서 약간씩 다르게 전처리 되기는 하지만, 일반적인 Win32환경에서 특별히 신경쓰지 않는한 저 규칙을 벗어나지 않을 것이다.) 아참, 잊을 뻔 했는데 VC++에서는 특별히 컴파일옵션을 만지지 않는한 디폴트로 __cdecl 호출규약을 사용한다.


자, 그러면 호출규약에 대해서는 직접 확인해보았으니 더이상 의문이 없을것 같지만, 왜 이렇게 여러가지 호출규약들이 존재해야 하는지는 궁금해할 사람들이 있을지 모르겠다. 일단 크게 __cdecl 과 __stdcall 호출규약의 커다란 차이점은 스택을 어디에서 정리해줄 것이냐의 차이점이다. 전자는 호출하는 쪽에서 후자는 호출되는 함수내부에서 스택을 정리해준다. 상식적으로 생각해서 호출받는 쪽에서 스택을 정리하는 __stdcall 쪽이 더 합리적이라고 생각될지 모르지만, __cdecl 이 필요한 이유는 따로 있다.


xprintf 계열의 가변인자 함수에서 그 이유를 찾을 수 있다. __stdcall 함수의 제작자들은 함수가 매개변수의 개수(정확히 말하면 매개변수를 받는 스택의 크기이다.)를 알고 있다는 가정하에서 함수를 구현하기를 원했다. 함수의 재사용성과 모듈화를 극대화하기 위한 조치였다고 볼 수 있는데, 이런 형태의 구조에서는 가변인자를 구현할 수 없다는 단점이 있었다. 일단 가변인자를 구현하려면 호출받는쪽에서 몇개의 매개변수가 들어올지 모르기 때문에 호출받는쪽에서 스택을 정리해줄 수가 없었고, 오로지 호출하는쪽에서만이 매개변수의 수를 알고 있을뿐이었다. 그러므로 당연히 가변인자를 지원하는 함수들은 스택을 호출하는쪽에서 정리해주어야만 했고 따라서 __cdecl 호출규약이 필요하게 된것이다. 사실, 이러한 호출규약에 따른 분류는 다분히 C언어의 관점에서 보았을때 그렇다는 말이다. 실제 CPU의 관점에서는 우리가 스택을 이용하던 레지스터를 이용하던 함수를 구현하던 그냥 쭉 흐르는듯이 프로그램이 진행되던 별 상관없다. 그냥 시키는대로만 할 뿐이다. 단지 운영체제의 많은 부분이 C언어로 구현되어 있으며, 우리가 후킹하려는 API또한 대부분 C언어로 구현되어 있으므로 C언어가 함수를 어떻게 구성하는지가 중요한것이다.


아, 갑자기 생각났는데 실제 함수의 이름은 내부적으로는(정확하게 말하면 Link시에) 다른이름으로 변환되어져서 사용되는데 __cdecl 함수는 앞부분에 밑줄이 붙고 __stdcall 함수들은 앞부분에 밑줄과 더불어서 뒤에 @와 매개변수의 크기(스택의 크기)가 붙는다. 이것만 보더라도 __stdcall 함수들은 함수스스로가 매개변수의 크기를 지정해두었다는 것을 알 수가 있다. (확인을 위해서는 extern "C" 문장으로 C 타입으로 함수를 선언해주어야만 한다. C++은 다중정의(오버로드)를 구현하기 위해 함수 이름을 더 복잡하게 만들어버리기 때문에 사람이 확인하기가 쉽지 않다.)


2. 흐름제어


흐름제어의 가장 중요한 요소인 조건문과 반복문에 대해서 알아보자. 이쯤되면 척하면 척일텐데 벌써 조건문과 반복문 예제를 만들어서 디스어셈블하고 있는 저 친구를 보라 !! (참, 영특하지 않은가? ^^)


(1) 조건문


67:       if(a > 3)

0040115F   cmp         dword ptr [ebp-4],3

00401163   jle         main+34h (00401174)

68:       {

69:           printf("3 보다 크다. !!\n");

00401165   push        offset string "3 \xba\xb8\xb4\xd9 \xc5\xa9\xb4\xd9. !!\n" (00422f84)

0040116A   call        printf (0040d780)

0040116F   add         esp,4

70:       }

71:       else

00401172   jmp         main+41h (00401181)

72:       {

73:           printf("3 보다 작거나 같다. !!\n");

00401174   push        offset string "3 \xba\xb8\xb4\xd9 \xc0\xdb\xb0\xc5\xb3\xaa \xb0\xb0\xb4

\xd9. !!\n" (0042

00401179   call        printf (0040d780)

0040117E   add         esp,4

74:       }


이제는 이정도만 봐도 알 수 있을것이다.


조건문에서 사용되는 어셈블리 명령어는 매우 여러가지이지만 그 방식은 거의 동일하다. 일단 cmp 명령을 만났다. 딱 보니까 지역변수 a와 정수 3을 비교하는것 같다. 그 아래 jle 명령을 보자. 이름에서 느껴지는 뉘앙스가 웬지, 작거나 같으면 점프(Jump if less or equal)을 연상시키지 않는가? 우리가 생각한 그대로다. 정확하게 두번째 블럭안으로 점프하게 된다. 그럼 그렇지 않다면 그 다음 문장, 바로 3보다 큰경우를 수행하는 첫번째 블럭을 수행하고 두번째 블럭을 건너뛰고 진행하게 된다.


자, 전체적인 흐름은 이해가 될것인데 좀더 구체적으로 살펴보자. 단순히 cmp 하나의 명령으로 어떤놈이 작고, 큰지, 또는 같은지를 알 수 있을까? 이것은 플래그 레지스터라는 놈을 이해하면 된다. 플래그 레지스터가 각각의 상태를 표현하기 위해 특정한 값들을 갖게된다. 예를 들면 cmp 명령으로 비교한 두개의 값이 같다면 ZR 플래그(또는 ZF)가 1로 세트된다. 이렇게 cmp나 test등의 비교명령은 플래그 레지스터를 이용해서 그 결과를 나타내는데 우리는 이러한 플래그들을 가지고 원하는 실행위치를 점프하게되는데, 이럴때 사용하는 것이 바로 jxx 계열의 조건점프명령이다. 우리는 지금까지 jmp 명령만을 보았는데 이것은 무조건 점프하는 명령이라면 아래에 리스트된 조건점프 명령들은 플래그들의 조합으로 비교의 결과를 인식하고 조건에 맞는 경우에만 점프하게 된다.


je (jump if equal)    : 같다면 점프한다. ( == )

jne (jump if not equal)    : 같지 않다면 점프한다. ( != )

jl (jump if less)    : 작다면 점프한다. ( < )

jg (jump if greater): 크다면 점프한다. ( > )

jge (jump if greater or equal)    : 크거나 같다면 점프한다. ( >= )

jle (jump if less or equal)        : 작거나 같다면 점프한다. ( <= )


뭐, 별로 어려울건 없을것이다. 실제로 매크로어셈블러 문서를 살펴보면 알게되겠지만 조건점프명령은 저것 말고도 여러가지가 더 있다. 명령어에서 e가 들어간 명령은 z로 바뀐 다른명령어로도 사용되는데 둘의 의미는 동일하다. 예를 들면 je와 jz는 동일하게 동작한다. 의미는 jz은 zero flag가 설정된 경우에 점프한다라는 의미이지만 비교결과가 같다면 zero flag가 설정되기 때문에 결과적으로 같은 동작을 수행한다. (실제 바이트코드를 보면 정말 그런지 알수 있을것이다. 이런것들은 초보자들에게 약간의 혼란을 가져오는데 실제로 Intel CPU 매뉴얼과 매크로어셈블러 매뉴얼이 표기하는 명령어 형식이나 플래그 레지스터의 이름등에서 약간의 차이가 있다. 사실 같은 의미인데 이름을 조금씩 다르게 지정해놓은것이 몇가지 존재한다.)


그런데, 어셈블리 명령과는 별개로 위의 디스어셈블 코드를 살펴보면 재미있는것을 발견할 수 있을텐데, 그것은 바로 우리가 제시했던 조건과는 반대되는 조건으로 검사하고 분기한다는 점이다. 우리는 3보다 크다면을 검사했는데 실제 디스어셈블된 코드를 보면 3보다 작거나 같다면으로 검사한다는 것이다. 이것은 컴파일러가 어셈블리코드를 생성할때 가급적 jmp명령을 사용하지 않기위해서 최적화를 수행하는 것이다. 실제로 반대로 구현해보면 jmp 명령을 두번 사용해야 한다는것을 알 수 있을것이다. 이것은 실제로 어셈블리로 프로그래밍할때에는 기본적인 사항인데 우리가 원하는 조건의 반대조건으로 검사하는 것이 프로그램을 더 작고 빠르게 만든다. jmp명령은 실행위치를 변경하는 상대적으로 비싼대가를 치뤄야 하는 명령이기 때문이다.


(2) 반복문


76:       for(int i=0; i<5; i++)

0040D7F1 C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0

0040D7F8 EB 09                jmp         main+53h (0040d803)

0040D7FA 8B 45 F8             mov         eax,dword ptr [ebp-8]

0040D7FD 83 C0 01             add         eax,1

0040D800 89 45 F8             mov         dword ptr [ebp-8],eax

0040D803 83 7D F8 05          cmp         dword ptr [ebp-8],5

0040D807 7D 0B                jge         main+64h (0040d814)

77:       {

78:           a++;

0040D809 8B 4D FC             mov         ecx,dword ptr [ebp-4]

0040D80C 83 C1 01             add         ecx,1

0040D80F 89 4D FC             mov         dword ptr [ebp-4],ecx

79:       }

0040D812 EB E6                jmp         main+4Ah (0040d7fa)


C언어에서 가장 많이 사용되는 for()문이다. 일단 지역변수 i의 값을 0으로 세트하고 바로 점프하는 명령이 있다. 어디로? 바로 조건식으로 점프한다. 조건식은 정수 5와 i를 비교한후, 크거나 같다면(역시 반대의 조건으로 검사하는것을 볼 수 있다.) 반복문을 탈출한다. 반복문 내에서는 지역변수 a를 1 증가하고, 지역변수 i를 1 증가하는 증감식으로 점프한다. 그런다음 다시 조건문으을 실행한다. 뭐 별로 어려울것 없다. 그럼 다음은 while()문을 보자.


76:       int i = 5;

0040D7F1 C7 45 F8 05 00 00 00 mov         dword ptr [ebp-8],5

77:       while(i--)

0040D7F8 8B 45 F8             mov         eax,dword ptr [ebp-8]

0040D7FB 8B 4D F8             mov         ecx,dword ptr [ebp-8]

0040D7FE 83 E9 01             sub         ecx,1

0040D801 89 4D F8             mov         dword ptr [ebp-8],ecx

0040D804 85 C0                test        eax,eax

0040D806 74 0B                je          main+63h (0040d813)

78:       {

79:           a++;

0040D808 8B 55 FC             mov         edx,dword ptr [ebp-4]

0040D80B 83 C2 01             add         edx,1

0040D80E 89 55 FC             mov         dword ptr [ebp-4],edx

80:       }

0040D811 EB E5                jmp         main+48h (0040d7f8)


위의 for()문과 똑같은 일을 수행하는 while()문이다. 역시 별로 어려울것 없으니 스스로 따라가보자.


여기서 또하나 재미난것은, 실제 어셈블리 명령어중에 loop라는 명령어가 존재한다. loop 명령은 ecx 레지스터에 지정된 수만큼 반복작업을 수행하는데 실제로 마이크로소프트 C컴파일러는 loop 명령을 사용하지 않는 경우가 대부분이다. 위의 예처럼 조건분기 명령을 사용해서 반복문을 구현한다. 혹시 디버그모드라서? 그렇다면 릴리즈로 빌드한뒤 디스어셈블된 코드를 살펴보자. 그러나 릴리즈모드에서도 별로 다르지 않다. (음... 쓰고나서 보니까 위의 예제를 릴리즈로 빌드하면 아마도 반복문이 사라져버릴것이다. 왜냐면 반복문에서 하는 유일한 작업인 a++; 문장은 프로그램 어디에도 영향을 주지않는 있으나마나한 코드이기 때문에 컴파일러가 릴리즈 최적화를 수행하면서 무시될것이다. 어쨋든 유효한 반복구문을 사용해서 확인해보자.)


또 참고로 얘기하면 일반적으로 while()문이 for()문보다 빠르다고 알려져있는데 위의 코드를 보면 왜 그런지 확실히 알 수 있을것이다. (코드길이가 작아서? 음... 것도 틀린건 아니지만 보다 중요한것은 for()문보다 while()문이 jmp 명령을 더 적게 사용하는것을 볼수 있다. 아까도 말했듯이 jmp 명령은 상대적으로 비싼 명령이다.)


3. 끝으로


이상으로 Win32 어셈블리 프로그래밍에 대한 강좌를 마칠까 한다. 사실 2회의 강좌로 어셈블리를 모두 이해할수 있을거라고는 생각하지 않지만, 적어도 C언어로 작성한 프로그램이 어떠한 형태로 구현되는지에 대한 감은 잡았을거라고 생각한다. 원래 지난시간에 약속한대로 Win32의 강력한 에러처리 메카니즘인 구조화 예외처리(SEH)에 대해서도 다루려고 했지만 시간상 미처 다루지 못했다. 실제로 API 후킹을 구현하게 되면 반드시 예외처리를 해주어야 하는데 그 이유는 시스템 DLL을 건드리는 작업은 매우 조심스러운 작업이기때문에 문제가 생기면 시스템을 재부팅해야 하는 경우가 비일비재하기 때문이다. 따라서 문제가 발생하면 즉시 에러를 복구하고 정상적으로 종료함으로써 다른 프로그램들에게 영향을 주지 않아야 한다. 구조화 예외처리에 관해서는 다음번 Hooking Code를 작성하는 부분에서 다시 얘기할 기회가 있으니, 그때 다루기로 하자.


필자가 소개한 어셈블리에 대한 내용은 극히 일부분이거나 기초가 되는 수준이라는것을 잊지말자. Intel x86 계열 CPU는 오늘 소개한 명령이외에도 문자열처리나 멀티미디어 데이터처리 등에 관한 무수히 많은 명령어들을 제공하며, 어셈블러 또한 무수히 많은 의사명령과 예약어등을 제공한다. 이 모든것을 모두 알아야 할 필요는 없다고 해도, 분명 자바나 베이식만을 다루는 사람과 어셈블리까지 아는 사람과는 프로그래밍을 바라보는 시각에서부터 엄청난 차이가 난다고 믿어 의심치 않는다.


4. 다음강좌에서는


이미 예고한대로 다음강좌에서는 Windows9x에서 지원하는 디바이스드라이버인 VxD에 대해서 얘기하도록 하겠다. 내용의 방대함으로 모든것을 다룰수 없음에 우리가 필요한 부분만을 구현하는 쪽으로 강좌의 촛점을 맞춰나가겠다. 더불어서 보호모드와 운영체제 실행권한인 Ring0에 관한 설명과 디바이스드라이버를 통하지 않고 Ring0를 얻어내는 Windows의 뒷문(?)에 대해서도 얘기해보자. 디바이스드라이버에 대한 경험이 전혀 없는 독자라면 다음 강좌부터는 실습을 따라가는것조차 버거울텐데 그 이유는 아직도 디바이스드라이버 개발환경이라는것이 원시적이기 짝이 없기때문이다. 일단 VC++같은 멋진 통합환경과 디버거를 기대한다면 일찌감치 다른일을 찾아보는것이 빠를것이다. 내용이 내용이니만큼 준비해야할 것들과 다음강좌의 진행내용을 전체적으로 알아보자.


(1) Window 98 DDK(DeviceDriver Development Kit)

: 마이크로소프트 DDK 홈페이지(http://www.microsoft.com/ddk/)에 가면 구할수 있다. 각 운영체제별로 DDK를 따로 제공하는데, 우리는 일단 98에서 작업할것이므로 98용 DDK를 다운로드받길 바란다.


(2) Numega SoftIce

: 지난강좌에서 언급했던 John Robbinson이나 Matt Pietrek이 시스템엔지니어로 근무하는 Numega Software에서 제공하는 시스템 디버거이다. 커널모드와 사용자모드를 둘다 지원하는 거의 유일한 디버거이며, 크래커들의 필수품으로 애용될만큼 디버거보다는 크래킹 도구로 널리 알려져 있다. 문제는 이것은 상용프로그램이란점인데(가격또한 만만치 않다.) 회사오너를 졸라서 구입하든 또 다른 경로(?)를 통해서 구하든 각자 알아서 구해오도록 하자. (필자에게 보내달라고 하지는 말기 바란다. 필자는 아직 딸린식구는 없지만, 앞길이 구만리같은 젊은이기에... ^^)


(3) 그밖에 익숙한 텍스트편집기

: 정 쓸만한게 없다면 VC++를 사용해도 된다. 그리고 취향에 따라 다르겠지만 노트패드나 DOS용 에디터를 사용해도 무관하다. 필자의 경우는 울트라에디터를 사용하는데 프로그래머를 위한 지원이 잘되어 있는 편집기라고 생각한다.


****************************************************************************************

데브피아 팁게 편집.. 강좌가 중간에 끝나서 아쉽.. -_-;

+ Recent posts