unless’s blog

日々のちょっとした技術的なことの羅列

なぜGolangが一部のBinary Toolを自前で実装しているのか

これはなに?

これはKyash Advent Calendar 2022 21日目の記事です
KyashでBackend Engineerをしている @uncle__ko です
お金の入出金を司るチームや決済領域を司るチームなどを経験したあと、現在はTechチームの生産性向上に向けた取り組みを行うチームのリードを行っています
興味がある方はぜひ、下記の記事を御覧ください

blog.kyash.co

Golangは自前でobjdumpやnm、addr2lineなどのToolを内包している

さっそく本題に入りますが、Golangには objdumpnmaddr2line などのbinary解析でよく使用されるToolが内包されている珍しい言語です

これからのbinary解析でよく使用されるToolはGNU Binutilsなどで提供されているので、わざわざ自前で管理しておくメリットは薄く感じます

www.gnu.org

それなのに、なぜこのようなToolを内包させてるのかなとふと疑問に思ったので、考古学者になって歴史探索しようかなと思いました

GNU Binutilsなどで管理されているBinary Tool

Golangが独自で管理している3つのBinary Toolについて、そもそもGNU Binutilsでの挙動を軽くおさらいしておきます

objdump

見たままではあるのですが、これはobjfileの情報を表示するためのToolです
optionでどの情報を表示するかを制御します

sourceware.org

叩いてみるのがはやいと思うのでシンプルな Hello, World を出力するコードで試してみます

package main

import "fmt"

func main() {
  fmt.Println("Hello World!")
}
$ go build hello.go
$ objdump -h hello

hello:  file format mach-o 64-bit x86-64

Sections:
Idx Name             Size     VMA              Type
  0 __text           0008c1a7 0000000001001000 TEXT
  1 __symbol_stub1   00000114 000000000108d1c0 TEXT
  2 __rodata         0003a4d6 000000000108d2e0 DATA
  3 __typelink       00000548 00000000010c77c0 DATA
  4 __itablink       00000070 00000000010c7d20 DATA
  5 __gosymtab       00000000 00000000010c7d90 DATA
  6 __gopclntab      0005e170 00000000010c7da0 DATA
  7 __go_buildinfo   00000120 0000000001126000 DATA
  8 __nl_symbol_ptr  00000170 0000000001126120 DATA
  9 __noptrdata      00010640 00000000011262a0 DATA
 10 __data           000074f0 00000000011368e0 DATA
 11 __bss            0002f120 000000000113dde0 BSS
 12 __noptrbss       00004988 000000000116cf00 BSS
 13 __zdebug_abbrev  00000129 0000000001172000 DATA, DEBUG
 14 __zdebug_line    0001e087 0000000001172129 DATA, DEBUG
 15 __zdebug_frame   00005d40 00000000011901b0 DATA, DEBUG
 16 __debug_gdb_scri 00000046 0000000001195ef0 DATA, DEBUG
 17 __zdebug_info    0003745a 0000000001195f36 DATA, DEBUG
 18 __zdebug_loc     0001cc3f 00000000011cd390 DATA, DEBUG
 19 __zdebug_ranges  000084cb 00000000011e9fcf DATA, DEBUG

とりあえずわかりやすいようにsection headerを表示してみました
Macでbuildしたコードなのでfile formatが mach-o になってますね

実行環境は下記です

$ sw_vers
ProductName:    macOS
ProductVersion: 12.6
BuildVersion:   21G115
$ go version
go version go1.19.2 darwin/amd64

また、objdumpを使って逆アセンブルしたりもできます

$ objdump -d -j __text hello | head -10

hello:  file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000001001000 <_runtime.text>:
 1001000: ff 20                         jmpq    *(%rax)
 1001002: 47 6f                         outsl   (%rsi), %dx
 1001004: 20 62 75                      andb    %ah, 117(%rdx)
 1001007: 69 6c 64 20 49 44 3a 20       imull   $540689481, 32(%rsp,%riz,2), %ebp ## imm = 0x203A4449

nm

nmはオブジェクトファイルに含まれているシンボルをリストアップするToolです

sourceware.org

先程のhelloに対して実行すると下記のような感じです

$ nm hello | head -10
00000000010c58d8 s _$f64.3eb0000000000000
00000000010c58e0 s _$f64.3f50624dd2f1a9fc
00000000010c58e8 s _$f64.3f847ae147ae147b
00000000010c58f0 s _$f64.3fd0000000000000
00000000010c58f8 s _$f64.3fd3333333333333
00000000010c5900 s _$f64.3fe0000000000000
00000000010c5908 s _$f64.3fe8000000000000
00000000010c5910 s _$f64.3ff0000000000000
00000000010c5918 s _$f64.3ff199999999999a
00000000010c5920 s _$f64.3ff3333333333333

