go言語のデバッガ(delveとgdb)とcore dump

  • 54
    いいね
  • 0
    コメント

この記事は、Fujitsu Advent Calenderの3日目の記事です。

はじめに

 Fujitsu Advent Calenderを作った@YasunoriGoto1です。なんか、予想以上にすごい反響があってびっくりしています。温かい言葉をくれた方には厚く御礼申し上げます。また、厳しい言葉をかけてくださった方にも今後ともよろしくお願いします。
 設立に至った経緯は2日目の記事@tnaotoが書いてくれた内容のとおりです。「社外で実施するのは手続きとか色々ハードル高いよなー」って思っていたところに、@tnaotoや「かっこいい人」を始め、色々な人から背中を押してもらった感じです。
 「かっこいい人」が出た時には、「今この瞬間を逃してはならない」とばかりにかなり急いで作ったので、Advent Calendarの趣旨とかQiitaがどういう場所なのかとか、正直色々広報不足で至らなかったところが多数あると思うのですが、温かい目で見ていただけると幸いです。

 なお、個人としてだいぶ前から申し込んでいたLinuxのAdvent Calender の方には「Linuxの不揮発メモリ対応」について最近ずっと調べていたので、それについて書けたらいいなと思ってます。
 それに、社内のAdvent Calenderがあるって@tnaotoが書いてますが、そちらも社内向けに記事を書くつもりです。自分もメンターとして参加しているOSSコミュニティ参加のWorkshopであるOSS Gate の紹介をして、うちの会社からビギナーの参加者を募りたいなと思っています。

さて、3日目には流石に何か技術的な話を書かないといけないと思うので、そろそろ本題に入りますね。go言語は経験が非常に浅いので拙い内容かもしれませんが、頑張って書いてみます。

delve

 go言語のデバッガとして私が一番好きなのはdelveです。AtomやVisual Studio Codeとかのプラグインもあるので、そういうエディタ上で作業したい人にもおすすめです。今はpre1.0とのことですが、今後に十分期待できると思っています。

delveについて私が気に入っているのは、go言語の内部の構造まできちんと見せてくれていることです。OSのthreadとgoroutineの関係もちゃんと把握することができますし、channelの内部の状況などもはっきり見せてくれている点です。

 では、まずはdelveの使い方を紹介するために、簡単なプログラムをインタラクティブにデバッグをしてみましょう。delveはドキュメントにあるとおり、go getでダウンロードできます。簡単ですね。ついでにgolangとdlvのバージョンも示しておきます。

$ go version
go version go1.7.4 linux/amd64

$ go get github.com/derekparker/delve/cmd/dlv

$ dlv version
Delve Debugger
Version: 0.11.0-alpha
Build: 

まずは、Hello Worldのようなプログラムを用意して。

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

これをdelveで動かしてみましょう。

$ dlv debug hello.go 
Type 'help' for list of commands.
(dlv) list main.main:1
   1:   package main
   2:   
   3:   import "fmt"
   4:   
   5:   func main() {
   6:       fmt.Println("Hello World")
   7:   }
(dlv) break main.main:1
Breakpoint 1 set at 0x40102f for main.main() ./hello.go:6
(dlv) c
> main.main() ./hello.go:6 (hits goroutine(1):1 total:1) (PC: 0x40102f)
     1: package main
     2: 
     3: import "fmt"
     4: 
     5: func main() {
=>   6:     fmt.Println("Hello World")
     7: }

さて、これでブレークポイントまで来ました。この時のthreadやgoroutineの状態はどうなっているでしょうか?

dlv) goroutines
[4 goroutines]
* Goroutine 1 - User: ./hello.go:6 main.main (0x40102f)
  Goroutine 2 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42816a)
  Goroutine 3 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42816a)
  Goroutine 4 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42816a)
(dlv) threads 
* Thread 18736 at 0x40102f ./hello.go:6 main.main
  Thread 18779 at 0x452537 /home/goto/go/src/runtime/sys_linux_amd64.s:95 runtime.usleep
  Thread 18780 at 0x452953 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex
  Thread 18781 at 0x452953 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex
  Thread 18782 at 0x452953 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex

