C++ 컴파일러들은 아주아주 훌륭합니다. 최적화를 켜면 이리저리 쿵짝쿵짝 뭔가 대단해보이는걸 해치워서 코드를 빠르게 만들죠. 요런류의 최적화중 중요한 한가지 기법이 특정 변수를 register를 사용하게 만드는 겁니다. 넵. 대단합니다. 멋지죠. DRAM보다는 Cache가 빠르고 Cache보다는 Register가 빠르니까요. 빠른게 최고인겁니다.
하지만, 디버깅 과정에서는 재앙입니다. 저런 최적화를 수행하게 되면.. 변수가 날아갑니다. 특히 this포인터는 무용지물이 됩니다. MSVC의 경우, this포인터는 CX레지스터를 이용하는데.. 최적화를 수행할 경우 CX레지스터를 범용 연산 레지스터로 씁니다. 즉.. CX레지스터를 다른 용도로 쓰게 되고.. this포인터는 이상한 값을 남발하죠. (보통 0인 경우가 많습니다.) 재현이 가능한 버그라면, 최적화를 끄고 재현해서 문제를 해결할 수 있겠지만, Post-mortem디버깅(크래쉬덤프를 이용한 디버깅)이나 타이밍 문제로 버그가 발생하는 동시성 문제라면 최적화를 끌수도 없습니다. 전자는 끄는게 불가능하고, 후자는 경험상 단언컨데 최적화 끄면 문제가 안생길 확률이 은근히 높습니다. OTL
가장 근본적인 방법은 disassemble된 코드를 읽어서 this포인터를 찾는 것이지만.. 사실상 힘들죠. 그 값이 유지되고 있다는 보장도 없구요. 결국은 포기하기 마련입니다. 아니면, 소설을 쓰거나요. 음… 일단 상황을 정리해보면 아래 3가지로 정리가 될겁니다.
- 최적화를 끄고 재현이 가능한 상황.
- 최적화를 끄면 재현이 안되는 상황.
- 재현이 불가능한 상황(Post-mortem)
1번의 경우에는 앞에서 언급한대로 그냥 최적화를 끄고 디버깅을 하면 됩니다. 문제는 2번과 3번인데, 2번은 코드를 살짝 수정해서 문제가 되는 부분의 this pointer를 전역변수로 저장해두면 됩니다. 의외로 간단하지만 쓸만하죠. 마지막, 3번이 골아픕니다.
Post-mortem 디버깅이란 프로세스가 비정상종료될때 남기는 정보를 이용해 버그를 잡아내는 방법을 의미합니다. 보통 Win32플랫폼에서는 이를 위해 MS에서 제공하는 DBGHelp라이브러리를 이용해 미니덤프파일을 남기게 됩니다. 물론, Dr.Watson을 이용해도 되지요.
미니덤프를 이용해 프로세스가 비정상종료될때의 위치를 파악한다고 해도, 앞에서 이야기한 것처럼 최적화된 바이너리라면, 문제를 찾아내기 어려울 경우가 발생합니다. 이때 사용가능한 기법이 메모리검색입니다.
네? 메모리 검색이라구요??
네. 메모리 검색입니다. 우리에게 필요한 것은 잃어버린 this 포인터이고, 결국은 메모리안에 존재합니다. 그러므로, 어떻게든 찾아내면 되는 것이죠! 이제 어떻게 찾아내는지가 관건일텐데.. 바람직한 C++프로그래머라면, 아마도 소멸자를 가상함수로 선언해두었을 가능성이 높습니다. 사실 이거 하나면 충분하죠. 무슨 이야기냐구요?
C++은 다형성을 구현하기 위해서, 가상함수테이블을 사용합니다. 그리고, 각 인스턴스들은 가상함수테이블에 대한 포인터를 들고 있습니다. 즉, 인스턴스의 메모리 레이아웃에 포인터변수가 선언되어 있는 것이지요. (_vftable!) 잃어버린 this 포인터가 가리키는 메모리, 즉 해당 클래스의 인스턴스가 차지하고 있는 공간에는 분명히 가상함수테이블에 대한 주소가 적혀있을겁니다. 슬슬 느낌이 옵니다. ㅎㅎㅎ
자. 이제 _vftable의 주소와 이 주소를 갖고있는 메모리를 찾으면 됩니다. 🙂
To be continued.