출처: http://kuaaan.tistory.com/102 [달토끼 대박나라~!! ^^]





디버거로 실행시킬 때 모든 오류가 발생해준다면 오죽이냐 좋겠냐마는 현실은 그렇지가 않아서... Debug 빌드에서는 죽지 않다가 Release빌드에서만 죽는 경우라던지, 테스트베드에서는 죽지 않다가 실망에서 부하가 발생해야 죽는 경우, 한 1만대 정도 설치하면 한대 꼴로 재현되는 경우 등 개발을 하다보면 별의별 문제가 발생하게 마련이다. 


보통 Release 빌드에서 오류가 발생했을 경우, 개발자에게는 다음과 같이 죽은 주소 정보 정도만 제공된다. Release 빌드한 실행파일로 디버그를 해야 하는데...





죽은 주소가 0x0040101a랜다. 이 주소가 실제 코드의 어느 지점인지를 어떻게 찾을 수 있을까?

특정 메모리 주소를 소스코드와 매핑시키는 데 사용되는 방법이 .map 파일과 .cod 파일이다.



1. map 파일, cod파일이란?

  map 파일은... 이런 놈이다.

- 각 함수들의 Entry Point 주소를 저장한  파일

- Build시에 각 모듈별 exe별로 혹은 dll 별로 1개씩 생성

- 오류가 발생한 주소를 map 파일에서 분석하면 어느 함수의 몇번째 Bytes에서 죽었는지 추적이 가능

- map 파일 생성시 옵션을 주기에 따라 소스코드의 라인 정보나  Export 정보 등도 볼 수 있지만

  보통 map파일에서는 어느 함수에서 죽었는지와 주소 Offset까지만 확인하고 더 자세한 분석은 

  cod파일에서 하게 됨

 cod 파일이란... 이런 놈이다.

- 메모리 주소(Offset)과 어셈블리 코드, 그리고 소스 라인 수와 해당 C++ 소스코드의 mapping 정보를 저장한 파일

- Build 시에 cpp 파일마다 1개씩 생성된다. 즉, 해당 exe가 10 개의 cpp로 구성되어 있다면, 1개의 map 파일과 10개의 cod 파일이 생성된다.

- cod 파일에는 cpp 파일 하나마다 생성되는 obj 파일 내에서의 Offset이 기록된다.

- map 파일에서 어느 함수의 127번째 Byte 에서 죽었는지 확인되었을 때, 해당 함수의 127번째 Byte가 소스코드 상에서 어느 위치인지 확인하는데 쓰인다.



2. map 파일 생성하기

  map 파일을 얻기 위해서는 Build 시에 map 파일을 생성하도록 Visual Studio에 설정을 해주어야 한다.

  Visual Studio 6.0 은 Project Settings 창에서 "Link" 탭을 열고 "General" 카테고리의 "Generate Map File" 체크를 한다.

  Visual Studio 2005 의 경우 Project Property 설정 창에서 "Linker > Debugging" 섹션의 "Generate Map File" 부분을 "Yes"로 설정해주면 된다.



체크한 후부터는 빌드 시에 exe 혹은 dll이 생성되는 Output 폴더에 map 파일이 생성된다.





3. cod 파일 생성하기

  cod 파일을 생성하기 위해서는...  

  Visual Studio 6.0 에서는 "Project Setting" 창의 "C/C++"탭에서 "Listing Files" 카테고리를 열어 "Listing File Type"에 cod 파일에 저장할 정보를 선택해주면 된다. 보통 "Assembly, Machine Code, and Source" 를 선택한다.

  Visual Studio 2005 에서는 "Project Property" 창에서 "C/C++ > Output files" 를 선택한 후 "Assembler Output" 에 "Assembly, Machine Code and Source Code"를 선택하면 된다.





4. map 파일 분석하기

  map 파일을 분석하는 것은 아주 쉽다.

  맨 처음의 오류화면 캡쳐에 찍혔던 0x0040101a가 어느 함수에 해당하는지를 먼저 분석해보자.

  map 파일의 구조는 다음과 같다.




