Breakpad 로 CrashReporter 만들기

최근 한 3주 정도 바쁘더니, 어제 오늘은 좀 여유가 생겨서, 얼마 전에 코드를 읽어 본 google-breakpad를 써서 C++ 코드로 동작하는 바이너리의 crashdump를 특정 서버에 보고하는 간단한 라이브러리(래퍼) + 툴을 작성했다.

내가 이걸 필요로 하는 건, 팀에서 만드는 각종 서버 바이너리를 팀 외부의 여러 곳에서 쓰고 있는데, 이에 대한 덤프를 전달 받는다거나 하는 게 너무 번잡하기 때문 – 실제 라이브 서비스가 되면 다른 얘기지만, 그 전의 개발하고 테스트하는 단계에선 피드백 단위가 너무 크다. 그래서 최대한 빨리 크래시 정보를 얻어내기 위해 간단한 유틸리티를 작성한 것.

일단 본론으로 들어가서, 아주 간단히 breakpad를 소개하자면, Google Chrome처럼, 여러 플랫폼을 지원하는 프로그램을 위한 크래시 덤프를 다루기 위한 툴이다. ((실제로는 Google의 Chrome, Picasa, Earth,  그리고 Mozilla Firefox, Camino등에서 쓰고 있다고 한다))  Win32 개발자들이 접하는 minidump나, *nix 개발자들이 접하는 coredump를 breakpad 포맷(이라기보단 함수 맵)으로 변경하고, 이를 이용해서 플랫폼이 바뀌어도 같은 형태의 스택 트레이스(stack trace)를 볼 수 있게 해주는 툴이다.

좀 더 세부적으로 보면 다음과 같은 부분으로 되어 있다.

  1. (개별 사용자 용) 크래시가 발생했을 때, 이를 breakpad 에서 사용하는 포맷으로 덤프를 남겨주는 부분: in-process 덤프만 있는 게 아니라, 다른 프로세스에서 dump를 남기는 방식(out-of-process dump)도 지원한다
  2. (Build-System 용) 디버그 정보를 읽어서 breakpad 내부 형식으로 바꾸는 부분 : Win32 pdb 나 –g 옵션을 넣고 빌드한 *nix 바이너리에서 심볼 데이터를 뽑아낸다
  3. (Crash Collector 용) 1에서 나온 정보를 가지고 2를 이용하여 스택 트레이스를 뽑아내는 부분

사실 2, 3 부분은 Windows 환경에서만 프로그래밍 한다면 그다지 중요하지 않다; 어차피 breakpad 도 덤프 자체는 minidump를 쓰고 있고, breakpad 소개의 Build / User / CrashCollector system 다이어그램에 나온 과정도, 디버그 정보가 애초에 분리되어 빌드 되는 환경(/Z7 같은 걸 쓰면 모르겠지만)에선 그다지 더 편해질 건 없다.

그렇지만 1에서 Windows named-pipe를 써서, 크래시가 발생한 프로세스가 아니라, 안전하게 동작 중인 프로세스에서 덤프를 남길 수 있다는 점(Out-of-process 덤프), 그리고 이 덤프 남기는 부분의 코드에서 제공하는 callback 지점들이 적당해서, 이걸 이용해서 간단하게(!) 실제 배치된 시스템의 덤프를 중앙의 서버로 모으는 작업을 간편하게 작성할 수 있었다.

일단 out-of-process로 덤프를 남길 수 있기에, 크래시가 발생한 바이너리에서 할 수 없는 일들 – 메모리 신규 할당, heap 메모리 참조, 기타 등등 ((크래시가 일어나면 exception handler 자체의 스택 일부를 빼면 거의 아무것도 믿을 수 없게 된다; heap, 다른 스레드의 메모리, 다른 스레드 스택 등등 오염되어 제대로 참조할 수 없는 가능성이 크다)) – 을 맘대로 해도 되기 때문에 웹 서버나 다른 서버로 덤프를 보내는 일이 쉬워진다.

그리하여, 다음과 같은 코드를 짰다. 이 중 다른 서버 바이너리를 작성/배포하는 사람이 신경 써야 하는 건 1에서 노출한 1개의 함수 뿐.

  1. Breakpad의 CrashGenerationClient를 래핑하고, 외부 프로세스에 덤프 처리를 맡기는 부분을 .lib 파일로 분리했다. 이 라이브러리는 초기화 시점에 2를 CreateProcess로 실행한다.
  2. Breakpad의 CrashGenerationServerhttp_upload 부분을 이용해서 crashreport.exe라는 툴을 만들었다.  이 exe는 1에서 크래시가 나면 그 덤프를 만들고, 이 때 호출되는 콜백에서 http로 덤프 파일을 3에게 준다.
  3. 2에서 쓴 breakpad의 http uploader가 HTTP POST(multipart form-data)로 dump 파일을 전송하는 작업을 한다. Python의 BaseHTTPRequestHandlerdo_POST를 override하고, cgi.FieldStorage 를 이용해서 이 dump를 서버 측에 저장하는 작업을 하게 했다(~40 LOC).

이제 1을 링크해서 사용하는 서버 바이너리는, 2와 같이 배포하기만 하면, 크래시 발생 후에는 미리 지정한 특정 서버에 고스란히 덤프가 쌓이게 된다. 라이브 서비스 들어가기 전에는 테스트 망에서 RSS feed-notifier 류의 툴만 써 주면 이제 간단히 확인(…).

Update (2010-10-29)

전체 메모리 덤프 (MiniDumpWithFullMemory) 옵션을 줘도 풀 덤프가 남지 않아서 — 사실 이렇게 생각한게 모든 삽질의 시작 — 어제 저녁에 야근했는데, 원인이 너무 허무 했다. 저 옵션으로 덤프를 남기게 했더니, 실제로는 덤프 디렉토리에 blah-blah.dmpblah-blah-full.dmp 이렇게 덤프가 두 개 생성되더라. 그리고 “덤프 생성 완료” callback 에 전달되는 파일 이름은 저 중 앞 쪽의 덤프.

이건 소스 고쳐서 쓰거나, 컴파일 옵션?을 찾거나 해야 할 듯. 아니면 그냥 -full 붙이던가