概要
なんかいろいろgo tool調べてたら go tool compile
と go tool link
つかって手動ビルドできそうだったのでやってみるというやつ。
go tool compile
するとobject fileなり(オプション指定すれば)archiveつくれたりする。
go tool link
すると↑の成果物をリンクして実行可能バイナリにできる。
そもそも linkname
ディレクティブは go tool compile
がobject fileなり吐くときに評価してるので、生のobject fileみればシンボル名を変えたりしてるだけということが魂から理解できるので、後半ではそっちにも触れます!
それではやっていくぞ〜
なお例には selfbuild/main.go
package main import "fmt" func main() { fmt.Println("Hello world") }
go tool compile
go tool compile
たたくとhelpでます。optionは省略しますが以下みたいなコマンドのよう
go tool compile usage: compile [options] file.go... ...
要はgo file単位でojebct file(かオプション次第でarchive)を得ることができるっぽい。
おもむろにやってみる
$ go tool compile main.go $ ls go.mod main.go main.o
object fileが得られている
ちなみにこいつに go tool nm
だすとシンボルでる
$ go tool nm main.o 15a9 D ""..inittask 15c9 r ""..stmp_0<1> 1542 T "".main 15d9 r "".main.stkobj<1> U fmt..inittask U fmt.Fprintln 1727 R gclocals·1a65e721a2ccc325b382662e7ffee780 16db R gclocals·33cdeccccebe80329f1fdbee7f5874cb 16e3 R gclocals·69c1753bd5f81501d95132d08af04464 171e R gclocals·f207267fbf96a0178e8758c6e3e0ce28 17e0 ? go.cuinfo.packagename. U go.info.[]interface {} U go.info.error 17e4 ? go.info.fmt.Println$abstract U go.info.int 1916 R go.itab.*os.File,io.Writer 16ee R go.string."Hello world" U gofile..$GOROOT/src/fmt/print.go U gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go U gofile..<autogenerated> U os.(*File).Write 178a T os.(*File).close 16eb r os.(*File).close.arginfo1<1> U os.(*file).close U os.Stdout 16d9 R runtime.gcbits.01 16da R runtime.gcbits.02 1816 R runtime.memequal64·f 180e R runtime.nilinterequal·f 18a6 R type.*[]interface {} 181e R type.*interface {} U type.*os.File 1719 R type..importpath.fmt. 1708 R type..namedata.*[]interface {}- 16f9 R type..namedata.*interface {}- 18de R type.[]interface {} U type.int 1856 R type.interface {} U type.io.Writer
go tool link
これもざっとhelpだす
go tool link usage: link [options] main.o ...
とりあえず叩けばlinkできるようなのでざっとためす。
optionでは -buildmode
とかが挙動に影響ありそうだけど、デフォでは普通に実行可能バイナリつくるだろという雰囲気。( go help buildmode
したかんじarchiveにするか実行可能バイナリにするかいくつか選べる的なやつっぽい)
多分普通にやるだけならoptionとかなくてもできるはずなのでとりあえず動かす
$ go tool link main.o $ ls a.out go.mod main.go main.o $ ./a.out Hello world
だいじょうぶそう
そういえば -buildid
オプションきになってかるくみたけど、キャッシュ用のキーをバイナリにうめこんでるぽい。今回は無視
linkname 探検隊
そもそもlinknameの説明が このへん に書かれてることからもわかるように、linknameによってobject fileのシンボルを書き換えるのはcompileのフェーズでやってます。
ほんじゃあいろいろ試してどういうobject fileが吐かれるかお目々で見てみるぞ〜これで魂でlinknameが理解できる
linknameつかわん例
まずは普通にmainで定義するとどういうシンボルになるかみるぞ〜
package main func lower(c byte) byte { return c | ('x' - 'X') } func main() { print(string(lower('A'))) }
$ go tool compile main.go
ほんで go tool nm
するとobject file, archive, 実行ファイルのシンボルがわかるのでやってみる
go tool nm main.o 752 D ""..inittask 6f3 T "".lower 822 r "".lower.arginfo1<1> 6f7 T "".main 81a R gclocals·33cdeccccebe80329f1fdbee7f5874cb 825 R gclocals·69c1753bd5f81501d95132d08af04464 82d R gclocals·9fb7f0986f647f17cb53dda1484e0f7a 86f ? go.cuinfo.packagename. 873 ? go.info."".lower$abstract U go.info.uint8 U gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go
当然だけど外部パッケージの処理みたりはしてない
外部の非公開処理にシンボルを書き換える
linkname
つかってstrconvの処理を呼び出してみる
package main import ( _ "strconv" _ "unsafe" ) //go:linkname lower strconv.lower func lower(c byte) byte func main() { print(string(lower('A'))) }
$ go tool compile main.go
ほんで go tool nm
する
$ go tool nm main.o 609 D ""..inittask 5aa T "".main 681 R gclocals·69c1753bd5f81501d95132d08af04464 69a R gclocals·9fb7f0986f647f17cb53dda1484e0f7a 6d0 ? go.cuinfo.packagename. U gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go U strconv..inittask U strconv.lower 689 R type..importpath.strconv. 692 R type..importpath.unsafe
strconv.lowerがシンボルになってげなことがわかる
余談だけど、objdumpで出力のぞいたかんじ、多分メモリレイアウトさえあってれば型一致してなくても呼び出せるっぽい
linknameしてるけど置き換えたいシンボル先をimportしてない
さっきの例から _ "strconv"
をコメントアウト
package main import ( //_ "strconv" _ "unsafe" ) //go:linkname lower strconv.lower func lower(c byte) byte func main() { print(string(lower('A'))) }
$ go tool compile main.go
go tool nm main.o 540 D ""..inittask 4e1 T "".main 5b0 R gclocals·69c1753bd5f81501d95132d08af04464 5c0 R gclocals·9fb7f0986f647f17cb53dda1484e0f7a 5f6 ? go.cuinfo.packagename. U gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go U strconv.lower 5b8 R type..importpath.unsafe.
さっきの例と比べて R type..importpath.strconv
が消えてる。
おもむろにlinkしてみる
$ go tool link main.o main.main: relocation target strconv.lower not defined
はい。realocation先が存在しないのでおこです。実行時にstrconvの処理が読み込まれてないからですね。
よその実装なりを上書き
いままでの例はlinknameの置き換え先に実装があって、手元の定義をそっちに向ける例でした。
linknameは実装を置き換えちゃうこともできるのでためしてみよう
package main import ( "time" _ "unsafe" ) //go:linkname now time.Now func now() time.Time { return time.Time{} } func main() { print(time.Now().String()) }
$ go tool compile main.go
$ go tool nm main.o a5f D ""..inittask a09 T "".main b77 R gclocals·33cdeccccebe80329f1fdbee7f5874cb b9f R gclocals·54241e171da8af6ae173d69da0236748 b7f R gclocals·69c1753bd5f81501d95132d08af04464 b95 R gclocals·9fb7f0986f647f17cb53dda1484e0f7a c14 ? go.cuinfo.packagename. U gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go U gofile..<autogenerated> U time..inittask a02 T time.Now bf2 T time.Now U time.Time.String b87 R type..importpath.time. b8d R type..importpath.unsafe.
シンボルは普通にtime.Nowに向いてそうだ。
objdumpしてtime.Nowをみてみる
$ go tool objdump main.o TEXT time.Now(SB) gofile../Users/yuya.okumura/go/src/github.com/convto/selfbuild/main.go main.go:10 0xa02 31c0 XORL AX, AX main.go:10 0xa04 31db XORL BX, BX main.go:10 0xa06 31c9 XORL CX, CX main.go:10 0xa08 c3 RET ...
linkname定義した now()
くんが自分自身を time.Now()
だとおもいこんじゃってるぞ〜もともとの time.Now()
くんがかわいそう
mainモジュールから見るとtime.Nowはこの子だと思っちゃうよ〜〜ということです
あきらめたこと
別パッケージのgoファイルを個別にcompileしてまとめてlinkするの、期待するpathとかlink出力先がよくわかんなかったので諦めた。 しばらくバトってみたけどだいぶしんどい感じだった...