[Go] Go에서 Core dump 파일 생성하기

[Go] Go에서 Core dump 파일 생성하기
Photo by Minna Hamalainen / Unsplash

[요약]

  • GOTRACEBACK 환경변수를 crash로 설정
  • ulimit의 core file size를 해제
  • 빌드시 컴파일러 최적화로 인해 core 파일 분석시 일부 정보가 안보일 수 있음
  • core 파일은 IDE(Goland), dlv, gdb 등으로 분석 가능
  • /proc/sys/kernel/core_pattern을 변경하면 core 파일 생성 경로 및 이름을 변경할 수 있다

Go 어플리케이션에서 panic이 발생했을 때 어떻게 원인을 파악하는가? Stack trace만 보고 원인을 파악하면 참 좋겠지만 그것이 어려울 때가 있다. Panic 발생시 프로그램의 메모리의 스냅샷을 가진 core dump 파일은 개발자의 이러한 부담을 조금 줄여줄 수 있다.

안타깝게도 Go 어플리케이션의 기본 설정은 core dump 파일을 따로 만들지 않는다. 이번 글에서는 core dump 파일을 생성하고 싶을 때 어떤 것들을 설정해줘야 하는지 소개하겠다.

GOTRACEBACK 환경변수

Reference: https://pkg.go.dev/runtime

GOTRACEBACK 환경변수는 Go 프로그램이 recover 불가한 panic 등으로 실패했을 때 생성하는 output을 관리한다.

GOTRACEBACK의 값에 따라 실패시 Go 프로그램의 동작이 변경된다.

GOTRACEBACK의 값에 따른 동작은 다음과 같다(GOTRACEBACK의 기본 값은 single이다).

  • none: 고루틴
  • single: 현재 고루틴의 stack trace를 출력하고 runtime system 내부의 함수는 생략한 뒤 exit code 2로 종료. 현재 고루틴이 없거나 실패가 runtime 내부에서 일어나면 전체 고루틴 stack trace 출력.
  • all: 유저가 생성한 모든 고루틴의 stack trace를 보여줌
  • system: all과 비슷하지만 runtime function을 위한 stack frame과 runtime에 의해 내부적으로 생성된 고루틴도 추가로 보여줌
  • crash: system과 비슷하지만 exit하지 않고 operating system-specific하게 crash한다. 예를 들어 Unix 시스템에서는 crash는 core dump를 트리거하기 위해 SIGABRT를 raise한다.
  • wer: crash와 비슷하지만 Window Error Reporting(WER)을 disable한다.

Go 프로그램에서 core dump를 생성하려면  GOTRACEBACK을 crash로 설정해야 한다. Core가 발생하면 리눅스는 프로세스에게 SIGABRT 시그널을 보내고 해당 프로세스의 core dump를 생성한다.

ulimit

ulimit은 shell 및 프로세스 시작시 사용할 수 있는 리소스를 조절한다.

ulimit -a를 하면 현재 설정된 limit을 확인할 수 있다.

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 20
file size               (blocks, -f) unlimited
pending signals                 (-i) 16382
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) unlimited
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

나의 맥북을 포함한 대부분의 운영체제에서는 core file size가 0으로 되어 있다. 이 값을 풀어줘야 core file이 생성될 수 있다.

# 코어 파일 크기 제한을 두지 않는다.
$ ulimit -c unlimited

Core dump 파일 생성 방법

예제 코드를 통하여 Core dump 파일을 만들어보자.

package main
 
import "fmt"
 
func division(n int) int {
    return 100 / n
}
 
func main() {
    i := 0
    for ; i < 10; i++ {
        fmt.Println(division(i - 5))
    }
}
예시 코드

코어 파일 크기 제한을 풀고, GOTRACEBACK 값을 설정한 이후 어플리케이션을 실행한다.

$ ulimit -c unlimited
// compiler optimization이 들어감. 디버깅시 일부 정보가 안읽힐 수 있음
$ go build -o={binary_name}
// compiler optimization을 하지 않음
$ go build -o={binary_name} -gcflags="all=-N -l"
$ GOTRACEBACK=crash ./{binary_name}
빌드 및 실행

빌드할 때 compiler optimization의 여부가 core file을 분석할 때 영향을 준다. 그냥 go build를 하면 optimization이 적용되는데 core file 분석시 일부 정보가 보이지 않을 수 있다. -gcflags="all=-N -l"은 inlining과 optimization을 비활성화시켜 디버깅에 도움을 준다. 물론 프로덕션에 이 옵션을 키는 것은 좋지 않을 것이다.

위 명령어로 실행을 했다면 stack trace들과 함께 core로 시작하는 dump file이 생성된 것을 확인할 수 있다.

Core dump 파일 분석 방법

Goland로 확인하는 법

RUN → Open Core Dump → executable에 바이너리, Core dump에 dump 파일을 놓는다.

자세한 방법은 링크 참조

dlv로 확인하는 법

delve를 설치하지 않았다면 설치한다. go 1.16 이상부터 아래와 같은 커맨드로 설치가 가능하다

$ go install github.com/go-delve/delve/cmd/dlv@latest

아래와 같이 dump 파일을 분석할 수 있다

$ dlv core {binary_name} {core_dump_file_name}

dlv 사용법은 간단히 다음과 같다

$ (dlv) bt // backtrace 출력
$ (dlv) stack -full // stack까지 출력
$ (dlv) frame {number} // backtrace에서 출력된 number를 입력하면 해당 frame의 코드를 볼 수 있음
$ (dlv) locals // crash 당시에 남아있는 local 변수의 값 출력
 
$ (dlv) p {variable_name} // crash 당시 특정 변수의 값 출력

GDB로 확인하는 법

$ gdb {binary_name} {core_dump_file_name}

GDB 사용법은 생략한다.

Core dump 파일 경로 변경하기

core dump 파일은 일반적으로 바이너리를 실행한 디렉토리에 생성된다.

core dump 파일은 OS에서 생성하기에 파일 경로를 변경하려면 OS의 설정을 변경해야 한다.

linux 계열 운영체제들은 /proc/sys/kernel/core_pattern 파일이 존재하는데, 기본 값은 core이다.

$ cat /proc/sys/kernel/core_pattern
core

이 상태에서 core dump 파일이 만들어지면 이름이 core.PID로 생성이 된다.

만약 /var/crash라는 폴더에 덤프 파일을 생성하고 싶으면 값을 다음처럼 바꾸면 된다

echo "/var/crash/%e.%s.core" > /proc/sys/kernel/core_pattern
%e is the filename
%g is the gid the process was running under
%p is the pid of the process
%s is the signal that caused the dump
%t is the time the dump occurred
%u is the uid the process was running under

Reference

https://www.jetbrains.com/help/go/exploring-go-core-dumps.html#create-a-go-core-dump-file-on-linux

https://rakyll.org/coredumps/

https://pkg.go.dev/runtime

https://ss64.com/bash/ulimit.html

https://princepereira.medium.com/core-dump-analysis-in-golang-using-delve-3c66e0a40e7f

https://github.com/go-delve/delve/issues/1368