티스토리 뷰

맵 파일을 활용하자 #1 에서 긁어왔습니다.

* Release mode 에서 map파일에 저장된 메모리 정보를 이용하여 어떤 함수에서 문제가 발생하였는지 추정해 볼 수 있다 .
==== 출처 : 데브피아 VC++ 강좌와 TIP 자료실 ====

맵 파일을 활용하자 #1

1) 소개

프로그래머는 프로그램을 개발하면서 디버깅 과정에서 여러 가지 테스트를 합니다.

물론, 항상 개발자 PC에서는 프로그램이 완벽하게 동작합니다. 참 신기합니다. 정말로 정말로 개발자 PC에서는 완벽 합니다.

하지만, 이런 완벽한 프로그램을 배포하고, 나서 곧 사용자들의 끝임없는 버그 리포트로 인하여 프로그래머는 좌절을 맞보게 됩니다.

그리고, 아무것도 모르는 초보유저님에게 우리의 프로그램은 메모리 주소만 알려주고 운명해 버리는 Crash 현상도 발생합니다.

유저님께서 버그 리포트에 아래와 같은 이미지만을 달랑 첨부하여 보냅니다.

그림1) [1]

물론 이런 리포트가 왔을 때 나이롱 필자가 하는 레파토리가 있습니다.

그분 윈도우가 이상해서 그래요~ ㅡㅡ;;

하지만 이러면 안된다는 것을 현명한 개발자 분들은 알고 있을 것입니다.

우리는 최소한 Offset : 0x000010f9를 보고 어느 함수를 실행하다 프로그램이 운명하셨는지 알아야 디버깅을 할 수 있습니다.

바로 이러한 것을 알 수 있게 해주는 것이 맵 파일입니다.

2) 맵 파일 만들기

맵 파일을 만들기 위해 가장 먼저 해야 할 일은 물론 맵 파일을 만드는 것입니다.

디폴트가 생성 안함이므로, VC툴에서 생성하도록 변경해야 합니다.

1. 가장 먼저 할 일은 메뉴에서 프로젝트->속성을 클릭합니다.

2. 프로젝트 속성 페이지 다이얼로그에서 링커->디버깅을 선택합니다.

3. 속성 이름 중에 맵 파일 생성이라는 속성의 값을 예로 바꾸어 줍니다.[2]

그림 2) [3]

위와 같이 만들고 나서 릴리즈 모드로 컴파일을 하면, .\Release\프로젝트이름.map 파일이 생성됩니다.

물론 릴리즈 생성 경로나 맵 파일에 이름을 주면 위치와 이름이 바뀝니다.

3) 맵 파일 읽기

먼저 테스트를 위해 간단한,,, 정말로 정말로 간단한 콘솔 프로젝트 예제 프로그램을 하나 작성합니다.

예제 1) [4]

#include "stdafx.h"

#include <windows.h>

void GetName(char* p)

{

*p = 'J'; // 메모리 0번 주소에 값을 쓰므로, 이부분에서 Crashed

}

int _tmain(int argc, _TCHAR* argv[])

{

char buff[20];

GetName(0); // 메모리 0번을 포인터로 넘겨 줌

MessageBox(NULL, buff, "", MB_OK);

return 0;

}

위에 using the MAP file 프로그램을 빌드하면 릴리즈 폴더에 using the MAP file.map 라는 맵 파일이 생성되며, 메모장 등으로 열면 예제 2와 예제 3 같은 내용의 텍스트를 볼 수가 있습니다. [5]

예제 2)

using the MAP file // 모듈 이름입니다.

Timestamp is 42eb32dc (Sat Jul 30 16:57:16 2005) // 타임스템프입니다.

Preferred load address is 00400000 /* 요게 선점되는 메모리 주소입니다. 즉, 우선적으로 0x0040000 번지에 적재 됩니다. */

Start Length Name Class