DebuggingTest

 Timestamp is 499fbe7b (Sat Feb 21 17:42:35 2009) --> map 파일이 생성된 시간

 Preferred load address is 00400000 --> exe의 ImageBase 주소. 이것은 말그대로 "선호되는" 주소일 뿐 DLL 이 실제 Load되는 주소는 Load 시점에서 Dynamic하게 결정되므로 DLL에 대해서는 이 값은 신뢰할 수 없다.

 Start         Length     Name                   Class   --> 첫번째 섹션은 프로세스 이미지에 로드될 각 섹션들의 베이스 주소를 나타낸다. 

 0001:00000000 00000824H .text                   CODE

 0002:00000000 000000a8H .idata$5                DATA

 0002:000000a8 00000004H .CRT$XCA                DATA

 0002:000000ac 00000004H .CRT$XCAA               DATA

                           ....

  Address         Publics by Value              Rva+Base       Lib:Object  --> 두번째 섹션에는 각 함수들과 글로벌 변수들의 위치가 나타난다. 관심을 가질 곳은 여기!!!

 0000:00000001       ___safe_se_handler_count   00000001     <absolute>

 0000:00000000       ___ImageBase               00400000     <linker-defined>

 0001:00000000       _main                      00401000 f   DebuggingTest.obj 

 0001:0000004e       @__security_check_cookie@4 0040104e f   MSVCRT:secchk.obj

 0001:00000300       _mainCRTStartup            00401300 f   MSVCRT:crtexe.obj --> Rva+Base 옆에 표시된 소문자 "f"는 "함수"를 의미한다. f가 표시되지 않는 항목은 Global 변수를 의미한다.

                                         ...

  Static symbols  --> 세번째 섹션에는 Static 함수 및 변수들의 정보가 표시된다.

 0001:0000005d       _pre_cpp_init              0040105d f   MSVCRT:crtexe.obj

 0001:000000a8       ___tmainCRTStartup         004010a8 f   MSVCRT:crtexe.obj

 0001:0000021e       _pre_c_init                0040121e f   MSVCRT:crtexe.obj



map 파일에서 관심을 가질 곳은 각 함수와 Global 변수들의 주소가 표시된 두번째 섹션이다.

헤더를 보면 네가지 항목으로 구분되어 표시된다.


- Address : 문서에 따라 여러가지 이름으로 표시되지만, 이 항목이 의미하는 것은 Segment와 Offset 으로 표시된 상대주소이다. (Segment 정보는 첫번째 섹션에 나타나 있다.)


- Publics by Value : 함수 혹은 전역변수 등 표시되는 심볼의 이름이다. 이 이름들은 C++ 의 Name Mangling 규칙에 따라 변형되어 표시되지만 대충 알아볼만은 하다. 예를 들어

void Func2();

이런 함수는 map파일에서는 다음과 같이 표시된다.

0002:000000f4       ??_C@_0BD@BPDCHPKM@?$FLFunc2?$FN?5Point?51?5?$AN?6?$AA@ 004020f4     DebuggingTest.obj

Func2 함수는 메모리 주소 0x004020f4 부터 시작된다는 것을 알 수 있다.



- Rva+Base : 문서에 따라 "Linear Address", "Absolute Address"등 여러가지 이름으로 해석되며, 결국 우리가 메모리 상에서 나타나는 주소는 이 주소이다. 우리가 찾고자 하는 주소 0x0040101a 는 여기서 찾아야 한다. Rva+Base 옆에 소문자 "f"가 찍혀있는 항목들은 "함수"라는 의미이며, "f"가 없는 항목들은 변수라는 의미이다.


- Lib:Object : 이 함수가 어느 모듈에 속해있는지를 나타낸다.




이제 진짜로 map 파일의 Rva+Base 항목에서 Exception이 발생한 주소(0x0040101a)를 찾아보자.
map파일 두번째 섹션의 Rva+Base 항목을 처음부터 뒤지면서 우리가 찾고자 하는 주소(0x0040101a)를 처음으로 넘어서는 지점을 찾는다.
Address         Publics by Value              Rva+Base       Lib:Object
0000:00000001       ___safe_se_handler_count   00000001     <absolute>
0000:00000000       ___ImageBase               00400000     <linker-defined>
0001:00000000       _main                      00401000 f   DebuggingTest.obj  --> 여기까지는 넘어서지 않았다.
0001:0000004e       @__security_check_cookie@4 0040104e f   MSVCRT:secchk.obj  --> 넘어섰다.
0001:00000300       _mainCRTStartup            00401300 f   MSVCRT:crtexe.obj  ...



위의 map파일 상에서 0x0040101a라는 주소는 _main 함수의 시작지점보다는 뒤에 있고, @__security_check_cookie@4 함수의 시작지점 보다는 앞에 있음을 알 수 있다. 따라서 0x0040101a는 _main 함수 중의 한 지점 임을 알 수 있다.


그렇다면 0x0040101a는 _main함수의 어느 부분일까? 위에서 언급한 바와 같이 .map파일로는 정확한 코드는 집어낼 수 없지만 함수와 함수 시작지점으로부터의 Offset을 계산할 수 있다. 이 Offset값을 .cod 파일에서 뒤지면 Exception이 발생한 코드를 찾아낼 수 있다.

