안녕하세요. 저번에 작성한 포스팅에서 좀 더 수정을 해보도록 하겠습니다.

일단 제가 작성한 코드를 먼저 보여드리겠습니다.

전에 작성한 소스코드에 몇가지 부분이 수정되거나 추가되었지요? 하나씩 살펴봅시다.

- wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); 

-> 기존코드

- wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); 

-> 수정코드

우리가 만들려는 abex crack me 2는 윈도우 창이 검은색입니다. 그러니까 배경색을 하얀색이 아닌 검은 색으로 바꾸었습니다. 결과를 봅시다.

검은색 윈도우 창이 생성되었습니다. 다음으로 넘어갑시다.

- wndclass.lpszClassName = L"HELLOWINDOWS"; -> 기존코드

- wndclass.lpszClassName = L"abexcm2"; -> 수정코드

윈도우의 클래스 이름을 수정하였습니다. 이렇게 수정한 이름은 CreateWindow함수의 첫 번째 인자로 들어가게 됩니다. 

제 소스에서는 윈도우 창을 하나만 띄우기 때문에 인자에 wndclass.lpszClassName를 직접 넣어줬지만 여러개의 윈도우 창을 띄울 경우에는 각 윈도우 창을 구분해야 하므로 가독성을 높이기 위해 lpszClassName에 저장된 문자열을 넣어주는 것이 좋습니다. 

사실 이부분은 안바꿔도 프로그램 실행에 지장이 없습니다. 다음으로 넘어갑시다.

- hWnd = CreateWindow(wndclass.lpszClassName, L"Hello Windows                            Application",

        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,

        CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wndclass.hInstance, NULL);

-> 기존코드

- hWnd = CreateWindow(wndclass.lpszClassName, L"abex 2nd crackme",

        WS_POPUPWINDOW, 820, 462,

        280, 155, NULL, NULL, wndclass.hInstance, NULL);

-> 수정 코드

먼저 두번째 인자의 타이틀 이름을 바꿔주었습니다. 결과를 봅시다.

잘 안보이시면 사진을 눌러 확대해서 보시기 바랍니다. 타이틀 이름이 바뀐 것을 알 수 있습니다.

다음은 세번째 인자입니다. 세번쨰 인자는 윈도우 스타일을 받는 인자였지요? 여기서 abex crack me2를 다시 봅시다.

우리가 생성한 창하고는 다르게 최소창, 닫기, 타이틀 등이 없는 윈도우 창입니다. 즉 이 윈도우 창을 만들기 위해서는 이 세번째 인자값을 다르게 주어야 합니다. 이러한 윈도우창은 팝업창과 비슷한 스타일입니다. 그러므로 세번째 인자에 팝업창 스타일을 전달해 봅시다. 팝업창을 의미하는 정의된 상수값은 WS_POPUPWINDOW입니다.

그리고 크기를 지정해줍시다. 이 WS_POPUPWINDOW창은 크기를 CW_USEDEFAULT로 주면 창이 보이지 않습니다. 그래서 사용자가 임의로 크기를 지정해주어야 합니다. abex crack me 2는 가로 약 280, 세로 약 155입니다. 결과를 한 번 봅시다.

완전 검은색인 팝업윈도우창이 생성되었습니다.

다음은 좌표입니다. 방금 생성된 창은 CW_USEDEFAULT로 인해 x,y 좌표가 0,0으로 된 상태입니다. abex crack me 2를 딱 실행하면 어떻게 됩니까? 화면의 가운데 쯤에 생성되지요? x와 y의 좌표도 abex crack me 2에 맞춰 줍시다. abex crack me2의 x좌표값은 약 820, y좌표 값은 약 462입니다. 결과를 한 번 봅시다.

중앙에 생성된 윈도우 창을 확인할 수 있습니다.

이제 창은 그럴듯하게 맞춰줬습니다. 이제 창에 글자를 적을 차례입니다.

창에 무언가를 그리는 것은 WndProc 함수에서 처리합니다.  WndProc함수를 살펴봅시다.

- HDC hdc;

HDC도 핸들을 의미합니다. 그러면? HWND와 HDC의 차이가 무엇일까요?

HDC는 디바이스 컨텍스트 핸들을 의미합니다. HWND는 윈도우 핸들을 의미하지요.

이 디바이스 컨텍스트(DC)라는 것은 그래픽 관련 옵션을 모아놓은 구조체를 뜻합니다.즉 무언가를 출력을 할 때 쓰이는데 이렇게 쓰일려면 당연히 출력대상을 알아야합니다. 그리고 그 출력대상을 저장하는 핸들이 바로 HDC입니다. 보통 출력을 한다는 것은 윈도우 창의 출력을 의미하므로 대부분 윈도우창에서 hDC를 얻어옵니다.

(ex : GetDC 함수, BeginPaint 함수 )

정리하면 출력대상을 저장하기 위한 자료형입니다.

- PAINTSTRUCT ps;

PAINTSTRUCT 형의 ps라는 변수를 선언합니다. PAINTSTRUCT 의 정의를 봅시다.

이름에서 알 수 있듯이 구조체를 뜻하는 군요. 이 구조체를 자세히 살펴봅시다.

이 PAINTSTRUCT은 생성된 응용프로그램의 화면에 관한 정보를 가져와 저장시킬 때 사용합니다. 

인자를 살펴봅시다. 

*HDC hdc;

정보를 가져올 응용프로그램의 출력대상을 저장합니다. 즉 응용프로그램 화면의 핸들이죠.

* BOOL fErase;

이 멤버는 배경을 다시 그려줘야 하는지에 대한 여부를 저장합니다. TRUE이면 그려줄 피필요가 없다는 것을 의미하고 FALSE라면 다시 그려줘야 한다는 뜻입니다.

나머지 인자들은 나중에 설명드리겠습니다.

정리하면 이 ps는 화면의 정보를 받을 때 쓰입니다.

- HFONT font;

HFONT는 폰트 핸들입니다. 만들거나 가져온 폰트의 핸들을 저장시키기 위해 사용합니다.

- HPEN pen;

HPEN은 펜 핸들입니다. 여기서 펜이란 선을 의미합니다. 만들거나 가져온 선의 핸들을 저장시키기 위해 사용합니다.

- LPCTSTR str = L"abex’  2nd crack me";

화면에 출력할 문자열을 따로 저장시켜 str로 선언해주었습니다. 물론 따로 선언안하고 함수 인자에 직접 적어주셔도 무방합니다. 다만 직접 적어주는 방식은 나중에 많은 문자열을 수정할 때 시간이 조금 걸립니다.

- hdc=BeginPaint(hWnd, &ps);

BeginPaint의 설명을 봅시다.

이 함수는 쉽게 설명하면 윈도우 로부터 DC를 얻어오는 함수입니다. 인자를 살펴봅시다.

* HWND hwnd

DC를 얻어올 윈도우의 핸들을 의미합니다.

* LPPAINTSTRUCT lpPaint

 LP는 LongPointer를 의미합니다. 즉 PAINTSTRUCT의 주소를 인자로 받습니다.

정리하면 이 함수는 윈도우 창으로부터 DC를 받아와 PAINTSTRUCT의 멤버에 저장시켜 HDC로 반환하는 함수입니다.

GetDC함수랑 비슷한 기능을 합니다. 하지만 제가 GetDC함수를 사용하지 않는 이유가 있습니다. 이것은 나중에 설명드리겠습니다.

 - font1 = CreateFont(18,0,0,0,700,0,0,0,HANGEUL_CHARSET,0,0,0,

DEFAULT_PITCH | FF_ROMAN,L"궁서");

폰트를 생성시켜 저장하는 코드입니다. CreateFont의 정의를 봅시다.

자 인자 설명 들어갑니다.

* int nHeight

글자의 높이를 받습니다.

* int nWidth

글자의 너비를 받습니다.

* int nEscapement

글자의 기울기를 받습니다.

* int nOrientation

글자 하나하나의 기울기를 받습니다. 사용할 필요가 없으니 0을 줍니다.

* int fnWeight

글자의 굵기를 받습니다. 400은 보통이며 700은 진하게를 의미합니다. 범위는 0에서1000 입니다.

* DWORD fdwItalic

받은 값이 TRUE이면 기울기(Italic)로 설정합니다.

* DWORD fdwUnderline

받은 값이 TRUE이면 밑줄로 설정합니다.

* DWORD fdwStrikeOut

받은 값이 TRUE이면 취소선으로 설정합니다.

* DWORD fdwCharSet

