Debugging and Symbolizing Crash Dumps in Xcode

OSXDEV

Jump to: navigation, 찾기

Xcode는 개발과정에서 애플리케이션의 버그를 꼼꼼히 살피고, 분리하여 제거할 수 있는 툴을 풍족하게 제공하고 있다. 그러나 애플리케이션이 릴리즈를 위해 빌드된다면, 이러한 안전망은 사라져버려 생산품의 버그를 찾는일이 어렵게 된다. 디버깅 정보가 없다면 Xcode 디버거나 Shark같은 친숙한 디버깅 툴들은 거의 무용지물이나 마찬가지다. 애플리케이션이 충돌을 발생시키면 충돌된 내용 담는 로그의 내용은 오직 16진수의 주소값뿐이다. 이러한 정보로는 왜 어플리케이션이 실패하였는지를 알기가 힘들다.

간단한 해결책으로 릴리즈된 어플리케이션에 디버깅 정보를 넣을 수 있다. 하지만 이러한 해결책은 일반적으로 다음과 같은 두가지 이유때문에 받아들여지기 힘들다. 첫째로, 디버깅 정보는 실행파일 자체보다 크기가 몇배는 커서 배포에 커다란 문제가 된다. 둘째로, 포함된 심볼과 변수 정보는 여러분의 어플리케이션 설계를 원하지 않는 곳에 노출시킬 우려가 있다.

그러므로 우리가 목적하는 바는 다음과 같은 개발 흐름이다.

  • 릴리즈 빌드에서는 디버깅 정보를 제거하여 배포하기 좋게 만든다.
  • 친숙한 툴을 이용하여 릴리즈 빌드를 디버그 할 수 있도록 어느정도의 정보를 남겨둔다.
  • 충돌된 리포트의 정보가 충돌을 일으킨 원본 소스의 위치를 참조할 수 있게 한다.

이 문서에는 이러한 목표를 달성할 수 있는 Xcode의 솔루션에 대해 알아볼 것이다. 여러분은 완성된 어플리케이션에서 심볼 정보를 제거하고, 이러한 어플리케이션을 디버그 하고 충돌내용이 기록된 로그를 어떻게 분석할 것인지에 대한 여러가지 테크닉을 배우게 될 것이다. 이 문서의 첫번째 파트는 최종 어플리케이션의 빌드와 디버그를 집중적으로 다룰 것이다. 그 이후의 부분에서 디버깅 정보가 없는 충돌 덤프에 대해 알아본다. 앞으로 설명한 기법들은 모든 C, C++, Objective-C 어플리케이션에 모두 적용된다.

주의: 이 문서는 Xcode 2.4.x를 기준으로 설명되어 있다.


목차

[편집] 'Out of the Box' 솔루션의 사용

일반적인 개발 스타일은 어플리케이션을 최소한의 최적화와 최대한의 디버깅 정보로 빌드하는 것이다. 그렇게 함으로써 디버거와 성능 측정 도구들은 컴파일된 코드에 대한 최대한의 정보를 얻을 수 있게 된다. 어플리케이션이 충분히 디버그 되었을때, 디버깅 정보를 제거하여 다시 빌드하게된다. 최종 버젼은 사용자에게 배포하기 적합한 형태가 된다.

Xcode 프로젝트 템플릿은 이러한 빌드 설정을 정확하게 제공하고 있다. 새로운 프로젝트는 디버그 설정과 릴리즈 빌드 설정을 포함하고 있는데, 디버그 설정은 완전한 디버그 정보를 포함하고 최소한의 최적화를 수행하며, 릴리즈 빌드 설정은 최소한의 심볼 정보를 포함하고 최대한 최적화된 코드를 생성해낸다. 이러한 설정은 간단하고 많은 프로젝트에 적합하지만, 결점을 가지고 있다.

만약 릴리즈 버젼에서 문제가 발생한다면, 코드를 디버그하기가 어렵다. 디버깅 정보가 없으면 GNU Debugger (GDB)와 다른 분석 도구들은 굉장히 제한적으로 동작하게 된다. 만약 어플리케이션이 충돌을 발생한다면, 충돌 로그는 메쏘드나 함수 이름을 포함하지만 소스의 행번호는 포함하지 않는다.

