8月にGoの1.17が出て、関数のcalling conventionが引数と返り値についてスタックベースからレジスタベースに変わりましたね。というわけで予定通りABI変更のプロポーザルを読もうかな、と思ったらその冒頭に、
This will remain backwards compatible with existing assembly code that assumes Go’s current stack-based calling convention through Go’s multiple ABI mechanism.
なんじゃそりゃー、というわけで、じゃあまずそっちのプロポーザルを読まなきゃ&読みました、というのが今回のお話。タイトルがProposal: Create an undefined internal calling conventionで、ますます良くわからない。いろいろ寄り道しながら1周半ぐらい読んだら分かってきた気がするので整理する。
Disclaimer
- ABI変更のプロポーザルを先に読んだほうが理解には効きそう
- 変更前後のABIをほとんど分かってない
- Goのアセンブリを分かっていない
- そもそもGoを書いたことがない
Context
- Go 1.11までのcalling conventionはシンプルだけど非効率
- Go 1系で約束されている後方互換にcalling conventionは含まれない
- calling conventionを壊すとGoアセンブリで書かれたプログラムが壊れる
- アセンブリは少ないけど暗号系とか重要なライブラリの中心になっている
- だから壊せないし、移行期間を用意しても期待できないから、そのまま動いてほしい
凡例
- A: アセンブリで書かれたプログラム
- G: 普通のGoで書かれたプログラム
たぶん、こんな感じ
- 既存のABIを
ABI0とする - 新しいコンパイラが内部的に扱うABIを
ABIInternalとする - この2つは一致しているところから始まって、後者は変化していく
- だからan undefined internal calling convention
- native implementation
- Aは
ABI0で書かれている - Gは全関数で
ABIInternal向けのエントリーポイントを用意する
- Aは
- ABI Wrapper
- Aから呼ばれるGの関数には
ABI0向けのエントリーポイントも用意する - Gから呼ばれるAの関数には
ABIInternal向けのエントリーポイントも用意する
- Aから呼ばれるGの関数には
ABIInternalを変化させて満足したら、そのABIをABI1とする- 新しいABIを公布する際には、アセンブリでABIを指定できるようにする。
感想
実際どういうアセンブリ(コンパイル結果)になるんだろ。
例えば、こんな感じ?(Goのアセンブリの文法は一切反映させてない)
新しい関数A_for_ABIInternal:
レジスタから引数を取り出して使う
いろいろやる
レジスタに返り値を詰める
新しい関数A_for_ABI0:
スタックから引数を取り出して、レジスタに入れる
call 新しい関数A_for_ABIInternal
レジスタから返り値を取り出して、スタックに積む
このプロポーザルの大前提として、Goにはdynamic linkが無くてコンパイル時に静的に解決できる、というのがありそうだけど、どうなんだろ。真偽についても影響についてもそんなに自信は無い。dynamic linkありでABIを固定させない場合、ABIが定まる前の時代のSwiftみたいにバイナリごとにruntime埋め込みが必要になるのかな。ただし、あれはiOSが持ってるSwift標準ライブラリとのブリッジだけのはず。(それ以外はコンパイル時に解決というか、同じコンパイラ(つまり同じABI)でビルドしたものしか扱えなくて、実質的にソースからビルドするしかない世界だった。)
Swiftのこと考えてたら気づいたけど、このプロポーザルは、新しいABIを不定のまま処理系をリリースし続けられるってのもポイントなのか。というか、そういうタイトルになってるね。レジスタベースのcalling conventionっていうゴールに惑わされてた。
dynamic linkではなさそうけど、今はBinary-Only Packagesというのもあるみたいで、これはGoアセンブリで書かれたプログラムと同じ扱いになるのかな。