글자의 설정값을 받습니다. 한글을 출력하고 싶으시면 HANGEUL_CHARSET로 설정해주시면 됩니다. 물론 abex crack me2는 한글이 없기 때문에 다른 설정을 해주셔도 무방합니다.

* DWORD fdwOutPutPrecision

출력 정확도를 받습니다. 사용할 필요가 없으니 0으로 줍시다.

* DWORD fdwClipPrecision

클리핑 정확도를 받습니다. 사용할 필요가 없으니 0으로 줍시다.

* DWORD fdwQuality

출력의 품질을 받습니다. 사용할 필요가 없으니 0으로 줍시다.

* DWORD fdwPitchAndFamily

자간과 폰트를 받습니다. 여기서는 값을 DEFAULT_PITCH로 주어 자간을 기본값으로 설정했습니다. 폰트는 FF_ROMAN을 주었습니다. FF_ROMAN의 설명을 봅시다.

MS의 세리프를 폰트로 사용하겠다는 뜻입니다.

* LPCTSTR lpszFace

사용할 글꼴을 유니코드로 받습니다. 여기서는 궁서 글꼴을 사용하기로 했습니다.

인자가 정말 미친듯이 많습니다. 그만큼 글꼴 설정이 많다는 것을 알 수 있습니다.

- SelectObject(hdc,font);

만든 font를 등록합니다. SelectObject의 설명을 봅시다.

인자를 살펴봅시다.

* HDC hdc

등록할 DC를 인자로 받습니다. 

* HGDIOBJ hgdiobj

GDI 객체 핸들을 받습니다. 여기서는 만든 폰트의 핸들인 HFONT가 들어갑니다.

정리하면 그래픽 관련 핸들을 받아 DC에 등록합니다. 이렇게 등록된 FONT는 그 후 글자에 폰트가 자동으로 적용됩니다.

- pen = CreatePen(PS_SOLID,2,RGB(189,189,189));

선의 속성을 지정하는 핸들을 만들어 pen에 저장시킵니다. CreatePen을 살펴봅시다.

인자를 살펴봅시다.

* int fnPenStyle

펜 스타일을 설정합니다. 우리는 실선을 사용할 것이므로 PS_SOLID로 지정합시다.

* int nWidth

선의 굵기를 받습니다. 저는 2로 주었습니다.

*COLORREF crColor

선의 색깔을 받습니다. 자료형 COLORREF를 살펴봅시다.

타입이 DWORD입니다. 즉 4바이트를 받습니다. 

MSDN의 설명을 참고하면 상위바이트는 0으로 고정이며 그 다음 바이트는 파란색, 그 다음 바이트는 녹색, 최하위 바이트는 빨강색을 의미합니다.

머리가 아프니 RGB라는 함수를 사용합니다. RGB 정의를 봅시다.

따로 설명이 필요없겠지요? 단위가 1바이트이므로 2의 8승인 256개를 표현할 수 있습니다. 즉 0~ 255를 뜻하지요. 우리가 원하는 색은 회색이고 이 회색의 RGB값은 (189,189,189)입니다. 

- SelectObject(hdc,pen);

font에 이어 pen도 hdc에 등록시킵니다. 이후에 선을 그리면 pen의 선 속성이 자동으로 적용됩니다.

- SetTextColor(hdc,RGB(255,0,0,));

글자의 색깔을 지정하는 함수입니다. SetTextColor함수의 설명을 봅시다.

앞서 했던 인자들이니 설명은 넘기겠습니다.

글자의 색깔을 받아 hdc에 등록합니다. 여기서는 빨간색으로 등록하였습니다.

- SetBkColor(hdc,RGB(0,0,0,));

글짜의 음영 색깔을 지정합니다. 인자는 SetTextColor랑 똑같습니다. 이 함수를 쓰지 않으면 기본적으로 하얀색이 음영색으로 지정됩니다. 우리는 배경이 검은색이므로 검은색으로 맞춰주기 위해 음영색을 검은색으로 지정하였습니다.

- TextOut(hdc, 40,7,str,lstrlen(str));

DC에 글자를 출력하는 함수입니다. 정의를 한번 봅시다.

인자 설명 합니다.

* HDC hdc

출력할 DC의 핸들을 받습니다.

* int nXStart

DC내에서 출력할 글자의 x좌표를 의미합니다. 이 좌표는 글자가 시작하는 위치입니다.여기서는 40을 주었습니다.

*int nYStart

DC내에서 출력할 글자의 x좌표를 의미합니다. 여기서는 7를 주었습니다.

*LPCTSTR lpString

출력할 유니코드 문자열을 받습니다. 여기서는 아까 저장시킨 str을 주었습니다.

* int cchString

출력할 유니코드 문자열의 길이를 받습니다. 여기서는 lstrlen함수를 사용하여 길이를 주었습니다. 물론 직접 계산하여 숫자를 주어도 되지만 길이가 긴 문자열일 경우 계산이 귀찮아지므로 lstrlen함수를 사용했습니다.

그런데 왜 strlen함수가 아닌 lstrlen함수를 사용했을까요? 각각의 정의를 봅시다.

인자가 유니코드입니다.

인자가 멀티바이트코드 입니다.

아시겠지요?

- MoveToEx(hdc,0,30,NULL);

선을 그리기 위해 사용한 함수입니다. 보통 LineTo 함수와 같이 사용합니다.

정의를 봅시다.

* HDC hdc

출력할 DC를 받습니다.

* int x

선을 그릴 때 시작점의 x좌표를 뜻합니다. 여기서는 0으로 주었습니다.

* int y

선을 그릴 때 시작점의 y좌표를 뜻합니다. 여기서는 30으로 주었습니다.

* LPPOINT lpPoint

위 인자의 x,y 좌표를 변수에 저장시킬 때 그 변수의 주소를 인자로 받습니다. 사용하지 않을려면 NULL값을 줍니다.

즉 이 함수는 선을 그릴 때의 시작점을 지정할 때 사용합니다.

- LineTo(hdc,300,30);

대충 감이 오시지요? 이 함수는 선의 도착점을 지정합니다. 즉 MoveToEx의 시작점부터 LineTo의 끝점까지 선으로 연결한다는 뜻입니다.

인자는 MoveToEx의 인자에서 lpPoint만 없어졌습니다.

여기서 x좌표는 300, y좌표는 30을 주었습니다. 

- MoveToEx(hdc,185,30,NULL);

  LineTo(hdc,185,200);

또 하나의 선을 더 그리기 위해 함수를 호출하였습니다. 좌표값을 보면 세로로 직선을 그린다는 것을 알 수 있습니다.

- DeleteObject(font);

DC에 등록된 핸들을 지우기 위해 사용합니다. 보통 다른 font 핸들을 등록할때 지우거나 처리를 다 끝내고 메모리 효율을 위해 지웁니다. 인자는 SelectObject에서 hdc를 뺀 것입니다.

- DeleteObject(pen);

DC에 등록된 pen 핸들을 지웁니다.

- EndPaint(hWnd, &ps);

Paint의 끝을 알리는 함수입니다. 쉽게 얘기하면 DC를 반환하는 함수이죠. BeginPaint함수의 반대라고 생각하시면 됩니다. 인자는 BeginPaint함수랑 동일합니다. 보통 메모리 효율을 위해 사용합니다.

자 소스코드 설명이 끝났습니다. 이제 실행을 해봅시다.

조금씩 비슷해져가고 있습니다. 

네 이것으로 포스팅을 마쳤...으면 좋겠지만 추가로 설명하겠습니다.

WndPorc 함수의 코드를 다시 봅시다.

알다시피 이 함수는 메세지를 처리하는 함수입니다.

그런데? 윈도우 창이 생성되고 나면 이 메세지함수가 셀 수 없을 정도로 많이 실행됩니다. 즉 이 함수가 실행될 때 마다 우리가 작성한 코드에 의해 창에 글자와 선이 출력됩니다. 

아니 이미 한번 출력된 코드인데 계속 출력하라고 하면 메모리 효율이 극도로 떨어지겠죠? 그래서 우리는 이 코드가 매번 실행되는 것이 아닌 필요할 때만 실행되도록 할 필요가 있습니다. 그러므로 우리는 WndProc함수에서 WM_PAINT 메세지를 받았을 때 이 코드를 처리하도록 수정해줍시다.

물론 수정하기 전에 WM_PAINT 메세지를 설명해주어야겠지요?

기본적으로 윈도우 창이 생성될 때 WndProc에서는 몇 개의 메세지를 받습니다. 이러한 메세지는 큐에 저장되지 않고 바로 WndProc로 전달되기 때문에 GetMessage함수가 쓰이지 않습니다.