기본 릴리즈 빌드가 최소한의 메쏘드/함수 이름 정보를 포함하는 어플리케이션을 생성해낸다는 사실을 주목하자. 이러한 방식은 디버그 정보를 포함하지 않는것과 완전한 디버그 정보를 포함하는 것 사이의 절충안이다. 이러한 방식의 장점은 충돌 리포트를 진단하기 충분한 정보를 제공해 준다는 것이다. 단점은 클래스와 메쏘드 이름이 어플리케이션에 내장됨으로써 사용자에게 더 많은 정보가 노출된다는 것이다.


[편집] 릴리즈 빌드에 디버그 정보를 포함하기

가장 간단한 해결책은 릴리즈 어플리케이션을 두번 빌드하는 것이다. 디버깅 정보를 포함해서 한번, 제거하고 또 한번 한다. Project > Edit Project Settings 를 선택한다. Configurations 탭을 클릭하고 Configurations 화면의 Edit configuration list: 에서 Release 를 선택하고 Duplicate 버튼을 눌러 복사한다. 새로운 설정의 이름을 "Debug Release" 라고 바꾼다. 프로젝트의 Targets 그룹에서 여러분의 어플리케이션을 선택하고 Info 윈도우를 연다. Build 탭의 Configuration 메뉴에서 Debug Release를 선택하고, 밑에서 Generate Debug Symbols을 찾아 체크한다 (Code Generation 부분에서 찾을 수 있다). 여기까지 되었으면, 프로젝트 빌드 설정이 아래 표1 과 같을 것이다.

Build Settings Value
Generate Debug Symbols YES

표 1: Debug Release Configuration 에서 빌드 설정

릴리즈 설정으로 프로젝트를 한번 빌드한 후 다시 디버그 릴리즈 설정으로 빌드해보자. 두 개의 어플리케이션의 실행가능한 코드는 동일하다, 차이는 오직 디버그 정보를 포함하고 있느냐의 여부이다. 디버그 릴리즈 빌드를 통해 성공적으로 디버그를 수행할 수 있다. 그리고 어플리케이션은 소스의 행번호와 메쏘드 네임을 완전히 포함하고 있다. 이러한 사실을 확인해보기 위해서, 어플리케이션에 의도적으로 실행시에 충돌을 일으킬 수 있는 코드를 추가해보기로 하겠다. 코드는 목록1과 같다.

#define CRASH_CODE 1
#if CRASH_CODE
	(void)strlen((const char *)1);
#endif

목록1: 충돌을 발생시키는 소스 코드

빌드하고 파인더에서 어플리케이션을 실행시켜보자. Xcode 에서 실행하면 안된다. Xcode는 충돌 정보를 가로채어 디버거를 실행시켜버린다. 위의 코드가 실행되면 어플리케이션은 충돌을 발생시킨다. 충돌 리포트를 받기위해서, Crash Reporter를 사용가능하게 해야한다. CrashReporter에 관한 자세한 사항은 기술 문서(Technical Note) TN2123을 보기 바란다.

이러한 접근법은 간단하고 우리의 두번째 목표를 충족시켜주며, 어느정도 첫번째 목표도 충족시켜 준다. 이제 릴리즈 버젼의 어플리케이션을 쉽게 디버그 할 수 있게 되었으며, 완전한 심볼 정보를 포함하는 충돌 리포트가 생성될 것이다. 그러나 릴리즈 빌드가 발생시키는 충돌 정보는 여전히 알아보기 힘들다. 그리고 무거운 디버깅 정보를 포함하는 어플리케이션을 사용자에게 제공하는건 적절치 못하다.

여기엔 더 심각한 단점이 있다. 빌드를 두번하는 것은 많은 시간을 소모한다. 좀더 중요한 사실은, 릴리즈 어플리케이션과 디버그 릴리즈 어플리케이션이 동일할거라는 보장이 없다. 이것은 Debug Release 빌드가 릴리즈된 버젼과 똑같은 버그를 가지고 있지 않을 수도 있다는 사실을 말해준다. 더욱 나쁜 것은, 이 두 빌드가 동일한지 확인할 방법이 없다는 것이다. 여러분은 신중하게 릴리즈 빌드와 디버그 릴리즈 빌드 설정을 병행하여 유지하여야 한다. 어느 차이점이 생기면 어플리케이션은 서로 다르게 되버린다. 개발 환경에서 이러한 요구사항을 유지하는 것은 매우 어려운 일이다.


[편집] 최종 결과물에서 디버그 정보 제거하기

