본문으로 건너뛰기

호랑이는 죽어서 가죽을 남기고 프로그램은 죽어서 덤프를 남긴다.

· 약 20분
01010011
Sr. Software Engineer, Engineering Manager

tiger

서론

아무리 소프트웨어를 잘 만들었더라도 구동중인 프로그램이 사용자 환경에서 비정상 종료되는 문제는 필연적이다.

속된 말로 '프로그램이 죽는' 현상이 이러한 비정상 종료에 해당하는데, 개발자 입장에서는 왜 이러한 '죽음'이 발생하는지 파악이 어렵다. 왜냐하면 개발자의 PC에서는 프로그램이 죽지 않기 때문이다.(It works on my machine)
너무도 다양한 사용자 환경은 기상천외한 문제를 일으킨다.(바닐라 아이스크림 알러지가 있는 자동차)

이러한 안타까운 죽음의 원인을 부검하기 위해, 개발자는 프로그램이 실행되었던 주변환경에 대한 다양한 정보를 수집한다. 허나 아무리 다양한 주변 정보를 수집한다 하더라도 직접적인 사인은 시체를 확인해야만 하듯, 프로그램이 비정상 종료된 원인은 크래시가 발생한 시점에 메모리에 적재된 스냅샷을 확인해야만 한다.

그렇다. 호랑이는 죽어서 가죽을 남기고 한우는 죽어서 T본 스테이크를 남기듯, 프로그램은 죽어서 메모리 덤프를 남긴다. 이 글에서는 메모리 덤프가 무엇인지 알아보고, 다양한 사용자 환경에서 덤프를 수집하고 처리하기 위해 어떤 과정들이 이뤄지는지를 알아보겠다.

프로그램의 생애주기(정상종료 vs 비정상종료)

프로그램의 비정상 종료가 무엇인지 설명하려면 정상적으로 실행되어 종료되는 상황이 어떤 흐름으로 진행되는지 알아둘 필요가 있다.

먼저 소스코드가 실행가능한 프로그램으로 변환되는 과정을 개략적으로 살펴보자. 컴파일러는 소스코드를 역할에 따라 다양한 중간형태(=Object File)로 변환한다.
다음은 Linux의 Object File 형식이다. 다른 플랫폼의 경우 명칭이 좀 다를 수 있으나 개념은 크게 다르지 않다.

elf overview 출처: https://cs4157.github.io/www/2024-1/lect/15-elf-intro.html

  • text section: 컴파일 타겟 머신에서 실행 가능한 기계어 코드
  • data section: 초기화된 전역변수/static 변수
  • bss section: 초기화되지 않은 전역변수의 메타정보
  • 그밖에: 읽기 전용(rodata)나 심볼테이블(symtab) relocation 정보(rel.text/rel.data) 등등

이 중간형태의 Object file들이 실행가능한 형태의 Executable file로 결합이 되어야 비로소 최종적인 프로그램의 형태가 된다.

Java나 Python, Typescript 같은 Managed 환경은 동작방식도 다르고, 이 글의 메인 주제인 Crash Dump를 (웬만하면)남기지 않기 때문에 여기서는 다루지 않는다.

프로그램이 실행되기 위해서는 Object file들을 결합 후 메모리에 적재하여야 한다. 이 과정에서 링커는 여러 오브젝트 파일을 하나의 실행 파일로 결합하고, 로더는 이 실행 파일을 메모리에 적재한다. 이 실행파일 형식은 플랫폼에 따라 다르다.

  • Windows: PE(Portable Executable)
  • Linux: ELF(Executable and Linkable Format)
  • macOS: Mach-O(Mach Object)