0001:00000000 00003b98H .text CODE

0002:00000000 000000ccH .idata$5 DATA

…….

맵 파일의 상위 부분은 예제 2와 같은 정보를 가지고 있습니다.

맵 상위 부분은 모듈이름과 프로젝트를 링크한 타임스템프가 있습니다.

다음 빨강색 주석 중요합니다. using the MAP file.exe 프로그램이 실행될 때 메모리에 적재되는 스타트 위치가 됩니다.

즉, Crash 발생시에 오프셋 100을 알려준다면, Crash 발생된 메모리 위치는 0x00400100 (0x00400000 + 0x100)이 됨을 알 수 있습니다.

그럼 이 Preferred 주소는 어떻게 결정되는지 갑자기 궁금해 집니다.

이 주소는 프로젝트->속성->링커->고급 고정기준주소속성에서 변경 가능 합니다. [6]

그림 3)

MSDN에 보면 default location for an .exe file (at 0x400000) or a DLL (at 0x10000000)라고 되어 있습니다. [7]

예제 3)

…….

Address Publics by Value Rva+Base Lib:Object

0000:00000000 __except_list 00000000 <absolute>

0000:00000002 ___safe_se_handler_count 00000002 <absolute>

0001:00000000 _main 00401000f using the MAP file.obj

0001:00000067 @__security_check_cookie@4 00401067f LIBC:secchk.obj

0001:00000075 __amsg_exit 00401075f LIBC:crt0.obj

…….

// 이하 생략

맵 파일은 예제2의 내용 다음에 예제3과 같은 내용을 볼 수 있습니다.

위에 예제 3에는 public된 함수 이름과 적재 주소를 볼 수 있습니다.

예를 들면 main 함수는 00401000f 주소부터 시작 됩니다.

위에 예제 1 프로그램을 실행 시키면 아래와 같은 Crashed Address를 알려줍니다.

그림 4)

위에 그림4 에서 중요한 것은 빨강색 박스 안에 있는 Offset 주소 입니다.

Offset 0x0000101a는 프로그램이 메모리에 적재된 시작 위치에서의 오프셋을 말합니다.

그럼 이제 우리는 프로그램 시작 주소에서 위에 오프셋을 더하면 오류가 발생한 함수를 구할 수 있습니다.

Preferred 주소가 0x00400000이므로, 0x0040101a 명령을 수행 하다가 프로그램이 비정상 종료되었다는 것을 알 수 있습니다.[8]

그럼 0x0040101a 보다 큰 주소를 맵 파일에서 찾아 볼까요??? [9]

예제 3을 참조해 보면 @__security_check_cookie@4 함수의 주소가 00401067f 이므로, 분명 위에 함수 바로 이전 함수에서 죽었는데…

보니깐 _main 함수가 바로 위에 있네요… 맞습니다.

예제 4)

0001:00000000 _main 00401000f using the MAP file.obj

// 0x0040101a 주소는 이 곳에 포함되는 주소 입니다.

0001:00000067 @__security_check_cookie@4 00401067f LIBC:secchk.obj

저희 프로그램의 Crash는 main에서 발생 한 것입니다.

이런… 무엇인가 이상합니다. 분명 필자가 예제1의 소스에서 *p 명령에서 Crash가 발생한다고 했습니다.

*p명령은 void GetName(char* p) 함수에서 호출됩니다.

이런… 이제 보니 Map 파일에 GetName 함수가 없습니다.

아무리 눈 씻고 찾아봐도 보이질 않습니다. 어떻게 된 것일까요???

벌써 눈치를 채셨을 분도 계실 것입니다.

이것은 컴파일러의 최적화 기능과 관련이 있습니다.

그렇습니다. 우리의 컴파일러는 속도 향상을 위해서 void GetName(char* p) 함수를 인라인으로 만들었던 것입니다.

정말이지 이 똑똑한 기능을 끄려면 어떻게 해야할까요???