먼저 WM_CREATE 메세지가 전달됩니다. 이 메세지는 CreateWindow 함수가 전달한 것입니다. 즉 윈도우 창이 생성되기 직전에 받는 메세지이지요.

윈도우 창이 생성 된 다음 받는 메세지는 WM_MOVE, WM_SIZE 등의 메세지입니다. 당연히 이러한 메세지도 바로 WndProc로 전달됩니다. 대충 메세지를 다 받고 나면 마지막으로 받는 메세지가 WM_PAINT메세지 입니다.  우리는 이 WM_PAINT메세지를 받았을 때 코드가 처리되도록 수정하는 겁니다.  그렇다면 여기서 의문이 하나 생깁니다.

다른 메세지도 많은데 왜 WM_PAINT메세지에서 처리하나요?

예를 하나 보여드리겠습니다. 자 WM_MOVE 메세지를 받았을 때 출력을 하도록 수정해봅시다.

참고로 BeginPaint와 EndPaint함수는 WM_PAINT 메세지에서만 동작하는 함수입니다. 그 외의 메세지에서 DC를 할당받고 해제할려면 GetDC와 ReleaseDC함수를 써야합니다. 기능은 비슷하니 설명은 생략하겠습니다. 

WM_MOVE는 윈도우 창이 움직였을 때 발생하는 이벤트입니다. 자 이제 코드를 실행시켜 봅시다.

어라? 아무것도 안뜹니다. 분명 제가 GetMessage를 거치지 않고 WndProc함수에서 WM_MOVE를 무조건 받는다고 되어있는데 왜 출력이 되지 않을까요?

자 이번엔 윈도우 스타일을 바꿔봅시다. 아래와 같이 바꿔주세요.

WS_POPUPWINDOW속성을 WS_OVERLAPPEDWINDOW로 바꿨습니다. 실행을 해봅시다.

그냥 실행만 시켰을 뿐인데 출력이 되었습니다. 프로그램을 이동시키지 않았으니까 WM_MOVE 이벤트도 발생이 없었을 것입니다. 즉 GetMessage를 거치지 않고 WM_MOVE 메세지를 받아서 처리를 한 것입니다.

그러면 앞의 팝업 창은 WM_MOVE 메세지를 못받았을까요? 

아닙니다. 디버그를 해본 결과 WM_MOVE 메세지를 받았습니다.! 그런데 왜 팝업창에서는 출력이 되지 않았을까요?

그 이유는 팝업 윈도우 창의 특성 때문입니다. 팝업 윈도우 창은은 크기를 바꿀 수가 없고 이동시킬 수도 없습니다. 그렇기에 WM_MOVE나 WM_SIZE 메세지를 받아서 처리를 해도 실제로는 적용이 되지 않는 것입니다. 

그러면 방법이 없을까요? 아닙니다. 바로 WM_PAINT 메세지가 있습니다.! 메세지를 WM_PAINT로 수정해봅시다.

결과를 봅시다.

네 잘나오는군요.

참고로 WM_PAINT 메세지에서는 GetDC함수를 쓰지 않습니다. 왜 쓰지 않느냐고 하니 간단히 얘기해드리겠습니다. 

사실 이 WM_PAINT메세지는 무효화 영역이 생겼을 경우 발생하는 메세지 입니다. 즉

윈도우 생성 -> WM_PAINT(GetMessage를 거치지 않은) 메시지 처리

-> 이후에 오는 WM_PAINT는 무효화 영역이 생겼을 경우 GetMessage가 큐에서 뽑아서 DispatchMessage에 의해 WndProc로 전달합니다.

무효화 영역이라는 것은 간단하게 얘기하면 윈도우와 윈도우가 겹쳐진 영역 또는 최소화, 최대화로 인해 창이 사라진 영역을 의미합니다. 이러한 경우가 발생됐을 경우 WM_PAINT가 생성됩니다.

자 여기서 본론을 말하겠습니다. 이 WM_PAINT메세지는 GetMessage에서 뽑아올 때 지워지지가 않습니다. 보통 메세지를 가져와서 확인을 한 후 지워줘야 하는데 이 WM_PAINT메세지는 GetMessage가 지우질 않습니다. 즉 WM_PAINT메세지를 처리한 후에도 지워지지 않은 상태로 계속 큐에 남아있어 GetMessage가 또 WM_PAINT메세지를 받아 처리합니다. 즉 무한으로 WM_PAINT메세지를 처리하게 됩니다. 

끔찍하지요? 그러면 어떻게 해야할까요? 이를 방지하기 위해 BeginPaint함수를 쓰게 됩니다. 이 함수는 DC를 받아온 후 WM_PAINT 메세지를 큐에서 지웁니다.! 그래서 정상적으로 실행을 하게끔 합니다. GetDC함수는 DC만 얻어오고 이러한 작업을 하지 않습니다. 

그렇다면 BeginPaint함수만 사용하면 되느냐고 하니 이 함수는 WM_PAINT 메세지에서만 동작합니다. 즉 그 외의 메세지는 GetDC를 이용합니다. 

네 이것으로 포스팅을 진짜 마치겠습니다.


Posted by englishmath
,

안녕하세요. 이번 포스팅에선 abex crack me 2를 직접 제작해보겠습니다.

일단 동작을 한 번 봅시다.


이름과 시리얼을 받는 칸이 있고 버튼이 달려있습니다. 이번 프로그램은 메세지박스만으로는 구현이 힘들 것 같군요. 그래서 이번엔 윈도우 창을 만들어서 구현해야 할 것 같습니다. 소스가 생각보다 복잡해지니 몇번 나눠서 포스팅 하겠습니다.

일단 제가 짜놓은 소스코드를 한 번 봅시다.

네 제법 양이 되지요? 근데 이것은 말 그대로 틀만 짜놓은 소스입니다. 자 한줄씩 분석해봅시다.

- LRESULT WINAPI WndProc(HWND, UINT, WPARAM, LPARAM);

WndProc는 WindowProc의 약자입니다. MSDN의 설명을 봅시다.

반환값이 LRESULT라고 되어있습니다. LRESULT가 대체 무슨 자료형일까요?

LRESULT의 L은 long을 뜻합니다. 그런데 대문자 L이지요? 즉 LONG를 뜻합니다. 

그러면 대체 long랑 LONG랑 뭐가 다른 걸까요?

long은 정수형 4바이트죠? 그런데 말입니다. 이 자료형의 크기가 컴파일러마다 다르게 나옵니다. 어떤 컴파일러에서는 4바이트인데 어떤 컴파일러에서는 8바이트로 나오기도 합니다. 그래서 윈도우 API에서는 이 long이란 자료형의 크기를 4바이트로 정해놓은 자료형이 따로 있습니다. 그 자료형이 LONG입니다. 그리고 이 자료형은 windows.h파일에 정의되어 있습니다. 한번 보시죠.

정의를 보시면 long을 LONG으로 정의해 놓았습니다. 크기를 한번 살펴볼까요?

제 컴파일에서는 둘다 4바이트가 나왔지만 다른 컴파일러에선 long이 8바이트가 나올 수 있습니다. 

자 Long에 대한 설명이 끝났으니 이제 본론인 LRESULT를 알아봅시다. 정의를 한번 볼까요?

LONG_PTR을 LRESULT로 정의시켜 놓았군요. LONG은 알겠는데 그 뒤의 PTR이 뭔지 모르시겠죠? 그런데 여러분. 우리 이 PTR 어디서 많이 보시지 않았습니까?

바로 리버싱 할 때 입니다. 사진을 한 번 봅시다.

DWORD PTR이라고 되어있습니다. 아니 DWORD 그냥 쓰면 될걸 왜 굳이 PTR을 달아놓았을 까요?

일단 PTR을 설명하기 전에 32비트와 64비트 환경에 대해 간단히 설명하겠습니다. 

여러분 32비트 운영체제와 64비트 운영체제의 차이점이 무엇인지 아십니까? 여러가지가 있겠지만 대표적으론 32비트는 메모리가 32비트로 사용된다는 것이고 64비트는 메모리가 64비트로 사용된다는 것입니다. 자 여기서 하나 물어봅시다. 

32비트 프로그램이 64비트에서 동작될까요? 

네 물론 동작합니다. 어떻게 하냐구요? 직접 동작시켜보았으니까요. 자 그러면 하나 더 물어보겠습니다. 

왜 동작이 가능할까요?