프로그램이 메모리에 적재되었을 때 text / data / bss 같은 영역의 크기는 고정적이다. 한편, 프로그램이 동작함에 따라 동적으로 크기가 늘었다 줄었다 하는 영역이 있는데, TLS(Thread Local Storage)에서 자라나는 콜스택이나 힙이 그러한 예이다. 콜스택은 함수 호출 시 스택 프레임을 추가하고, 함수 종료 시 제거하는 구조다. 힙은 프로그램 실행 중 메모리를 할당하고 해제하는 영역이다.

프로그램의 특정 기능을 사용하기 위해 버튼을 누르거나, 화면을 터치하는 등의 행위를 하면 그 기능을 수행하기 위해 구성된 함수들이 차례대로 호출된다. 때로는 크기가 얼마나 될지 모르는 데이터를 읽고 쓰는 작업이 필요하기도 하다. 이 과정에서 콜스택과 힙이 자라나게 된다.

정상적인 사용자 시나리오에서는 프로그램이 모든 작업을 마치고 종료될 때, 콜스택과 힙에 할당된 모든 메모리가 정리되고, 모든 리소스는 OS에 반환된다. 이로써 프로그램은 정상 종료 상태에 진입하게 된다.

Alt + XCtrl + C 로 프로세스를 중단시키는 것도 정상 종료의 범주에 들어야 할까? 관점에 따라 다르겠지만 일단 필자가 글을 통해 다루려는건 회복 불가능한 상태에 진입한 프로그램이다. Alt + XCtrl + C로 프로그램을 종료한다는건 사용자의 의도에 의해 프로그램이 종료되는 것이므로 이 글에서는 정상 종료로 간주한다.

하지만 프로그램이 예기치 못한 상황에 직면하면 비정상 종료가 발생할 수 있다. 이는 마치 사람이 가서는 안 되는 곳 - 군사분계선 바깥 지역이나 은행 금고 등 -에 발을 들이거나, 다른 사람들에게 치명적인 피해를 입히는 행위를 했을 때 정부가 이를 제재하고 감옥으로 보내는 것과 비슷하다. 마찬가지로 OS도 시스템의 안정성을 위해 계약되지 않은 행위를 하는 프로그램을 강제로 종료시킨다. 시스템 메모리의 보호된 영역 또는 잘못된 주소를 참조하거나, 리소스를 과도하게 점유하는 경우 운영체제는 해당 프로그램을 강제로 종료하는데, 이것이 우리가 흔히 말하는 크래시이다.

예외처리

이러한 갑작스러운 비정상 종료를 제어하기 위해 프로그래밍 언어와 운영체제는 예외처리 수단을 제공한다. 이 글에서는 OS 수준의 예외처리에 대해서만 다룬다.

  • Windows: SEH(Structured Exception Handling)
  • Linux: Signal
  • macOS: Mach Exception & Signal(Mach Exception의 우선순위가 더 높음)

비교 표: 플랫폼별 관리 예외/신호

문제 유형Windows (SEH)Linux (Signals)macOS (Mach + Signals)
잘못된 메모리 접근STATUS_ACCESS_VIOLATIONSIGSEGV, SIGBUSSIGSEGV, SIGBUS, EXC_BAD_ACCESS
스택 오버플로우STATUS_STACK_OVERFLOWSIGSEGV (간접적으로 발생)SIGSEGV (간접적으로 발생)
잘못된 명령어STATUS_ILLEGAL_INSTRUCTIONSIGILLSIGILL, EXC_BAD_INSTRUCTION
0으로 나누기 등STATUS_FLOAT_DIVIDE_BY_ZEROSIGFPESIGFPE, EXC_ARITHMETIC
리소스 초과STATUS_INSUFFICIENT_RESOURCESSIGXCPU, SIGXFSZSIGXCPU, SIGXFSZ

OS 별로 서로 다른 덤프 포맷

크래시가 발생하면 운영체제는 메모리 스냅샷을 기록하여 디버깅과 원인 분석에 활용한다. 그러나 각 운영체제는 크래시 시점의 메모리 상태를 저장하는 방식과 포맷이 서로 다르다. 각 OS는 고유의 덤프 포맷을 사용하여 크래시 상황을 분석하는 데 필요한 정보를 제공한다.