Offset 계산 : (찾고자하는 주소) - (해당 주소가 속한 함수의 Entrypoint) 

                  = 0x0040101a - 0x00401000 = 0x001a



따라서 Exception이 발생한 지점은 main 함수의 시작지점으로부터 0x001a Bytes 떨어진 지점임을 알 수 있다.





5. cod 파일 분석하기
 이제 main함수가 정의된 cpp 파일의 cod 파일을 분석해 main함수의 0x001a번째 Bytes 에 어떤 코드가 있는지 확인해보자.

 cod 파일은 다음과 같이 생겼다.


... (앞부분 생략)

PUBLIC _main

; Function compile flags: /Ogtpy

; File d:\src\test_code\debuggingtest\debuggingtest\debuggingtest.cpp

; COMDAT _main

_TEXT SEGMENT

_argc$ = 8      ; size = 4   --> 함수의 파라메터 정의

_argv$ = 12      ; size = 4

_main PROC      ; COMDAT  -->  _main 함수 시작

; 15   : {

  00000 56   push  esi

; 16   :  printf ("[Main] Point 1\r\n");   --> C++ 코드로 16번째 Line에 해당하는 기계어 코드가 다음에 나온다.

  00001 8b 35 00 00 00     --> 위의 printf 문에 해당하는 기계어 코드이다. 이 기계어 코드의 Offset값은 0x0001이다.

                                         이부분에 해당하는 어셈코드가 밑에 파란색 부분이다....

 00   mov  esi, DWORD PTR __imp__printf   --> 위의 기계어 한줄이 어셈으로는 이렇게 세줄이 된다.

  00007 68 00 00 00 00  push  OFFSET ??_C@_0BB@CCJLHOBA@?$FLMain?$FN?5Point?51?$AN?6?$AA@

  0000c ff d6   call  esi


; 17   : 

; 18   :  printf ("[Main] Point 2\r\n");

  0000e 68 00 00 00 00  push  OFFSET ??_C@_0BB@DACONBPO@?$FLMain?$FN?5Point?52?$AN?6?$AA@

  00013 ff d6   call  esi

; 19   : 

; 20   :  *(LPINT)NULL = 0; // Exception!!

; 21   : 

; 22   :  printf ("[Main] Point 3\r\n");

  00015 68 00 00 00 00  push  OFFSET ??_C@_0BB@IIJCLGJL@?$FLMain?$FN?5Point?53?$AN?6?$AA@

  0001a c7 05 00 00 00             --> 찾았다!!!

 00 00 00 00 00  mov  DWORD PTR ds:0, 0

  00024 ff d6   call  esi

  00026 83 c4 0c  add  esp, 12   ; 0000000cH

; 23   :  return 0;

  00029 33 c0   xor  eax, eax

  0002b 5e   pop  esi

; 24   : }

  0002c c3   ret  0

_main ENDP

_TEXT ENDS

END




위와 같이 Exception이 발생한 지점을 C++ 코드 상에서 정확하게 집어낼 수 있다.


한가지 주의할 것은 위의 COD 파일 내에서의 "0001a" 이라는 offset은 main함수 내에서의 offset이 아니라 main.cpp이 build된 main.obj 내에서의 offset이라는 점이다. 단지 이 경우는 main()의 시작 offset이 main.obj 의 시작 지점과 동일했기 때문에 우연히 같은 주소가 나타났을 뿐이다.

예를 들어 main()함수의 첫번째 코드 주소가 0x01000 이었다고 하면, main()함수의 0x00001a offset 지점의 위치는 다음과 같이 계산된다.


cod파일에서 확인한 해당obj내에서 main함수 시작위치 offset + map파일에서 확인한 main함수내에서 Crash지점 offset        = 0x01000 + 0x00001a = 0x0101a


즉, COD 파일 내에서 0x0101a 를 찾으면 된다.





6. DLL 안에서 발생한 Exception 추적하기

  위와 같이 exception이 exe 에서 발생했을 때는 exe의 map파일을 이용해 추적하면 된다.

그렇다면... 만약 exception이 dll 안에서 발생했을 때는?? 당근 dll의 map 파일을 사용해야 하긴 하는데... 여기에 약간의 문제가 생긴다.



죽은 화면은 아까와 비슷하지만...  0x003c103e 라는 주소 부터가 이미 exe것이 아님을 느낄 수 있다. (exe는 0x00400000부터 시작하니까!!) 당근 저 주소를 가지고 exe의 map 파일을 뒤져봤자 찾아지지 않는다.


