[インデックス 15899] ファイルの概要
このコミットは、Go言語のvet
ツールにアセンブリチェッカーを追加するものです。これにより、Goの関数宣言と対応するアセンブリコードの間で、引数のサイズやオフセット、名前の不一致といった潜在的なエラーを検出できるようになります。特に、Goの型システムとアセンブリレベルでのメモリレイアウトの整合性を検証し、開発者が手書きアセンブリコードを書く際のバグを減らすことを目的としています。
コミット
commit b5cfbda21236d273047f6aaec04df29162c26901
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 22 15:14:40 2013 -0400
cmd/vet: add assembly checker
Fixes #5036.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7531045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b5cfbda21236d273047f6aaec04df29162c26901
元コミット内容
cmd/vet: add assembly checker
このコミットは、Goのvet
ツールにアセンブリチェッカーを追加します。
Issue #5036 を修正します。
変更の背景
Go言語では、パフォーマンスが重要な部分や、特定のハードウェア機能にアクセスする必要がある場合、Goのコードから直接アセンブリ言語で記述された関数を呼び出すことがあります。これらのアセンブリ関数は、Goの関数宣言と厳密に一致する引数と戻り値のメモリレイアウトを持つ必要があります。しかし、手書きのアセンブリコードでは、Goの型システムが期待するメモリレイアウト(例えば、引数のサイズやスタック上のオフセット)とアセンブリコードが実際にアクセスするオフセットやサイズが一致しないというヒューマンエラーが発生しやすい問題がありました。
このような不一致は、実行時エラーや未定義の動作を引き起こす可能性があり、デバッグが非常に困難です。この問題を解決するため、Goの標準ツールであるvet
に、Goの関数宣言とアセンブリコードの整合性を自動的にチェックする機能が追加されました。これにより、開発者はアセンブリコードの記述ミスを早期に発見し、より堅牢なGoプログラムを開発できるようになります。
前提知識の解説
Go言語のvet
ツール
go vet
は、Goプログラムの疑わしい構造を報告するツールです。コンパイラが検出できないが、バグの原因となる可能性のあるコードパターンを静的に分析します。例えば、到達不能なコード、誤ったprintf
フォーマット文字列、ロックの誤用などを検出します。このコミットにより、アセンブリコードの整合性チェックが新たなチェック項目として追加されました。
Goのアセンブリ言語
Go言語は、独自の擬似アセンブリ言語を使用します。これは、一般的なアセンブリ言語(x86-64のAT&T構文やIntel構文など)とは異なり、Goのランタイムと密接に統合されています。Goのアセンブリは、主に以下の特徴を持ちます。
- 擬似命令:
TEXT
,DATA
,GLOBL
などの擬似命令を使用します。 - レジスタとメモリ参照:
AX
,BX
などのレジスタや、x+0(FP)
のようなフレームポインタ(FP)からのオフセットによるメモリ参照を使用します。 - フレームポインタ (FP): Goのアセンブリでは、関数の引数や戻り値はスタックフレーム上に配置され、フレームポインタ(
FP
)からのオフセットでアクセスされます。x+0(FP)
は、引数x
がフレームポインタから0バイトのオフセットにあることを意味します。 - ビルドタグ (
+build
): Goのソースファイルやアセンブリファイルには、特定のビルド条件(OS、アーキテクチャなど)を指定するためのビルドタグを記述できます。例えば、// +build amd64
は、そのファイルがamd64
アーキテクチャでのみビルドされることを示します。
Goの型とメモリレイアウト
Goの各型は、特定のサイズとアライメントを持ちます。例えば、int8
は1バイト、int32
は4バイト、int64
は8バイトです。ポインタ、チャネル、マップ、関数、スライス、文字列、インターフェースなどの複合型も、それぞれ定義されたメモリレイアウトを持ちます。
- 文字列 (string): ポインタと長さ(
len
)の2つのフィールドで構成されます。例えば、amd64
ではポインタが8バイト、長さが8バイトで、合計16バイトになります。 - スライス ([]T): ポインタ(
base
)、長さ(len
)、容量(cap
)の3つのフィールドで構成されます。 - インターフェース (interface{}): 型情報(
_type
または_itable
)とデータポインタ(_data
)の2つのフィールドで構成されます。空インターフェース(interface{}
)と非空インターフェース(メソッドを持つインターフェース)で内部表現が異なります。
アセンブリチェッカーは、Goの関数宣言からこれらの型のメモリレイアウトを正確に推測し、アセンブリコードがそのレイアウトに正しくアクセスしているかを検証します。
技術的詳細
このアセンブリチェッカーは、Goのvet
ツールに統合され、以下の主要なコンポーネントとロジックで構成されています。
-
アセンブリファイルの識別と解析:
src/cmd/go/pkg.go
とsrc/cmd/go/vet.go
の変更により、go vet
コマンドが.s
(アセンブリ)ファイルを認識し、処理対象に含めるようになりました。src/cmd/vet/main.go
では、File
構造体にcontent []byte
フィールドが追加され、アセンブリファイルの生の内容を保持できるようになりました。また、doPackage
関数内でGoファイルだけでなくアセンブリファイルも解析対象とし、asmCheck(pkg)
を呼び出すように変更されました。- アセンブリファイルは、
go/parser
ではなく、正規表現ベースのカスタムパーサーによって解析されます。
-
Go関数宣言からの期待されるアセンブリレイアウトの生成 (
asmParseDecl
):src/cmd/vet/asmdecl.go
に新しく追加されたasmParseDecl
関数がこの機能の中核を担います。- この関数は、Goの抽象構文木(AST)から関数の引数と戻り値の宣言を受け取ります。
- 各引数/戻り値のGoの型(
int
,string
,[]byte
,interface{}
など)に基づいて、その型がアセンブリレベルで占めるべきサイズ(size
)とアライメント(align
)を計算します。 asmArch
構造体(386
,arm
,amd64
など)は、ポインタサイズ(ptrSize
)や整数サイズ(intSize
)といったアーキテクチャ固有の情報を持ち、これに基づいて正確なサイズ計算を行います。- 複合型(
string
,slice
,interface
)については、その内部構造(例:string
は_base
と_len
)を考慮し、それぞれに対応するasmVar
(アセンブリ変数)を生成します。これにより、アセンブリコードが複合型の個々のフィールドにアクセスする際のオフセットも検証できるようになります。 - 計算されたオフセットとサイズを持つ
asmVar
オブジェクトが、asmFunc
構造体内に格納されます。asmFunc
は、特定のアーキテクチャにおけるGo関数の引数/戻り値の期待されるメモリレイアウトを完全に記述します。
-
アセンブリコードの解析と検証 (
asmCheck
,asmCheckVar
):asmCheck
関数は、パッケージ内のすべてのアセンブリファイルを走査します。- アセンブリファイルの先頭にある
+build
タグやファイル名(例:_amd64.s
)から、対象となるアーキテクチャを特定します。 TEXT
擬似命令を正規表現で解析し、アセンブリ関数名と、Goの関数宣言から得られたasmFunc
情報を紐付けます。- アセンブリコード内の
x+offset(FP)
のようなフレームポインタ参照を正規表現(asmNamedFP
,asmUnnamedFP
)で抽出し、変数名とオフセットを特定します。 asmCheckVar
関数が、個々のアセンブリ変数参照を検証します。- オフセットの検証: アセンブリコードで指定されたオフセットが、Goの関数宣言から期待されるオフセットと一致するかをチェックします。
- サイズの検証: アセンブリ命令(例:
MOVB
,MOVW
,MOVL
,MOVQ
)が操作するデータのサイズと、Goの型が持つ本来のサイズが一致するかをチェックします。例えば、int8
(1バイト)に対してMOVW
(2バイト移動)を使用している場合、エラーを報告します。 - 名前の検証: Goの関数宣言にない変数名がアセンブリコードで使用されている場合、エラーを報告します。
- 複合型の内部フィールド検証:
string_base
,slice_len
などの複合型の内部フィールドへのアクセスも、正しいオフセットとサイズで行われているかを検証します。
-
エラー報告:
- 不一致が検出された場合、
vet
ツールはファイル名、行番号、アーキテクチャ、そして具体的なエラーメッセージ(例: "invalid MOVW of x+0(FP); int8 is 1-byte value")を標準エラー出力に報告します。
- 不一致が検出された場合、
このチェッカーは、Goの型システムとアセンブリ言語の間のギャップを埋め、手書きアセンブリの品質と信頼性を向上させるための重要な静的解析機能を提供します。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
-
src/cmd/vet/asmdecl.go
(新規ファイル):- Goの関数宣言からアセンブリレベルでの引数/戻り値の期待されるレイアウトを生成するロジック(
asmParseDecl
)。 - アセンブリコードを解析し、Goの宣言との整合性をチェックするロジック(
asmCheck
,asmCheckVar
)。 - アセンブリ命令のオペランドサイズを推測するロジック。
asmKind
,asmArch
,asmFunc
,asmVar
といった、アセンブリチェックに必要なデータ構造の定義。
- Goの関数宣言からアセンブリレベルでの引数/戻り値の期待されるレイアウトを生成するロジック(
-
src/cmd/vet/main.go
:vet
ツールのメイン処理にasmdecl
チェックを追加するためのフラグ定義。File
構造体にcontent []byte
フィールドを追加し、アセンブリファイルの生の内容を読み込めるように変更。doPackage
関数内で、Goファイルだけでなく.s
ファイルも解析対象に含め、asmCheck
関数を呼び出すように変更。
-
src/cmd/go/vet.go
:runVet
関数が、Goファイルだけでなくアセンブリファイル(pkg.sfiles
)もvet
ツールの入力として渡すように変更。
-
src/cmd/vet/Makefile
:errchk
コマンドの実行時に、.go
ファイルだけでなく.s
ファイルも対象に含めるように変更。
-
テストファイル (
src/cmd/vet/test_asm.go
,src/cmd/vet/test_asm1.s
,src/cmd/vet/test_asm2.s
,src/cmd/vet/test_asm3.s
):test_asm.go
は、アセンブリで実装されるGo関数の宣言を提供します。test_asm1.s
(amd64
用)、test_asm2.s
(386
用)、test_asm3.s
(arm
用) は、意図的にエラーを発生させるアセンブリコードを含んでおり、vet
ツールがこれらのエラーを正しく検出できるかを検証します。これらのファイルには、期待されるエラーメッセージがコメントとして記述されています(例:// ERROR "..."
)。
コアとなるコードの解説
src/cmd/vet/asmdecl.go
このファイルは、アセンブリチェッカーの心臓部です。
データ構造
asmKind int
: アセンブリ変数の種類を表します。バイトサイズ(1, 2, 4, 8)の他に、asmString
,asmSlice
,asmInterface
,asmEmptyInterface
といった特殊な種類があります。asmArch struct
: アーキテクチャ固有の情報を保持します。name
(例: "386", "amd64", "arm")、ptrSize
(ポインタのサイズ)、intSize
(int型のサイズ)、bigEndian
(エンディアン) を含みます。asmFunc struct
: Go関数に対応するアセンブリ関数の期待される情報を保持します。arch
(対象アーキテクチャ)、size
(引数と戻り値の合計サイズ)、vars
(名前付き変数マップ)、varByOffset
(オフセットによる変数マップ) を含みます。asmVar struct
: 個々のアセンブリ変数の詳細を保持します。name
、kind
、typ
(Goの型文字列)、off
(フレームポインタからのオフセット)、size
、inner
(複合型の場合の内部変数) を含みます。
正規表現
アセンブリコードを解析するための正規表現が多数定義されています。
asmPlusBuild
:+build
タグを抽出。asmTEXT
:TEXT
擬似命令を解析し、関数名、フレームサイズ、引数/戻り値のサイズを抽出。asmDATA
:DATA
またはGLOBL
擬似命令を検出。asmNamedFP
:name+offset(FP)
形式の名前付きフレームポインタ参照を抽出。asmUnnamedFP
:offset(FP)
形式の名前なしフレームポインタ参照を抽出。asmOpcode
: アセンブリ命令のオペコードとオペランドを抽出。
asmCheck(pkg *Package)
この関数がアセンブリチェックのメインエントリポイントです。
pkg.hasFileWithSuffix(".s")
でアセンブリファイルが存在するかを確認します。knownFunc
マップを初期化し、Goの関数宣言から期待されるアセンブリレイアウトを格納します。- パッケージ内のGoファイルを走査し、
decl.Body == nil
(つまり、アセンブリで実装される関数宣言)の関数について、f.asmParseDecl(decl)
を呼び出してknownFunc
に情報を追加します。 - パッケージ内のアセンブリファイル(
.s
ファイル)を走査します。 - 各アセンブリファイルの行を読み込み、正規表現を使って
TEXT
命令やフレームポインタ参照を検出します。 TEXT
命令が見つかった場合、対応するGo関数宣言のasmFunc
情報を取得し、アセンブリコードで指定された引数/戻り値の合計サイズがGo宣言と一致するかを検証します。asmUnnamedFP
で名前なし引数へのアクセスを検出した場合、警告を発します。asmNamedFP
で名前付き引数へのアクセスを検出した場合、asmCheckVar
を呼び出して詳細な検証を行います。
(f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc
Goの関数宣言から、各アーキテクチャにおけるアセンブリレベルでの引数/戻り値の期待されるレイアウトを生成します。
arches
(386
,arm
,amd64
)ごとにasmFunc
を作成します。- 関数の引数(
decl.Type.Params.List
)と戻り値(decl.Type.Results.List
)を走査します。 - 各フィールド(引数/戻り値)のGoの型を
switch
文で判定し、その型がアセンブリレベルで占めるべきsize
とalign
を決定します。- 基本型(
int8
,int32
,int
,*byte
など)は直接サイズが決定されます。 - 複合型(
string
,slice
,interface{}
)は、その内部構造(例:string
は_base
と_len
)を考慮し、複数のasmVar
を生成します。これにより、アセンブリコードが複合型の個々のフィールドにアクセスする際のオフセットも検証できるようになります。
- 基本型(
- 計算された
offset
とsize
、kind
、typ
を持つasmVar
を生成し、fn.vars
(名前によるマップ)とfn.varByOffset
(オフセットによるマップ)に追加します。 - 最終的に、各アーキテクチャに対応する
asmFunc
のマップを返します。
asmCheckVar(warnf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar)
アセンブリコード内の個々の変数参照を検証します。
- アセンブリ命令のオペコード(例:
MOVB
,MOVW
,MOVL
,MOVQ
)から、操作されるデータの期待されるサイズ(src
,dst
)を推測します。 - 参照されている変数のGoの型から得られた
v.kind
(期待されるサイズ)と、アセンブリ命令から推測されたkind
(実際の操作サイズ)を比較します。 off != v.off
の場合、オフセットの不一致として警告を発します。kind != 0 && kind != vk
の場合、サイズまたは型の不一致として警告を発します。例えば、1バイトのint8
に対して4バイトのMOVL
命令を使用している場合などです。- 複合型の場合、
v.inner
を使って内部フィールド(例:string_base
,string_len
)のオフセットとサイズも考慮して検証します。
src/cmd/vet/main.go
var report = map[string]*bool{...}
に"asmdecl": flag.Bool("asmdecl", false, "check assembly against Go declarations"),
が追加され、go vet -asmdecl
でアセンブリチェッカーを明示的に有効にできるようになりました。File
構造体にcontent []byte
が追加され、アセンブリファイルのバイト内容を保持できるようになりました。doPackage
関数内で、strings.HasSuffix(name, ".go")
でGoファイルかアセンブリファイルかを判別し、アセンブリファイルの場合はparser.ParseFile
をスキップし、File
構造体にcontent
のみをセットするように変更されました。pkg.files = files
で、解析されたすべてのファイル(Goファイルとアセンブリファイル)がPackage
構造体に格納されるようになりました。asmCheck(pkg)
がdoPackage
の最後に呼び出され、パッケージ全体のアセンブリチェックが実行されるようになりました。
テストファイル (src/cmd/vet/test_asm*.s
)
これらのファイルは、アセンブリチェッカーがどのようなエラーを検出するかを示す具体的な例です。各行のコメントに// ERROR "..."
という形式で、期待されるエラーメッセージが記述されています。
例えば、test_asm1.s
の以下の行は、int8
型の変数x
(1バイト)に対してMOVW
命令(2バイト)を使用しているため、サイズ不一致のエラーが報告されることを示しています。
MOVW x+0(FP), AX // ERROR "[amd64] invalid MOVW of x+0(FP); int8 is 1-byte value"
また、x+1(FP)
のように、Goの宣言から期待されるオフセット(x+0(FP)
)と異なるオフセットでアクセスしている場合もエラーが報告されます。
MOVB x+1(FP), AX // ERROR "invalid offset x+1(FP); expected x+0(FP)"
これらのテストファイルは、アセンブリチェッカーがGoの型システムとアセンブリコードの間の厳密な整合性を強制することを示しています。
関連リンク
- Go vet documentation: https://pkg.go.dev/cmd/vet
- Go assembly language: https://go.dev/doc/asm
- Issue #5036: cmd/vet: check assembly against Go declarations: https://github.com/golang/go/issues/5036
参考にした情報源リンク
- Go source code (specifically
src/cmd/vet/asmdecl.go
and related files from the commit) - Go documentation on
go vet
and assembly language. - General knowledge of Go's internal memory layout for various types.
- Analysis of the provided commit diff.
- https://go.dev/doc/asm
- https://pkg.go.dev/cmd/vet
- https://github.com/golang/go/issues/5036
- https://golang.org/cl/7531045 (Go Code Review tool link from the commit message)