컴파일러 옵션에서 변경 가능합니다. [10]

그림 5)

위에 최적화 콤보박스를 사용 안 함(/Od)으로 체크 합니다.

이제 다시 리빌드를 해 볼까요?

새로 생성된 맵 파일은 예제 5와 같습니다.

예제 5)

0000:00000000 __except_list 00000000 <absolute>

0000:00000002 ___safe_se_handler_count 00000002 <absolute>

0001:00000000 ?GetName@@YAXPAD@Z 00401000f using the MAP file.obj

// Crash 0x00401006는 여기서 발생됩니다. [11]

0001:00000010 _main 00401010 f using the MAP file.obj

0001:0000007a @__security_check_cookie@4 0040107a f LIBC:secchk.obj

Exe 프로그램을 다시 한번 실행해 봅니다.

그림 6)

위와 같이 오프셋 00401006은 예제 5에서 보는 것과 같이 GetName함수와 main함수 사이이므로, GetName 함수 명령을 실행하다.

Crash가 발생 되었다는 것을 알수 있습니다.

그런데 예제 5를 보면 함수 이름이 좀 이상합니다.

분면히 포인터 변수를 메개변수로 주었는데 알 수 없는 이상한 이름 ?GetName@@YAXPAD@Z 으로 바뀌었군요!

이것은 C++ 네이밍 규칙 때문 입니다.

이것을 해석해 주는 유틸리티 프로그램이 VC에 포함되어 있습니다.

undname.exe[12]라는 프로그램입니다.

사용법 또한 간단합니다. 프롬프트창에서 “undname 디코딩할 이름”을 입력하면 됩니다.

예제 6)

C:\ undname ?GetName@@YAXPAD@Z

결과는 그림 7과 같습니다.

그림 7)

4) 마치면서...

이제 우리는 릴리즈 모드에서 발생하는 Crash들이 어느 함수 안에서 발생하는지 알 수 있게 되었으며, 사용자 버그 리포트에 대해 조금 더 유연함을 가질 수 있게 되었습니다.

글을 다 쓰고 나니 먼가 빠진 듯한 느낌이 듭니다...

그렇습니다... 거의 주소가 고정되어있는 exe를 예로 들고, 동적으로 주소가 변할 수 있는 DLL에 대한 예제는 다루지 않았습니다.

죄송합니다. 오늘은 너무 피곤해서 조만간에 세컨드 에디션을 다시 쓰기로 하겠습니다.

지금까지 글재주 없고 여러가지 부족한 내용의 글을 읽어 주셔서 감사 합니다.

그래도 토욜날 이 글 쓰느라고 울 아가도 못 만났습니다.

그러니 돌은 던지지 말아주세요...

마지막으로 저는 미뎅이를 ♡합니다 ^^;;

무더운 2005년 07월 30일 토욜밤

- 특공 홍기 -


[1] 윈도우 버전과 개발툴 설치 유무에 따라 위에 다이얼로그는 약간씩 달라집니다.

[2] 그림 2 참조 합니다.

[3] 닷넷 2003툴 기준입니다. VC 6.0 툴도 비슷하므로, 어렵지 않게 찾을 수 있습니다.

[4] 예제는 using the MAP file라는 이름으로 첨부되어 있습니다.

[5] 예제에서는 using the MAP file.map 라는 이름으로 생깁니다.

[6] 그림 3을 참조합니다.

[7] MSDN 주소 ms-help://MS.MSDNQTR.2005APR.1033/vccore/html/_core_.2f.BASE.htm

[8] Prederred + Offset를 하여 Crashed 주소를 구할 수 있습니다.

[9] 여기서는 예제 3을 참조해 보세요.

[10] 그림 5를 참조합니다.

[11] 그림 6을 참조하세요

[12] C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin\undname.exe

VC 6.0의 경우도 Bin폴더에 포함되어 있습니다.

댓글