まだgo func()などでgoroutineを新たに作っていないのに、この時点でgoroutineが4つ、threadが5つ出来ていることがわかりますね。goroutineが4つ動作しているのは、今使っているPCの論理CPU数が4つあるので、それ合わせてmainのgoroutineの他に、予備でいつでも動作できるように待機しているgoroutineを3つ起動しているように見えます。go func()などでgoroutineを作ると、mainが動作している以外のgoroutineにディスパッチされて動作しそうですね。また、論理CPU数分のthreadの他に、golang自体が制御のために使うthreadが一つ余分にできているようです。

goroutineのback traceのほか、この制御用threadのback traceも表示できます。

(dlv) goroutine 1
Switched from 1 to 1 (thread 18736)
(dlv) bt
0  0x000000000040102f in main.main
   at ./hello.go:6
1  0x0000000000427d0f in runtime.main
   at /home/goto/go/src/runtime/proc.go:183
2  0x00000000004518b0 in runtime.goexit
   at /home/goto/go/src/runtime/asm_amd64.s:2086

(dlv) thread 18779
Switched from 18779 to 18779
(dlv) bt
0  0x0000000000452537 in runtime.usleep
   at /home/goto/go/src/runtime/sys_linux_amd64.s:95
1  0x0000000000431586 in runtime.sysmon
   at /home/goto/go/src/runtime/proc.go:3606
2  0x000000000042ab29 in runtime.mstart1
   at /home/goto/go/src/runtime/proc.go:1126
3  0x000000000042a9ff in runtime.mstart
   at /home/goto/go/src/runtime/proc.go:1096
4  0x00000000004529c4 in runtime.clone
   at /home/goto/go/src/runtime/sys_linux_amd64.s:439

thread コマンドとgoroutine コマンドで、それぞれのthread、goroutineの解析をすることが出来ます。また、アセンブラで記述されているgo言語内部のコアな箇所でもこんなふうに表示してくれます。

(dlv) list 
> main.main() ./hello.go:6 (hits goroutine(1):1 total:1) (PC: 0x40102f)
> runtime.usleep() /home/goto/go/src/runtime/sys_linux_amd64.s:95 (PC: 0x452537)
    90:     MOVL    $0, DX
    91:     MOVL    $0, R10
    92:     MOVQ    SP, R8
    93:     MOVL    $23, AX
    94:     SYSCALL
=>  95:     RET
    96: 
    97: TEXT runtime·gettid(SB),NOSPLIT,$0-4
    98:     MOVL    $186, AX    // syscall - gettid
    99:     SYSCALL
   100:     MOVL    AX, ret+0(FP)
(dlv) regs
     Rip = 0x0000000000452537
     Rsp = 0x000000c42003dee0
     Rax = 0xfffffffffffffdfe
     Rbx = 0x0000000000000000
     Rcx = 0x0000000000452537
     Rdx = 0x0000000000000000
     Rdi = 0x0000000000000000
     Rsi = 0x0000000000000000
     Rbp = 0x000000c42003def0
      R8 = 0x000000c42003dee0
      R9 = 0x000000c4200004e0
     R10 = 0x0000000000000000
     R11 = 0x0000000000000246
     R12 = 0x000000000042a9a0
     R13 = 0x00000000000000f3
     R14 = 0x0000000000000030
     R15 = 0x0000000000000003
Orig_rax = 0x0000000000000017
      Cs = 0x0000000000000033
  Eflags = 0x0000000000000246
      Ss = 0x000000000000002b
 Fs_base = 0x000000c42002e068
 Gs_base = 0x0000000000000000
      Ds = 0x0000000000000000
      Es = 0x0000000000000000
      Fs = 0x0000000000000000
      Gs = 0x0000000000000000

この例だといささか退屈かもしれないので、もうちょっとひねったプログラムにしてみましょう。こんなふうに、意図的にハングする意地悪なプログラムではどうでしょうか?goroutineがお互いにチャネルを待ち合わせしながらmainは無限ループで待ってみます。

package main

import "fmt"

func goroutineA(c1 chan int, c2 chan int) {
    _ = <-c2
    c1 <- 1
    return
}

func goroutineB(c1 chan int, c2 chan int) {
    fmt.Println("c1", <-c1)
    c2 <- 2
    return
}

func main() {
    c1 := make(chan int)
    c2 := make(chan int)

    go goroutineA(c1,c2)
    go goroutineB(c1,c2)

    for {
    }
}

実行した後、ctrl+CのSIGINTでデバッガを一時停止させます。