Xcode는 실행파일에서 디버깅 심볼을 제거할 수 있는 몇가지 옵션을 내장하고 있다. 그중에 하나가 Strip Linked Product 빌드 설정이다. 보통은 설정되있지만, Deployment Postprocessing 설정이 함께 설정되있지 않다면 아무 효과도 없다. Deployment Postprocessing은 나머지 빌드 설정을 전체적으로 통제하는 일종의 스위치 같은 것이다. 이것은 설치 명령과 함께 xcodebuild 툴을 실행시키는 것과 거의 비슷하다.

다시 target 빌드 설정을 열고, 릴리즈 설정에서 debugging symbols를 켠다. 프로젝트 빌드 설정에 가서, 릴리즈 설정에서 Strip Linked Product와 Deployment Postprocessing 항목을 체크 한다. 여러분의 프로젝트 설정은 이제 표2 와 같을 것이다.

Build Settings Value
Deployment Postprocessing YES
Strip Linked Product YES

표 2: 릴리즈 설정에서 프로젝트 빌드 설정

실행파일에서 디버그 심볼을 제거하는 것은 첫번째 목표를 달성시켜준다. 이것은 모든 디버깅 심볼 정보가 완전히 제거된 최종 실행파일을 생성해준다. 그 결과로 이러한 방법은 두번째 목표를 실패하게 한다. 이것은 디버그 할 수 없으며, 충돌 레포트는 아무런 프로그램 심볼 정보를 포함하고 있지 않다.

이러한 두가지 해결책을 결합하면, 두가지 목적을 달성할 수 있다. Deployment Postprocessing을 릴리즈 설정에서는 설정하고, 디버그 릴리즈 설정에서는 해제하는 것이다. 그러나, 이 해결책도 첫번째 해결책의 또다른 단점을 갖고 있다.


[편집] 디버깅 정보를 별도의 파일로 분리해 내기

이제 필요한 것은 최종 실행파일에서는 디버깅 정보를 제거하고, 동시에 나중에 디버그하기 위해 이러한 정보를 저장하는 빌드 과정이다. 최종 실행파일에서 단순히 디버그 정보를 제거하는 대신에, 디버그 정보를 추출해서 따로 저장하는 더 나은 해결책이 있다. 어플리케이션은 한번 빌드되고, 추출된 디버그 정보는 개발자가 사용가능하며, 더이상 최종 생산품에 내장되지도 않다.

2.4 버전에서 Xcode는 dSYM의 파일 포맷을 갖는 Debugging with Attributed Record Formats (DWARF) 기능을 포함했다. 디버그 정보 형식을 DWARF로 설정하면, 링커는 dSYM 확장자를 갖는 분리된 파일 패키지를 생성해 내는데, 이것은 실행파일의 심볼 정보 복사본을 포함하고 있다. 얼마안되는 참조정보가 실행파일에 포함되어 툴로 하여금 생성된 프로그램에 맞는 dSYM 파일을 인식할 수 있게 해준다.

내장된 디버그 정보는 이제 안전하게 실행파일에서 제거해도 된다. 디버깅 정보가 제거된 실행파일이 로드되면 GNU 디버거는 그에 맞는 dSYM 파일을 확인한다. 만약 발견되면 자동으로 DWARF 디버깅 정보를 사용하게 된다. dSYM 파일은 반드시 간단한 실행파일이나 어플리케이션 번들같은 생산물과 같은 디렉토리에 들어있어야 한다.

어플리케이션 타켓의 릴리즈 설정에서, 표 3과 같이 dSYM방식의 디버그 정보를 설정한다.

Build Settings Value
Generate Debug Symbols YES
Debug Information Format DWARF with dSYM File

표 3: 릴리즈 설정에서 타겟 빌드 설정

주의할 점은 dSYM 파일은 ZeroLink 와 함께 사용될 수 없고, 이것은 일반적으로 디버그 정보의 복사복을 유지하면서, 디버그 정보가 제거된 생산물에 적합하다는 것이다. 빌드 설정에서, 일반적인 stabs나 DWARF 형식을 계속해서 사용할 수 있다.

그리고 이전의 해결책에서와 마찬가지로 최종 실행파일에서는 디버그 정보가 제거되게 설정한다. 표 4와 같다.

Build Settings Value
Deployment Postprocessing YES
Strip Linked Product YES
Use Separate Strip YES
Strip Debug Symbols During Copy NO

표 4: 릴리즈 설정에서 타겟 빌드 설정

