Go 言語で実装されたツールを運用したり解析する側の視点から、Go 言語が生成するバイナリについて気になることを調べた。
$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
$ go version
go version go1.20.6 linux/amd64
シングルバイナリ
基本的に Go 言語では、コンパイルするとシングルバイナリが生成される。たとえば次のようなファイルを用意し、
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
ビルドすると、次のようにスタティックリンクされたバイナリが生成される。
$ go build main.go
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=VOmH25e6q3aHXP8qYXoJ/7XuACssHaoDoUXT0WzJg/KJ5LGTrd1i_tBbDeyYbK/RM8LacYb7tb_4wQzJ-J0, with debug_info, not stripped
これはつまり、バイナリさえあれば実行できるということであり、デプロイが容易に実現できる。 (たとえば Python や Java は実行時に python コマンドや java コマンドといった言語ランタイムが必要だけども、そういうのが必要ない。また動的にリンクするライブラリも一切ないので、サーバ環境にも依存しない。要は異なるディストリビューションでも動作するので、Ubuntu でビルドしたバイナリが Alpine でも動く。ただし OS レベルで(Windows と Linux とか)異なっていたり、CPU アーキテクチャ(amd と arm とか)が異なる場合、ターゲットを指定してビルドし直す必要がある。しかしこれも、クロスプラットフォームに対応してるため、容易に実現できる。)
libc 非依存
バイナリは libc などのライブラリに依存していない。 ためしに nm コマンドでシンボルを参照しても、libc 系のシンボルが見当たらないことが分かる。
// libc はこういうシンボルがある
$ nm /lib64/libc.so.6 | grep -i libc
...
0000000000098260 T _IO_do_write@@GLIBC_2.2.5
00000000000971b0 T _IO_file_write@@GLIBC_2.2.5
000000000013ebd0 t __GI___libc_write
000000000013cc00 T __libc_pwrite
000000000013cc00 t __libc_pwrite64
000000000013ebd0 t __libc_write
...
// libc で検索してもなにもでない
$ nm main | grep -i libc
つまり fmt.Println
は、libc の write(3)
や write(2)
すら使わずに、頑張って言語内で処理を実装している。 例として fmt.Println
の処理をデバッガ(delve)で追っていったときのバックトレースを示すと、 write システムコールを実行するまでの処理がすべてGo言語ランタイムで実装されている様子がわかる。
(dlv) bt
0 0x0000000000402fec in runtime/internal/syscall.Syscall6
at /usr/lib/golang/src/runtime/internal/syscall/asm_linux_amd64.s:35
1 0x0000000000402fd3 in syscall.RawSyscall6
at /usr/lib/golang/src/runtime/internal/syscall/syscall_linux.go:38
2 0x000000000048fce5 in syscall.Syscall
at /usr/lib/golang/src/syscall/syscall_linux.go:82
3 0x000000000048f64d in syscall.write
at /usr/lib/golang/src/syscall/zsyscall_linux_amd64.go:939
4 0x000000000048f32c in syscall.Write
at /usr/lib/golang/src/syscall/syscall_unix.go:206
5 0x0000000000492578 in internal/poll.ignoringEINTRIO
at /usr/lib/golang/src/internal/poll/fd_unix.go:794
6 0x000000000049218d in internal/poll.(*FD).Write
at /usr/lib/golang/src/internal/poll/fd_unix.go:383
7 0x0000000000492fbe in os.(*File).write
at /usr/lib/golang/src/os/file_posix.go:48
8 0x0000000000492bdd in os.(*File).Write
at /usr/lib/golang/src/os/file.go:175
9 0x0000000000497d0f in fmt.Fprintln
at /usr/lib/golang/src/fmt/print.go:305
10 0x0000000000497e30 in fmt.Println
at /usr/lib/golang/src/fmt/print.go:314
11 0x000000000049d166 in main.main
at ./main.go:6
12 0x000000000043a253 in runtime.main
at /usr/lib/golang/src/runtime/proc.go:250
13 0x0000000000466d61 in runtime.goexit
at /usr/lib/golang/src/runtime/asm_amd64.s:1598
スタックの一番上(呼び出し階層的にはいちばん下の)の asm_linux_amd64.s なんかは、CPU ごとの呼び出し規約に基づいてせっせとレジスタに値を設定しているようす。 これを Go 言語の対応アーキテクチャ全てに対して実装しているのですごい根性だなと思った(まあクロスプラットフォームを実現するには仕方ないんだろうという気はするけど)。
Linux + AMD の例(github.com/golang src/runtime/internal/syscall/asm_linux_amd64.s)
TEXT ・Syscall6<ABIInternal>(SB),NOSPLIT,$0
// a6 already in R9.
// a5 already in R8.
MOVQ SI, R10 // a4
MOVQ DI, DX // a3
MOVQ CX, SI // a2
MOVQ BX, DI // a1
// num already in AX.
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok
NEGQ AX
MOVQ AX, CX // errno
MOVQ $-1, AX // r1
MOVQ $0, BX // r2
RET
引用元: github.com/golang src/runtime/internal/syscall/asm_linux_amd64.s
あれシングルバイナリじゃないよ
net や os/user や plugin パッケージを利用していると、動的リンクなバイナリが生成される。
The packages in the standard library that use cgo are net, os/user, and plugin.
https://tip.golang.org/doc/go1.20 から引用
ためしに次のようなコードを用意し、
package main
import "net"
func main() {
net.Listen("tcp", ":8080")
}
ビルドすると、動的リンクされているのがわかる。
$ go build main.go
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=RtOkL0gOon8UmsnohlKi/sOksXQWeguzFrlUev7th/fCKo576J_6KWxtfCKPQD/al1JQX23WpGZyXE1VE2Q, with debug_info, not stripped
$ ldd main
linux-vdso.so.1 (0x00007ffe72af8000)
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f336975a000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3369400000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3369774000)
なぜこうなっているかというと、Go言語でいろいろ再実装してるとはいえ、独自実装よりも多機能な(libcなどの) API を利用したいこともあるということらしい。
When cgo is available, the cgo-based resolver is used instead under a variety of conditions: on systems that do not let programs make direct DNS requests (OS X), when the LOCALDOMAIN environment variable is present (even if empty), when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, when the ASR_CONFIG environment variable is non-empty (OpenBSD only), when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the Go resolver does not implement, and when the name being looked up ends in .local or is an mDNS name.
https://pkg.go.dev/netから引用
シングルバイナリにする
これを Go 言語の実装に切り替えることができる。
CGO_ENABLED=0
にすれば CGO(外部ライブラリの実行機能)をオフにしてすべてを Go 実装に切り替えられる。
$ CGO_ENABLED=0 go build main.go
$ ldd main
not a dynamic executable
またタグを用いて、もう少し細かい単位で Go 実装に切り替えることもできる。
$ go build -tags netgo main.go
$ ldd main
not a dynamic executable
しかしこれだと外部ライブラリを利用するよりも機能は落ちてるわけで、libc などを使いつつシングルバイナリにする方法もあるらしい。 https://github.com/golang/go/issues/40711
$ go build -ldflags "-linkmode 'external' -extldflags '-static'" main.go
# command-line-arguments
/usr/bin/ld: /tmp/go-link-3204084976/000004.o: in function `_cgo_cbcce81e6342_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:58: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b620cdb6dcf06c68a44a25bc579dc877d84a762f, for GNU/Linux 3.2.0, with debug_info, not stripped, too many notes (256)
// libc の getaddrinfo がリンクされているのが分かる
$ nm main | grep -i getaddrinfo
...
00000000004e23e0 T getaddrinfo
(実行ログによると、getaddrinfo 使うなら結局実行時に共有ライブラリが必要になるっぽい。Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
。これは、getaddrinfo が利用する libnss がもろもろの事象で静的リンクできないために発生しているんだと思われる https://stackoverflow.com/questions/2725255/create-statically-linked-binary-that-uses-getaddrinfo。)
外部ライブラリを利用する範囲
net や os/user パッケージを利用していると、動的リンクなバイナリが生成されるのを先ほど確認した。では動的リンクされている場合、どの処理が外部ライブラリを利用するのか。
はじめは動的リンクされる原因になった最低限のパッケージ(今回でいうと net)だけが外部ライブラリを利用するのかなと思ったが、ltrace のログと golang の実装を見るに、CGO が設定されている処理(runtime/cgo
配下)が外部ライブラリを使うようになっている。
ltrace (libc の関数呼び出しをトレースできるツール)を実行してみると、net パッケージの処理と関係なさそうな関数も実行されているのが分かる。
$ ltrace ./main 2>&1 | awk -F '(' '{print $1}' | sort | uniq -c
1 +++ exited
2 __errno_location
1 free
5 malloc
21 mmap
1 pthread_attr_destroy
5 pthread_attr_getstacksize
5 pthread_attr_init
1 pthread_cond_broadcast
4 pthread_create
4 pthread_detach
1 pthread_mutex_lock
1 pthread_mutex_unlock
8 pthread_sigmask
113 sigaction
3520 sigaddset
55 sigemptyset
4 sigfillset
3584 sigismember
参考 調査したこと
以下、golang の実装で確認したことをいちおう記載する。今回は、libc の sigismember(3) について調べた。
sigismember(3) は、runtime/cgo/gcc_sigaction.c
(Go 言語じゃなくて C 言語で書かれている) の x_cgo_sigaction
から呼ばれていたが、
int32_t
x_cgo_sigaction(intptr_t signum, const go_sigaction_t *goact, go_sigaction_t *oldgoact) {
...
if (sigismember(&oldact.sa_mask, (int)(i+1)) == 1) {
oldgoact->mask |= (uint64_t)(1)<<i;
}
...
引用元: github.com/golang src/runtime/cgo/gcc_sigaction.c
この x_cgo_sigaction
は runtime/cgo/sigaction.go
で _cgo_sigaction
に代入されている。
//go:build (linux && amd64) || (freebsd && amd64) || (linux && arm64) || (linux && ppc64le)
...
//go:cgo_import_static x_cgo_sigaction
//go:linkname x_cgo_sigaction x_cgo_sigaction
//go:linkname _cgo_sigaction _cgo_sigaction
var x_cgo_sigaction byte
var _cgo_sigaction = &x_cgo_sigaction
引用元: github.com/golang src/runtime/cgo/sigaction.go
var 宣言の前によく分からないコメントがいろいろあるが、これはコンパイル時の動作を規定するためのコンパイルディレクティブというものらしい。
参考 https://pkg.go.dev/cmd/compile
go:cgo_import_static x_cgo_sigaction
は、シンボルの解決を静的に行いますよという設定のようす。なお、今回のように標準パッケージだけを使ってる場合は external linking mode ではなく internal linking mode のようなので、この説明はほとんど無視してよさげ。
//go:cgo_import_static <local>
In external linking mode, allow unresolved references to
<local> in the go.o object file prepared for the host linker,
under the assumption that <local> will be supplied by the
other object files that will be linked with go.o.
Example:
//go:cgo_import_static puts_wrapper
引用元: <src/cmd/cgo/doc.go>
go:linkname
は、シンボルに別名をつける設定のようす(Go言語の x_cgo_sigaction と C言語で書かれた x_cgo_sigaction を同名にし、src/runtime/cgo_sigaction.go で宣言された _cgo_sigaction と本ファイル中の _cgo_sigaction を同名にする、ということかなと思うけど、ちょっと自信ない。)。
//go:linkname localname [importpath.name]
The //go:linkname directive conventionally precedes the var or func declaration named by “localname“, though its position does not change its effect. This directive determines the object-file symbol used for a Go var or func declaration, allowing two Go symbols to alias the same object-file symbol, thereby enabling one package to access a symbol in another package even when this would violate the usual encapsulation of unexported declarations, or even type safety. For that reason, it is only enabled in files that have imported "unsafe".
引用元: https://pkg.go.dev/cmd/compile
_cgo_sigaction
は runtime/sys_linux_amd64.s
から呼ばれていて、
// Call the function stored in _cgo_sigaction using the GCC calling convention.
TEXT runtime・callCgoSigaction(SB),NOSPLIT,$16
MOVQ sig+0(FP), DI
MOVQ new+8(FP), SI
MOVQ old+16(FP), DX
MOVQ _cgo_sigaction(SB), AX
MOVQ SP, BX // callee-saved
ANDQ $~15, SP // alignment as per amd64 psABI
CALL AX
MOVQ BX, SP
MOVL AX, ret+24(FP)
RET
引用元: github.com/golang src/runtime/sys_linux_amd64.s
アセンブラで書かれた callCgoSigaction
は runtime/cgo_sigaction.go
から呼ばれている。
//go:build (linux && amd64) || (freebsd && amd64) || (linux && arm64) || (linux && ppc64le)
...
func sigaction(sig uint32, new, old *sigactiont) {
...
if _cgo_sigaction == nil || inForkedChild {
sysSigaction(sig, new, old)
} else {
...
switch {
case g == nil:
// No g: we're on a C stack or a signal stack.
ret = callCgoSigaction(uintptr(sig), new, old)
...
引用元: github.com/golang src/runtime/cgo_sigaction.go
上記関数内で、_cgo_sigaction
の有無で、 CGO実装(callCgoSigaction
) か Go言語実装(sysSigaction
) に処理を振り分けているのが分かる。 再掲になるが _cgo_sigaction
は、go:cgo_import_static x_cgo_sigaction
を利用して、 C 言語で書かれた x_cgo_sigaction
を代入している。
ではどういうときに C 言語で書かれた x_cgo_sigaction
がリンクされるかというと、コンパイラがコンパイル時(リンクよりも前)に CGO を有効にするかどうかを判断し、有効の場合は /runtime/cgo
をリンク対象に追加しているっぽい。
具体的な実行例をみてみると、net などのパッケージを利用していないコードを用意し、
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
-x
オプション(ビルド時に実行するコマンドを表示するオプション)付きでビルドすると、次の出力が得られた。
$ go build -x main.go
WORK=/tmp/go-build2932228152
mkdir -p $WORK/b001/
cat >/tmp/go-build2932228152/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/home/ec2-user/.cache/go-build/72/72a2112743ff1b37df3fad97f89ee28966d2dda5acb9f23ba1513bcade4b4357-d
packagefile fmt=/home/ec2-user/.cache/go-build/13/13b2aa2ed078e1af52c9e07b0bfeb60dbe893651873a3c6de851491e4483fdfe-d
packagefile runtime=/home/ec2-user/.cache/go-build/11/119735f0aa138cb3c311a5b166cd0f2832cfe3c4d271d36c9300945f623664cf-d
packagefile errors=/home/ec2-user/.cache/go-build/b6/b678c79eb8da6d6a473f8380b22597b83967c48db9862877417ddd064f694c55-d
packagefile internal/fmtsort=/home/ec2-user/.cache/go-build/ea/ea1f6ce73d6c7c8fd77dc934c0bf849243cfaa059ee1a092535898ed0298c44b-d
packagefile io=/home/ec2-user/.cache/go-build/37/37ee3a95d56e4208217eee21d2a346f0a65438d6b238eb9f2c16a08bd5f11194-d
packagefile math=/home/ec2-user/.cache/go-build/f9/f92e0aaa886de0acae9c4b854758e39e90403c434b7d444225f83d069bec794f-d
packagefile os=/home/ec2-user/.cache/go-build/d4/d4bc468d34d3a3eaacbb9a941aa69717220211f339a6affa530b87092e913c35-d
packagefile reflect=/home/ec2-user/.cache/go-build/8c/8c74af9a6b7e8306890dfcdc6b108c7d4fc63b2340de985ab42076ee849415ae-d
packagefile sort=/home/ec2-user/.cache/go-build/8a/8a4f8657d377343bd02b64fa339d73e9ff5949faf3a43353d98f5dff88769783-d
packagefile strconv=/home/ec2-user/.cache/go-build/b0/b065c7ddd928b7afd410d0513827c240d8ab25f266f233559fed6a176d8fcb98-d
packagefile sync=/home/ec2-user/.cache/go-build/24/2400470fc035d44518bb5cfb1f16998de63e6d004d337e1ce599de7f89f75dd4-d
packagefile unicode/utf8=/home/ec2-user/.cache/go-build/e4/e4c50a6301bb89ff10e66c8447a1b58b0a2ed75398b6cc9f350613fe4709a098-d
packagefile internal/abi=/home/ec2-user/.cache/go-build/ad/ad787505082ec9321c40b1af7d481aca027756fa3814561f3dbc156d9e9dfb2a-d
packagefile internal/bytealg=/home/ec2-user/.cache/go-build/e5/e50b35c31035dd6597f0da4574c9983b82a8e3daab4cec747c148e7bd352254b-d
packagefile internal/coverage/rtcov=/home/ec2-user/.cache/go-build/0f/0f7b732c9cd50201dccd7598cd2f3d5f162232cd042d62594d4526297dac5496-d
packagefile internal/cpu=/home/ec2-user/.cache/go-build/51/518ca676ab97a0d822e3a1be183b70870dd1b058752f23cb109b45f34b927431-d
packagefile internal/goarch=/home/ec2-user/.cache/go-build/55/55d836efde774357ccb5263dc77ee92341665461381ecf46c45f17fbe6517fbb-d
packagefile internal/goexperiment=/home/ec2-user/.cache/go-build/31/3160b15a7c56dc24f369abc767080621b0bbd088b96aa3b3c966e276511a53ee-d
packagefile internal/goos=/home/ec2-user/.cache/go-build/35/358380e8e8c205306a800d8ffbdc00fe8936336facee2788b49338c09890418b-d
packagefile runtime/internal/atomic=/home/ec2-user/.cache/go-build/17/17b3f8388c6f575213351a79688b9944b945621ea6ae22240885eb0e1bdc4743-d
packagefile runtime/internal/math=/home/ec2-user/.cache/go-build/01/01a47e6a2dfea4cfda19fd88f91491eefa71e21f81ac59bf056eebbc3532e539-d
packagefile runtime/internal/sys=/home/ec2-user/.cache/go-build/d8/d8d4d355609b5751c50a0eede1ac3d8ed962e10c812e8f453efc29f5838247c1-d
packagefile runtime/internal/syscall=/home/ec2-user/.cache/go-build/97/97675356a5a3d64b1262f99d599437575bd252a8bdeabf89c1d12db7ca2e90fd-d
packagefile internal/reflectlite=/home/ec2-user/.cache/go-build/05/05ca7d1a520ab6a22a03e782cbfad878ea40b0f28401ec42624f6525d525b2fe-d
packagefile math/bits=/home/ec2-user/.cache/go-build/f5/f5f874c24fe9c2e67cc4abbd09967f7f186033a826fb397ee5303ee98897b8ab-d
packagefile internal/itoa=/home/ec2-user/.cache/go-build/b1/b18fe2e4d037bfc6fa1b5ea85047fcdb41f9e92bbc39fd5d0159f3a2aec709ba-d
packagefile internal/poll=/home/ec2-user/.cache/go-build/71/71e8224c9a715225ce9e69c9c5ffbe7610c9af7b99d0413476e46c86563947a5-d
packagefile internal/safefilepath=/home/ec2-user/.cache/go-build/7c/7cb13ba2139893dd2d7be18dc33de37d82b684440adccf3f98015739e1b7724d-d
packagefile internal/syscall/execenv=/home/ec2-user/.cache/go-build/26/26a4350637748f9a89a8522f70423fa91701944a8183c0feb5140be54f411495-d
packagefile internal/syscall/unix=/home/ec2-user/.cache/go-build/27/27943238e6847386c9b148da98b9db32a7dd555e84a21c5394fdaf2ae8b9a557-d
packagefile internal/testlog=/home/ec2-user/.cache/go-build/6c/6c40925db9be3313cfa034039882a62bd742ec4f0c700c8abfae0ce5a993596e-d
packagefile io/fs=/home/ec2-user/.cache/go-build/87/87fe9fd6dd1354d985e01900b3dc9ae8739ff0960d09a72bae2d74cc2257f44b-d
packagefile sync/atomic=/home/ec2-user/.cache/go-build/14/1452a945412223032b1841e454501cd9b01911a91bf93cd7bce4c458ae0a3e9d-d
packagefile syscall=/home/ec2-user/.cache/go-build/a3/a3ce111455a5aae6defae83094f17000efc7a35d720303f060e04e8e4b715856-d
packagefile time=/home/ec2-user/.cache/go-build/a0/a05298afd68d7e7f3bba4512fb1e85c3e5a1dd8f273693f005fe1db5d7304036-d
packagefile internal/unsafeheader=/home/ec2-user/.cache/go-build/57/57a0b4abb1ebb05ef6dbfafdf0e89c6ff0f72725db89b14fa7a8f8f7d488f32c-d
packagefile unicode=/home/ec2-user/.cache/go-build/f1/f18beb9b2bc4d7d109b3e89c39f705c77d6023e36e82469bec28824666c3d697-d
packagefile internal/race=/home/ec2-user/.cache/go-build/40/40c46222af829e6d0fe176989f4af974796b774bb8cd9263b87d3d89df0275f0-d
packagefile internal/oserror=/home/ec2-user/.cache/go-build/ba/ba978f17257739e8cf0ea9e9aaa1982501dc47201858299c492a5f824a125cb3-d
packagefile path=/home/ec2-user/.cache/go-build/77/77a41592bb9a2e198b6430aa46e64d8582d8fbe4a6d960b5cb08517237541a67-d
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nbuild\t-buildmode=exe\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=linux\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=3YgrdUmX4yQEsBdyU3mE/51jYVDRWd7gZWaPWLZPZ/SbOWrSMYrtqFtTdvsVoE/3YgrdUmX4yQEsBdyU3mE -extld=gcc /home/ec2-user/.cache/go-build/72/72a2112743ff1b37df3fad97f89ee28966d2dda5acb9f23ba1513bcade4b4357-d
/usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
cp $WORK/b001/exe/a.out main
rm -r $WORK/b001/
/tmp にビルド用の一時ディレクトリを生成し、importcfg.link
にリンクの設定を書き込んでから /usr/lib/golang/pkg/tool/linux_amd64/link
を実行していることが分かる。なお /usr/lib/golang/pkg/tool/linux_amd64/link
は go tool link
で実行されるバイナリで、つまりはオブジェクトをリンクしているようす。
で、net パッケージを利用した場合、このリンク設定が変わる。具体的には、次のようなコードを用意し
package main
import "net"
func main() {
net.Listen("tcp", ":8080")
}
-x
オプション(ビルド時に実行するコマンドを表示するオプション)付きでビルドすると、先ほどの出力とは異なり、packagefile runtime/cgo
が出力された。
$ go build -x main.go
...
packagefile time=/home/ec2-user/.cache/go-build/a0/a05298afd68d7e7f3bba4512fb1e85c3e5a1dd8f273693f005fe1db5d7304036-d
packagefile runtime/cgo=/home/ec2-user/.cache/go-build/36/3642501d225a91cc36f7b4f4a5ff01c4f2ba78af86c504b8cd389e405393c308-d
...
runtime/cgo 配下には C 言語で書かれた x_cgo_sigaction
などがあるので、それがリンクされることになり、CGO 実装に切り替わるということらしい。
ということで、動的リンクされている場合、runtime/cgo
配下の処理はごっそり外部ライブラリを利用することになりそう。
$ vim src/runtime/cgo/
Display all 91 possibilities? (y or n)
abi_amd64.h cgo.go gcc_freebsd_amd64.c gcc_linux_s390x.c gcc_s390x.S libcgo_unix.h
abi_arm64.h dragonfly.go gcc_freebsd_arm.c gcc_loong64.S gcc_setenv.c libcgo_windows.h
asm_386.s freebsd.go gcc_freebsd_arm64.c gcc_mips64x.S gcc_sigaction.c linux.go
asm_amd64.s gcc_386.S gcc_freebsd_riscv64.c gcc_mipsx.S gcc_signal_ios_arm64.c linux_syscall.c
asm_arm.s gcc_aix_ppc64.c gcc_freebsd_sigaction.c gcc_mmap.c gcc_signal_ios_nolldb.c mmap.go
asm_arm64.s gcc_aix_ppc64.S gcc_libinit.c gcc_netbsd_386.c gcc_signal2_ios_arm64.c netbsd.go
asm_loong64.s gcc_amd64.S gcc_libinit_windows.c gcc_netbsd_amd64.c gcc_solaris_amd64.c openbsd.go
asm_mips64x.s gcc_android.c gcc_linux_386.c gcc_netbsd_arm.c gcc_traceback.c setenv.go
asm_mipsx.s gcc_arm.S gcc_linux_amd64.c gcc_netbsd_arm64.c gcc_util.c sigaction.go
asm_ppc64x.s gcc_arm64.S gcc_linux_arm.c gcc_openbsd_386.c gcc_windows_386.c signal_ios_arm64.go
asm_riscv64.s gcc_context.c gcc_linux_arm64.c gcc_openbsd_amd64.c gcc_windows_amd64.c signal_ios_arm64.s
asm_s390x.s gcc_darwin_amd64.c gcc_linux_loong64.c gcc_openbsd_arm.c gcc_windows_arm64.c
asm_wasm.s gcc_darwin_arm64.c gcc_linux_mips64x.c gcc_openbsd_arm64.c handle.go
callbacks.go gcc_dragonfly_amd64.c gcc_linux_mipsx.c gcc_openbsd_mips64.c handle_test.go
callbacks_aix.go gcc_fatalf.c gcc_linux_ppc64x.S gcc_ppc64x.c iscgo.go
callbacks_traceback.go gcc_freebsd_386.c gcc_linux_riscv64.c gcc_riscv64.S libcgo.h
そもそも動的リンクは推奨されてない?
Cgo is not Go.
Go 言語の作者である Rob Pike 氏が講演で発言した格言。そんなに使いたくなることあるか?と。Go 言語実装でいいんじゃねと。 ここらへんの細かい懸念点などは https://dave.cheney.net/2016/01/18/cgo-is-not-go が詳しかった。
そんなこともあってか「proposal: don’t include cgo with net on Unix by default」という Issue が 2018 年ごろに上がっていたよう。ただこの時の議論だと「まれに libc の機能が必要になるユーザはいるし、デフォルトで Go 実装にしてしまうと数%の人は不幸な事態に陥る。不要なら無効にできるんだから、デフォルトは動的リンクにするよ。(超意訳)」ということでクローズされている。なるほど。
そしてディストリビューションが配布してるパッケージは、だいたい(全てかは定かではないけど自分が知る限りは)共有リンクされてるんだよな…。
$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
$ go version $(which docker)
/usr/bin/docker: go1.19.9
$ go version $(which dockerd)
/usr/bin/dockerd: go1.19.9
$ ldd $(which docker)
linux-vdso.so.1 (0x00007ffe38136000)
libc.so.6 => /lib64/libc.so.6 (0x00007f5e1f200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5e22256000)
$ ldd $(which dockerd)
linux-vdso.so.1 (0x00007ffd8d1fe000)
libsystemd.so.0 => /lib64/libsystemd.so.0 (0x00007f8c02cd1000)
libdevmapper.so.1.02 => /lib64/libdevmapper.so.1.02 (0x00007f8bfe7a2000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8bfe400000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f8c02cc7000)
liblzma.so.5 => /lib64/liblzma.so.5 (0x00007f8bfe777000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f8bfe75d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8c02db3000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f8bfe730000)
libudev.so.1 => /lib64/libudev.so.1 (0x00007f8bfe6ff000)
libm.so.6 => /lib64/libm.so.6 (0x00007f8bfe624000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f8bfe362000)
(ディストリビューターからしてみれば、標準ライブラリに寄せたほうが脆弱性対応もやりやすいだろうし、シングルバイナリにしたせいで脆弱性対応のために再コンパイルして回ったりが大変という問題もありそうだから、まあ仕方ないのかなという気はする。詳しくは知らない。)
そして Amazon Linux 2023 では、Go 言語も CGO を使っているという。つまり、Go is CGO.
(英文法誤り)
$ ldd /usr/bin/go
linux-vdso.so.1 (0x00007fff70d1f000)
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f381c25c000)
libc.so.6 => /lib64/libc.so.6 (0x00007f381c000000)
/lib64/ld-linux-x86-64.so.2 (0x00007f381c277000)
まあ自分でビルドしてテストしてリリースするアプリケーションは、メリットデメリットを考慮しつつ、シングルバイナリでもいいかもしれないね、くらいの捉えかたをすればいいんでしょうか。今年こそは Go 言語自体を勉強する予定なので、何かわかれば追記しよう。