이런 훅은 우리 프로세스 안의 특정 스레드 안에다가만 설치할 수도 있고 시스템의 모든 스레드에다가 설치할 수도 있는데, 32비트 운영체제로 오면서 후자 같은 global 훅(혹은 시스템 훅)을 설치하기 위해서는 훅 프로시저는 반드시 DLL에 따로 존재해야 하는 약간의 번거로움이 생겼습니다.
global 훅은 동작 방식의 특성상 모든 프로세스들에 나의 코드를 주입하는 가장 간편한 방법입니다. 굳이 윈도우 메시지 훅킹이 아니더라도 다른 종류의 훅킹을 위해서라도 윈도우 훅이 사용됩니다. 한컴사전의 노클릭 단어 인식이라든가 과거의 한스타, 그리고 Dependency Walker의 EXE 프로파일 기능처럼 API 훅킹이 동원되는 프로그램도 살펴보시면 별도의 DLL 파일이 존재하는 것을 알 수 있는데, 이는 API 호출을 변조하는 코드 자체를 삽입하기 위해서 윈도우 훅을 사용한 것입니다.
Global 훅은 완전히 특이한 프로그래밍 패러다임을 제공합니다. 다른 프로세스에서 완전 제각기 따로 실행되는 함수를 만들 수 있습니다.
가령, A라는 프로세스에서 B라는 DLL에 들어있는 훅 프로시저로 global 훅을 설치했습니다. 그러면 A와 B 사이의 통신 방법이 문제가 됩니다. 통상 A는 B의 동작 방식을 결정하는 입력 데이터를 주고, B는 훅킹을 통해 얻은 결과를 A에다 전달해야 할 것입니다.
이를 위해 보통 메시지를 쓰면 무난하죠. B에서 A로 통신할 때야 우리끼리만 쓰는 WM_USER+n이라든가, 심지어 WM_COPYDATA를 바로 보내도 무난하지만, 다른 프로세스 안에 존재하기 때문에 메시지가 겹칠 우려가 있는 B를 “향해서” 메시지를 보낼 때는 RegisterWindowMessage로 값이 안 겹치는 게 보장되는 별도의 메시지를 등록해서 쓰는 게 안전합니다.
또한 프로세스 A가 자신의 주소 공간에 로드되어 있는 DLL B의 변수값을 바꾼다고 해서 다른 프로세스들의 주소 공간에 로딩되어 있는 DLL B의 인스턴스의 변수값이 바뀌지는 않는다는 것도 조심해야 합니다. global 훅 프로시저는 메시지를 받는 그 프로세스의 주소 공간을 기준으로 호출된다는 것!
이걸 헷갈려서는 안 됩니다. 변수 초기화 같은 걸 잘못하면 훅을 설치한 프로세스 A 안의 B만 제대로 동작하게 되고, 다른 프로세스에 침투한 B는 그렇지 못하게 됩니다.
이것 때문에 과거에 굉장히 주의가 필요했던 점이 뭐냐 하면 훅 핸들 값을 공유하는 것이었습니다.
훅 프로시저는 자신에게 걸린 메시지를 처리한 뒤, CallNextHookEx 함수로 그 메시지를 다음 훅에다가 전달도 해 줘야 했습니다. 운영체제에 갈고리질을 하는 놈이 나만 있는 것 아니기 때문에... 그런데 윈도우 9x는 유독 이때 자신이 받은 훅 핸들값도 알아서 전달해 줘야 했습니다.
프로세스 A가 자신의 주소 공간에 있는 B DLL에다가 훅 핸들을 넘겨준다고 해도, 다른 주소 공간에 복제된 다른 B DLL의 인스턴스는 그 값을 알 수 없었습니다.
그래서 이 값은 숫제 메모리 맵드 파일 같은 공유 메모리를 만들어서 넘겨주거나, #pragma data_seg 같은 전처리기로 별도의 공유 섹션을 만들어서 그 전역변수에다 핸들을 공유해야 했지요.
그런데 윈도우 2000 이상, 아니 NT 계열은 그럴 필요가 없습니다. CallNextHookEx 함수를 호출하는 것 자체만으로 이 문맥에서의 훅 핸들은 알아서 감지가 됩니다. 당연히 그렇게 되는 게 이치에 맞죠. 그럼, 윈도우 95보다 NT가 3.1 시절부터 먼저였는데 왜 애시당초 아무 쓸모없던 HHOOK 인자를 받는 게 있었을까? 그건 아마 16비트 시절의 훅킹 API의 프로토타입을 그대로 베끼다 보니 그렇게 된 게 아니었나 싶군요. 32비트 윈도우로 와서 WinMain의 hPrevInstance 인자가 완전 무의미해졌지만 여전히 그대로 남아있는 것처럼.
그럼에도 불구하고 훅킹 API에 관한 한 윈도우 9x는 NT를 100% 닮지 못하고, 그렇다고 해서 아예 3.1 시절처럼 단일 주소 공간도 아니면서(핸들 값 공유도 어려운 환경에서) 꽤 불편한 프로그래밍 관행을 개발자에게 강요하게 되었던 것 같습니다.
윈도우 9x 시절에만 해도 global 훅 프로그래밍은 굉장히 조심스럽게 해야 했습니다. 훅 프로시저에서 뭔가 뻑이 났다간 그건 90% 이상 운영체제 다운으로 연결됐습니다. 디버깅은 더욱 힘들었음. 보호 모드 운영체제란 말이 무색할 정도였어요. 그만큼 운영체제가 안전보다는 열악한 PC 환경에서 효율 내지 도스와의 호환성 위주였으며, 불안정하고 응용 프로그램의 위험한 동작에 대해 취약했습니다.
하지만 XP 정도 되니, 특히 비스타는 이 정도로 안정성이 강화될 줄은 몰랐습니다. 훅 프로시저에 굉장히 어이없는 실수가 들어있었는데 그냥 그 훅만 싹 없어지고 프로그램은 잘 돌아가더군요. 깜짝 놀랐음. 윈도우 9x였으면 당장 blue dead screen이었을 겁니다.
윈도우 2000 때만 해도 IME/TSF 모듈이 해당 응용 프로그램을 뻗게 만들 수 있었는데, XP 이후부터는 안 그렇더군요. 자체적으로 예외 핸들링을 합니다.
global 훅 프로그래밍을 할 때 괴로운 점.
훅을 거둬들이고 모든 프로그램을 종료한 뒤에도 훅 프로시저가 들어있는 DLL의 lock이 즉시 풀리지 않는 경우가 많다는 것입니다. 훅을 해제한 뒤에도 이 DLL이 여전히 몇몇 프로세스의 주소 공간에서 사라지지 않고 상주해 있기 때문입니다. 그런데 3~5분 정도 기다리고 나면 없어져 있어요. 그러니 DLL을 이것저것 고치면서 자주 리빌드를 하기가 힘듭니다. -_-;;
비슷한 예로 글꼴도 있답니다. 프로젝트 파일로부터 최종 TTF 파일을 만들어서 여타 프로그램에서 테스트를 했습니다. 그런데 한번 그러고 나면, 그 프로그램을 종료한 후에도 내가 새로 설치한 TTF 파일이 여전히 in use 상태여서 지워지거나 교체가 안 되는 겁니다. 그러니 글꼴을 빈번히 수정하고 테스트하기가 힘들죠.
이럴 때 VMware가 해결책이 될 수 있습니다. 가상 머신을 만들어서 훅 프로그램을 실행하거나 글꼴을 설치하기 직전 순간의 스냅샷을 만든 후, 테스트를 하고 나서 스냅샷 시점으로 revert 하기. -_-;; 그게 저절로 파일 lock이 풀리길 기다리거나 재부팅을 하는 것보다 더 빠르더군요. ㄱㅅ!
뭐, global 훅 얘기로 길어졌습니다만, 내 응용 프로그램 안에서의 작은 규모의 훅도 충분히 쓰일 일이 있으며 특히 MFC에서는 내부적으로 이런 훅을 사용합니다. 일반적인 방법으로 잡기 힘든 메시지들도 MFC의 단일 프레임워크 하에서 일관성 있게 처리시키기 위한 목적이기도 하고요,
또 모든 대화상자들을 부모 윈도우 기준으로 중앙에다 재배치시키기 위해서도 훅을 사용해 대화상자가 생성되는 시점을 가로챕니다.
그냥 DialogBox 같은 함수만 호출해 보면 잘 알다시피 대화상자가 중앙에 뜨지 않습니다. MFC는 modal 대화상자만 중앙으로 옮겨 주기 때문에, 그냥 Create 함수로 modeless 대화상자를 만들어 보면 당장 그 위치 차이를 감지할 수 있습니다.
Posted by 사무엘