그렇다면 Exception이 발생한 TestDll.dll의 map파일을 살짝 살펴보자.

 TestDll


 Timestamp is 49a10238 (Sun Feb 22 16:43:52 2009)


 Preferred load address is 10000000


 Start         Length     Name                   Class

 0001:00000000 00000838H .text                   CODE

 0002:00000000 00000084H .idata$5                DATA

                               ...


map 파일의 시작부분을 보면 "Preferred load address is 10000000"이라고 되어 있다. 이 말인 즉슨, 이 DLL이 선호하는 Load Address가 0x10000000 이라는 의미 외에도 "이 map파일은 Dll의 Base Address가 0x10000000 이라고 가정하고 (즉, 0x10000000에 로드되었다고 가정하고) 작성되었다"는 의미로 해석해야 한다.

따라서, Dll의 Load Address가 0x10000000이 아닐 때는 (99.9% 아닐것이다.) 이를 보정해서 map파일을 해석해주어야 한다.


다음은 Dll을 Load하는 코드의 일부이다.

 HMODULE hDll = LoadLibrary("TestDll.dll");

 if (hDll == NULL)

 {

  printf ("[Main] Fail to Load DLL!\r\n");

  return FALSE;

 }

 

 printf ("[Main] Load DLL Address : 0x%08p\r\n", hDll);


위의 LoadLibrary했을 때 얻어오는 리턴값 HMODULE 핸들이 로드된 Dll의 Base Address 가 된다.

프로세스에 로드된 모든 Dll 의 Base Address를 얻어오는 방법은 EnumProcessModules API를 사용하는 방법과 프로세스 이미지의 PE 헤더를 분석하는 방법이 있는데 이 방법은 다음에 기회되면... ^^


위의 샘플 코드에서는 Dll의 Base Address 가 0x003C0000 였다.


그러면 dll의 map파일은 다음과 같이 분석할 수 있다.

1) Exception이 발생한 Addr : 0x003c103e
2) Dll의 Base Addr : 0x003C0000
3) Exception이 발생한 위치의 Dll 내에서의 Offset : 0x003c103e - 0x003C0000 = 0x103e
4) Dll의 Base Addr이 0x10000000 이라고 가정했을 때의 Exception Address : 0x10000000 + 0x103e = 0x1000103e

따라서... dll의 map 파일에서 0x1000103e 를 찾으면 된다.




7. map 파일과 cod 파일의 관리에 관하여

  위와 같이 Exception이 발생했을 때 해당 exe 의 정확한 map파일과 cod 파일만 가지고 있다면 거의 정확히 Exception이 발생한 위치를 발생한 위치를 집어낼 수 있다. 문제는 모든 실행파일의 버젼이 바뀔때마다 해당 버젼의 map 파일과 cod 파일을 백업받고 보관한다는 것이 상당히 귀찮다는 것이다. 이럴 때 Visual Studio의 "Post Build Script" 기능을 사용하여 매번 Build할 때마다 map 파일과 cod 파일을 자동으로 백업받도록 설정할 수 있다.

 

  VS6.0에서는 "Project Settines"창의 "PostBuild" 탭에, VS2005에서는 Build Events > Post-Build Event 에 설정한다. 스크립트의 내용은 Dos 명령어 혹은 배치파일 형태이면 되며, 경로나 파일명 등에 Visual Studio에서 제공하는 매크로를 사용할 수 있다.




다음은 매번 빌드할 때마다 현재 날짜/시간으로 된 폴더를 생성하고 exe파일과 map파일, cod파일을 백업받는 스크리트의 예제이다.

@echo off

set DirName=%date:~,4%%date:~5,2%%date:~8,2%%time:~,2%%time:~3,2%%time:~6,2%

mkdir $(OutDir)\mapfile\%DirName%

copy $(OutDir)\*.map $(OutDir)\mapfile\%DirName%

copy $(InputDir)\$(ConfigurationName)\*.cod $(OutDir)\mapfile\%DirName%

copy $(OutDir)\*.exe $(OutDir)\mapfile\%DirName%


위와 같은 PostBuild 스크립트 등록을 해놓으면 다음과 같이 Build 끝날 때 Dos명령 실행결과가 함께 Message에 표시된다.



위의 스크립트를 적용하면 빌드 한번 할때마다 exe와 map, cod가 백업되므로... 아래와 같은 폴더들이 우글우글하게 생기게 된다. 꼭 필요할 때 map파일을 사용할 수 있다면 이정도야 감수해야겠지? ㅋ






<참고자료>

Finding crash information using the MAP file  By Wouter Dhondt 



출처: http://kuaaan.tistory.com/102 [달토끼 대박나라~!! ^^]