ちりもつもればミルキーウェイ

好奇心に可処分時間が奪われる

go build せずにバイナリをつくって linkname を理解する

概要

なんかいろいろgo tool調べてたら go tool compilego 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ファイルもガリガリいじる)

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

これもざっと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出力先がよくわかんなかったので諦めた。 しばらくバトってみたけどだいぶしんどい感じだった...