생각해보십시요. 32비트 프로그램은 주소를 32비트 스택에 저장시킵니다. 즉 주소값은 32비트이지요. 그런데 64비트 환경에서는 스택 하나가 64비트입니다. 즉 주소값이 64비트여야 한다는 것이지요. 64비트 환경에서 32비트 프로그램을 실행시키면 32비트 주소가 64비트 주소로 인식하겠습니까? 당연히 문제가 생깁니다.

어? 그러면 2개의 스택을 써서 주소를 저장시키면 되지 않나요? 그러면 64비트에서도 동작이 정상적으로 될 거 같은데요?

물론 2개의 스택으로 주소를 저장시키면 64비트에서도 동작합니다. 그런데? 32비트프로그램을 32비트에서 동작할 때는 어떻게 됩니까? 물론 정상적으로 실행됩니다. 근데 스택 1개로 충분한 것을 2개로 잡아서 처리하니까 메모리 효율이 극도로 떨어집니다.

이를 위해 나온것이 PTR입니다. 즉 일반 32비트 자료형 뒤에 PTR을 붙여서 32비트에서는 주소를 32비트로 사용하고 64비트에서는 주소를 64비트로 사용하게 된 것이지요.

ex) INT_PTR, DWORD_PTR, LONG_PTR

즉 이 PTR은 32비트 환경과 64비트 환경 간의 호환성을 위해 사용됩니다.

사설이 길어졌군요. 본론만 말하면 LRESULT는 LONG_PTR 즉 LONG이라고 보시면 됩니다.

CALLBACK은 설명드렸지요? __stdcall 호출 방식입니다.

자 그럼 이 WndProc 함수는 정수 4바이트를 반환하는 함수입니다. 인자를 하나씩 살펴봅시다.

* HWND 

핸들입니다. 앞서 설명드렸으니 넘어갑시다. 이 인자는 윈도우의 핸들을 받습니다.

* UINT

이 자료형의 인자는 메세지를 양의 정수로 받습니다.

* WPARAM, LPARAM

새로운 자료형이 나왔습니다. 정의를 한 번 봅시다.

UINT와 LONG이군요. WPARAM은 양의 정수이고 LPARAM은 그냥 정수네요.

WPARAM가 자료형인 인자는 메세지의 추가정보를 받습니다.

LPARAM가 자료형인 인자도 메세지의 추가정보를 받습니다.

즉 정리하면 WndProc 함수는 윈도우 창에 들어온 메세지(마우스,키 값) 등을 처리하는 함수를 뜻합니다.

윈도우 창을 띄울려고 코딩할 때에는 이 함수를 무조건 선언해줘야 합니다. 선언이 안되있으면 윈도우창을 못띄웁니다. 당연한 겁니다.

기본적으로 이 함수가 없으면 우린 윈도우창을 닫을 수가 없습니다. 마우스 위치를 입력받은 메세지를 처리 못하니까 말이죠.

지금 코드에서는 이 WndProc 함수가 선언만 되어 있습니다. 즉 틀만 구현해 놓은 것이지요. 이렇게 틀만 구현해 놓은 이유는 바로 뒤에 실행될 WinMain함수에서 사용하기 때문입니다. 

이제 WinMain을 살펴봅시다.

- Hwnd hwnd;

hwnd라는 핸들자료형을 선언합니다.

- MSG msg

msg라는 MSG자료형을 선언합니다. MSG가 무슨 자료형일까요? 물론 여러분이 생각하시는대로 message의 약자입니다. 정의를 한번 볼까요?

정의를 살펴보니 구조체로 선언되어 있습니다. 구조체 안에 여러 자료형으로 선언된 변수가 들어있네요. 즉 MSG로 선언된 변수는 저러한 멤버들을 참조할 수 있게 됩니다. 한번 볼까요?

보시다시피 멤버 6개를 참조할 수 있습니다. 각 멤버들의 설명은 나중에 하겠습니다. 일단 이 MSG가 메세지 속성 값을 저장해놓은 구조체 라는 것만 아시면 됩니다. 다음을 봅시다.

- WNDCLASS wndclass

WNDCLASS 자료형인 wndclass를 선언합니다. WNDCLASS의 정의를 한번 봅시다.

WNDLASSW로 정의되어 있네요. 뒤쪽에 W가 하나 붙었지요? 이 W는 이 함수가 유니코드로 처리되는 함수라는 것을 의미합니다. WNDCLASSW의 정의를 한번 봅시다.

구조체가 나오는 군요. 앞의 MSG랑 비슷한 맥락입니다. 이 WNDLASS는 윈도우의 속성값을 저장해 등록해놓은 구조체입니다. 자 다음의 소스를 봅시다.

 - wndclass.style = CS_HREDRAW | CS_VREDRAW;

WNDLASS의 멤버 style로 접근하여 값을 줍니다. 이 멤버 style은 윈도우가 출력되는 형태를 저장시키는 변수입니다. 그런데 값이 CS_HREDRAW와 CS_VREDRAW이군요. 

style 타입이 정수이니까 이 CS_HREDRAW와 CS_VREDRAW은 define으로 정의된 상수라는 것을 알 수 있습니다. MSDN의 설명을 봅시다.

해석을 해드리겠습니다.

CS_HREDRAW은 윈도우의 width 즉 윈도우 창의 너비가 바꼈을 경우 윈도우 창을 다시 그립니다. 

CS_VREDRAW은 윈도의 height 즉 윈도우 창의 높이가 바꼈을 경우 윈도우 창을 다시 그립니다. 

즉 style의 값을 이렇게 주었다는 것은 윈도우의 너비, 높이가 바뀌면 다시 윈도우 창을 다시 그려라 라는 뜻입니다. 중간에 | 가 있죠? 이 | 은 or을 뜻합니다.

즉 윈도우 창의 높이 혹은 너비가 바뀌면 다시 그려라 라는 뜻으로 해석이 가능합니다.

다음으로 갑시다.

 - wndclass.lpfnWndProc = WndProc;

wndclass의 멤버변수 lpfnWndProc에 WndProc를 대입하지요? 이 WndProc는 우리가 앞에 선언한 함수를 의미합니다. 그런데 뭔가 이상하지 않습니까? WndProc함수는 분명 인자가 있었습니다. 그런데 여기에서는 인자 없이 함수만 쓰였습니다. 이게 가능할까요? 예제를 한번 봅시다.

분명 구조체 result의 자료형도 int이고 sum의 반환값도 int인데 함수만으로는 저장이 안됩니다. 인자까지 다 전달해야 비로소 완벽히 저장이 되는 것이지요. 그런데 어떻게 

wndclass.lpfnWndProc = WndProc 이러한 형태가 가능한 걸까요? 정답은 lpfnWndProc의 자료형에 있습니다.

lpfnWndProc의 자료형이 WNDPROC이죠? WNDPROC의 정의를 한번 봅시다.

이 WNDPROC이란 자료형이 포인터 변수로 되어있네요. 그것도 CALLBACK 포인터 변수입니다. CALLBACK이 함수호출규약인 __stdcall이죠? 즉 __stdcall 방식으로 호출하는 함수의 주소를 저장하는 변수가 이 WNDPROC입니다.

자료형이 포인터 변수이니까 주소로 받아야 합니다. 그래서! WndProc 원형만 적어 WndProc 의 주소를 lpfnWndProc에 저장하겠다는 뜻이지요. 물론 주소도 아무 주소도 받지 않고 CALLBACK호출 방식의 함수만 저장한다는 뜻입니다.

그렇기 때문에 아무리도 리턴값과 멤버의 자료형을 똑같이 맞춰줘도 사용자가 만든 sum이란 함수는 호출방식이 __stdcall이 아니라서 위와 같은 식이 성립하지 않습니다.

즉 lpfnWndProc 멤버는 메세지처리를 하는 __stdcall 방식의 함수 주소를 저장합니다.

- wndclass.cbClsExtra = 0

클래스의 여유메모리를 뜻합니다. 즉 클래스를 처리하는 도중 특수한 경우가 발생했을 경우 이 메모리를 갖다 쓴다는 뜻입니다. 아직 우리는 여유메모리를 사용할 필요가 없으므로 0으로 주었습니다.

- wndclass.cbWndExtra = 0;

윈도우의 여유메모리를 뜻합니다. 윈도우를 처리하는 도중 특수한 경우가 발생했을 때 이 메모리를 갖다 씁니다. 우리는 0으로 줍시다.

- wndclass.hInstance = hInstance;

생성하려는 윈도우의 핸들을 저장시킵니다. 여기서 hInstance값은 WinMain의 첫번째 인자값입니다.

- wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION)

자료형이 HICON이지요? HICON의 정의를 한번 봅시다.

DECLARE_HANDLE이라는 것이 나오지요? 쉽게 얘기하면 핸들을 새로 선언한다는 뜻입니다. 즉 HICON라는 핸들을 정의한다. 라는 뜻입니다. 그러면? HWND와 무슨 차이가 있을까요?

별 차이 없습니다. HWND은 핸들 윈도우, HICON은 핸들 아이콘입니다. 즉

HICON은 아이콘의 핸들을 받는다는 뜻입니다.

그렇다면 Icon멤버는 윈도우의 아이콘을 저장하는 멤버라는 것을 알 수 있습니다. 

자 이제 LoadIcon에 대해 알아봅시다. MSDN의 설명을 봅시다.

반환값이 HICON입니다. 즉 아이콘 핸들을 반환하다는 뜻입니다. 인자를 살펴봅시다.

 * HINSTANCE

아이콘의 핸들을 받습니다. 만약 받지 않을 시 NULL을 지정합니다. 저는 일단 NULL값을 주었습니다. 나중에 바꿀 것입니다.

* lpIconName

자료형이 LPCTSTR이므로 아이콘 이름을 유니코드 문자열로 받습니다. 여기서는 

IDI_APPLICATION을 넣었지요? 정의를 확인해봅시다.

응용프로그램의 기본 아이콘이라고 정의되어 있습니다.

자 다음으로 갑시다.

- wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);

HCURSOR 자료형은 HICON이랑 비슷합니다. 정의를 살펴봅시다.

HICON이랑 같다고 나옵니다. 즉 이름만 다르고 핸들을 받는 것은 똑같다는 것입니다.

그러면 HCURSOR은 어떤 핸들을 받냐고 하니 마우스 커서를 핸들로 받습니다.

LoadCursor함수를 살펴봅시다.

구조가 HICON이랑 거의 똑같습니다.  커서의 핸들과 커서이름을 인자로 받아 핸들을 반환합니다. 커서이름인자 부분에 IDC_ARROW라고 적혀있지요? IDC_ARROW의 정의를 봅시다.

표준 화살표라고 되어있네요. 다음으로 넘어갑시다.

- wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

hbrBackground의 자료형은 HBRUSH입니다. 정의를 살펴봅시다.

그냥 정의된 핸들이군요. 이 HBRUSH는 브러쉬 핸들을 뜻합니다. 즉 브러쉬핸들을 저장하겠다 라는 뜻이지요. 

자 값을 넣는 쪽을 보면 GetStockObject함수가 쓰였습니다. 한번 살펴봅시다.

반환값이 HGDIOBJ 자료형입니다. 이 HGDIOBJ는 Handle GDI 오브젝트를 뜻합니다.

여기서 GDI란 그래픽 디바이스 인터페이스의 약자로 그래픽을 도와주는 장치들을 모아놓은 집합을 의미합니다. 여기서 장치는 함수,자료형 등 일 것이고 오브젝트는 객체라는 뜻을 가지고 있으므로 HGDIOBJ란 그래픽 관련 함수와 자료형 등 을 모아놓은 객체라고 보시면 됩니다. 

그래서 이 객체에는 여러가지 자료형을 포함하고 있습니다.(HGDIOBJ, HPEN 등)

그런데 우리가 받아야 할 자료형은 hbrBackground의 자료형인 HBRUSH이죠? 그래서 HGDIOBJ에서 HBRUSH만 뽑아오도록 형변환을 시켜준 것입니다.

자 이제 하나 있는 인자를 살펴봅시다.

fnObject라고 되어있는데 이 자리에 제가 WHITE_BRUSH를 적었지요? 정의된 상수입니다. 목록을 한 번 봅시다.

하얀색 브러쉬를 뜻합니다. 즉 여기서는 하얀색 브러쉬를 인자로 받아 브러쉬 핸들을 넘겨준다는 것이지요. 여기서 brush는 그냥 배경화면이라고 보시면 됩니다.

- wndclass.lpszMenuName = NULL;

메뉴 이름을 저장하는 멤버입니다. 우리는 아직 메뉴가 필요없으니 NULL로 주었습니다.

- wndclass.lpszClassName = L"HELLOWINDOWS";

lpszClassName의 자료형은 LPCTSTR입니다. 즉 유니코드 상수 문자열을 받습니다.

이 멤버는 윈도우의 클래스 이름을 저장합니다. 여기서 윈도우 클래스 이름은 윈도우를 의미하는 변수라고 보시면 됩니다. 나중에 이 윈도우를 생성시킬 때 이 이름을 사용하니 잘 기억해 두셔야 합니다.

여기까지가 wndclass의 멤버에 값을 등록하는 거였습니다. 참고로 저 멤버중 하나라도 빠지면 정상 동작안합니다. 하나도 빠짐 없이 입력해 주세요.

- RegisterClass(&wndclass);

RegisterClass함수는 WNDCLASS로 선언된 윈도우 클래스를 등록하겠다는 것입니다. 이렇게 등록된 윈도우클래스는 나중에 윈도우 창을 생성시킬 때 사용합니다.

MSDN의 정의를 봅시다.

WNDCLASS로 선언한 윈도우 클래스의 주소를 인자로 받습니다. 반환형이 ATOM이네요. ATOM의 정의를 봅시다.

WORD이군요. 즉 윈도우 클래스의 주소를 2바이트로 넘긴다는 뜻입니다.

다음으로 넘어갑시다.

- hWnd = CreateWindow(wndclass.lpszClassName, L"Hello Windows Application",

        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,

        CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wndclass.hInstance, NULL);

드디어 대망의 CreateWindow함수가 나왔습니다. 여태까지 작성한 윈도우 클래스를 기반으로 윈도우 창을 만드는 것입니다. MSDN의 정의를 봅시다.

반환값은 핸들입니다. 인자들을 하나씩 살펴 봅시다.

* lpClassName

클래스 이름을 뜻합니다. 아까 우리가 만든 wndclass.lpszClassName의 값이 들어갑니다.

* lpWindowName

윈도우 이름을 인수로 받습니다. 여기서 받는 이름은 윈도우 창이 생성될 때의 제목이 됩니다.

* dwStyle

윈도우 창의 스타일을 인수로 받습니다. 여기서  쓰인 값은 

WS_OVERLAPPEDWINDOW값입니다. 정의를 한 번 봅시다.

이 윈도우창은 중복된 윈도우창이라는군요. 이것이 무슨 소리냐 하면 

WS_OVERLAPPEDWINDOW 값은 밑의 윈도우 창을 나타내는 6개의 속성을 다 포함한다는 뜻입니다. 즉 WS_OVERLAPPEDWINDOW를 써놓으면 갖출건 다 갖춘 윈도우 창 을 만들 수 있다는 것입니다. 이것은 나중에 자세히 말씀드리겠습니다.

* x

윈도우 창을 처음 생성할 때의 x좌표 값을 의미합니다. 여기서 CW_USEDEFAULT값을 설정해주면 윈도우 창이 생성될 때 OS에서 기본값 x를 설정해 줍니다.

* y

윈도우 창을 처음 생성할 때의 y좌표 값을 의미합니다. 여기서 CW_USEDEFAULT값을 설정해주면 윈도우 창이 생성될 때 OS에서 기본값 y를 설정해 줍니다.

* nWidth

윈도우 창을 처음 생성할 때 창의 너비를 뜻합니다. 여기서 CW_USEDEFAULT값을 설정해주면 윈도우 창이 생성될 때 OS에서 기본값 너비를 설정해줍니다.

* nHeight

윈도우 창을 처음 생성할 때 창의 높이를 뜻합니다. 여기서 CW_USEDEFAULT값을 설정해주면 윈도우 창이 생성될 때 OS에서 기본값 높이를 설정해줍니다.

* hWndParent

부모 윈도우 핸들을 의미합니다. 아직 우리는 부모 윈도우 창이 없으므로 일단 NULL값을 주었습니다. 이것은 나중에 설명드리겠습니다.

* hMenu

자료형이 HMENU입니다. 정의를 한 번 봅시다.

반환은 메뉴핸들로 반환하는 군요. 즉 메뉴 핸들을 인자로 받습니다. 아직 우리는 메뉴를 사용하지 않을 것이므로 NULL값을 주었습니다.

* hInstance

생성할 윈도우의 핸들을 의미합니다. 여기선 우리가 정의해놓은 윈도우클래스의 핸들을 가지고 옵니다.

