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의 CrashGenerationServer와 http_upload 부분을 이용해서 crashreport.exe라는 툴을 만들었다.  이 exe는 1에서 크래시가 나면 그 덤프를 만들고, 이 때 호출되는 콜백에서 http로 덤프 파일을 3에게 준다.
  3. 2에서 쓴 breakpad의 http uploader가 HTTP POST(multipart form-data)로 dump 파일을 전송하는 작업을 한다. Python의 BaseHTTPRequestHandler의 do_POST를 override하고, cgi.FieldStorage 를 이용해서 이 dump를 서버 측에 저장하는 작업을 하게 했다(~40 LOC).

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

 

Update (2010-10-29)

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

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

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

11 Comments

  1. 우왕 굳! 근데 이 라이브러리 자체의 문제점은 혹시 없나요? 신뢰하고 써도 되는 툴인지 싶어서요.

    • 훌륭하지. 근데 http_upload.h/.cc 는 좀 수정이 필요할 듯…
      이거 덤프를 메모리에 풀 로드(…)해서 벡터로 만들었다가 보낸다 Orz.

  2. breakpad 내부 형식이 아닌 native dump라면 source/symbol server도 잘 구축해 놓아야 할 듯.
    우리 동네(?)에서는 크래시가 발생하면 windbg로 fulldump를 만드는데 덤프 용량이 최소 7~8GB…
    복사 해 오는데만도 한나절…orz

    • 그 덤프 압축하면 용량 꽤 줄지 않나요? 저는 업로더 코드 고쳐서 .gz 로 보내게 만드는 중이에요(…).
      압축해서 용량 줄어드나 좀 봐주시죠(…)

      • 압축하면 용량이 확~~! 줄어들지만 그걸 압축하고 푸는데 또 한나절….orz * 2

        • 근데 압축 안하면 답이 안나오는게(?), WinInet 에 있는 함수들로는, 4G 이상 못 보내더라고요 Orz
          WinInet 에 있는 함수들로도 4G 이상 보내 지네요. 버퍼 디스크립터에 있는 길이 필드를 내부적으론 안 확인하는듯…
          그래서 그냥 on-the-fly 로 압축하면서 보내면, 큰 시간 손실은 없지 않을까요?

  3. 서버 프로그램 시작할 때 CrashReporter도 같이 띄우시나요? 덤프 요청할 때마다 리포팅용 새 프로세스 만들어서 돌리는 방법을 시도해봤는데 쓰는 컴이 구려서 그런지 (…) 새 프로세스 띄울 때 WaitForSingleObject에 준 대기 시간도 그냥 씹어 드시는 경우도 있어서 난감하더군요. (사용하려는 목적이 release_assert에 가까워서 반응 시간이 좀 중요합니다.)

    • 서버 시작 할 때 CrashReporter도 같이 시작합니다.
      정확히는,

      1. 필요한 정보(?)는 서버 바이너리가 다 들고있는 채로, CrashReporter 실행 시키면서 정보를 전부 주고,
      2. CrashReporter는 자기가 초기화 다 끝내면 서버 바이너리에 이걸 알려주고,
      3. 서버 바이너리는 실제 동작(?)을 시작

      하는 형태로 만들었습니다. 어차피 1~3 모두 .lib 안에 들어가는거라 다시 작성할 필요는 없었고요 ~_~.

      결정적으로 크래시 발생했을 때 커널 객체를 만들거나, 메모리 할당해서 메시지 만들고 하는거 자체가 안될 때가 있더라고요(…). 그래서 그냥 시작할 때 다 같이 시작하는 방법을;;

Leave a Reply