$ dlv debug chantrial.go
Type 'help' for list of commands.
(dlv) c
recieved SIGINT, stopping process (will not forward signal)> main.main() ./chantrial.go:24 (PC: 0x401325)
    19:     c2:=make(chan int)
    20: 
    21:     go goroutineA(c1,c2)
    22:     go goroutineB(c1,c2)
    23: 
=>  24:     for {
    25:     }
    26: }
(dlv) goroutines
[6 goroutines]
  Goroutine 1 - User: ./chantrial.go:24 main.main (0x401325)
* Goroutine 2 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42858a)
  Goroutine 3 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42858a)
  Goroutine 4 - User: /home/goto/go/src/runtime/proc.go:260 runtime.gopark (0x42858a)
  Goroutine 5 - User: ./chantrial.go:6 main.goroutineA (0x40101d)
  Goroutine 6 - User: ./chantrial.go:13 main.goroutineB (0x4010bf)
(dlv) threads
* Thread 21435 at 0x401325 ./chantrial.go:24 main.main
  Thread 21748 at 0x452957 /home/goto/go/src/runtime/sys_linux_amd64.s:95 runtime.usleep
  Thread 21749 at 0x452d73 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex
  Thread 21750 at 0x452d73 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex
  Thread 21751 at 0x452d73 /home/goto/go/src/runtime/sys_linux_amd64.s:388 runtime.futex

channelを受け取ろうとしているgoroutineを見てみましょう。frameを指定することで、goroutineBのchannel c1の内容も見ることが出来ます。send は空なのに対してrecvにアドレスと思われる値が入っているので、確かに誰かが待っているという状況に見えますね。

(dlv) goroutine 6
Switched from 2 to 6 (thread 21435)
(dlv) bt
0  0x000000000042858a in runtime.gopark
   at /home/goto/go/src/runtime/proc.go:260 
1  0x000000000042863d in runtime.goparkunlock
   at /home/goto/go/src/runtime/proc.go:265
2  0x000000000040489e in runtime.chanrecv
   at /home/goto/go/src/runtime/chan.go:496
3  0x00000000004045be in runtime.chanrecv1
   at /home/goto/go/src/runtime/chan.go:378
4  0x00000000004010bf in main.goroutineB
   at ./chantrial.go:13
5  0x0000000000451cd0 in runtime.goexit
   at /home/goto/go/src/runtime/asm_amd64.s:2086
frame 4 p c1
chan int {
    qcount: 0,
    dataqsiz: 0,
    buf: unsafe.Pointer(0xc42006c060),
    elemsize: 8,
    closed: 0,
    elemtype: *runtime._type {
        size: 8,
        ptrdata: 0,
        hash: 4149441018,
        tflag: 7,
        align: 8,
        fieldalign: 8,
        kind: 130,
        alg: *(*runtime.typeAlg)(0x4f83d0),
        gcdata: *1,
        str: 835,
        ptrToThis: 35232,},
    sendx: 0,
    recvx: 0,
    recvq: waitq<int> {
        first: *(*sudog<int>)(0xc42007e000), <--- ★
        last: *(*sudog<int>)(0xc42007e000),}, <--- ★
    sendq: waitq<int> {
        first: *sudog<int> nil,
        last: *sudog<int> nil,},
    lock: runtime.mutex {key: 0},}

スタックの一番上はgo言語の内部の待ち合わせ処理かな? Linux kernelでいうところのschedule()みたいなことをやっていたりするんでしょうか?

(dlv) list /home/goto/go/src/runtime/proc.go:260
 255:       mp.waittraceev = traceEv
 256:       mp.waittraceskip = traceskip
 257:       releasem(mp)
 258:       // can't do anything that might move the G between Ms here.
 259:       mcall(park_m)
 260:   }
 261:   
 262:   // Puts the current goroutine into a waiting state and unlocks the lock.
 263:   // The goroutine can be made runnable again by calling goready(gp).
 264:   func goparkunlock(lock *mutex, reason string, traceEv byte, traceskip int) {
 265:       gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)

チャネルを受け取るgo内部の処理はここのようです。なにやらenqueueとかしてそうですね。

