Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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ツールに統合され、以下の主要なコンポーネントとロジックで構成されています。

  1. アセンブリファイルの識別と解析:

    • src/cmd/go/pkg.gosrc/cmd/go/vet.goの変更により、go vetコマンドが.s(アセンブリ)ファイルを認識し、処理対象に含めるようになりました。
    • src/cmd/vet/main.goでは、File構造体にcontent []byteフィールドが追加され、アセンブリファイルの生の内容を保持できるようになりました。また、doPackage関数内でGoファイルだけでなくアセンブリファイルも解析対象とし、asmCheck(pkg)を呼び出すように変更されました。
    • アセンブリファイルは、go/parserではなく、正規表現ベースのカスタムパーサーによって解析されます。
  2. 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関数の引数/戻り値の期待されるメモリレイアウトを完全に記述します。
  3. アセンブリコードの解析と検証 (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などの複合型の内部フィールドへのアクセスも、正しいオフセットとサイズで行われているかを検証します。
  4. エラー報告:

    • 不一致が検出された場合、vetツールはファイル名、行番号、アーキテクチャ、そして具体的なエラーメッセージ(例: "invalid MOVW of x+0(FP); int8 is 1-byte value")を標準エラー出力に報告します。

このチェッカーは、Goの型システムとアセンブリ言語の間のギャップを埋め、手書きアセンブリの品質と信頼性を向上させるための重要な静的解析機能を提供します。

コアとなるコードの変更箇所

このコミットのコアとなる変更は、主に以下のファイルに集中しています。

  1. src/cmd/vet/asmdecl.go (新規ファイル):

    • Goの関数宣言からアセンブリレベルでの引数/戻り値の期待されるレイアウトを生成するロジック(asmParseDecl)。
    • アセンブリコードを解析し、Goの宣言との整合性をチェックするロジック(asmCheck, asmCheckVar)。
    • アセンブリ命令のオペランドサイズを推測するロジック。
    • asmKind, asmArch, asmFunc, asmVarといった、アセンブリチェックに必要なデータ構造の定義。
  2. src/cmd/vet/main.go:

    • vetツールのメイン処理にasmdeclチェックを追加するためのフラグ定義。
    • File構造体にcontent []byteフィールドを追加し、アセンブリファイルの生の内容を読み込めるように変更。
    • doPackage関数内で、Goファイルだけでなく.sファイルも解析対象に含め、asmCheck関数を呼び出すように変更。
  3. src/cmd/go/vet.go:

    • runVet関数が、Goファイルだけでなくアセンブリファイル(pkg.sfiles)もvetツールの入力として渡すように変更。
  4. src/cmd/vet/Makefile:

    • errchkコマンドの実行時に、.goファイルだけでなく.sファイルも対象に含めるように変更。
  5. テストファイル (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: 個々のアセンブリ変数の詳細を保持します。namekindtyp (Goの型文字列)、off (フレームポインタからのオフセット)、sizeinner (複合型の場合の内部変数) を含みます。

正規表現

アセンブリコードを解析するための正規表現が多数定義されています。

  • asmPlusBuild: +buildタグを抽出。
  • asmTEXT: TEXT擬似命令を解析し、関数名、フレームサイズ、引数/戻り値のサイズを抽出。
  • asmDATA: DATAまたはGLOBL擬似命令を検出。
  • asmNamedFP: name+offset(FP)形式の名前付きフレームポインタ参照を抽出。
  • asmUnnamedFP: offset(FP)形式の名前なしフレームポインタ参照を抽出。
  • asmOpcode: アセンブリ命令のオペコードとオペランドを抽出。

asmCheck(pkg *Package)

この関数がアセンブリチェックのメインエントリポイントです。

  1. pkg.hasFileWithSuffix(".s")でアセンブリファイルが存在するかを確認します。
  2. knownFuncマップを初期化し、Goの関数宣言から期待されるアセンブリレイアウトを格納します。
  3. パッケージ内のGoファイルを走査し、decl.Body == nil(つまり、アセンブリで実装される関数宣言)の関数について、f.asmParseDecl(decl)を呼び出してknownFuncに情報を追加します。
  4. パッケージ内のアセンブリファイル(.sファイル)を走査します。
  5. 各アセンブリファイルの行を読み込み、正規表現を使ってTEXT命令やフレームポインタ参照を検出します。
  6. TEXT命令が見つかった場合、対応するGo関数宣言のasmFunc情報を取得し、アセンブリコードで指定された引数/戻り値の合計サイズがGo宣言と一致するかを検証します。
  7. asmUnnamedFPで名前なし引数へのアクセスを検出した場合、警告を発します。
  8. asmNamedFPで名前付き引数へのアクセスを検出した場合、asmCheckVarを呼び出して詳細な検証を行います。

(f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc

Goの関数宣言から、各アーキテクチャにおけるアセンブリレベルでの引数/戻り値の期待されるレイアウトを生成します。

  1. arches386, arm, amd64)ごとにasmFuncを作成します。
  2. 関数の引数(decl.Type.Params.List)と戻り値(decl.Type.Results.List)を走査します。
  3. 各フィールド(引数/戻り値)のGoの型をswitch文で判定し、その型がアセンブリレベルで占めるべきsizealignを決定します。
    • 基本型(int8, int32, int, *byteなど)は直接サイズが決定されます。
    • 複合型(string, slice, interface{})は、その内部構造(例: string_base_len)を考慮し、複数のasmVarを生成します。これにより、アセンブリコードが複合型の個々のフィールドにアクセスする際のオフセットも検証できるようになります。
  4. 計算されたoffsetsizekindtypを持つasmVarを生成し、fn.vars(名前によるマップ)とfn.varByOffset(オフセットによるマップ)に追加します。
  5. 最終的に、各アーキテクチャに対応するasmFuncのマップを返します。

asmCheckVar(warnf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar)

アセンブリコード内の個々の変数参照を検証します。

  1. アセンブリ命令のオペコード(例: MOVB, MOVW, MOVL, MOVQ)から、操作されるデータの期待されるサイズ(src, dst)を推測します。
  2. 参照されている変数のGoの型から得られたv.kind(期待されるサイズ)と、アセンブリ命令から推測されたkind(実際の操作サイズ)を比較します。
  3. off != v.offの場合、オフセットの不一致として警告を発します。
  4. kind != 0 && kind != vkの場合、サイズまたは型の不一致として警告を発します。例えば、1バイトのint8に対して4バイトのMOVL命令を使用している場合などです。
  5. 複合型の場合、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の型システムとアセンブリコードの間の厳密な整合性を強制することを示しています。

関連リンク

参考にした情報源リンク