Exception이 발생한 주소를 확인하는데 성공했다고 해도, 그 주소가 우리가 작성한 코드가 아니라면 별 쓸모가 없습니다. 예를 들어서 기껏 주소를 추적했더니 wcsncpy API 내의 주소였다고 하면... 별 쓸모가 없죠. 우리가 관심이 있는 것은 그때 wcsncpy를 호출한 곳이 어디인지 일 것입니다.
CallStack을 추적하기 위해서는 다음과 같은 몇가지 개념을 이해해야 합니다.
1. 함수를 호출할 때 생기는 일들
함수를 호출할 때는 다음과 같은 일들이 벌어집니다.
1) 함수에 넘겨줄 파라메터를 Stack에 Push2) 호출할 함수에서 리턴한 다음에 수행할 명령어의 주소 (Return Address) 를 Stack 에 Push3) 호출할 함수의 주소로 jmp
위의 세가지 과정 중 2번째와 3번째 과정이 call이라는 어셈코드로 이루어집니다.
nResult = Calculate(0x37, 0x63);
위의 C 코드 한줄은 다음과 같은 어셈 코드로 번역됩니다.
0042DAE5 push 63h // 호출할 함수에 전달할 파라메터를 스택에 Push
0042DAE7 push 37h
0042DAE9 call Calculate (42B695h)
0042DAEE mov dword ptr [nResult],eax // 함수의 리턴값(eax)을 nResult에 저장
0042DAE7 push 37h
0042DAE9 call Calculate (42B695h)
0042DAEE mov dword ptr [nResult],eax // 함수의 리턴값(eax)을 nResult에 저장
사실은 다음과 같이 실행되는 거죠 -- ①
0042DAE5 push 63h // 호출할 함수에 전달할 파라메터를 스택에 Push
0042DAE7 push 37h
0042DAE9 push 0042DAEEh // RET Push
jmp Calculate (42B695h) // 다음 함수로 Jump
0042DAEE mov dword ptr [nResult],eax // 함수의 리턴값(eax)을 nResult에 저장
0042DAE7 push 37h
0042DAE9 push 0042DAEEh // RET Push
jmp Calculate (42B695h) // 다음 함수로 Jump
0042DAEE mov dword ptr [nResult],eax // 함수의 리턴값(eax)을 nResult에 저장
2. 함수가 호출된 후에 생기는 일들
이부분을 이해하려면 일단 ESP와 EBP에 대해 이해해야 합니다.
-
ESP (Extended Stack Pointer) 레지스터 : 현재 스택의 가장 위에 들어있는 데이터를 가리키고 있는 포인터입니다. Intel CPU에서는 스택이 거꾸로 (높은 주소에서 낮은 주소로) 자라므로 데이터가 하나 Push될 때마다 ESP 값은 감소합니다. ESP는 다음번 Data를 Push할 위치 가 아니라 "다음번에 Pop했을 때 뽑아낼 데이터의 위치"를 가리키고 있습니다.
-
EBP (Extended Base Pointer) 레지스터 : 현재 스택의 가장 바닥을 가리키는 포인터입니다. 새로운 함수가 호출될 때마다 EBP 레지스터 값이 지금까지 사용했던 스택 꼭대기의 위(더 낮은 주소)에 위치하게 되고, 새로운 Stack이 시작됩니다. 따라서 EBP 레지스터는 새로운 함수가 호출되거나, 현재 실행중인 함수가 종료되어 리턴될 때마다 값이 달라집니다.
-
ESP나 EBP의 값을 읽을 때 주의할 점 한가지는 32비트 머쉰에서 ESP나 EBP 모두 4바이트 단위로 변한다는 것입니다. 만약 ESP가 0x0012FE88 를 가리키고 있고 현재 스택의 맨 윗칸에 "0"이라는 값이 들어있다면, 0x0012FE88 에서 0x0012FE8B에 걸친 4바이트의 공간이 모두 0x00 으로 채워져 있다는 의미가 되겠습니다. 따라서 ESP 레지스터는 메모리상의 어느 한 지점을 가리킨다기 보다는 ESP에서 ESP + 3 바이트 까지로 이루어진 4바이트짜리 스택 "한칸"을 가리킨다고 생각하는 편이 헷갈릴 일이 적습니다.
-
새로운 함수가 시작할 때는 새로운 Stack이 시작됩니다. 새로운 Stack을 시작하기 위해서는 이전 Stack을 복구하기 위한 정보를 Stack에 저장해야 하기 때문에 새로운 함수가 시작되는 부분에서는 항상 다음과 같은 어셈블리 코드를 볼 수 있습니다. -- ②push ebp -- 이전 스택의 Base 주소를 저장 (Push)한다.mov ebp, esp -- 현재 스택의 꼭대기를 새로운 스택의 Base로 설정한다. (새로운 스택의 시작!)
3. StackWalk 의 원리
자 이제 위의 ① 번 어셈 코드와 ② 번 어셈 코드를 연결지어 보면 다음과 같이 됩니다.
이건 호출하는 부분의 어셈 코드입니다. 파라메터를 차례로 Push 한 후 Call이 실행되는 것이 보입니다.
이건 호출된 직후의 어셈 코드입니다. 위에서 언급한 EBP를 저장하고 새로운 스택을 시작하는 코드가 보입니다.
두가지를 이어붙여보면 함수가 시작한 직후의 Stack의 모습을 그려볼 수 있습니다. (push ebp가 실행된 직후이고, 아직 mov ebp, esp는 실행되기 전입니다.)
위의 그림에서 보면 새로운 스택의 꼭대기(ESP가 가리키는 곳)에 방금 Push한 이전 함수의 EBP(0x0012fda0)가 저장되어 있고, 그 밑에 이 함수를 call하기 직전에 저장된 Return Address 0x0042daee 가 보입니다.(선택된 부분) 그 밑에는 함수에 건네진 파라메터 0x63과 0x37이 보이죠? (Little Endian 으로 읽어야 합니다.)
여기서 mov ebp, esp 까지 실행되어 함수가 완전히 시작된 상태의 Stack을 그림으로 표현하면 다음과 같이 됩니다.
여기서 중요한 건,
EBP에 4바이트를 더한 위치에는 현재의 함수를 호출한 위치 (Return Address)가 저장되어 있다.
그리고,
현재 EBP가 가리키는 곳에는 이전 함수의 EBP가 저장되어 있다.
는 것이죠. 그렇다면. 이전 함수의 EBP에는 무엇이 저장되어 있을까요? 재귀적으로 생각해보면 전전 함수의 EBP가 저장되어 있다는 것을 추측할 수 있습니다. 이전함수의 EBP + 4바이트 위치에는 무엇이 저장되어 있을까요? 이전 함수를 호출한 지점의 주소 (이전 함수의 RET)가 저장되어 있겠죠.
반복하여 생각해보면 다음과 같이 하면 CallStack을 추적할 수 있습니다.
1. 현재 EBP + 4 바이트 위치의 값을 찍는다.
2. 현재 EBP에 저장된 주소로 이동한 후 + 4 바이트 위치의 값을 찍는다.
3. 저장된 주소(이전 EBP)로 이동한 후 + 4 바이트 위치의 값을 찍는다.
4. 저장된 주소(이전 EBP)로 이동한 후 + 4 바이트 위치의 값을 찍는다.
...
어찌보면
EBP란 "Stack"을 Entry로 가지는 Linked List의 연결 포인터와 같은 존재
라고도 말할 수 있습니다.
4. StackWalk 코드
위에서 살펴본 원리를 코드로 구현하면 다음과 같이 됩니다.
5. 주의 사항
간혹 위의 방법으로 CallStack이 나오지 않는 경우가 있습니다.
1) Buffer OverFlow 등으로 인해 Stack에 저장된 RET가 훼손된 경우 (보통 "스택이 깨졌다"고 하죠)
2) 옵티마이즈 과정에서 코드가 꼬이는 경우 : 이 경우는 Call Stack이 약간 이상하게 나올 수 있습니다만... Call Stack이 이상하다기 보다는... Exception이 발생한 위치 자체가 좀 이상하게 찍히기 때문에 제대로 추적이 안되는 경우가 있습니다. 이건 옵티마이져가 어셈코드를 뒤죽박죽 시키기 때문인데요... 이런 현상이 발생한 지점의 어셈 코드를 보아야 이해가 됩니다. :)
3) "Omit Frame Pointers(/Oy-)" 옵션을 설정하면 위와 같은 방법으로 CallStack을 추적할 수 없습니다. 이 옵션은 EBP를 Base Pointer가 아닌 일반 General Pointer로 사용함으로서 실행속도를 증가시키는 최적화 옵션입니다. (EBP를 다른 용도로 써버리면 이전의 스택을 어떻게 찾아갈까요? 그건 잘 모르겠습니다. ^^;;)
※ 참고 :
'C++ > Debug' 카테고리의 다른 글
Leak Debugging(1) - CRT 디버그함수를 이용한 메모리 누수(Memory Leak) 탐지하기 (0) | 2009.07.30 |
---|---|
Process Explorer와 디버그 심볼(PDB 파일)을 이용해 실행중인 프로세스 분석하기 (0) | 2009.06.04 |
Debugging Tips (3) - Just-In-Time Debugger를 이용하는 방법 (3) | 2009.03.02 |
Debugging Tips (2) - Access Violation 핸들링하기 (3) | 2009.02.22 |
Debugging Tips (1) - .map 파일과 .cod 파일 분석하기 (21) | 2009.02.22 |