(dlv) list /home/goto/go/src/runtime/chan.go:496
 491:       mysg.g = gp
 492:       mysg.selectdone = nil
 493:       mysg.c = c
 494:       gp.param = nil
 495:       c.recvq.enqueue(mysg)
 496:       goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
 497:   
 498:       // someone woke us up
 499:       if mysg != gp.waiting {
 500:           throw("G waiting list is corrupted")
 501:       }

 frame 2を指定して、goroutine内部のchannel処理を見てみましょう。gpとcというのが表示できるようです。まだ良くわかりませんが、go言語の深淵を覗くことができそうですね。
 (私もまだ理解できていないのですが、go言語内部のソースをパラパラと読むと、確かGとMというのがしょっちゅう出てきて、どうもそれが内部構造の理解の鍵となっていそうなのですよね。gpはGのポインタかな?とかあたりをつけていけば、何やら解析できそうで中々面白そうです)。
 cは値からすると先ほどのc1のようです。

(dlv) frame 2 p gp
*runtime.g {
    stack: runtime.stack {lo: 842350632960, hi: 842350634976},
    stackguard0: 842350633680,
    stackguard1: 18446744073709551615,
    _panic: *runtime._panic nil,
    _defer: *runtime._defer nil,
    m: *runtime.m nil,
    stackAlloc: 2048,
    sched: runtime.gobuf {sp: 842350634432, pc: 4359562, g: 842350466336, ctxt: unsafe.Pointer(0x0), ret: 0, lr: 0, bp: 842350634464},
    syscallsp: 0,
    syscallpc: 0,
    stkbar: []runtime.stkbar len: 0, cap: 2, [],
    stkbarPos: 0,
    stktopsp: 842350634928,
    param: unsafe.Pointer(0x0),
    atomicstatus: 4,
    stackLock: 0,
    goid: 6,
    waitsince: 0,
    waitreason: "chan receive",
    schedlink: 0,
    preempt: false,
    paniconfault: false,
    preemptscan: false,
    gcscandone: false,
    gcscanvalid: false,
    throwsplit: false,
    raceignore: 0,
(以下略)

(dlv) frame 2 p c
*runtime.hchan {
    qcount: 0,
    dataqsiz: 0,
    buf: unsafe.Pointer(0xc42006c060),
    elemsize: 8,
    closed: 0,
    elemtype: *runtime._type {
        size: 8,
        ptrdata: 0,
        hash: 4149441018,
        tflag: 7,
        align: 8,
        fieldalign: 8,
        kind: 130,
        alg: *(*runtime.typeAlg)(0x4f83d0),
        gcdata: *1,
        str: 835,
        ptrToThis: 35232,},
    sendx: 0,
    recvx: 0,
    recvq: runtime.waitq {
        first: *(*runtime.sudog)(0xc42007e000),
        last: *(*runtime.sudog)(0xc42007e000),},
    sendq: runtime.waitq {
        first: *runtime.sudog nil,
        last: *runtime.sudog nil,},
    lock: runtime.mutex {key: 0},}

という感じで、勉強すれば色々解析できそうな予感がします。

go言語オフィシャルデバッガ

 go言語のオフィシャルサイトではgdbベースのデバッガが紹介されています。gdbといえばC言語使いなら一度はお世話になったことはあるかと思いますが、そのgdbにgo言語の内部を解釈するためのpythonスクリプトを噛ませて実現しているデバッガで、go言語のソース中についています。

 ただですね、このデバッガはちょっと問題があるんですよね。当該のサイト自身にこんなこと書いてます。私が最初に見たのは2年以上前ですが、それから全然変わってないのですよね。


GDB does not understand Go programs well. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger, even when the program is compiled with gccgo. As a consequence, although GDB can be useful in some situations, it is not a reliable debugger for Go programs, particularly heavily concurrent ones. Moreover, it is not a priority for the Go project to address these issues, which are difficult. In short, the instructions below should be taken only as a guide to how to use GDB when it works, not as a guarantee of success.

In time, a more Go-centric debugging architecture may be required.


気を取り直して、やってみましょう。gdbではまずgo言語でビルドしたプログラムを用意しましょう。最適化の処理が邪魔になるので、buildの際には以下のように-gcflagsを指定します。

go build -gcflags "-N -l" chantrial.go

gdbを実行すると、何やらpythonスクリプトを設定してくださいと言われたようです。.gdbinitに設定が必要なようですね。

$ gdb chantrial 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from chantrial...done.
warning: File "/home/goto/go/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
    add-auto-load-safe-path /home/goto/go/src/runtime/runtime-gdb.py
line to your configuration file "/home/goto/.gdbinit".
To completely disable this security protection add
    set auto-load safe-path /
line to your configuration file "/home/goto/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
    info "(gdb)Auto-loading safe path"
(gdb) 

では、pythonスクリプトを設定してみましょう。こんどは上手く行ったようです。

$ cat ~/.gdbinit 
add-auto-load-safe-path /home/goto/go/src/runtime/runtime-gdb.py

$ gdb chantrial 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from chantrial...done.
Loading Go Runtime support.

では、いよいよ実行

(gdb) run
Starting program: /home/goto/golang/chantrial 
[New LWP 8201]
[New LWP 8202]
[New LWP 8203]
[New LWP 8204]
^C
Thread 1 "chantrial" received signal SIGINT, Interrupt.
0x0000000000401323 in main.main () at /home/goto/golang/chantrial.go:24
24      for {

goroutineとthreadの概略は以下で把握できます。

(gdb) info goroutine
* 1 running  runtime.systemstack_switch
  2 waiting  runtime.gopark
  3 waiting  runtime.gopark
  4 waiting  runtime.gopark
  5 waiting  runtime.gopark
  6 waiting  runtime.gopark

(gdb) info thread
  Id   Target Id         Frame 
* 1    LWP 8197 "chantrial" 0x0000000000401323 in main.main () at /home/goto/golang/chantrial.go:24
  2    LWP 8201 "chantrial" runtime.usleep () at /home/goto/go/src/runtime/sys_linux_amd64.s:95
  3    LWP 8202 "chantrial" runtime.futex () at /home/goto/go/src/runtime/sys_linux_amd64.s:388
  4    LWP 8203 "chantrial" runtime.futex () at /home/goto/go/src/runtime/sys_linux_amd64.s:388
  5    LWP 8204 "chantrial" runtime.futex () at /home/goto/go/src/runtime/sys_linux_amd64.s:388

goroutine 1に切り替えようとしたら、エラーになってしまいました。うーん。この意地悪プログラムをCtrl+Cで止めるような状況とは相性が悪かったのでしょうか?こういう使い方を想定していないのかもしれません。

(gdb) goroutine 1
Python Exception <class 'ValueError'> not enough values to unpack (expected 2, got 1): 
Error occurred in Python command: not enough values to unpack (expected 2, got 1)

backtraceもなんだかあっさりした出力です。

(gdb) bt
#0  0x0000000000401323 in main.main () at /home/goto/golang/chantrial.go:24

他のthreadはgoroutineのディスパッチ待ちとかに見えるし、goroutineで使っている、channelの解析とかは、つらそうです。なんというか、やっぱりちょっと微妙な感じですね。

core dumpが読めないか?

 C言語経験者ならcore dumpを解析したことがあるかもしれませんが、go言語でも実はcore dumpを出力するようにすることが出来ます。core dumpのような異常が起きた時の状態を残す仕組みというのは、トラブルシューティングのためには非常に重要な存在です。

  • スタックトレースの出力だけでは根本原因がわからないバグでも、core dumpがあればそれを調べることができる。
  • 年に一回の率で発生するような再現実験が難しいバグでも、core dumpがあれば異常が発生した原因を追求することができる。「年に一回の頻度で起きるバグのせいで日本の経済が毎年止まります」では、洒落にならないですよね。
  • デッドロックとかスローダウンとか、外からはよくわからない状態の時に、意図的にsignalを投げてcoreを吐かせて内部の状態を解析することができる。
  • メモリのビットエラーのようなハード異常が原因の時でも、発生した時の状況が残っている(かもしれない)。ちなみに、ユーザランドではなくkernelでの話ですが、筆者はハード異常が原因で見事にbitが反転したkernelのcrash dumpを何度か調査したことがあります。メモリも壊れるときは壊れるのです。

 何でも出来てしまうC言語ではバグによる異常も発生しやすいので、core dumpの重要性は言うまでもありません。最近の言語系では、異常が発生しないように様々な施策を行っているので、従来と比べたらcore dumpの重要性は落ちていると思います。が、現代的な言語系でも上記のような理由により、まだまだcore dumpが必要な局面はあるのでは無いかと思っていたりします。
 最近でもあるPythonのプログラムが、(原因は聞きませんでしたが)何らかの理由によりcore dumpを吐いたという話を聞いています。プログラム言語自体にすらバグがある可能性だってあるし、もしかしたらその原因を調査したうえでそれを修正しないといけない局面だってあるわけです。

 ここ数年ではgolangで作ったDockerやkubernetesのようなOSSが増えてきましたが、そういうインフラを支えるところに年一回の頻度でよくわからない異常が発生したらどうしましょうか?というわけで、go言語でcore dumpが調査できるデバッガという観点でも見てみましょう。

go言語のプログラムのcoreを採取してみる

go言語では、実行する際の環境変数を指定すれば簡単に採取できるようになります。GOTRACE
BACK=crashとすれば良いのです。

export GOTRACEBACK=crash

余談ですが、CentOS7のdocker daemonはこの設定が有効になっていたりします。

$ cat /etc/systemd/system/multi-user.target.wants/docker.service 
[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.com
After=network.target rhel-push-plugin.socket
Wants=docker-storage-setup.service

[Service]
Type=notify
NotifyAccess=all
EnvironmentFile=-/etc/sysconfig/docker
EnvironmentFile=-/etc/sysconfig/docker-storage
EnvironmentFile=-/etc/sysconfig/docker-network
Environment=GOTRACEBACK=crash                 <------ ★
ExecStart=/usr/bin/docker-current daemon \
          --exec-opt native.cgroupdriver=systemd \
          $OPTIONS \
   :
   :

なお、core dumpを採取するには言語に限らずOS側の設定も必要です。Linux上ではulimitコマンドでcore dumpを採取するサイズを変更して、ダンプを出力できるようにする必要があります。デフォルトは0なので、ここではunlimitedに指定します。

$ ulimit -c unlimited
$ ulimit -c
unlimited

さあ準備が出来ましたから、先ほどのgdbの時にビルドした意地悪プログラムを実行してみましょう。

$ ./chantrial 

coreを取る目的でgolangが受け取ることができるsignalはSIGABRTだけです。別コンソールからシグナルを投げてみます。

$ kill -ABRT <プロセス番号>

スタックトレースの後、core出力メッセージが表示されます。

SIGABRT: abort
PC=0x401321 m=0

goroutine 1 [running]:
main.main()
    /home/goto/golang/chantrial.go:24 +0xc1 fp=0xc42003ff48 sp=0xc42003ff08
runtime.main()
   :
 (スタックトレースの出力)
  :
    /home/goto/go/src/runtime/asm_amd64.s:240 +0x5b fp=0xc420069f88 sp=0xc420069f78
rax    0xca
rbx    0x4fa4b0
rcx    0x452d73
rdx    0x0
rdi    0xc420058110
rsi    0x0
rbp    0xc420069df0
rsp    0xc420069da8
r8     0x0
r9     0x0
r10    0x0
r11    0x286
r12    0x0
r13    0xf1
r14    0xc
r15    0x0
rip    0x452d71
rflags 0x286
cs     0x33
fs     0x0
gs     0x0
中止 (コアダンプ)

さて、これをデバッガに読み込ませればよいのです。が、delveでcore dumpを解析できれば最高なのですが、残念ながらその使い方はざっと見た限り見当たりませんでした。coreを見るのではなく、動作中のプロセスをdelveにアタッチして解析するdlv attachコマンドはあるのですが、運用中のプロセスにデバッガをアタッチするわけには行きませんからね。
ここは、gdbを頼るしか無いようです。以下のようにcoreを読みこませれば、シグナルを受け取った状態で停止したことが確認できます。

$ gdb chantrial core 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from chantrial...done.
[New LWP 9754]
[New LWP 9751]
[New LWP 9750]
[New LWP 9753]
[New LWP 9752]
Core was generated by `./chantrial'.
Program terminated with signal SIGABRT, Aborted.
#0  runtime.raise () at /home/goto/go/src/runtime/sys_linux_amd64.s:110
110     RET
[Current thread is 1 (LWP 9754)]
Loading Go Runtime support.

とはいえ、goroutineコマンドはここでもエラーが出てしまいますし、gdbでここから調査するのはなかなか骨が折れそうです。

まとめなど

 というわけで、go言語のデバッガとcore dumpについて書いてみました。delveにはすごく期待しているので、core dumpを読み込めるようになってくれると嬉しいです。(気が向いたら、delveにFeature Requestとか出しているかもしれませんが…。)
 ではでは。

(注意)この記事は個人の見解であり、所属する会社、組織を代表するものではありません。

この投稿は Fujitsu Advent Calendar 20163日目の記事です。