출처: http://kuaaan.tistory.com/102 [달토끼 대박나라~!! ^^]
디버거로 실행시킬 때 모든 오류가 발생해준다면 오죽이냐 좋겠냐마는 현실은 그렇지가 않아서... Debug 빌드에서는 죽지 않다가 Release빌드에서만 죽는 경우라던지, 테스트베드에서는 죽지 않다가 실망에서 부하가 발생해야 죽는 경우, 한 1만대 정도 설치하면 한대 꼴로 재현되는 경우 등 개발을 하다보면 별의별 문제가 발생하게 마련이다.
보통 Release 빌드에서 오류가 발생했을 경우, 개발자에게는 다음과 같이 죽은 주소 정보 정도만 제공된다. Release 빌드한 실행파일로 디버그를 해야 하는데...
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파일 상에서 0x0040101a라는 주소는 _main 함수의 시작지점보다는 뒤에 있고, @__security_check_cookie@4 함수의 시작지점 보다는 앞에 있음을 알 수 있다. 따라서 0x0040101a는 _main 함수 중의 한 지점 임을 알 수 있다.
그렇다면 0x0040101a는 _main함수의 어느 부분일까? 위에서 언급한 바와 같이 .map파일로는 정확한 코드는 집어낼 수 없지만 함수와 함수 시작지점으로부터의 Offset을 계산할 수 있다. 이 Offset값을 .cod 파일에서 뒤지면 Exception이 발생한 코드를 찾아낼 수 있다.
Offset 계산 : (찾고자하는 주소) - (해당 주소가 속한 함수의 Entrypoint)
= 0x0040101a - 0x00401000 = 0x001a
따라서 Exception이 발생한 지점은 main 함수의 시작지점으로부터 0x001a Bytes 떨어진 지점임을 알 수 있다.
... (앞부분 생략)
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파일은 다음과 같이 분석할 수 있다.
따라서... 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 [달토끼 대박나라~!! ^^]
'C++' 카테고리의 다른 글
timer - timer_create(linux), SetTimer(windows), CreateTimerQueue(windows) (0) | 2017.09.24 |
---|---|
c++ console project에서 console창을 띄우지 않고 프로그램 실행시키는 방법 (0) | 2017.08.29 |
cmake - 샘플 (0) | 2017.05.29 |
minidump (0) | 2017.04.25 |
[펌] Debugging Tips (4) - Call Stack 추적하기 (StackWalk) (0) | 2017.04.24 |
[펌] 서버 클라이언트 구분이 없는 UDP 소켓 프로그래밍 (0) | 2017.04.20 |
기초적인 IOCP 서버 개발 팁. 연결에서 종료까지... (0) | 2017.03.27 |
LFH (Low Fragmentation Heap), TBB alloc (0) | 2016.08.08 |