* lpParm

이 인자의 자료형은 LPVOID입니다. 정의를 봅시다.

반환값이 없는 포인터 함수를 의미하는군요. 여기서 이 인자는 생성 윈도우 정보를 받습니다. 이것은 나중에 자세히 알려드리겠습니다. 일단 NULL로 주었습니다.

자 이것으로 CreateWindow 함수 설명을 다 끝냈습니다. 이 함수의 결과값은 핸들이므로 우리가 선언한 hwnd에 들어갈 것입니다. 다음으로 넘어갑시다.

 - ShowWindow(hWnd, nCmdShow);

생성한 윈도우를 화면에 나타내주는 함수입니다. 설명을 봅시다.

반환값은 BOOL 이므로 참, 거짓 둘중 하나입니다. 자 인자들을 하나씩 살펴봅시다.

* hwnd

생성한 윈도우 핸들입니다. CreateWindow의 핸들값이 들어있는 hwnd를 넣어주었습니다.

* nCmdShow

윈도우창을 화면에 띄울 때 어떤 방식으로 띄울지를 받습니다. 우리는 WinMain의 매개변수를 주었습니다. 보통 WinMain의 매개변수를 인자로 주면 기본 설정은 SW_SHOW가 됩니다. 정의를 한 번 봅시다.

정해진 크기와 위치에 윈도우 창을 활성화 시킵니다.

- while (GetMessage(&msg, NULL, 0, 0))

    {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }

while은 반복이지요. 무엇을 반복한다는 것일까요? 조건문에 적힌 GetMessage를 살펴봅시다.

반환형은 참, 거짓입니다. 이 GetMessage함수는 말그대로 메세지를 받아옵니다. 인자들을 살펴봅시다.

* lpMsg

자료형이 LPMSG입니다. 정의를 한번 봅시다.

MSG구조체의 포인터를 의미하는 자료형이군요. 즉 MSG구조체의 주소값을 받습니다.

* hwnd

메세지가 발생한 윈도우의 핸들을 의미합니다. 아직 그런 윈도우가 없으므로 NULL을 줍시다.

* wMsgFilterMin

이 인자는 최소정수값을 의미합니다. 이 함수에서 메세지를 받을 때에는 정수형으로 받습니다. 그런데 메세지 번호가 한 두개 이겠습니까? 아니지요?  즉 필요한 메세지만 받기 위하여 최소 정수값을 인자로 받습니다.

* wMsgFilterMAX

위의 인자가 최소정수값을 받는다면 이 인자는 최대정수값을 받습니다.

추가로 이 두 인자가 둘다 0일 경우에는 메세지를 필터링하지 않고 모두 받습니다. 

자 이제 GetMessage 함수 설명이 끝났습니다. 그러면 이러한 GetMessage 함수를 왜 반복문에 돌리는 것일까요?

일반적으로 Windows GUI 응용프로그램은 실행된 후에 단지 윈도우를 출력할 뿐이며일반적으로 아무것도 하지 않습니다. 이런 프로그램은 사용자가 키보드 입력이나 마우스 버튼 클릭으로 이벤트가 발생되면 그때마다 대응되는 처리를 하는 방식이기 때문이지요.

그럼 그 이벤트를 어떻게 처리를 하느냐고 하니 컴퓨터의 장치 중 하나가 이를 감지하여 응용프로그램에 넘겨줍니다. 이 때 그 이벤트는 응용프로그램의 큐에 저장이 되는데 이 큐에 저장된 이벤트를 메세지라고 합니다. 참고로 큐는 스택의 반대개념이라고 보시면 됩니다. 이렇게 저장된 메세지는 GetMessage함수에서 받아서 처리를 하게 됩니다.

메세지는 끊임없이 받아야 하므로 반복문에 GetMessage함수를 넣어 처리를 하도록 하는 것입니다. 언제까지 처리하냐구요? 생성된 윈도우가 종료될 때까지 입니다.

정리하면 이 GetMessage함수는 메세지를 받아서 이 메세지가 WM_QUIT라면 FALSE 아니라면 TRUE를 반환합니다. 

자 이제 반복문 안에 있는 함수를 살펴봅시다.

 - TranslateMessage(&msg);

TranslateMessage함수의 정의를 봅시다.

인자는 MSG의 주소를 받습니다. 예도 GetMessage함수와 비슷합니다. 하지만 이 함수는 메세지를 인자로 받았을 때 이 메세지가 키보드 관련 메세지일 경우 이 키보드 관련 메세지에서 추가적인 정보를 뽑아내 반환합니다. 즉 a라는 키를 입력한 메세지를 인자로 받았을 경우 이 메세지에서 ctrl 등의 키가 눌러졌는지 안눌러졌는지에 대한 추가 정보를 뽑아내서 메세지에 덧붙입니다.

즉 키보드 관련 메세지를 받아서 처리할 경우에만 TRUE를 반환하고 그 외의 메세지를 받았을 경우에는 FALSE를 반환합니다.

- DispatchMessage(&msg);

정의를 한 번 봅시다.

이 함수도 메세지를 인자로 받아 LRESULT 형식으로 반환합니다. 즉 정수형으로 반환하는 것이지요.

이 함수는 간단하게 얘기하면 받은 메세지를 msg의 멤버에 저장시켜 LRESULT 값으로 윈도우 프로시저에 전달시킵니다. 여기서 윈도우 프로시저란 WndProc함수를 말합니다.

최종적으로 정리하면

while (GetMessage(&msg, NULL, 0, 0))

    {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }

는 윈도우 창이 생성된 직후 큐에 저장된 메세지를 읽어들여 처리하는 부분입니다.

즉 GetMessage 함수로 메세지를 읽어오고 TranslateMessage 함수로 키보드 관련 메세지를 처리한 후 DispatchMessage 함수로 인해  WndProc함수에 전달됩니다.

이 함수가 없으면 윈도우 창이 생성된 후 추가적으로 오는 메세지를 받지 못해 바로 종료가 되버립니다. 궁금하시분 들은 이 while을 주석처리하고 실행해보십시요.

여기까지가 WinMain의 설명입니다. 너무 많지요? 근데 아직 덜 끝났습니다. 조금만 더 힘내봅시다. 이제 앞에서 선언된 WndProc함수를 구현해줘야 합니다. WndProc함수의 내부를 봅시다.

- switch (message)

    {

    case WM_DESTROY:

        PostQuitMessage(0);

    }

return DefWindowProc(hWnd, message, wParam, lParam);

switch문은 다들 아시죠? 정수를 받아서 해당되는 case문을 실행하는 명령어입니다.

여기서는 message를 받습니다. 이 message는 DispatchMessage 함수에서 받아온 메세지를 받는 매개변수입니다. (2번 째 인자) case문을 살펴봅시다.

* WM_DESTROY

윈도우 창이 파괴되었다는 것을 알리는 메세지입니다. DispatchMessage에서 받은 메세지가 WM_DESTROY라면 PostQuitMessage함수를 호출합니다. PostQuitMessage의 정의를 봅시다.

이 함수는 WM_QUIT라는 메세지를 큐에 저장시킵니다. 즉 종료하라는 메세지를 저장시키는 거지요. 이 함수 인자는 종료코드를 의미하는데 지금은 쓸 일이 없으므로 0을 주었습니다.

이 함수로 인해 큐에 WM_QUIT 메세지가 저장되고 나중에 GetMessage에서 WM_QUIT를 받으면 리턴값을 FALSE로 반환하고 이로 인해 반복문을 빠져나가서 return 0을 실행하고 윈도우 프로그램이 정상적으로 종료됩니다.

- return DefWindowProc(hWnd, message, wParam, lParam);

DefWindowProc함수의 정의를 봅시다.

WndProc함수랑 똑같은 인자와 반환값이다. 그러면 대체 DefWindowProc랑 WndProc의 다른 점이 뭘까요? 

DefWindowProc 의 Def는 Default입니다. 즉 기본을 의미하지요. 보통 WndProc에서 처리되지 않은 메세지를 자동으로 처리할 때 쓰입니다.

우리가 작성한 WndProc에서는 WM_DESTROY메세지만 처리가 되었지요? 그런데 그 외의 메세지도 있을 것이 아닙니까. 그러한 메세지를 자동으로 처리해 반환하도록 DefWindowProc함수를 사용하였습니다.

제가 작성한 소크코드 설명이 전부 끝났군요. 창 하나띄우는데 정말 많은 코드가 사용되었습니다. 이제 작성한 코드를 실행시켜 봅시다.