1. Windows

  • Minidump: Windows 운영체제는 Minidump 포맷을 사용하여 크래시 정보를 저장한다. Minidump는 크래시 원인을 분석하는 데 필요한 최소한의 정보를 포함하며, 파일 크기가 작아 전송과 분석이 용이하다. 보통은 힙을 포함하지 않으며, 옵션으로 힙 영역을 포함시키더라도 전체 내용을 포함하지 않기 때문에 파일 크기가 상대적으로 작다.
  • Full Dump: 전체 메모리 덤프를 포함하여 크래시 시점의 모든 메모리 상태를 기록한다. 힙, 전역변수, 확장 레지스터, 기타 리소스 등의 모든 내용을 포함하여 파일 크기가 크고 분석이 복잡할 수 있다.

2. Linux

  • Core Dump: Linux 운영체제는 Core Dump 포맷을 사용하여 크래시 정보를 저장한다. Core Dump는 프로세스의 메모리 이미지와 레지스터 상태를 포함하며, 디버깅에 유용한 정보를 제공한다. 그러나 파일 크기가 크고 다루기 어려운 경우가 많다.

3. macOS

  • Apple Crash Report: macOS는 Apple Crash Report 포맷을 사용하여 크래시 정보를 저장한다. 이 포맷은 크래시 시점의 스택 트레이스, 메모리 내용, 레지스터 상태 등을 포함하며, 분석을 위해 Xcode와 같은 도구와 함께 사용된다.

4. Android

  • Tombstone: Android 운영체제는 Tombstone 포맷을 사용하여 크래시 정보를 저장한다. Tombstone은 크래시 시점의 스택 트레이스, 메모리 내용, 레지스터 상태 등을 포함하며, adb와 같은 도구를 사용하여 분석할 수 있다.

이렇듯 각 운영체제의 덤프 포맷은 크래시 원인 분석에 필요한 정보를 제공하지만, 그 구조가 다르기 때문에 크로스 플랫폼 환경에서 어플리케이션을 배포하는 개발자는 파편화된 덤프 포맷을 일일이 관리해야 한다. 이는 고통스러운 작업이기에 덤프를 다루는 일관된 방법이 필요해지는 이유가 된다.

Minidump Format 과 Chromium Breakpad Project

지금까지 우리가 알아본 것을 요약하면,

  • 프로그램은 정상적으로 실행되다가 예기치 못한 상황에 직면하면 비정상 종료한다.
  • 운영체제는 비정상 종료 시점의 메모리 스냅샷을 저장하여 디버깅과 원인 분석에 활용한다.
  • 각 운영체제는 프로그램의 메모리 적재 방식이 서로 다르며, 크래시 덤프 포맷도 다르다.
  • 또한 각 운영체제마다 예외를 처리하는 방식도 다르다.

브라우저나 JVM, .NET 처럼 크로스 플랫폼 환경에서도 동일한 방식의 동작을 보장하는 레이어를 지닌 어플리케이션은 위와 같은 크래시 덤프의 수집 및 분석 시 파편화 문제가 크게 체감되지 않을 수 있다. 허나 여러 플랫폼 타겟을 지원하는 네이티브 어플리케이션의 경우, 이러한 크래시 파편화는 개발자가 문제를 찾기 어렵게 한다.

2008년 9월, 구글은 Chromium이라는 오픈소스 웹브라우저 프로젝트를 발표하였는데, 이 프로젝트의 주요 목표 중 하나는 모든 플랫폼에서 일관된 사용자 경험을 제공하는데 있었다. 필연적으로 여러 플랫폼에서 발생하는 크래시 정보를 효과적으로 수집하고 분석하기 위한 수단이 필요했는데, 이를 위한 Chromium의 해결책이 바로 Breakpad Project이다.

