Windows API에는 FreeLibraryAndExitThread라는 함수가 있다.
얘가 하는 일은 이름이 암시하는 바와 같다. 주어진 모듈에 대해 FreeLibrary를 한 뒤 ExitThread를 호출하여 지금 실행 중인 자기 스레드를 종료한다.
어지간하면 응용 프로그램이 두 함수를 차례로 직접 호출해 주면 되며, 둘을 나란히 호출해야 할 일 자체도 실무에서는 매우 드물다. DLL을 제거하는 건 대체로 응용 프로그램이 종료될 때 같이 행해지지, 특정 스레드의 실행이 끝나자마자 칼같이 행해지는 일은 없기 때문이다. 그런데 왜 이런 함수가 정식으로 별도로 제공되는 걸까(Windows NT 3.1 첫 버전은 아니고 3.5에서 추가됨)? 다 이유가 있기 때문이다.
어떤 DLL이 별도의 스레드를 생성해서 자신의 코드를 동시에 실행하고, 실행 후에는 자기 자신을 스스로 깔끔하게 제거하여 사라지게 설계되어 있다고 치자. 즉, DLL을 로딩한 모듈이 그걸 수동으로 따로 해제하는 게 아니라, 그 DLL이 자기 임무를 다한 후 스스로 사라진다는 것이다. 이건 비록 흔한 디자인 형태는 아니지만 말이다.
이 경우, DLL의 코드가 자신에 대해서 FreeLibrary(hMyDLL)을 함부로 호출해 버려서는 곤란하다. 그러면 자기 스레드가 활동하던 모듈이 메모리에서 사라져 버리기 때문에 그 다음에 실행될 스레드 실행 종료 부분에 해당하는 코드(return 0 내지 ExitThread)까지 사라진다. 스레드는 정상적으로 종료되지 못하고 곧장 access violation이 발생한다.
그렇다고 ExitThread를 먼저 호출하면? 이 DLL을 실행하는 주체인 스레드의 실행이 끝나 버리기 때문에 FreeLibrary가 호출될 기회가 없다. 마치 실행 중인 EXE 자신을 제거하는 코드를 실행하는 것과 비슷한 맥락의 딜레마가 발생한다.
모듈(공간) 해제와 스레드(시간) 종료가 동시에 행해져야 할 경우, 이것은 운영체제가 알아서 동시에 수행해 줘야 한다.
FreeLibraryAndExitThread를 호출한 경우, 내부적으로 FreeLibrary가 됐더라도 그 다음으로 ExitThread를 호출하는 코드는 그 DLL이 아니라 운영체제가 책임을 지기 때문에 안전하다.
한편, Windows에서는 EXE나 DLL의 로딩이 파일 자체를 가상 메모리 주소에다 곧장 연결하는 memory-mapped file 기법으로 행해진다. 그렇기 때문에 실행 중인 프로그램 파일이 중간에 지워지는 것은 운영체제가 허용하지 않는다. 즉 DLL과 스레드 문제로 비유하자면 FreeLibrary 단계에서 이미 실패한다는 것이다.
그렇다고 자신을 먼저 종료해 버리면 자기 자신 파일을 지우는 코드가 실행되지 못할 테니, 이 역시 동시에 충족될 수 없는 모순이 된다. ExitProcessAndDeleteFile 같은 전용 함수라도 있어야 할 것 같은데.. Windows API에는 그런 건 없다.
다만 EXE와는 달리 실행 중인 배치 파일은 자기 자신을 제거하는 게 가능하다. 그래서 설치 제거 프로그램 같은 데서는 내부적으로 EXE를 지우고 자기 자신도 삭제하는 배치 파일을 만들어서 이걸 내부적으로 실행한 뒤, 자기 프로그램은 최대한 신속하게 종료함으로써 흔적 없는 '자폭'을 한다. 배치 파일은 당연히 IF EXISTS와 GOTO loop가 있어서 파일이 완전히 지워질 때까지 삭제 시도를 반복한다.
배치 파일은 자가삭제가 가능하긴 하지만 삭제된 뒤의 명령들은 실행되지 못하고 에러와 함께 실행이 끝난다. 옛날에 4DOS 같은 MS-DOS 대체 명령 프롬프트에는 BTM (Batch to memory)처럼 모든 내용을 메모리로 읽은 뒤에 실행하는 특수한 배치 파일이 있긴 하지만 이것은 MS 기반의 오리지널 셸에 있는 기능은 아니다.
물론 설치/제거 프로그램이라면 요즘은 직접 짜는 게 아니라 Windows Installer를 사용하는 게 대세이니 저런 꼼수를 직접 구현해야 할 필요는 더욱 없어지긴 했다. 자기 정체를 요리 조리 교묘하게 숨겨야 하는 악성 코드나 돼야 필요할까?
거의 모든 Windows API 함수들은 뭘 실행하더라도 성공/실패 여부를 되돌리는 리턴값이 있다. 리턴값이 없는 void형 함수는 정말 실패를 절대로 걱정할 필요가 없는 예외적인 물건을 제외하면 매우 드물다.
그러나 ExitProcess 내지 ExitThread처럼 뭔가 자신의 실행을 종료하고, 실행 후에 리턴값을 받을 주체 자체가 없어지는 함수라면 리턴값이 응당 void이다. 종료하는 동작이 실패할 리도 없을 테고 말이다.
FreeLibraryAndExitThread도 예외가 아니다. 모듈 핸들을 잘못 줄 경우 FreeLibrary 부분은 실행이 실패할 수 있지만, 후반부의 ExitThread 부분은 언제나 성공이 보장되기 때문이다.
그러고 보니 메시지 큐에다가 WM_QUIT를 넣어 주는 PostQuitMessage도 종료와 관계가 있는 전용 함수이며 void 형이다. WM_QUIT 메시지는 GetMessage 함수로 구성된 message loop을 끝내는 역할을 하며, 윈도우 프로시저를 통해 전달되지는 않는다.
MSDN은 저 메시지는 반드시 PostQuitMessage라는 전용 함수를 통해서(주로 메인 윈도우의 WM_DESTROY 타이밍 때) 넣어야지, 수동으로 PostMessage(hWnd, WM_QUIT, 0, 0) 같은 식으로 넣어서는 안 된다고 명시한다.
사실 저건 특정 윈도우가 대상이 아니라 스레드의 메시지 큐 자체가 대상이기 때문에 hWnd에 아예 NULL을 주거나, PostThreadMessage(GetCurrentThreadId(), WM_QUIT, 0, 0)과 비슷하다.
다만, WM_QUIT은 여느 메시지와 동일하게 취급되는 게 아니라 스레드의 내부 상태 차원에서 종료 플래그가 붙은 것으로 특수하게 처리되기 때문에 PostQuitMessage 같은 별도의 함수가 존재하는 것이다. 제거 딜레마의 해결 필요 때문은 아니다.
Posted by 사무엘