하얀색 배경의 창이 띄워졌습니다. 그런데 우리가 만들려는 abex crack me 2랑 너무 다르지요? 그래서 다음포스팅에서는 이 소스코드를 기반으로 abex crack me2 랑 비슷하게 만들어보겠습니다.

이상으로 포스팅을 마치겠습니다.







Posted by englishmath
,

안녕하세요. 이번 포스팅의 주제는 프로그래밍 카테고리에서 직접 제작한 abex crack me를 리버싱해보겠습니다.

그 전에 잠깐만 소스코드를 수정하겠습니다.

변수를 EAX와 ESI로 쓰니 나중에 설명할 때 레지스터 EAX,ESI랑 혼동할 가능성이 있어 변수를 그냥 a, b로 바꿨습니다.

자 소스코드수정이 완료되었으면 올리디버거로 열어봅시다.

일반 abex crack me랑 다르게 나오는 군요. 당연합니다. 일반 abex cracke me는 델파이로 만들어졌고 우리가 만든 abex cracke me는 C로 만들어졌으니까요.

문자열을 찾아봅시다.

해당하는 문자열을 찾았으면 주소로 점프합시다.

우리가 작성한 메세지박스와 겟드라이브타입 함수가 있는 것을 볼 수 있습니다. 즉 여기가 c언어의 WinMain부분인 것을 알 수 있습니다. 이 WinMain의 시작주소는 012A13D0이군요. C언어로 작성되어서 그런지 오리지널 crack me와 많이 다릅니다.

한줄씩 실행해 봅시다.

빨간박스의 명령어들은 스택프레임을 할당하기 위한 명령어입니다. 어떤 함수든 호출되는 순간 스택에 그 함수를 위한 새로운 영역이 할당되는데 이 할당된 영역을 스택프레임이라고 합니다. 여기에서는 WinMain이란 함수 영역이므로 WinMain을 위한 스택프레임을 할당하는 작업이라고 볼 수 있습니다. 여기서 잠깐 스택을 살펴봅시다.

먼저 WinMain함수를 호출하기 전입니다.

위 스택은 WinMain함수를 호출하기 전의 함수 스택 프레임입니다. F7키를 눌러 함수를 호출해봅시다.

WinMain함수를 호출하게 되면 스택에 리턴주소가 들어가게 됩니다. 이 리턴주소는 나중에 CPU가 WinMain 함수의 처리를 다 끝내고 실행할 다음 주소를 의미합니다. 즉 CPU는 이 스택에 들어있는 주소로 이동해 명령어를 계속 수행하는 것이지요.

이 리턴주소를 이용하는 해킹을 버퍼오버플로우 해킹이라고 합니다. 이것은 나중에 자세히 설명드리겠습니다.

이제 명령어들을 하나씩 살펴봅시다.

*PUSH EBP

EBP레지스터의 값을 스택에 넣습니다. 현재 EBP값은 WinMain함수를 호출하기 전의 함수 시작주소입니다. EBP가 함수의 시작주소를 저장하는 레지스터인것은 다들 아시죠? 그런데 이제 WinMain을 호출하였으니 WinMain함수의 시작주소를 EBP에 넣어야 합니다. 하지만 그냥 넣으면 나중에 WinMain함수 처리가 끝나고 WinMain함수를 호출하기 전의 함수를 처리할 때 EBP값을 찾지 못해 에러가 생겨버립니다.

그러므로 WinMain함수 주소를 넣기전에 스택에 값을 넣어 나중에 WinMain함수처리를 끝내고 다시 가져가는 식으로 합니다. 쉽게 말해 EBP를 백업하는 것이지요.

*MOV EBP, ESP

EBP백업을 끝냈으면  WinMain함수시작주소를 넣어야 합니다. 현재 WinMain함수시작주소는 ESP에 들어있으므로 ESP의 값을 EBP에 넣어줍니다. 이 명령어를 실행하면 EBP의 값이 WinMain함수시작주소가 되므로 스택프레임이 생성됩니다. 현재 스택을 봅시다.

MOV EBP, ESP 실행 전, 즉 WinMain의 스택프레임 생성 전의 스택

MOV EBP, ESP 실행 후, 즉 WinMain의 스택프레임 생성 후의 스택

사진의 체크된 부분을 잘 보시면 선이 끊겨져 있는 것을 볼 수 있습니다.

즉 저 끊어진 선을 기준으로 스택프레임이 나뉘어져 버린 겁니다. 위의 두줄은 WinMain의 스택프레임, 아래는 WinMain을 호출하기 전 함수의 스택프레임입니다.

* SUB ESP, 0D8

SUB는 뺀다는 것이지요. ESP에서 16진수 0D8만큼의 값을 빼라는 것입니다. 그럼 대체 왜 값을 뺸다는 것일까요?

사실 이부분은 스택을 할당받는 부분입니다. 스택을 할당받을 때 스택주소는 작아집니다. 즉 ESP가 0019F800일 때 스택을 하나 할당하면 현 스택의 주소는

0019F800-4 = 0019F7FC가 된다는 것이지요.

0D8은 10진수로 216이고 하나의 스택은 4바이트이므로 

SUB ESP, 0D8은 216/4 = 54개의 스택을 할당받는 명령어입니다.

실제로 명령어를 실행하면 54개의 스택이 늘어난 것을 확인할 수 있습니다.

자 이제 다음 의미있는 명령어를 살펴봅시다.

잘 보이실지 모르겠네요. 빨간 박스 친 부분이 의미있는 부분이니까 하나씩 살펴봅시다.

* MOV DWORD PTR SS:[EBP-14],0

주소가 [EBP-14]인 스택에 0을 대입합니다. 이 명령어가 왜 있을까요? 정답은 소스코드에 있습니다.

int a,b = 0;

소스를 보시면 변수 b에 0을 대입하라고 되어있습니다. 즉 여기서 주소가 [EBP-14]인 스택은 b를 의미하는 것을 알 수 있습니다. 그러면 하나 의문이 생깁니다.

왜 a는 없죠?

답을 드리자면 a는 선언만 되어있기 때문에 스택을 할당받지 않습니다.

int a,b = 0 라는 것은 

int a

int b = 0

을 뜻하기 때문에 선언만 된 a는 스택을 할당받지 않고 0이 들어간 b만 스택에 할당받습니다. 즉 주소가 [EBP-14]인 스택은 b를 의미합니다. 다음으로 넘어갑시다.

* PUSH 0, PUSH OFFSET 00EC588C ~~~~~~

이 부분은 Messagebox함수를 호출하는 부분입니다. 소스코드를 보면

MessageBox (NULL, L"Make me think your HD is a CD-Rom.",L"abex' 1st crackme", MB_OK);

입니다. 그런데 뭔가 이상하지 않습니까?

분명 첫 인자는 HWND인데 왜 int형인 type이 먼저 들어갈까요?

이것은 스택의 구조가 후입선출 구조이기 때문입니다.

후입선출이란 나중에 들어온 값이 먼저 나가는 구조를 뜻합니다. 그런데 후입선출이랑 인자전달이랑 무슨 관계가 있을까요?

c언어에서 함수를 실행할 때 인자가 왼쪽부터 전달되는 것은 다들 아실 겁니다. 즉 여기서 Messagebox함수를 실행하면 인자를 hwnd부터 받습니다. 헌데 스택은 후입선출구조입니다. 스택에 값을 hwnd부터 넣어버리면 나중에 Messagebox가 실행되서 스택에서 값을 꺼낼 때 맨 나중에 넣은 MB_OK(type)가 먼저 꺼내지게 됩니다. 이렇게 되면 오류가 생겨버립니다.

그래서 HWND가 먼저 스택에 꺼내질 수 있도록 가장 나중에 넣는 것입니다.

아 그리고 명령어를 보시면 OFFSET이라는 글자가 들어있습니다. 이것은 상수를 의미합니다. C언어로 따지면 const 비슷한 겁니다.

OFFSET 주소 식으로 적혀있으니 이 주소에 있는 값들은 상수이다 라는 것을 의미합니다. 주소가 00EC588C 라고 되어있으니 어떤 값이 들어있는지 한번 봅시다.

우리가 입력한 문자열이 들어있습니다. 유니코드 상수 문자열이므로 영어임에도 불구하고 2바이트로 잡혀있습니다. 만약 문자열 내용을 바꾸시고 싶으시면 여기서 수정하실 수 있습니다.

그 다음 명령어는 GetDrivetype함수를 호출합니다. 이 함수가 호출되고 나면 결과값은 EAX에 저장됩니다. 간단하니까 넘어갑시다.

