콜백함수와후킹기법 (Callback functions and Hooking Techniques) Win32 API 를이용하여어플리케이션을개발하다보면, 콜백함수에대한내용을많이보게된다. 그렇다면이렇게흔히보게되는콜백함수란과연무엇일까? 그리고, 이런콜백함수를이용해서할수있는것중에서후킹기법을이용하면윈도우운영체제에서사용할수있는여러가지마우스작업이나키보드작업등을중간에서조작하여활용하는것이가능하다. 이번장에서는콜백함수와후킹기법에대해알아보도록한다. 콜백함수 (Callback functions) 콜백함수는대단히유용함에도별로사용되지않는것중의하나이다. 이를활용하면중복된코드의양을줄일수있으며, 읽기가쉬우면서도직관적인모듈을디자인할수있다. 콜백이란프로시저나함수를다른프로시저나함수의파라미터로넘겨서그함수에특정이벤트가발생할때호출될수있도록하는함수를말한다. 콜백함수의실행이완료되면제어권은원래의프로시저 ( 함수 ) 로넘어온다. 예를들어, 객체의배열이있고이객체들의특정메소드를공통으로실행시키고싶다고하자. 이경우에는배열을루프를돌리면서특정메소드를호출하는방법을사용할수있다. 그러면, 동시가아닌여러시점에서여러개의다른메소드를실행시키고싶다고하자. 이때에는루프와같은단순한방법으로는해결할수가없다. 이럴때콜백함수를활용하면유용한해결책을구할수있다. 즉, 객체들에적용해야하는마스터프로시저를작성하고이들각각이각객체를파라미터로넘겨주면서콜백프로시저를호출하면된다. 또한, 콜백함수를달리말하면어플리케이션에서구현된함수의주소를 DLL 에포함되어있는함수에보낼수있는것을말한다. DLL 의함수는어플리케이션의함수에게다시정보를담아서보낸다. 콜백함수와 Win32 API 콜백함수를설명할때빠지지않고단골로설명하는윈도우 API 함수에는 EnumFonts() 함수가있다. EnumFonts() 함수는주어진장치컨텍스트에서사용이가능한폰트를나열하는함수이다. 각각의나열되는폰트는 EnumFonts() 함수가어플리케이션의함수로콜백을하게된다. 즉, 각폰트에대한정보를돌려주는것이다. 이런과정이나열할폰트가모두나열되거나, 콜백함수가더이상의나열을중지하고자하는의미의 0 을반환할때까
지계속된다. 대부분의콜백함수를파라미터로사용할수있는윈도우 API 함수는 lpdata 파라미터역시사용할수있도록허용하고있다. 보통이런 lpdata 파라미터는어플리케이션이정의한데이터를나타내는역할을한다. lpdata 파라미터가 DLL 에전달될때에는보통 LongInt 형으로전달된다. 그러므로, LongInt 형의데이터가아닌보다복잡한형태의데이터를넘기고자할경우에는데이터구조체의포인터를 LongInt 로형변환 (typecast) 한뒤에 DLL 의함수를호출한다. 그리고, DLL 의함수에의해서처리된후에콜백함수로넘어오는 LongInt 형의데이터를다시포인터로형변환하면데이터구조체에접근할수있게된다. 앞에서도잠시언급했지만, 콜백함수가윈도우 API 함수에의해호출될때에는윈도우 API 함수에넘겨준 lpdata 파라미터가다시콜백함수가사용할수있도록넘어오게된다. 이렇게함으로써어플리케이션에서전역변수를선언하지않아도프로세스간의장벽을넘을수있게된다. 간단한예를들어보면모든가능한폰트를드롭-다운리스트박스에보여주는대화상자를가정해보자. 이렇게하려면, WM_INITDIALOG 메시지를처리할때 EnumFonts() 함수를호출해야하는데, 이때폰트의정보를처리할콜백함수의주소를담아서호출해야한다. 콜백함수는아마도넘어온각폰트의이름을리스트박스에추가하는역할을하게된다. 이때문제가하나있는데, 콜백함수가리스트박스에대한핸들을가지고있지않기때문에, 리스트박스에대한핸들을전역변수에담아서처리해야한다. 그런데, 이때 lpdata 파라미터를이용해서리스트박스의핸들을콜백함수에서받게되면이러한문제를해결할수있다. 물론전역변수를쓰는것도하나의해결책이되겠지만, 언제나전역변수가적은수로유지하는것이좋은습관이므로가능한 lpdata 파라미터를활용하는것이좋다. 전역변수를사용할때또하나의문제점은콜백함수를 16 비트 DLL 에구현할경우에는데이터를저장한전역변수가콜백을이용할때데이터가파괴될가능성이있다. 예를들어, 앞의폰트대화상자를여러개의어플리케이션에서사용한다고가정하자. 아마도이런경우에는각각의어플리케이션에서폰트대화상자를구현하기보다는, 폰트대화상자를 DLL 에구현하고이를사용하기를원할것이다. 그런데, 이때 DLL 에서리스트박스의핸들을저장하기위해전역변수를사용한다면, 하나이상의어플리케이션이동시에대화상자를로드할때문제가발생할수있다. 각각의폰트대화상자의복사본 (copy) 은각기다른리스트박스의핸들을담게되므로, 전역변수를사용해서는문제가발생하게된다. 이럴때에는리스트박스의핸들을 lpdata 파라미터를이용해서 EnumFonts() 함수에넘겨주면된다. 이런문제는 Win32 에서는 DLL 이독립적인기억공간을확보하기때문에문제가되지않는다. 콜백함수를이용한 API 활용예제 콜백함수를이용해서현재시스템에서돌아가는메인윈도우의캡션을보여주는예제를가
지고콜백함수의기본적인사용방법을익혀보도록하자. 먼저사용할윈도우 API 함수인 EnumWindows API 함수의정의부분을살펴보자. function EnumWindows(lpEnumFunc: TFNWndEnumProc; lparam: LPARAM): BOOL; stdcall; 여기서 lpenumfunc 의파라미터는콜백함수의주소를넘기게되며, lparam 파라미터는어플리케이션에서정의한데이터를나타낸다. 이때콜백함수의프로시저형은윈도우도움말을바탕으로어플리케이션의 type 선언문에서선언해주어야한다. 그러므로, 다음과같은형태의프로시저형을어플리케이션의인터페이스섹션에선언한다. 여기서파라미터의데이터형만맞으면콜백함수로사용하는데에는전혀지장이없다. 사실이런프로시저형을직접 type 선언문에선언하지않아도사용할콜백함수의파라미터의데이터형과순서, 수만맞으면아무런상관없이사용할수있다. type EnumWIndowsProc = function(hwnd: THandle; Param: Pointer): Boolean; stdcall; 첫번째파라미터는각메인윈도우의핸들이고, 두번째파라미터는 EnumWindows 함수를호출할때넘겨주는값이다. 사실파스칼에서 TFNWndEnumProc 형은제대로정의된것이아니고, 단지포인터일뿐이다. 즉, 함수에적절한파라미터를넘겨주고나서는이를포인터로사용해서호출하는대신그함수의주소를이용한다는것을의미한다. 새로운어플리케이션을시작하고폼에리스트박스와버튼을하나씩얹어서폼을다음그림과같이디자인한다.
그러면콜백함수로사용할함수를다음과같이제작한다. function GetCaption(HWnd: THandle; Param: Pointer): Boolean; stdcall; var Text: String; SetLength(Text, 100); GetWindowText(HWnd, PChar(Text), 100); Form1.ListBox1.Items.Add(IntToStr(HWnd) + ':' + Text); Result := True; 이런콜백함수는파라미터만맞으면사용이가능하다. 이콜백함수의역할은윈도우의캡션을문자열로읽어서리스트박스에추가한다. 그러면, 이콜백함수를이용하기위한 Button1 의 OnClick 이벤트핸들러를다음과같이작성하도록한다. procedure TForm1.Button1Click(Sender: TObject); var EnumWin: EnumWindowsProc; ListBox1.Items.Clear; EnumWin := GetCaption; EnumWindows(@EnumWin, 0); 앞의코드는단지이렇게사용할수도있다는것을보여주기위한것으로, 변수로선언한 EnumWin 의선언과이변수에 GetCaption 함수의주소를저장해서 EnumWindows API 함 수를호출하는과정을다음과같이간단히처리하는것과내용은같은것이다. procedure TForm1.Button1Click(Sender: TObject); ListBox1.Items.Clear; EnumWindows(@GetCaption, 0);
이제이프로그램을실행하고버튼을클릭하면다음과같은실행화면을볼수있을것이다. 콜백함수의실용적활용 콜백함수를이용하는방법을익히는좋은예제로디렉토리를뒤지면서파일을검색하고, 파일이발견될때마다콜백함수를호출하여이를표시하는등의것을예로들수있겠다. 콜백함수를사용하기위해서는먼저프로시저형을유닛의 type 섹션에다음과같이선언해야한다. TFileSearchCallback = procedure(filename: string) of Object; 이와같이콜백함수를사용하기위해서는객체의메소드로서의프로시저형을선언해서사 용한다. 여기서파라미터형이맞을경우얼마든지콜백으로사용이가능하다. 그리고나서는실제로콜백함수를호출할프로시저를다음과같이선언한다. procedure SearchDirectory(Dir, Condition: string; cb: TFileSearchCallback); 이프로시저의형태를살펴보면검색을할디렉토리와검색할문자열을나타내는파라미터인 Dir, Condition 은다른프로시저의형태와별로다를것이없지만, cb 파라미터에서앞에서선언한 TFileSearchCallback 프로시저형을사용하는것이중요한부분이다. 이파라미터의의미는여기에콜백함수를대입하여콜백을이용할수있게된다는것이다.
이제실제로디렉토리를검색하는예제어플리케이션을작성해보도록하자. 새로운어플리케이션을시작하자. 앞에서설명한대로콜백프로시저형을 type 섹션에선언하고, SearchDirectory 프로시저를 private 섹션에선언한다. 그리고, 다음과같이구현한다. procedure TForm1.SearchDirectory(Dir, Condition: string; cb: TFileSearchCallback); var SearchRec: TSearchRec; Value: LongInt; Value := FindFirst(Dir + '\' + Condition, faanyfile, SearchRec); while Value = 0 do cb(dir + '\'+ SearchRec.Name); Value := FindNext(SearchRec); 기본적으로이프로시저는 Dir 파라미터로넘어온디렉토리를검색해서파일을찾게되면콜백함수를호출하는데, 콜백함수에게현재파일의경로를포함한파일이름을파라미터로넘겨주게된다. 그러면이제실제로콜백을수행할콜백함수를작성하고예제를눈에보일수있도록만들어보자. 폼에 TButton, TListBox, TEdit, TDriveComboBox, TDirectoryListBox 컴포넌트를하나씩올려놓도록하자. 그리고 Button1 의 Caption 프로퍼티를 시작 으로, Edit1 의 Text 프로퍼티를 로다음과같이설정한다. 여기서 TEdit 컴포넌트에검색할문자열을입력하고버튼을클릭하면해당되는파일을찾아서파일의이름일 TListBox 컴포넌트에추가하도록하는것이다. 그리고, TDriveComboBox 와 TDirectoryListBox 를연결하기위해 DriveComboBox1 의 DirList 프로퍼티를 DirectoryListBox1 으로설정한다.
그리고나서, 다음과같이콜백프로시저를 private 섹션에선언한다. procedure FileSearchCallback(FileName: string); 이콜백함수는디렉토리에서파일을찾게되면, 파일이름을 TListBox 컴포넌트에추가하 여보여주는간단한함수로다음과같이작성하도록한다. procedure TForm1.FileSearchCallback(FileName: string); Listbox1.Items.Add(ExtractFileName(FileName)) 이제 Button1 의 OnClick 이벤트핸들러를다음과같이작성한다. procedure TForm1.Button1Click(Sender: TObject); ListBox1.Items.Clear; SearchDirectory(DirectoryListBox1.Directory, Edit1.Text, FileSearchCallback); 여기서앞에서예를든다른프로시저와는달리콜백함수의주소를 @FileSearchCallback 으로넘겨주지않고직접 FileSearchCallback 으로넘겨주는것에주목한다. 이것은델파이가사용하는오브젝트파스칼의특성에기인한것으로, C/C++ 의경우에는기본적으로객체의주소를포인터로선언하고, 이를이용해서프로그래밍을해야하므로명시적으로함
수의주소를가리키는연산자를사용해야하지만, 오브젝트파스칼은모든기본객체들이참조로사용되기때문에주소를나타내는 @ 를사용하지않아도잘작동한다. 이제이어플리케이션을컴파일해서실행하고, 적당한디렉토리와검색문자열을입력한후버튼을클릭하면다음과같은실행화면을얻을수있을것이다. 후크함수 (Hook functions) 후크함수는윈도우메시지시스템에삽입되어메시지에대한처리가일어나기전에메시지스트림에접근해서이를처리할수있는콜백함수이다. 흔히, 메시징시스템에다른후크가있는경우가있는데, 이때에는후크연쇄 (Hook chain) 에서의다음의후크를호출해서이들이모두동작할수있도록해야한다. 후크를이용해서시스템에서메시지트래픽을모니터하는서브루틴을제작한다든가, 목적윈도우프로시저에도달하기전에일부메시지를처리하는등의일을할수있다. 후크는각각의메시지를처리할때거쳐야하는과정을증가시키기때문에시스템의속도를다소저하시키는경향이있다. 그러므로, 후크는꼭필요할때설치했다가가능한빨리제거해주어야한다. 후크함수를설치할때에는 SetWindowsHookEx() API 함수를이용한다. 이함수를호출하면설치된후크함수에대한 32 비트핸들을얻게되며, 이핸들을이용해서후크를제거하거나다음의후크를호출할때이용한다. 후크를제거할때에는 UnHookWindowsHookEx() API 함수를, 다음후크를호출할때에는 CallNextHookEx() 함수를호출한다. 이때시스템전반에걸친호크는후크함수가 DLL 에위치해야하지만, 어플리케이션에지정된후크함수는어플리케이션이나 DLL 에모두위치할수있다.
후크연쇄 (Hook Chains) 윈도우는다른종류의많은후크의종류를가지고있다. 각각의후크의형은윈도우메시지처리메커니즘의다른면에접근한다. 예를들어, 마우스메시지에대한메시지트래픽을모니터할때에는 WM_MOUSE 후크를이용한다. 윈도우는각각의후크종류에따라분리된후크연쇄 (hook chain) 를가지고있다. 후크연쇄는후크프로시저로불리는어플리케이션이정의한콜백함수에대한포인터의리스트이다. 특정한종류의후크와연관된메시지가발생하면윈도우는메시지를후크연쇄에서참조하는각각의후크프로시저에넘기게된다. 후크프로시저의동작은연관된후크의종류에따라달라진다. 일부의후크프로시저는메시지를모니터할뿐이지만, 일부의경우에는메시지를변경하거나전달을중지시킬수있다. 후크프로시저설치와해제할때주의점 후크프로시저를설치할때에는 SetWindowsHookEx 함수를이용한다. 그리고, 이함수를호출할때에는호출하는후크의종류와프로시저가모든쓰레드와연관되어야하는지, 아니면특정쓰레드와연관되는지여부그리고프로시저엔트리포인터등을지정하게된다. 후크프로시저를설치하는어플리케이션은반드시전역후크프로시저를 DLL 에분리해야한다. 대신후크프로시저를설치하기전에 DLL 모듈의핸들을가지고있어야한다. 즉, LoadLibrary 함수를이용해서 DLL 모듈의핸들을얻은후, 이렇게핸들을얻으면 GetProcAddress 함수를이용해서후크프로시저의주소를얻을수있게된다. 마지막으로 SetWindowsHookEx 함수를이용해서후크프로시저의주소를적절한후크연쇄에설치한다. SetWindowsHookEx 함수에는모듈의핸들과후크프로시저의엔트리포인트, 그리고쓰레드에대한 identifier 을넘겨주는데, 보통은이값으로 0 을넘겨서시스템의모든쓰레드에반응할수있도록한다. 특정쓰레드에대한후크프로시저를해제 ( 후크연쇄에서프로시저의주소를삭제하는것 ) 할때에는 UnhookWindowsHookEx 함수에후크프로시저의핸들을지정해서호출하게된다. 전역후크프로시저를해제할때에도 UnhookWindowsHookEx 함수를호출하기는마찬가지이다. 그런데, 이함수는후크프로시저를담고있는 DLL 을해제하지않는다. 이는전역후크프로시저는모든윈도우기반의어플리케이션에서호출되기때문에, 이들이암시적으로 LoadLibrary 함수를호출하기때문으로, 이들이모두 FreeLibrary 를호출하게할수있는방법이없다. 그러므로, 결국에는모든어플리케이션을중지시키거나또는이들이모두 FreeLibrary 를호출하게해야한다. 이를해결하기위해서전역후크프로시저를설치할때에는설치함수를 DLL 에후크프로시저와함께제공한다. 이렇게하면설치어플리케이션이 DLL 모듈의핸들을가지고있을필요가없다. 각어플리케이션은반드시 DLL 과연결해야만후크를설치할수있게되고,
설치함수는 SetWindowsHookEx 함수의호출을통해 DLL 의모듈핸들을제공한다. 플리케이션은종료될때이 DLL 의핸들을통해서후크를해제하게된다. 어 윈도우후크의종류 윈도우의후크에는다음과같은종류들이있다. WH_CALLWNDPROC: SendMessage() 함수가호출될때마다호출되는윈도우프로시저후크 WH_CBT: CBT(computer based training) 후크는윈도우를생성, 파괴, 최대화, 이동, 크기변화등의이벤트가있거나, 마우스나키보드이벤트를제거하기전에또는입력포커스의변경이나시스템메시지큐의동기화기있기전에호출되는후크이다. WH_GETMESSAGE: GetMessage() 함수가어플리케이션큐에서메시지를가져올때마다호출되는후크이다. WH_HARDWARE: 어플리케이션이 GetMessage(), PeekMessage() 함수를호출하고, 처리할하드웨어이벤트 ( 마우스와키보드이벤트를제외한 ) 가있을때호출되는후크이다. WH_JOURNALRECORD: 시스템메시지큐에서메시지를삭제할때호출되는후크이다. WH_JOURNALPLAYBACK: 마우스나키보드메시지를시스템메시지큐에삽입할때이용되는후크이다. WH_KEYBOARD: 어플리케이션이 GetMessage(), PeekMessage() 함수를호출하고, WM_KEYUP 또는 WM_KEYDOWN 키보드메시지를처리해야할때호출되는후크이다. WH_MOUSE: 어플리케이션이 GetMessage(), PeekMessage() 함수를호출하고, 마우스메시지를처리해 야할때호출되는후크이다.
WH_MSGFILTER: 대화상자나메시지상자또는메뉴가메시지를불러올때, 메시지가처리되기전에호출되 는후크이다. WH_SHELL: 시스템이만들어놓은 notification 메시지가있을때호출되는후크이다. WH_SYSMSGFILTER: 대화상자나메시지상자또는메뉴가메시지를불러올때, 메시지가처리되기전에호출되 는시스템전체에대한후크이다. 기본적인후크예제 후크를어떻게설치하고해제하는지를코드를통해서알아보자. 시스템후크로사용할함수를 DLL 파일에담고, 이 DLL 을사용하여시스템후크를설치, 제거하는어플리케이션을하나제작한다. DLL 파일은 ExamLib3.DLL, 어플리케이션은 Exam3.EXE 로한다. 먼저, DLL 파일을만들기위해새로운 DLL 프로젝트를하나시작한다. 그리고다음의코드를입력하자. library ExamLib3; Uses SysUtils, Windows, Dialogs; function KeyboardProc(Code, WParam, LParam: Integer): LRESULT; stdcall; var Val: Integer; Val := -1; if (Code >= 0) then Val := CallNextHookEx(0, Code, WParam, LParam); Result := Val; exports KeyboardProc index 1;
end. 이 DLL 파일은실제로아무동작을하지않고, 다음의후크를호출하는역할만을한다. 그러면, DLL 파일을이용해서후크를설치, 해제하는어플리케이션을제작하자. 폼을생성할때후크를설치하고, 폼을파괴할때후크를해제하도록한다. 후크의핸들을 public 멤버인 HandleHook 에저장하고, DLL 의핸들을 private 멤버인 hinst 에저장하도록하자. 어플리케이션의소스는다음과같다. unit U_Exam3; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(tform) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private hdll: HInst; public HandleHook: HHook; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject);
hdll := LoadLibrary('ExamLib3'); if hdll = 0 then ShowMessage('DLL 을로드하지못했습니다.'); HandleHook := SetWindowsHookEx(WH_KEYBOARD, GetProcAddress(hDLL,PChar('KeyboardProc')), hdll, 0); if HandleHook = 0 then ShowMessage(' 후크를설치하지못했습니다.'); procedure TForm1.FormDestroy(Sender: TObject); if not UnhookWindowsHookEx(HandleHook) then ShowMessage(' 후크를해제하지못했습니다.'); end. 즉, 후크를설치할때우선 DLL 파일을 LoadLibrary 함수를이용해서메모리에적재한후 DLL 파일에저장되어있는함수의주소를후크프로시저로설정한다. 이때윈도우 API 함수인 SetWindowsHookEx 를사용하는데, 후크를설치하지못하면 0 이반환된다. 후크를제거할때에는설치된후크의핸들을이용해서 UnhookWindowsHookEx 함수를호출하면된다. 이함수의경우에는후크를제거하지못하면 False 를반환한다. 후크를이용한매크로작성기제작 이번에는실제사용가능하도록후크를응용해보자. 아래아한글이나워드, 그밖의많은에디터에보면대부분의경우키보드와마우스동작을기록했다가저장하고, 이를다시재생할수있는매크로기능을제공한다. 그러면, 윈도우의후크프로시저를이용해서키보드와마우스입력을기록했다가이를다시재생할수있는 DLL 과이를이용한간단한예제어플리케이션을제작해보자. 이예제는볼랜드에서제공하는기술백서 (Technical white paper) 에공개되었던어플리케이션에기초한것임을미리밝혀둔다. 이렇게메시지를기록하고, 재생하기위해서는시스템전체에적용되는 (system wide) WH_JOURNALRECORD, WH_JOURNALPALYBACK 윈도우후크를이용한후크함수를사용하게된다. 이때각각의윈도우후크에대한후크프로시저를 JournalRecord() 와
JournalPlayback() 이라고하자. JournalRecord 와 JournalPlayback 후크의콜백함수를설치하기위해윈도우 API 함수인 SetWindowsHookEx() 에후크콜백함수의주소를넘겨주어야한다. 이때 JournalRecord 와 JournalPlayback 콜백함수는시스템후크 (system wide hook) 이므므로, 반드시 DLL 에위치해야하며후크콜백함수의주소는인스턴스화한주소를넘겨주지않아도된다. 이함수를호출하면후크콜백함수에대한 32 비트핸들을반환하게되는데, 이핸들을이용해서다른후크함수가이미후크연쇄 (hook chain) 에존재하는지확인하고, 기록이끝나면후크를연쇄에서제거하는등의일을할수있다. 일단 JournalRecord 콜백함수의주소를가지고 SetWindowsHookEx() 를호출하면, 윈도우는즉시메시지의기록을시작한다. 이때 JournalRecord 콜백함수가호출되는조건은다음과같다. 1. 기록할키보드나마우스이벤트가존재할때 (HC_ACTION) 2. 시스템이모달상태에들어가거나 (HC_SYSMODALON), 모달상태에서빠져나올경우 (HC_SYSMODALOFF). 3. 시스템이후크연쇄에서다음후크를호출하기원할때 키보드나마우스이벤트가있을경우에는 HC_ACTION 후크와같은경우인데, 이이벤트를기록할수있다. 이때 EVENTMSG 형의구조체에대한포인터가 JournalRecord 콜백함수의 lparam 파라미터에담겨서넘어오게된다. 이벤트가발생한시스템시간은 EVENTMSG 구조체의 time 파라미터에담겨있다. 재생을하기위해서는 EVENTMSG 구조체를복사하고, 그복사본의 time 을적절하게조절하면된다. 즉, 메시지를기록할때시스템시간을얻은후각메시지의시간에서처음에얻은시스템시간을빼면, 기록이시작된후얼마의간격으로메시지들이발생했는지알아낼수있다. 이값들을재생이시작되는시간에더하면기록할때와같은간격으로메시지들을발생시킬수있다. 시스템이모달상태에들어갔을경우에는 HC_SYSMODALON 후크와같은경우로이때에는잠시기록을멈추고, 후크연쇄의다음메시지후크를호출하게하여야하며, 모달상태에서빠져나올경우의후크인 HC_SYSMODALOFF 의경우에다시기록을재개하도록하면된다. 만약코드값이 0 보다작을경우에는시스템이후크연쇄의다음메시지후크를호출할것을요구하게되며, 이때에는이를따라야한다. 그리고, 기록이끝났을경우에는윈도우 API 함수인 UnHookWindowsHookEx() 을 SetWindowsHookEx() 함수를호출했을때얻은 32 비트핸들을파라미터로해서호출하면후크콜백함수를후크연쇄에서제거하게된다. 기록된메시지를재생하고자할때에는또다시 SetWindowsHookEx() API 함수를 JournalPlayback 콜백함수의주소를파라미터로해서호출하면윈도우는즉시재생을시작하게된다. 재생을하는동안에는정상적인마우스와키보드입력이시스템에의해중지
된다. JournalPlayback 콜백함수는다음의조건에부합될때각각다음과같은역할을하여야 한다. 1. HC_SKIP: 다음메시지를불러온다. 수를호출해서, 후크를제거한다. 재생할메시지가더이상없으면 UnHookWindowsHookEx() 함 2. HC_GETNEXT: 현재의메시지를재생한다. 3. HC_SYSMODALON: 시스템이모달상태에들어간경우로, 메시지재생중에모달상태로들어간다는것은시스템에어떤문제가생겼음을의미한다. 그러므로, 후크연쇄의다음후크를호출하여이를처리할수있도록해준다. 4. HC_SYSMODALOFF: 시스템이모달상태에서빠져나온경우로, 윈도우는 JournalPlayback 콜백프로시저를제 거하고, 후크연쇄의다음후크를호출한다. 5. 코드가 0 보다작은경우 : 시스템이더이상의처리를원하지않고, 다음후크를호출하라고요구하는것이다. 이런시스템후크를구현하기위해서는 SetWindowsHookEx() 함수를호출하기전에, 첫번째메시지를받아서시스템시간을얻어야만각각의기록된메시지들이재생시에동기화되어동작하게할수있다. 이때윈도우는같은메시지를한번이상반복할것인지묻는다. 윈도우가현재메시지를재생할것인지를처음물어올때 JournalPlayback 콜백함수는현재시간과메시지가작동하도록되어있는시간의차이부분을반환한다. 만약이값이음수라면 0 을반환하도록해야한다. 또한, 같은메시지가한번이상요구될경우에도 0 을반환한다.
후크 DLL 의동작방식 실제핵심적인역할을담당하는 DLL 파일에는다음과같은세가지함수와윈도우가사용 하게될두가지후크함수가포함되어있다. 1. StartRecording() 2. StopRecording() 3. Playback() 4. JournalRecordProc() 5. JournalPlaybackProc() StartRecord() 함수는 JournalRecordProc() 후크를설치하고이벤트를기록하기시작하는역할을한다. StartRecord() 를호출하다가에러가발생하거나, 이미매크로를기록하거나재생하고있을때에는더이상의처리를하지않고, 0 을반환한다. 매크로의기록이이루어지는중간에는키보드와마우스이벤트를배열에저장한다. 참고로지나치게많은수의메모리소모를줄이기위해서최대치를설정한다. StopRecording() 가호출되면, 기록한이벤트들을디스크에저장한다. 그리고나서, JournalRecordProc() 후크를제거한다. 이때아무것도기록된것이없을경우에는 1 을후크를제거할때에러가발생하면 2 를에러코드로반환한다. 에러가없을때에는기록된메시지의수를반환한다. Playback() 함수는기록된매크로를재생할때호출한다. 에러가발생하거나, 재생할것이없으면더이상의처리를하지않고 0 을반환한다. 재생이끝나면, 매크로를재생하도록호출한어플리케이션을콜백한다. 그러므로, 이함수를호출하는어플리케이션은콜백함수를작성하여재생이끝났을때의처리를해줄수있다. 데이터의공유를위해서몇개의전역변수를선언했다. 물론이러한데이터공유를위해서는메모리맵파일등의보다세련된방법을선택할수도있겠으나, 여기에대해서는다음에다루도록한다. 일단 EventMsg 구조체배열에대한포인터인 PMsgBuff 라는전역포인터를정의하고, 이를이용해서메모리에이벤트를기록하고재생한다. 일단 DLL 이시작되면이포인터를 nil 로초기화하고, 매크로를기록하거나재생할때에만이포인터가실제로메모리블록을가리키도록한다. 다른경우에는언제나 nil 값을가지도록설정함으로써현재매크로가기록되거나, 재생되는지여부를확인할수있다. 그밖에사용하는전역변수에는다음과같은내용들을담게된다. 1. TheHook: 후크프로시저의 32 비트핸들 2. StartTime: 매크로의기록이나재생이시작되는시간 3. MsgCount: 기록된메시지의총수
4. CurrentMsg: 현재재생되고있는메시지 5. ReportDelayTime: 지체된시간을리포트해야하는지여부 6. SysModalOn: 시스템에현재모달상태에있는지여부 7. cbplaybackfinishedproc: 어플리케이션콜백함수의인스턴스주소 8. cbappdata: Playback() 함수에게넘겨주는어플리케이션데이터의파라미터 후크 DLL 의구현 그러면, 실제로 DLL 을제작해보자. 먼저 DLL 의프로젝트의이름을 Hook2Lib.dpr 로저장한다. 그리고, 사용할데이터형과전역변수를다음과같이선언한다. library Exam4Lib; uses Windows; type TwMsg = LongInt; TwParam = LongInt; TlParam = LongInt; const MAXMSG = 6500; type PEventMsg = ^TEventMsg; TMsgBuff = Array[0..MAXMSG] of TEventMsg; TcbPlaybackFinishedProc = procedure(appdata: Longint); stdcall; var PMsgBuff: ^TMsgBuff; TheHook: HHook; StartTime: Longint; MsgCount: Longint; CurrentMsg: Longint;
ReportDelayTime: Bool; SysModalOn: Bool; cbplaybackfinishedproc: TcbPlaybackFinishedProc; cbappdata: Longint; 상수로선언한 MAXMSG 는메시지를기록할때과도한메모리사용을막기위해서한도를정한것으로조절이가능하다. TcbPlaybackFinishedPrc 프로시저형은 DLL 의 Playback() 함수를호출하는어플리케이션에대한콜백을지원한다. 어플리케이션은이프로시저형에맞는콜백함수를작성하고, DLL 의 Playback() 함수에콜백함수의주소를넘겨주면 Playback() 함수의종료와함께이콜백함수를호출하게된다. 나머지전역변수에대해서는앞에서도간략히언급했으므로자세한설명은생략하겠다. 먼저매크로를기록할후크프로시저인 JournalRecordProc 함수의구현부분을살펴보자. function JournalRecordProc(Code: Integer; wparam: TwParam; lparam: TlParam): Longint; stdcall; Result := 0; case Code of HC_ACTION: if SysModalOn then if MsgCount > MAXMSG then PMsgBuff^[MsgCount] := PEventMsg(lParam)^; Dec(PMsgBuff^[MsgCount].Time, StartTime); Inc(MsgCount); HC_SYSMODALON: SysModalOn := True; CallNextHookEx(TheHook, Code, wparam, lparam); HC_SYSMODALOFF:
SysModalOn := False; CallNextHookEx(TheHook, Code, wparam, lparam); if Code < 0 then Result := CallNextHookEx(TheHook, Code, wparam, lparam); 일단이함수의리턴값을 0 으로설정하고, Code 파라미터의값에따른처리를해준다. 이코드의값이 HC_ACTION 인경우에는기록할키보드나마우스이벤트가있는것이므로 lparam 의메시지를버퍼에저장한다. 이때주의할것은메시지의시간을기록하는것인데, 최초에 StartRecord() 함수가호출되면 StartTime 전역변수에기록시작시각이기록되므로, 이벤트가발생한시각에서 StartTime 변수의값을빼면기록시작시각에서부터얼마뒤에일어난이벤트인지알수있다. 시스템이모달상태에들어가거나빠져나오는경우에는 SysModalOn 전역변수의값을설정하고, CallNextHookEx 함수를호출하여다음후크를실행한다. 또한, 처리할후크코드가아닌경우에도다음후크를실행한다. 그러면, 이러한매크로기록후크함수를설치하고기록을시작하게하는 StartRecording 함수를구현하도록하자. 이함수의구현부분은다음과같다. function StartRecording: Integer; stdcall; Result := 0; if pmsgbuff <> nil then GetMem(PMsgBuff, Sizeof(TMsgBuff)); if PMsgBuff = nil then SysModalOn := False; MsgCount := 0; StartTime := GetTickCount; TheHook := SetWindowsHookEx(WH_JOURNALRECORD, JournalRecordProc, hinstance, 0); if TheHook <> 0 then Result := 1;
end else FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; 먼저기본적인결과값을 0 으로설정하고, 메시지버퍼를검사해서버퍼에메모리가할당되어있으면이는메시지가기록되어있거나, 현재기록중임을의미하므로실행을중지한다. 이상이없으면버퍼에대한메모리를할당하고, 초기전역변수값을설정한다. 특히 StartTime 전역변수의값을설정할때에는 GetTickCount API 함수를이용한다. 그리고, WH_JOURNALRECORD 후크에대한후크함수를설치한다. 이때설치가성공적이면 0 이아닌값이리턴되므로결과값으로 1 을반환하고, 0 이리턴되면버퍼에대한메모리를해제한다. 매크로의기록을중지하는 StopRecording() 함수는구현이다소복잡하다. 단순히후크를해제하는것만이아니라버퍼에기록된메시지를디스크에저장하는역할을해주어야한다. 그렇기때문에파라미터로기록할파일이름을 PChar 형으로넘겨받는다. 실행결과에따라서반환되는결과값이다양한데, 성공적으로메시지가기록되고디스크에저장된경우에는저장된메시지의수를, 저장할메시지가없을때에는 1, 후크함수를제거하는데실패한경우에는 2 를반환하며 I/O 에러가발생한경우에는 0 을반환한다. 구현부분은다음과같다. function StopRecording(lpFileName: PChar): LongInt; stdcall; var TheFile: File; if PMsgBuff = nil then Result := -1; if UnHookWindowsHookEx(TheHook) = False then Result := -2;
TheHook := 0; if MsgCount > 0 then Assign(TheFile, lpfilename); {$I-} Rewrite(TheFile, Sizeof(TEventMsg)); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; Result := 0; {$I-} Blockwrite(TheFile, PMsgBuff^, MsgCount); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; Result := 0; {$I-} Close(TheFile); {$I+} if IOResult <> 0 then {$I-} Close(TheFile); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil; Result := 0; FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; Result := MsgCount; 소스가다소길지만내용은어렵지않다. 그리고, 중복되는부분이있으므로이를단축하면좋을것이다. 특별히설명할만한부분은없지만 $I 컴파일러지시자 (compiler directive) 를통해서 I/O 에러를처리하는부분에대해서잠시알아보자. 디폴트로표준 I/O 프로시저와함수를호출하면자동으로에러를검사하게되어있다. 만약에러가발생하면예외를발생시키거나예외처리가안될경우프로그램은실행을중단한다. 이런자동검사를여부를결정하는컴파일러지시자가 $I 이다. {$I-} 에의해 I/O 에러자동검사가꺼지게되는데, 이때에는 I/O 에러가발생해도예외가발생하지않는다. 그러므로, I/O 작업을한뒤에는 IOResult 함수를호출해서에러여부를알아보아야한다. 에러가발생하지않은경우에는 0 을반환한다. 앞의 StopRecording() 함수역시이런방법을이용해서 I/O 에러를처리한다. 이렇게해서매크로를기록하는부분에대한구현이모두끝났다. 이번에는매크로를재생하는부분을구현하도록하자. 먼저후크함수로사용될 JournalPlayback() 함수를다음과같이구현한다. 기본적인처리방침은 Code 값에따라서, HC_SKIP 인경우에는다음메시지를처리하되, 더이상의메시지가없으면후크함수를제거한다. HC_GENNEXT 는현재메시지를처리한다. 그밖의자세한내용은앞에서설명했으므로생략하도록하겠다. 눈여겨보아야할부분은 code 값이 HC_SKIP 인경우후크를제거할때와 HC_SYSMODALOFF 인경우메시지재생을마치는경우로이때에는어플리케이션에서넘겨준콜백함수를 cbplaybackfinishedproc(cbappdata) 와같은형태로호출한다. 이콜백함수는재생이끝났음을어플리케이션에알리면동작하게되는함수이다. function JournalPlaybackProc(Code: Integer; wparam: TwParam; lparam: TlParam): Longint; stdcall; var TimeToFire: Longint;
Result := 0; case Code of HC_SKIP: Inc(CurrentMsg); ReportDelayTime := True; if CurrentMsg >= (MsgCount-1) then if TheHook <> 0 then if UnHookWindowsHookEx(TheHook) = True then TheHook := 0; FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; cbplaybackfinishedproc(cbappdata); HC_GETNEXT: PEventMsg(lParam)^ := PMsgBuff^[CurrentMsg]; PEventMsg(lParam)^.Time := StartTime + PMsgBuff^[CurrentMsg].Time; if ReportDelayTime then ReportDelayTime := False; TimeToFire := PEventMsg(lParam)^.Time - GetTickCount; if TimeToFire > 0 then Result := TimeToFire; HC_SYSMODALON: SysModalOn := True; CallNextHookEx(TheHook, Code, wparam, lparam); HC_SYSMODALOFF:
SysModalOn := False; TheHook := 0; FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; cbplaybackfinishedproc(cbappdata); CallNextHookEx(TheHook, Code, wparam, lparam); if Code < 0 then Result := CallNextHookEx(TheHook, Code, wparam, lparam); 마지막으로 Playback() 함수를구현하도록하자. 이함수는재생할매크로파일의이름을 PChar 데이터형인파라미터로넘겨받고, 이와함께재생이끝난후호출할어플리케이션의콜백함수와어플리케이션데이터를파라미터로넘겨받아사용한다. 구현부분은다음과같다. function Playback(lpFileName: PChar; EndPlayProc: TcbPlaybackFinishedProc; AppData: Longint): Integer; stdcall; var TheFile: File; Result := 0; if PMsgBuff <> nil then GetMem(PMsgBuff, Sizeof(TMsgBuff)); if PMsgBuff = nil then Assign(TheFile, lpfilename); {$I-} Reset(TheFile, Sizeof(TEventMsg)); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil; {$I-} MsgCount := FileSize(TheFile); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; {$I-} Close(TheFile); {$I+} if IOResult <> 0 then if MsgCount = 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; {$I-} Close(TheFile); {$I+} if IOResult <> 0 then {$I-} Blockread(TheFile, PMsgBuff^, MsgCount); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; {$I-} Close(TheFile); {$I+}
if IOResult <> 0 then {$I-} Close(TheFile); {$I+} if IOResult <> 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; CurrentMsg := 0; ReportDelayTime := True; SysModalOn := False; cbplaybackfinishedproc := EndPlayProc; cbappdata := AppData; StartTime := GetTickCount; TheHook := SetWindowsHookEx(WH_JOURNALPLAYBACK, JournalPlayBackProc, hinstance, 0); if TheHook = 0 then FreeMem(PMsgBuff, Sizeof(TMsgBuff)); PMsgBuff := nil; Result := 1; 소스가다소길지만, 많은부분이중복되고 StopRecording() 함수와마찬가지로 I/O 에러처리를위해서길어진부분이많기때문이므로이해하기는쉬울것이다. 일단재생을하기위해메모리버퍼를설정하고, 디스크에있는메시지를버퍼에읽어들인후콜백함수와어플리케이션데이터를전역변수에저장한다. 그리고, 현재시각을 GetTickCount API 함수를통해서알아낸후 WH_JOURNALPLAYBACK 후크에대한후크함수를설치한다. 재생이성공적으로이루어진경우에는 1 을반환한다. 이로써 DLL 파일의구현이모두끝났다. 이를어플리케이션에서사용하려면함수를
export 해야한다. 이부분은다음과같이구현한다. exports JournalRecordProc index 1 name 'JOURNALRECORDPROC' resident, StartRecording index 2 name 'STARTRECORDING' resident, StopRecording index 3 name 'STOPRECORDING' resident, JournalPlayBackProc index 4 name 'JOURNALPLAYBACKPROC' resident, Playback index 5 name 'PLAYBACK' resident; 그리고, DLL 이처음시작되면메시지버퍼는메모리블록을가리키면안되므로다음과같 은코드를삽입한다. PMsgBuff := nil; end. 후크 DLL wrapper 의작성 앞의 DLL 프로젝트를컴파일한후, 맨처음예제와같은방식으로어플리케이션에서직접 DLL 을적재해서사용할수도있지만보다일반적이고, 쉽게사용하기위해서는 wrapper 유닛을작성해서사용하면편리하다. Wrapper 유닛이란 DLL 의함수를쉽게사용하기위해서파스칼문법에맞게만든인터페이스유닛으로, 이를이용하면인터페이스유닛의이름을사용하고자하는유닛의 uses 절에추가하는것으로해결할수있다. 참고로, 델파이의 Win32 API 지원도이러한 wrapper 유닛을통해서이루어지는것으로 windows.pas 등의유닛이대표적인 wrapper 유닛이다. 여기에대해서는 DLL 을다루는장에서더욱자세하게다루고있으므로이를참고하기바란다. 그러면, DLL 의 wrapper 유닛을다음과같이작성한다. unit HookExam; interface type TwMsg = LongInt; TwParam = LongInt; TlParam = LongInt;
TcbPlaybackFinishedProc = procedure(appdata: Longint); stdcall; function StartRecording: integer; stdcall; function StopRecording(lpFileName: PChar): LongInt; stdcall; function Playback(lpFileName: PChar; EndPlayProc: TcbPlaybackFinishedProc; AppData: Longint): Integer; stdcall; implementation function StartRecording; external 'ExamLib4' index 2; function StopRecording; external 'ExamLib4' index 3; function Playback; external 'ExamLib4' index 5; end. 예제어플리케이션의동작방식 후크 DLL 을사용하는어플리케이션에서는콜백함수로 PlaybackFinished() 를이용하도록하자. 이콜백함수는매크로의재생이완료되었을때호출된다. 즉, DLL 의 Playback() 함수에 PlaybackFinished() 콜백함수의주소를넘겨주어서 Playback() 이완료되는데로이를호출하게된다. 그런데, PlaybackFinished() 함수의주소를저장하기위해서프로그램시작시에전역변수를하나선언하고프로그램이종료될때이를해제하도록한다. 폼은매크로의기록을시작할때사용하는 기록시작 버튼, 기록을중지할때사용하는 기록중지 버튼, 매크로를재생할때사용하는 재생 버튼과어플리케이션을종료할때사용하는 완료 의네버튼으로구성한다. 폼이처음생성될때에는 기록시작 과 완료 버튼을사용가능하도록하고, 기록중지 와 재생 버튼은기록을중지시키거나, 재생할것이없으므로이를사용할수없도록설정한다. 각버튼의역할은다음과같다. 1. 기록시작 : 더이상의메시지를기록하거나기록중간에어플리케이션을종료하게하면안되므로, 기록시작 과 완료 버튼을사용불가능하게설정하고, 동시에 기록중지 버튼은사용이가능하도록설정한다. 그리고, DLL 의 StartRecording() 함수를호출해서메시지기록을시작한다. 이때 StartRecording() 함수가에러코드를반환하면사용자에게에러메시지를
보여주고, 기록시작 버튼이눌리기전의상태로재설정한다. 2. 기록중지 : 매크로를저장할파일이름을파라미터로 DLL 의 StopRecording() 함수를호출한다. 그리고 완료 와 기록시작 버튼을사용가능하도록설정해서, 세션을기록하거나어플리케이션에서빠져나갈수있도록허용한다. 또한, 기록이성공적으로이루어졌을경우에는 재생 버튼도사용할수있도록설정한다. 3. 재생 : 모든버튼을선택할수없도록설정하고, 재생할매크로가저장된파일이름과 PlaybackFinished() 콜백함수의인스턴스주소, 그리고어플리케이션이정의한데이터를받을파라미터로 DLL 의 Playback() 함수를호출한다. 매크로의재생이완료되면후크함수는어플리케이션의 PlaybackFinished() 콜백함수를호출해서재생이완료되었음을알림과동시에메인윈도우를어플리케이션데이터로넘겨준다. 이때 DLL 의 Playback() 함수가에러코드를반환하면, 재생 버튼이눌리기이전의상태로버튼들을재설정한다. PlaybackFininshed() 콜백함수가성공적으로호출되면 기록시작, 재생, 완료 버튼을사용가능하도록설정한다. 4. 완료 : 프로그램을완료한다. 그러면, 예제어플리케이션의폼을다음과같이디자인하자. 폼의각버튼의역할은앞에서간단히설명하였다. 먼저매크로를기록, 재생할파일이름을다음과같이상수로선언하고, 앞에서작성한후 크 DLL 을사용하기위해 uses 절에 HookExam 을추가한다.
const FILENAME = 'Hooking.MAC'; 그리고, 처음폼이생성될때의버튼들의초기값을다음과같이설정한다. procedure TForm1.FormCreate(Sender: TObject); Button1.Enabled := True; Button2.Enabled := False; Button3.Enabled := False; Button4.Enabled := True; 여기서 Button1 은 기록시작, Button2 는 기록중지, Button3 는 재생, Button4 는 완료 버튼이다. 각버튼의역할역시해당되는 DLL 함수를호출하고, 각버튼의 Enabled 프로퍼티를조절하는것으로각각의이벤트핸들러는다음과같다. procedure TForm1.Button1Click(Sender: TObject); Button2.Enabled := True; Button2.SetFocus; Button1.Enabled := False; Button3.Enabled := False; Button4.Enabled := False; if StartRecording = 0 then Button1.Enabled := True; Button1.SetFocus; Button2.Enabled := False; Button3.Enabled := False; Button4.Enabled := True; ShowMessage(' 기록을시작할수없습니다!');
procedure TForm1.Button2Click(Sender: TObject); if StopRecording(FILENAME) > 0 then Button1.Enabled := True; Button3.Enabled := True; Button3.SetFocus; end else Button1.Enabled := True; Button1.SetFocus; Button3.Enabled := False; Button2.Enabled := False; Button4.Enabled := True; procedure TForm1.Button3Click(Sender: TObject); Button1.Enabled := False; Button2.Enabled := False; Button3.Enabled := False; Button4.Enabled := False; if PlayBack(FILENAME, @PlaybackFinished, Handle) = 0 then Button1.Enabled := True; Button2.Enabled := False; Button3.Enabled := True; Button4.Enabled := True; Button3.SetFocus; procedure TForm1.Button4Click(Sender: TObject);
Close; 간단하므로이해에별무리가없을것으로믿는다. 그런데눈여겨보아야할부분은 Button3 의 OnClick 이벤트핸들러에서 Playback() 함수를호출하는부분으로, 재생이끝났을때이를처리할콜백함수의주소를파라미터로넘겨주게된다. 여기서는 PlaybackFinished 함수의주소를넘겨주었다. 그러므로, 이콜백함수를다음과같이작성한다. procedure PlaybackFinished(AppData: LongInt); stdcall; Form1.Button1.Enabled := True; Form1.Button2.Enabled := False; Form1.Button3.Enabled := True; Form1.Button4.Enabled := True; Form1.Button3.SetFocus; 이때 Form1 을앞에붙이는이유는이함수가 TForm1 클래스의메소드로선언되지않았기때문이다. 어쨌든후크 DLL 에의해 Playback() 함수가실행되면, 이함수가종료되는데로 PlaybackFinished() 프로시저가실행되어버튼들의활성화여부를결정하게된다. 이제이프로그램을실행시키면매크로기록, 재생을할수있게된다. 기록시작 버튼을누르고마음대로마우스와키보드를이용한작업을하고, 기록중지 버튼을누르면이메시지가매크로로기록이될것이며, 이를 재생 버튼을눌러서재현할수있다. 한번해보면얼마나유용하게쓰일수있을지알수있을것이다. 정리 (Summary) 이번장에서는 Win32 환경에서자주사용되는콜백함수에대한설명과콜백을이용하여구현할수있는후킹기법에대해서알아보았다. 콜백함수는후킹기법말고도다른곳에서도자주사용되므로, 확실하게이해해두는것이좋다.