鱒身(Masu_mi)のブログ

知った事をメモする場所。

debug aid in Go

GoでアサーションやDebug aidを行いたい。

例えば開発時はアサート失敗でpanicさせ気付ける様にしておき、リリースは完全にコードが消えていて欲しい。 そういう時は、ビルドオプションtagsを使いビルドを分岐させる事になる。

マクロ・プリプロセスがあればリリースビルドからデバッグ用コードを簡単に消せるけど、Goにはその様な機能はない。 そのためソースコード上にデバッグ用コードが残る。これが最適化で消されるかを確認した。

tl;dr

以下が判明したためGo言語でも積極的に Assertion, debugaidを利用した開発ができる。 特に定数+条件分岐を使うとリリースビルドから痕跡を完全に消せる。 ただしコミュニティにベタープラクティスとして受け入れられているかは不明。

  • 空の関数は実行バイナリから消える(引数での式の評価は実行される)
  • 定数 + 条件分岐の判定部分は消える
$ uname -a
Linux pit 3.10.0-229.el7.x86_64 #1 SMP Fri Mar 6 11:36:42 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
$ go version
go version go1.5.1 linux/amd64

空関数の確認

まず検証用のソースコードを準備する。

デバッグビルド

デバッグビルドを行いバイナリを確認したところcallされる(もちろん空関数ではない)。

$ go build -tags debug .
$ objdump -D -M intel ./build_tags | grep -A 20 '<main.main>' | lv
0000000000401000 <main.main>:
  401000:       64 48 8b 0c 25 f8 ff    mov    rcx,QWORD PTR fs:0xfffffffffffffff8
  401007:       ff ff
  401009:       48 8d 44 24 f8          lea    rax,[rsp-0x8]
  40100e:       48 3b 41 10             cmp    rax,QWORD PTR [rcx+0x10]
  401012:       0f 86 08 01 00 00       jbe    401120 <main.main+0x120>
  401018:       48 81 ec 88 00 00 00    sub    rsp,0x88
  40101f:       48 c7 c0 0a 00 00 00    mov    rax,0xa
  401026:       48 89 44 24 40          mov    QWORD PTR [rsp+0x40],rax
  40102b:       48 83 f8 00             cmp    rax,0x0
  40102f:       0f 9f 04 24             setg   BYTE PTR [rsp]
  401033:       e8 88 b5 06 00          call   46c5c0 <_/home/vagrant/works/playground/build_tags/debugaid.Assert>
  401038:       48 8b 5c 24 40          mov    rbx,QWORD PTR [rsp+0x40]
  40103d:       48 89 5c 24 48          mov    QWORD PTR [rsp+0x48],rbx
  401042:       31 db                   xor    ebx,ebx
  401044:       48 89 5c 24 60          mov    QWORD PTR [rsp+0x60],rbx
  401049:       48 89 5c 24 68          mov    QWORD PTR [rsp+0x68],rbx
  40104e:       48 8d 5c 24 60          lea    rbx,[rsp+0x60]
  401053:       48 83 fb 00             cmp    rbx,0x0
  401057:       0f 84 bc 00 00 00       je     401119 <main.main+0x119>
  40105d:       48 c7 44 24 78 01 00    mov    QWORD PTR [rsp+0x78],0x1
--
  401125:       e9 d6 fe ff ff          jmp    401000 <main.main>
  40112a:       00 00                   add    BYTE PTR [rax],al
  40112c:       00 00                   add    BYTE PTR [rax],al

リリースビルド

リリースビルドを行ってバイナリを確認したところ空関数はcallされていない。

$ go build .
$ objdump -D -M intel ./build_tags | grep -A 20 '<main.main>' | lv
0000000000401000 <main.main>:
  401000:       64 48 8b 0c 25 f8 ff    mov    rcx,QWORD PTR fs:0xfffffffffffffff8
  401007:       ff ff
  401009:       48 3b 61 10             cmp    rsp,QWORD PTR [rcx+0x10]
  40100d:       0f 86 f2 00 00 00       jbe    401105 <main.main+0x105>
  401013:       48 81 ec 80 00 00 00    sub    rsp,0x80
  40101a:       48 c7 c0 0a 00 00 00    mov    rax,0xa
  401021:       48 83 f8 00             cmp    rax,0x0
  401025:       0f 9f c1                setg   cl
  401028:       48 89 44 24 40          mov    QWORD PTR [rsp+0x40],rax
  40102d:       31 db                   xor    ebx,ebx
  40102f:       48 89 5c 24 58          mov    QWORD PTR [rsp+0x58],rbx
  401034:       48 89 5c 24 60          mov    QWORD PTR [rsp+0x60],rbx
  401039:       48 8d 5c 24 58          lea    rbx,[rsp+0x58]
  40103e:       48 83 fb 00             cmp    rbx,0x0
  401042:       0f 84 b6 00 00 00       je     4010fe <main.main+0xfe>
  401048:       48 c7 44 24 70 01 00    mov    QWORD PTR [rsp+0x70],0x1
  40104f:       00 00
  401051:       48 c7 44 24 78 01 00    mov    QWORD PTR [rsp+0x78],0x1
  401058:       00 00
  40105a:       48 89 5c 24 68          mov    QWORD PTR [rsp+0x68],rbx
--
  40110a:       e9 f1 fe ff ff          jmp    401000 <main.main>
        ...

ご覧の通りcall命令が消えている。 他にも細かくいろいろと変わっている事と副作用のない比較命令が消えてくれないのが少し残念。

定数 + 条件分岐の確認

条件分岐について消えてくれる事を願って確認した。 以下の3条件でmain.mainのアセンブリを比較した。

  • 分岐なし
  • 定数分岐(tags “”)
  • 定数分岐(tags “debug”)
  • 変数分岐

導入コード

分岐なし

func main() {
  fmt.Println("pure")
}

定数分岐

func main() {
  if debugaid.IsDebug {
    fmt.Println("Is Debug")
  } else {
    fmt.Println("Isn't Debug")
  }
}

変数分岐

定数分岐のdebugaid.IsDebugを変数として宣言するだけ。

結果

下記の様に定数分岐では分岐なしのコードと命令数に差が出ず、また目立った分岐が追加された様子がなかった。 一方、変数分岐で導入した場合は命令数に変化が現れた。 そのため定数+分岐はコンパイル時にちゃんと消してもらえていると考えられる。

開始アドレス 401000
終了アドレス: 分岐なし 4010f1
終了アドレス: 定数分岐(tags “”) 4010f1
終了アドレス: 定数分岐(tags “debug”) 4010f1
終了アドレス: 変数分岐 4011ea

今回の結果からGo言語でも assert, debugaid を使った開発を行えると判断した。