💹

go tool compileとdirective

2022/09/11に公開

概要

go toolコマンドのcompileとgoで定義されたdirectiveについてドキュメントをもとに個人の備忘録としてまとめていきます。

参考
https://pkg.go.dev/cmd/compile

go tool compile

通常、「go ツール コンパイル」として呼び出されるコンパイルは、コマンドラインで指定されたファイルで構成される単一の Go パッケージをコンパイルします。それから最初のソースファイルのベース名にサフィックス.oを付けたファイル名で単一のオブジェクトファイルを生成します。

オブジェクトファイルは他のオブジェクトと組み合わせてパッケージアチーブにしたりlinker(go tool link)に直接私たりできます。-packフラグと共に実行される場合、コンパイラーは中間オブジェクトファイルをバイパスして直接アーカイブを生成します。

生成されたファイルはパッケージによて出力されたシンボル(後続で補足)に関する型情報と、
パッケージによって他のパッケージからインポートされたシンボルによって使用される型に関する型情報を持っています。
そのため、パッケージPのクライアントCをコンパイルするときに、Pの依存関係のファイルを読み取る必要はなく、Pのコンパイル済み出力のみを読み取る必要があります。

コマンドラインusage

go tool compile [flags] file...

例えば、次のようなGoのファイルがあるとします。

package main

var i0 int
var i1 int = 0

func hoge() int {
	var x int = 0
	return x
}

このファイルをコンパイルするには次のように指定します。
指定するファイルはGoのソースファイルで全て同じパッケージである必要があります。

% go tool compile values.go

% ls
values.go	values.o

シンボル

生成したオブジェクトファイルに対してgo tool nm[1]を実行するとシンボルを出力できます。

% go tool nm values.o

次のような結果が出力されます。

         U
         U
         U
         U
     8b9 R gclocals·g2BeySu+wFnoycgXfElmcg==
     8ca ? go.cuinfo.packagename.main
     8c1 ? go.cuinfo.producer.<unlinkable>
         U go.info.int
     8ce ? go.info.main.i0
     8e6 ? go.info.main.i1
         U gofile../Users/sasaki-katsuharu/develop/gocompile/values.go
     7c7 D main..inittask
     793 T main.hoge
     7df B main.i0
     7df B main.i1
     7c6 T main.init
         U type.int

このように、オブジェクトファイル上では関数も変数も単なる「名前」として扱われます。
この「名前」のことをシンボルと呼びます。
このようなシンボル一覧のことを「シンボルリスト」や「ネームリスト」などと呼ばれます。
上のように各種のシンボルに対してDTなどのタイプが指定されていることがわかります。

シンボルを管理するための「シンボル情報」は関数や変数の定義ごとに存在し、
シンボル名、シンボルタイプ、シンボルの実体の位置などの情報を持ちます。
オブジェクトファイルはシンボルを管理するために、シンボル情報の配列として「シンボルテーブル」を持っています。
nmコマンドはファイル中のシンボルテーブルを検索してその一覧を出力するためのコマンドです。

リンク

go tool linkコマンドを実行することでオブジェクトファイルをリンクして実行可能ファイルを生成します。

ex)

% go tool link main.o

% ls
a.out	main.go	main.o

# 実行可能ファイルの実行
% ./a.out

compiler directives

Goのコンパイラはコメント形式でdirective[2]を受け取ります。

directiveではない通常のコメントと区別するために、directiveはコメント開始とdirective名の値だに空白を入れないようなフォーマットになっています。

line directive

▼directiveのいくつかのフォーマット

//line :line
//line :line:col
//line filename:line
//line filename:line:col
/*line :line*/
/*line :line:col*/
/*line filename:line*/
/*line filename:line:col*/
  • line directive[3]として認識されるために、コメントは//lineまたは/*lineと続けて空白 で始まり、少なくとも1つのセミコロン:を含む必要があります。
  • 1つのline directiveはコメントの直後にある文字のソース位置を、指定されたファイル、行、および列:から来たものとして指定します
  • //lineコメントでは、次の行の最初の文字です
  • /*lineコメントはコメント終端*/のすぐ後に続く文字の位置を示します
  • ファイル名が未指定の場合、列番号が空になるようにレコードされたファイル名は空になります。
    • そうでない場合は直近のレコードされたファイルになります
  • line directiveが列番号を特定しない場合、列は次のdirectiveとコンパイラが列番号をレポートしない限り"unknown"となります
  • line directiveのテキストは末尾から解釈されます。
    1. 末尾の:ddd から数字が解釈され取り除かれます(dddが>0の正しい数値の場合)
    2. 二つ目の:dddが1と同じように解釈され取り除かれます
    3. それよりも前のものは全てファイル名と解釈されます(空白やコロンを含む可能性もあります)
  • 不正な行や列の値はエラーとしてレポートされます

