SIer だけど技術やりたいブログ

Go 言語が生成するバイナリについて

linux golang

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)

TEXTSyscall6<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_sigactionruntime/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_sigactionruntime/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

アセンブラで書かれた callCgoSigactionruntime/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/linkgo 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 言語自体を勉強する予定なので、何かわかれば追記しよう。