デバッグ用の Docker バイナリを作り Docker 内 GDB で実行する

Docker のソースを静的に読んでいたんですが、どうしても限界がありますわね。そこで、GDB でラインデバッグしながら実行したいと思い立ちまして。

ただし、Go の文書にも書かれていますが、現状での GDB の Go 対応は完璧ではありませんので、C/C++ をデバッグする際ほどには快適ではないこともしばしばありますから注意してください(breakpoint をうまく補完してくれない、そもそも指定できない等)。 ∥ Debugging Go Code with GDB - The Go Programming Language

以下は Mac OS X 上の Boot2docker で試しましたが、Linux であれば特に環境依存はしないと思われます。

関連する過去記事:

はじめに

Docker (docker/docker) のソースレポジトリには Dockerfile が含まれており、そこから作った Docker コンテナを用いて Docker 自身をビルドするのが、公式のビルド手順となっています。ベースイメージは Ubuntu です。

このレポジトリの内容は、Docker Hub には docker-dev の名前で、公式ビルド&テスト環境として出ています。 ∥ docker-dev Repository | Docker Hub Registry - Repositories of Docker Images

また、この Dockerfile には ENTRYPOINT ["hack/dind"] が指定されていますので、docker run の際に指定するコマンドは「Docker in Docker」実行が可能な状態で走ります。詳しくは右記の README.md、及び、hack/dind の元ネタとなっている wrapdocker を参照のこと。 ∥ jpetazzo/dind

docker-dev をカスタマイズ

以上を踏まえまして、docker-dev をちょっと上書きして、ビルド&デバッグ用の Docker イメージを作ります。まずは Dockerfile。

FROM docker-dev

MAINTAINER Kiichiro NAKA <knaka@ayutaya.com>

RUN apt-get update -qq
RUN apt-get install -qqy \
     apt-transport-https \
     ca-certificates \
     lxc \
     iptables \
     gdb cgdb

VOLUME /var/lib/docker

RUN echo add-auto-load-safe-path /usr/local/go/src/pkg/runtime/runtime-gdb.py > ~/.gdbinit

やっていることは、GDB を設えて(ついでに Curses GDB (cgdb - the curses debugger) も入れて)、Docker のデータ領域を volume にしたことだけです。上記をビルドしておきます。名前はお好みで。

それと、GDB 用の Go 拡張が標準では読み込み可能パスに入っていないので、入れています[^1]。

Docker イメージをビルドします。

# cd ~/knaka-docker-dev/
# docker build -t knaka/docker-dev .

以上で Docker イメージは出来上がりです。

デバッグ版 Docker バイナリのビルド

できあがった Docker イメージのコンテナを用いて、Git から落としてきた Docker をビルドするには、以下のような感じになりますかね。Docker in Docker するので、--privileged は必須です。あと、そんなにシビアではないと思うんですが、Docker ソースのバージョンと Go のバージョンに留意してください。

そうだ、volume マウントの際に Boot2docker の場合は vboxsf を使いますんで、ホームディレクトリ以下でやってください。ていうか、そもそも ver 1.3 未満ではマウントができませんので注意してください。

# cd
# git clone https://github.com/docker/docker.git
# cd docker/
# docker run -it --rm --privileged \
 -v $PWD:/go/src/github.com/docker/docker \
 -e DEBUG=true -e BUILDFLAGS="-gcflags '-N -l'" \
 knaka/docker-dev hack/make.sh binary

ここでの要点としましては、環境変数の指定です。$DEBUG が指定されていない・もしくは空だと、リンク時に DWARF のデバッグ情報を落とすようになっています(hack/make.sh を参照のこと)ので、必ず何かを指定することです。

-gcflags は Go コンパイラへの指示で、-N で最適化を切り、-l でインライン化を切っています。ところがこれ、hack/make.sh 内での展開のしかたの問題で、BUILDFLAGS="-gcflags '-N -l'" と書くとエラーになると思います。hack/make/binary を直接修正してもいいですし、GitHub の私のフォーク(Convert BUILDFLAGS to array before array concat · 86221ae · knaka/docker)にある Docker を使ってもいいと思います。コンパイラのフラグについて、詳しくは go tool 6g --help を参照してください。

ビルドしたバイナリの GDB 下での実行

GDB 下で実行してみます。GDB での Go のデバッグについては、右記あたりをご参照ください。 ∥ Debugging Go Code with GDB - The Go Programming Language

# docker run -it --privileged --rm --name docker-dev \
 -v $PWD:/go/src/github.com/docker/docker \
 -v $PWD/bundles/$(cat VERSION)/binary/docker:/usr/bin/docker \
 knaka/docker-dev gdb docker
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...
(gdb) break main.main
Breakpoint 1 at 0x407880: file /go/src/github.com/docker/docker/docker/docker.go, line 27.
(gdb) break "github.com/docker/docker/engine.New"
Breakpoint 2, github.com/docker/docker/engine.New (~r0=0xbf9520) at /go/src/github.com/docker/docker/engine/engine.go:77
(gdb) run -d -D
Starting program: /usr/bin/docker -d -D
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7e2b700 (LWP 47)]
[New Thread 0x7ffff752a700 (LWP 48)]
[New Thread 0x7ffff6d29700 (LWP 49)]
[New Thread 0x7ffff6528700 (LWP 50)]
[New Thread 0x7ffff5d27700 (LWP 52)]

Breakpoint 1, main.main () at /go/src/github.com/docker/docker/docker/docker.go:27
27      func main() {
(gdb) list
22              defaultCaFile       = "ca.pem"
23              defaultKeyFile      = "key.pem"
24              defaultCertFile     = "cert.pem"
25      )
26
27      func main() {
28              if reexec.Init() {
29                      return
30              }
31
(gdb) continue
INFO[0040] +job serveapi(unix:///var/run/docker.sock)
DEBU[0040] Using graph driver aufs
DEBU[0040] Migrating existing containers
...

別のコンソールから上記のコンテナに exec で入れば、docker コマンドを叩けます(docker exec が使えるのは ver 1.3 以上です)。「busybox」イメージは小さいので、ちょい試す用途に向いています。 ∥ busybox Repository | Docker Hub Registry - Repositories of Docker Images

# docker exec -it docker-dev bash # ここはホスト
# docker run -it --rm busybox # これより Docker
Unable to find image 'busybox:latest' locally
busybox:latest: The image you are pulling has been verified
511136ea3c5a: Pull complete
df7546f9f060: Pull complete
ea13149945cb: Pull complete
4986bf8c1536: Pull complete
Status: Downloaded newer image for busybox:latest
/ # echo Hoge # これより Docker in Docker
Hoge
/ #

都度 --rm すると GDB のヒストリも消えてしまってやりづらいようでしたら、何とかしてください。

これで、チマチマと停めたりログを仕込んだりしながら、動的に解析ができるんじゃないでしょうか。


[^1]: Docker ビルド環境の Go はアップストリームの最新を取ってきているので問題が無いのですが、Ubuntu 14.04 などに入っている版の Go の runtime-gdb.py は、Python3 化されていないために実行時エラーになると思われます。右記の 2to3 の解法で動くようになりました。 ∥ Issue 6698 - go - gdb: upgrade to be python 3 compatible - The Go Programming Language - Google Project Hosting