ex)

//line foo.go:10 ファイル名がfoo.goで、行番号10が次の行の番号を示します
//line C:foo.go:10 ファイル名がC:foo.go(ファイル名にコロンは許可されます)で行番号が10です
//line  a:100 :10 ファイル名が" a:100 "(ファイル名の空白は許可されます)で行番号が10です
/*line :10:20*/x 文字"x"の位置が現在のファイルの10行目の20列目であることを示します
/*line foo: 10 */ このコメントは行番号に余分な空白があるためエラーとみなされます

line directiveは通常機械語に現れます。そのため、コンパイラとデバッガーは元の入力の位置をジェネレーターにレポートします。

line directiveは歴史的な経緯の特殊ケースで、そのほかの全てのdirectiveは//go:nameという形式になっており、Goのツールチェインによって定義されたことを意味します。

それぞれのdirectiveはその行ごとに置かれる必要があり、コメントの前には先頭スペースとタブのみが許可されます。それぞれのdirectiveはそのすぐ後に続くGoコード(通常は宣言文である必要があります)に適用されます。

//go:noescape

//go:noescape

ディレクティブ//go:noescapeはbodyなしの関数宣言がすぐ後に続く必要があります。
これは引数として渡されたポインタがヒープまたは関数から返された値にエスケープされることを関数が許可しないことを意味します。
この情報は、関数を呼び出すGoコードのコンパイラのエスケープ解析中に使用できます。

//go:uintptrescapes

//go:uintptrescapes

ディレクティブ//go:uintptrescapesは関数宣言が後に続く必要があります。
このディレクティブは関数の引数uintptrがuintptrに変換される可能性があり、ヒープに置かれて関数の呼び出し中keep aliveし続ける必要(型だけから見ると、呼び出し中にオブジェクトが不要になったように見える場合でも)があることを意味します。
ポインタからuintptrへの変換はこの関数への呼び出しの引数リストに含まれている必要があります
このディレクティブは低レベルのシステムコールの実装で必要とされその他の場合での使用は避けるべきです。

//go:noinline

//go:noinline

ディレクティブ//go:noinlineは関数宣言が後に続く必要があります。
このディレクティブは関数の呼び出しをインライン化しないことを意味し、コンパイラの通常の最適化の規則をオーバーライドすることを意味します。
このディレクティブは特別なランタイム関数やコンパイラのデバッグ時のみ必要となります。

//go:norace

//go:norace

ディレクティブ//go:noraceは関数宣言が後に続く必要があります。
このディレクティブは関数のメモリアクセスがrace detectorに無視される必要があることを意味します。
このディレクティブは通常、race detectorランタイムを呼び出すのが安全でない時に呼び出される低レイベルコードにおいて利用されます。

//go:nosplit

//go:nosplit

ディレクティブ//go:nosplitは関数宣言が後に続く必要があります。
このディレクティブは関数が通常のスタックオーバーフローのチェックを省略する必要があることを意味します。
このディレクティブは通常、呼び出し元のgoroutineがプリエンプトされるのが安全でないときに呼び出される低レベルのランタイムコードにおいて利用されます。

//go:linkname localname [importpath.name]

この特殊なディレクティブはこのディレクティブに続くCoコードには適用されません。
その代わりに、ディレクティブgo:linknameはソースコードでlocalnameとして宣言された変数または関数のオブジェクトファイルシンボル名としてimportpath.nameを使用するようにコンパイラに指示します。
importpath.nameが省略された場合、このディレクティブはシンボルのデフォルトオブジェクトファイルシンボル名を使用し、シンボルを他のパッケージからアクセスできるようにする効果のみを持ちます。
このディレクティブは型システムとパッケージのmodule性を上書きする可能性があるため、
unsafeimportされたファイルでのみ有効化されます。

脚注
  1. https://pkg.go.dev/cmd/nm@go1.19.1 ↩︎

  2. directiveはコンパイラに指示を出すcode statementです。直接実行されるプログラム行ではありませんがcompilerが処理する際のサポートをするためのものです。参考1:https://www.quora.com/What-is-a-directive-in-programming 参考2:https://e-words.jp/w/ディレクティブ.html#:~:text=ディレクティブ 【directive】 コンパイラディレクティブ %2F compiler directive&text=ディレクティブとは、コンピュータプログラム,を与えるためのもの。 ↩︎

  3. line directiveはdirectiveの1つで主要な目的はコード中の行番号とファイル名をリセットすることです。参考:https://www.educative.io/answers/what-is-the--sharpline-directive-in-c ↩︎

Discussion