addr2line

addr2lineはデバッグ情報を利用してファイル名と行番号の情報を取得するToolです

sourceware.org

こちらに関して、実はmacOSには入ってないです
興味がある人はLinux環境を用意して自分で叩いて見るのも面白いかもしれません
ぜひご自身で確認してみてください

Golangに内包されているBinary Tool

まずはGNU Binutilsと同じように動作するのか実際に叩いてみます

go tool objdump

$ go tool objdump -h hello
usage: go tool objdump [-S] [-gnu] [-s symregexp] binary [start end]

  -S    print Go code alongside assembly
  -gnu
        print GNU assembly next to Go assembly (where supported)
  -s string
        only dump symbols matching this regexp

optionがすべて許容されているわけではなさそうですね
必要最小限のものだけをGolangで作って管理しているのでしょう

与えられているoptionを指定して実行した結果も貼っておきます

$ go tool objdump -gnu hello | head -10
TEXT go.buildid(SB)
  :-1           0x1001000       ff20            JMP 0(AX)                            // jmpq *(%rax)
  cpu_x86.s:4       0x1001002       476f            OUTSD DS:0(SI), DX                   // rex.RXB outsl %ds:(%rsi),(%dx)
  cpu_x86.s:4       0x1001004       206275          ANDB AH, 0x75(DX)                    // and %ah,0x75(%rdx)
  cpu_x86.s:4       0x1001007       696c642049443a20    IMULL $0x203a4449, 0x20(SP), BP      // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp
  cpu_x86.s:4       0x100100f       226f36          ANDB 0x36(DI), CH                    // and 0x36(%rdi),%ch
  cpu_x86.s:4       0x1001012       6e          OUTSB DS:0(SI), DX                   // outsb %ds:(%rsi),(%dx)
  cpu_x86.s:4       0x1001013       51          PUSHQ CX                             // push %rcx
  cpu_x86.s:4       0x1001014       56          PUSHQ SI                             // push %rsi
  cpu_x86.s:4       0x1001015       33482d          XORL 0x2d(AX), CX                    // xor 0x2d(%rax),%ecx

go tool nm

$ go tool nm -h
usage: go tool nm [options] file...
  -n
      an alias for -sort address (numeric),
      for compatibility with other nm commands
  -size
      print symbol size in decimal between address and type
  -sort {address,name,none,size}
      sort output in the given order (default name)
      size orders from largest to smallest
  -type
      print symbol type after name

こちらも必要最小限のoptionが用意されているだけに見えます

実行した結果は下記です

$ go tool nm hello | head -10
 10c58d8 R $f64.3eb0000000000000
 10c58e0 R $f64.3f50624dd2f1a9fc
 10c58e8 R $f64.3f847ae147ae147b
 10c58f0 R $f64.3fd0000000000000
 10c58f8 R $f64.3fd3333333333333
 10c5900 R $f64.3fe0000000000000
 10c5908 R $f64.3fe8000000000000
 10c5910 R $f64.3ff0000000000000
 10c5918 R $f64.3ff199999999999a
 10c5920 R $f64.3ff3333333333333

go tool addr2line

$ go tool addr2line -h
usage: addr2line binary
reads addresses from standard input and writes two lines for each:
    function name
    file:line

こちらも必要最小限の機能です

objdumpの探索

ひと通りの振り返りも済んだので、まずはobjdumpの考古学をしていきます
とりあえず、現在のファイルはいつ作られたのかを追ってみます

実行ファイルはここです
このファイルの変更履歴を辿ってみます

何やら、このcommitが最初のようです

github.com

commit commentがしっかり記載されていますね

Update cmd/dist not to build the C version. Update cmd/go to install the Go version to the tool directory.

と記載があるので、Cで書いていたコードをGolangに置き換えたことが読み取れます

Update #7452

と記載があるので、そのissueも見てみる必要がありそうです

github.com

This is the basic logic needed for objdump, and it works well enough to support the pprof list and weblist commands.

上記を読むと、なにやらpprof list周りで何かがあったのかもしれません
もしかしたらissueに記載があるかもしれないので、そちらを辿ってみます

On darwin, running the list command in pprof produces an error from the go-supplied objdump. Weblist is similarly affected. What do you see instead? objdump: syminit: Undefined error: 0

issueを見ると、darwin環境だとpprofでprofilingするとエラーになっていたようですね

Go 1.2.1 (homebrew) works, tip does not. I believe it has something to do with one of the changes to address https://golang.org/issue/6853 and objdump hasn't been brought up-to date.

と記載があるので、別のissueの対応のせいでぶっ壊れたと推測していることがわかります

Russ氏も