Microsoft의 Minidump 포맷은 Breakpad에서 크로스플랫폼 환경에서 일관된 크래시 덤프 수집을 위해 채택한 형식이다. Breakpad Processor 디자인 문서에 따르면 왜 Minidump 포맷을 선택하였는지에 대한 이유가 자세히 기술되어 있다.

  • 경량화된 포맷: Minidump는 크래시 원인을 분석하는 데 필요한 최소한의 정보만을 포함하며, 파일 크기가 작아 전송과 분석이 용이하다.
  • 확장성: 다양한 CPU 아키텍처 및 운영체제를 지원하도록 설계되었으며, 다른 포맷들과는 다르게 확장이 용이하다.
  • 검증된 도구: Minidump 포맷은 Windows 운영체제에서 수 년간 검증된 포맷이기 때문에 안정성이 높으며, MS의 디버깅 도구들을 활용할 수 있다.

눈치빠른 사람들은 여기서 아직 해결되지 못한 파편화 문제를 알아챌 것이다. 바로 Debugging Symbol인데, Minidump를 쓰더라도 플랫폼 별로 서로 다른 Symbol 포맷은 여전히 문제가 된다.
이에 대한 Breakpad의 해법은 각 플랫폼의 Symbol을 Breakpad 만의 Human-Readable한 고유의 Symbol Format으로 변환하는 것이다.


아래 그림은 Breakpad 프로젝트가 어떻게 동작하는지에 대한 개략적인 flow를 보여준다. breakpad flow

Breakpad의 한계와 이를 개선하는 프로젝트들(Crashpad, Rust-Minidump)

breakpad with caliper Breakpad Client를 사용해보면 가끔씩 제대로 된 덤프가 수집되지 않는 경우가 있다. Breakpad Client가 덤프를 생성할 충분한 시간을 확보하지 못하거나, 덤프 생성 중에 프로세스가 비정상 종료되는 경우가 그 예이다. 앞서 그림을 보면 Breakpad Client는 사용자의 어플리케이션 프로세스 내부에서 동작하도록 설계되어 있는데, 어플리케이션이 종료되는 상황에서는 Breakpad Client가 덤프를 생성할 충분한 시간을 확보하지 못할 수도 있다.
Crashpad는 이러한 한계를 극복하기 위한 개선 프로젝트로, 별도의 Crash 수집 및 전송을 담당하는 Handler Process를 구성하여 이 문제를 해결하였다. crashpad overview

한편, Chromium의 Breakpad가 쌓아놓은 유산을 토대로 RIIR(Re-write It in Rust)한 Rust-Minidump 프로젝트도 주목할 만 하다. 2017년 luser라는 개발자가 Rust User Forum에 처음으로 소개한 이 프로젝트는 Rust 언어로 작성하였다는 것 만으로도 다양한 장점(메모리 안정성, 속도, 사용 편의성)을 갖고 있을 뿐 아니라 Rust Crate의 확장성을 활용하여 사용자가 원하는 기능을 쉽게 추가할 수도 있다. 덤프 분석 결과물을 JSON 형태로 출력해주는 편의 기능이나 cli 도구도 제공하므로 덤프 분석을 원하는 개발자들은 다양한 용도로 활용이 가능할 것이다.

결론

지금까지 크로스플랫폼 환경에서 발생하는 다양한 덤프를 수집하기 위해 알아야 하는 배경지식과, 파편화 문제를 해결해주는 Breakpad 프로젝트를 위시한 Crashpad, Rust-Minidump 등에 대해 알아보았다. 대개는 Sentry나 Google Crashlytics 등의 Managed Service를 사용하느라 직접 어플리케이션 덤프를 수집하고 분석할 일이 많지 않겠지만 어느 정도 규모가 있는 서비스를 운영, 직접 덤프를 수집하여야만 하는 개발자에게 이 글이 도움이 되길 바란다.

교양으로 알아둬도 괜찮지 않을까?