이러한 해결책은 우리의 첫째 목표와 둘째 목표를 모두 만족시키고, 유지하기 쉽다. dSYM 파일을 보관함으로써 나중에 최종판 어플리케이션을 디버그 할 수 있다. dSYM 파일을 이용하여 디버그 세션이 시작되기 때문에, 다른 특별한 과정없이 릴리즈 빌드된 어플케이션을 디버그 할 수 있다. dSYM 파일을 빼고 심볼 정보를 포함하지 않는 어플리케이션을 자유롭게 배포할 수 있다.


[편집] 최적화된 코드의 디버깅에 관하여

최적화된 코드를 디버깅할때 발생할 수 있는 미묘한 문제들에 대해 주의해야 한다. 최적화는 소스코드와 컴파일된 목적 코드사이의 1:1 관계를 깨뜨려버릴 수 있다, 그리고 이것은 원본 소스에서 코드의 행번호와 실행되는 주소간의 상호관계를 불가능하게 만든다. 이러한 이유로 디버깅 과정에서는 일반적으로 코드를 최적화시키지 않는다. 또다른 문제로 프로그램 실행의 흐름이 변경될 수 있다 (예로, 9번째 줄의 구문이 8번째 줄의 구문보다 먼저 실행될 수 있다). 이러한 특성은 중단점(breakpoint)을 설정하거나 디버거에서 코드를 제어하는 것을 어렵게 만든다.

이 문제에 대한 실제적인 해결책은 없다. 최적화는 컴파일러가 더 나은 성능을 위해 코드를 다시쓰는 것을 뜻한다.


[편집] 충돌 덤프를 심볼화 하기

어플리케이션이 충돌을 발생하면, 시스템의 crash reporter는 충돌 로그를 생성해 낸다. 이 로그는 어플리케이션과 프레임워크에서 얻을 수 있는 내용으로 주석이 달린 스택 추적(trace) 정보를 포함하고 있다. 표준 시스템 프레임워크는 기본적인 심볼 정보를 포함하고 있다, 그러나 어플리케이션이 디버깅 심볼이 제거된 상태라면 스택에 대한 쓸만한 정보는 로그에 기록되지 않을 것이다. 목록 2는 TempConverter.crash.log의 일부를 보여주고 있다.

Thread 0 Crashed:
0   com.yourcompany.TempConverter 	0x00002b84 0x1000 + 7044
1   com.apple.Foundation          	0x929ea9c8 _NSSetObjectValueAndNotify + 136
2   com.apple.Foundation          	0x929ea6f4 -[NSObject(NSKeyValueCoding) setValue:forKeyPath:] + 180
…
27  com.apple.AppKit              	0x937f887c NSApplicationMain + 452
28  com.yourcompany.TempConverter 	0x000029dc 0x1000 + 6620
29  com.yourcompany.TempConverter 	0x000026e0 0x1000 + 5856

목록 2: TempConverter.crash.log의 내용

TempConverter 실행파일이 심볼 정보를 포함하지 않기 때문에, TempConverter 코드 세그먼트내의 프로그램 위치가 16진수 오프셋 값으로 출력된다. atos ("address to symbol") 툴을 사용하여 이러한 주소값을 심볼 이름으로 변환할수가 있다. 만약 충분한 디버깅 정보가 가능하다면, 이 툴은 소스 파일 이름과 주소에 맞는 행번호까지 출력해준다. 터미널 윈도우를 열어서, TempConverter D/build/Release 폴더로 이동하여 목록 3에 나온 명령을 실행한다.

atos -o Debug/TempConverter.app/Contents/MacOS/TempConverter 0x00002b84
-[ConverterApplicationDelegate setCentigradeTemperature:] (in TempConverter) (ConverterApplicationDelegate.m:42))

목록 3: atos를 사용하여 메모리 주소를 심볼로 변환

atos 툴은 TempConverter 실행파일에 포함된 심볼을 읽어와서, 이 정보를 이용하여 0x00002b84 주소를 그에 맞는 심볼로 변환한다. 만약 DWARF dSYM 파일을 이용한다면, Xcode 3(Mac OS X 버젼 10.5)에 들어있는 atos를 이용해야 한다.

atos 명령어는 여러개의 주소를 인자로 받거나, 표준 입력으로 부터 주소의 목록을 읽어올 수도 있다. 몇개의 주소를 변환하기에는 편리하지만, 거대한 스택 추적 정보나 여러개의 충돌 로그에 대해서는 사용하기가 불편하다. 다행히, 목록 4에 나온 스크립트를 활용하여 이것을 자동으로 처리할 수 있다.

#!/bin/bash