This is a known problem; I need to rewrite objdump in Go to make it work again

ここでコメントしているので、このタイミングでGolangに書き直す必要があったのだと思われます
Golangに書き直す理由の話なので、今回はあまり関係はなさそうなので、一旦Cのコードがあったときのコードを辿っていくことにしましょう

ちなみに、この原因になっているissueもかなり面白い問題ではあり、興味がある人は追いかけてみてもいいかもしれません
このissueはいまだに閉じていない問題で、GolangのVersionが上がるたびにどんどんbinary sizeが大きくなって来ているので、なんとかしたいという問題です
興味がある人は下記を辿ってみてください

github.com

気を取り直して、Cの実装があった頃を辿ります
C言語のファイルが出来たのは、このタイミングのようです

github.com

こちらもcommit commentがしっかり記載されています
考古学をしていると、commit commentの大切さがよくわかります
自分自身もcommit commentをしっかり書こうと思わされますね

runtime/pprof: support OS X CPU profiling Work around profiling kernel bug with signal masks. Still broken on 64-bit Snow Leopard kernel, but I think we can ignore that one and let people upgrade to Lion.

OS Xのbugのせいで困ってるようです

Add new trivial tools addr2line and objdump to take the place of the GNU tools of the same name, since those are not installed on OS X.

なんと、OS Xには addr2lineobjdump が入っていないから、自前でtrivalなtoolsを作ったようですね
意図せず addr2line の謎も解けそうです

Adapt pprof to invoke 'go tool addr2line' and 'go tool objdump' if the system tools do not exist.

system toolsにこの2つが入っていないときに go tool を使うようにしたみたいですね

OS Xのbugについては、こちらのIssueに記載されています

github.com

メールでも言及されている通り、OS Xはvirtual cpu timer signalをCPUを使い切ったThreadにちゃんと送らないのが問題のようですね

OS X doesn't reliably deliver the virtual cpu timer signal to the thread that used up the cpu

Go Profiling - Symbols not available

結論

pprofが addr2lineobjdump を使用するため、 この2つのToolがinstallされていない環境でも動くように、自前でpprofが動く最小限の機能だけを自前で実装することにした
ということがわかりました
Golangはbinaryのdeliverをかなり重視している気がしていて、single binaryで動作出来るようにしているし、様々な環境でも動くように自分たちで腹くくってエコシステム周りを自前で実装してる印象があります

addr2lineの探索

objdumpの探索で一緒に解決されたので、そちらを参照ください

nmの探索

nmについても、同じようにhistoryを辿っていきましょう
ここを見る限りは、これが一番古いcommitのようです

github.com

cmd/nm: reimplement in Go

ここについてもCで書かれていたものをGolangで書き直していそうですね

The immediate goal is to support the new object file format, which libmach (nm's support library) does not understand. Rather than add code to libmach or reengineer liblink to support this new use, just write it in Go. The C version of nm reads the Plan 9 symbol table stored in Go binaries, now otherwise unused. This reimplementation uses the standard symbol table for the corresponding file format instead, bringing us one step closer to removing the Plan 9 symbol table from Go binaries.

ここに書いてある通り、libmatchが理解出来ない新しいobject fileのフォーマットサポートするために、libmatchをメンテするんじゃなく、Golangで書けばいいじゃないってことです
また、読み取るsymbol tableをstandardにすることで、GolangのbinaryからPlan 9のsymbol tableを削除するのに1歩近づいたそうです

とはいえ、Cの実装を持ち込んだのかはわからないので、historyを辿ってみます

github.com

これが最初のcommitですが、あまり情報がありませんね...
2008/8/4とかなり古いので、正式リリース前です

とはいえ、その後のcommitを見る限りはGolang特有のsymbol tableを読み取らせるためのような気もしますね

結論

ちゃんとしたことは分からないけど、おそらくGolang特有のsymbol tableを読み取らせたかったのだろうと思いました
Golangはかなりアセンブラも特殊なので、開発中に自前のnmあるほうが何かと便利だったのだろうという推測は出来きました
僕はここまでしか追いきれなかったので、ここで議論されてるなどの情報があればぜひ教えてください

最後に

今回はふとした疑問から、なぜBinary Toolの一部を自前で実装しているのかを調べてみました
Golangはかなりマルチプラットフォームを意識しているんだなと改めて実感しました

僕が考古学をするときはこのような形で過去のcommitや議論を追ったりしてます
他の方の参考になれば幸いですし、こんなやり方してますっていう他のアプローチがあればぜひ教えてほしいです

また、Kyashでは絶賛採用強化中です
Golangが好きなEngineerもこれからGolangを使って働いてみたいEngineerも大歓迎ですので、興味があればぜひ応募してみてください

株式会社Kyash の全ての求人一覧