첫줄을 보면 MOV DWORD PTR SS:[EBP-8],EAX 라고 되어있습니다.

아까 EAX가 GetDrivetype함수의 결과값이 저장되어있다고 했죠? 그 EAX값을

주소가 [EBP-8] 인 스택에 저장시킨다고 나와있습니다. 소스코드로 보면 

a = GetDriveType(L"C:\\"); 입니다. 즉 [EBP-8] 주소의 스택이 a라는 것을 알 수 있습니다. 해당스택을 확인하시면 결과값이 저장되어 있는 것을 알 수 있습니다.

2~4번째 줄은 차례대로 

MOV EAX,DWORD PTR SS:[EBP-14], 

ADD EAX,1, 

MOV DWORD PTR SS:[EBP-14],EAX 인 것을 확인 할 수 있습니다.

아까 [EBP-14]인 스택은 b라고 설명했죠? 그 b의 값을 EAX에 저장시키고 EAX의 값을 1 증가시킨 다음 다시 b에 저장시키는 명령어입니다. 소스코드로 보면

b += 1; 입니다. c에선 한줄로 되는 것을 어셈블리어에선 3줄로 처리합니다.

5~7번째 줄을 봅시다.

MOV EAX,DWORD PTR SS:[EBP-8]

SUB EAX,1

MOV DWORD PTR SS:[EBP-8],EAX

[EBP-8]의 스택은 a라고 앞에서 말씀드렸죠?  이 a의 값을 EAX에 저장시키고 EAX값을 하나 뺀 다음 이 EAX값을 a에 저장시키는 명령어입니다. 소스코드로 보면

a -= 1 입니다.

그 다음은 아까 설명했던 것들이니 넘어갑시다. 차례대로 b += 1 b += 1 a -= 1 을 실행합니다.

자 밑에서 3번째 줄부터 한번 봅시다.

MOV EAX,DWORD PTR SS:[EBP-8]

CMP EAX,DWORD PTR SS:[EBP-14]

JE SHORT 00EC147D

[EBP-8]의 스택은 a이니까 a의 값을 EAX에 저장시키고 그 EAX와 [EBP-14]값의 스택을 비교합니다. [EBP-14]의 스택은 b이므로 a의 값과 b의 값을 비교하는 명령어입니다.

CMP명령어와 두 값을 비교해서 같으면 ZF를 1로 아니면 ZF를 0으로 만들어 버립니다. 그리고 JE명령어는 ZF가 1이면 점프 0이면 점프를 하지 않습니다.

소스코드로 확인하면  if(a != b) else입니다

그런데 말입니다. 이 CMP라는 명령어가 단순이 비교하는 명령어가 아닙니다. 좀더 정확히 말하면 CMP 값1 값2 라는 명령어는 둘을 비교해서 값이 같으면 ZF = 1 아니면 ZF 0 이렇게 연산하는 것이 아니라 값1에서 값2를 뺀 값이 0이냐 아니냐로 계산합니다.

현재 EAX값이 1이지요? 그리고 DWORD PTR SS:[EBP-14]값은 3입니다.

즉 이 두 값을 CMP한다는 것은 1에서 3을 뺀 값을 이용해 연산을 하는 것을 의미합니다.

1에서 3을 빼면 -2가 나오죠? 즉 -2는 0이 아니기 때문에 ZF를 0으로 설정합니다.

반대로 EAX가 3이라면 3에서 3을 빼니까 값이 0이 나오죠? 결과값이 0이기 때문에 ZF를 1로 설정하는 식입니다.

좋은 예를 한번 들어봅시다. 지금부터 밑의 JE명령어를 JB로 바꿔봅시다.

JB명령어는 CMP연산 결과의 값이 0보다 작을 경우 점프하는 명령어입니다.

ZF를 한번 봅시다.

CMP결과값이 0이 아니므로 ZF도 0입니다.

이번에는 CMP명령어 부분에서 EAX값을 4로 바꿔봅시다.

둘다 ZF가 0임에도 불구하고 EAX가 1인 경우에는 점프하고 EAX가 4인 경우에는 점프를 하지 않습니다. 이는 결과값이 음수일 경우에만 JB에서 점프하기 때문입니다.

즉 ZF에 의해 점프를 하는 명령어가 있고 아닌 명령어가 있다는 것입니다. 

그런데 왜 이런것을 설명하냐면 if문의 조건이 여러 종류로 나올 수 있기 때문입니다. 

소스에서는 if(a != b) 였지요? 하지만 if(a>b) 일 수도 있고 if(a<=b)일 수도 있습니다. 이런식의 조건문을 연산할 때에는 (CMP,JE) (CMP,JNE) 가 아닌 (CMP,JB) (CMP,JA)  등 다양하게 나올 수 있기 때문에 CMP가 나왔는데 무조건 ZF만 보는 식은 코드를 분석하는데 어려움을 겪을 수 있다는 것을 알아두시기 바랍니다.

조금 이야기가 샜습니다. 다음으로 넘어갑시다.

이제 분기에 따라 메세지함수를 출력하면 처리는 다 끝난 것입니다. 이제 스택 정리를 해야합니다. 한번 봅시다.

* XOR EAX,EAX

EAX를 초기화시켜주는 명령어입니다. 이 명령어가 수행되면 EAX는 0이 됩니다.

소스코드는 return 0 입니다.

EAX가 함수의 반환값을 저장시키는 곳이니 0을 반환하겠다는 뜻입니다.

* ADD ESP, 0D8 

아까 SUB ESP, 0D8로 스택프레임을 할당한 것 기억하시나요? 이제 WinMain함수 처리가 끝났으므로 WinMain의 스택프레임은 필요가 없습니다. 그러므로 할당된 스택프레임을 해제하는 것입니다.

*MOV ESP,EBP

0D8 스택프레임은 해제되었으나 아직 약간의 스택프레임이 남아있을 수 있으니 완전히 해제하기 위해 스택위치를 돌려놔야합니다.. 현재 함수시작위치의 주소값을 ESP에 저장시킵니다.

*POP EBP

이전 스택프레임으로 돌아가기 위해 백업해놓은 EBP를 빼냅니다.

*RETN 10

WinMain함수를 호출할 때 리턴주소를 저장시켜 놓은 것 기억하십니까? 이제 그주소로 가는 것입니다.  그런데 뒤쪽에 숫자가 있죠? 이 숫자 10은 ESP+10을 뜻합니다.

16진수 10은 10진수로 16이므로 ESP+10은 스택 4개를 해제하겠다는 뜻인데요. 왜 스택 4개를 해제하는 것일까요? 

우리는 함수를 호출할 때 인자를 스택에 푸쉬한다는 것을 알 수 있습니다. 그리고 WinMain의 인자는 4개 입니다. 즉 각 인자를 푸쉬한 스택을 해제한다는 것을 알 수 있습니다. 이 것을 이용하면 이 함수가 인자를 몇개 받았는지 단박에 알 수 있지요.

RETN 4 -> 인자 1개 RETN 8 -> 인자2개 RETN C -> 인자 3개

이해하셨지요?

그러면 여기서 의문이 하나 생깁니다. 숫자가 없는 RETN은 뭔가요? 하고 말이지요.

그런 경우는 함수호출방식이 __cdecl이거나 __fastcall방식일 경우입니다.

__cdecl은 푸쉬한 스택을 해제할 때 함수내부에서 하지 않고 함수처리가 끝나고 리턴주소로 되돌아왔을 때 스택을 해제합니다. 예를 하나 들어봅시다. 우리가 전에 만든 콘솔 기반 abex crack me를 리버싱해보죠

RETN 뒤에 숫자가 없습니다. RETN 명령어를 실행해 봅시다.

우리가 만든 콘솔기반 abex crack me의 main함수는 __cdecl방식입니다. 그렇기 때문에 함수내부에서 처리를 하지 않고 밖으로 나온 뒤 푸쉬한 스택만큼 스택을 해제합니다.

응용기반 abex crack me는 함수내부에서 스택을 정리하기 때문에 밖으로 나왔을 경우에는 따로 스택 정리를 하지 않습니다. WinMain이 __stdcall방식이기 때문이죠.

__fastcall방식은 나중에 자세히 알려드리겠습니다.

네 리버싱 분석이 끝났군요. 분명 같은 동작을 하는 프로그램이지만 분석할 때는 조금 다르지요? 게다가 직접 만들었으니 더욱 재미가 있는 것 같습니다.

이상으로 포스팅을 마치겠습니다.






Posted by englishmath
,