AWK_SCRIPT=/tmp/symbolizecrashlog_$$.awk
SH_SCRIPT=/tmp/symbolizecrashlog_$$.sh

if [ [ $# < 2 ] ]
then
	echo "Usage: $0 [ -arch <arch> ] symbol-file [ crash.log, ... ]"
	exit 1
fi

ARCH_PARAMS=
if [[ "${1}" == '-arch' ]]
then
	ARCH_PARAMS="-arch ${2}"
	shift 2
fi

SYMBOL_FILE="${1}"
shift

cat > "${AWK_SCRIPT}" << _END
/^[0-9]+ +[-._a-zA-Z0-9]+ *\t0x[0-9a-fA-F]+ / {
	addr_index = index(\$0,\$3);
	end_index = addr_index+length(\$3);
	line_legnth = length;
	printf("echo '%s'\"\$(symbolize %s '%s')\"\n",substr(\$0,1,end_index),
		\$3,substr(\$0,end_index+1,line_legnth-end_index));
	next;
	}
{ gsub(/'/,"'\\\\"); printf("echo '%s'\n",\$0); }
_END

function symbolize()
{
	# Translate the address using atos
	SYMBOL=$(atos -o "${SYMBOL_FILE}" ${ARCH_PARAMS} "${1}" 2>/dev/null)
	# If successful, output the translated symbol. If atos returns an address, output the original symbol
	if [[ "${SYMBOL##0x[0-9a-fA-F]*}" ]]
 	then
		echo -n "${SYMBOL}"
	else
		echo -n "${2}"
	fi
}

awk -f "${AWK_SCRIPT}" $* > "${SH_SCRIPT}"

. "${SH_SCRIPT}"

rm -f "${AWK_SCRIPT}" "${SH_SCRIPT}"

목록 4: symbolizecrashlog 스크립트

심볼 정보가 빠진 TempConverter의 충돌 로그와 디버깅 정보를 포함하고 있는 실행파일의 복사본을 가지고 이 스크립트를 목록 5와 같이 이 스크립트를 실행해보자. (필요한 모든 파일은 현재 디렉토리에 들어있다고 가정한다.)

./symbolizecrashlog TempConverter TempConverter.crash.log > TempConverter.symbolized.log

목록 5: symbolizecrashlog 툴의 실행

이 symbolizecrashlog 스크립트는 하나 이상의 충돌 로그를 입력받고, 첫번째 인자에 오는 어플리케이션에 포함된 디버깅 정보를 사용하여 스택 추적정보의 주소값을 그에 맞는 심볼 값으로 변환해준다. 변환불가능한 주소값은 그대로 나둔다. 충돌 로그 파일이 주어지지 않으면, 표준입력으로 부터 충돌 로그를 읽어온다.


[편집] 프로세서 아키텍쳐 충돌

유니버설 바이너리는 여러개의 프로세서 아키텍쳐에 대한 실행파일을 담고 있다. atos툴과 symbolizecrashlog 스크립트는 현재 실행되고 있는 컴퓨터의 아키텍처에 맞게 주소 변환을 한다고 가정한다. 예를들어 파워맥(PowerPC)에서 발생된 충돌 로그를 파워맥 프로(Intel)에서 심볼화하는 것은 잘못된 결과를 발생한다, atos가 주소값을 인텔 실행파일에 적용시켜버리기 때문이다. 그래서 atos에 "ppc" 실행파일에 맞게 디버깅 심볼 정보를 사용하려고 알려주어야 한다. atos와 symbolizecrashlog는 -arch 옵션으로 주소값을 해석하는데 기준이 될 아키텍처를 설정할 수 있다. 목록 6은 이러한 예를 보여준다.

atos -arch ppc TempConverter 0x00002b84
symbolizecrashlog -arch i386 TempConverter TempConverter.crash.log > TempConverter.symbolized.log

목록 6: atos와 symbolizecrashlog 사용시 아키텍처 지정


[편집] 결론

디버깅 정보를 제거한 최종 어플리케이션의 생성과, 같은 어플리케이션을 디버그하고 문제를 진단해야할 필요성 사이에 균형을 잡는 일은 개발을 복잡하게 만든다. 다행이도 Xcode는 정확하게 얼마나 많은 양의 디버그 정보를 생성해 낼지와, 어디에 저장될 것인지를 조절할 수 있는 방법을 자체적으로 지원하고 있다.


[편집] 참고 자료


원문 : http://developer.apple.com/tools/xcode/symbolizingcrashdumps.html 번역 : sunil