[インデックス 17938] ファイルの概要
このコミットは、Go言語のcmd/cgo
ツールにおける重要な変更を導入しています。具体的には、GoからC言語へエクスポートされる関数が使用する構造体に対して、GCCの__gcc_struct__
属性を適用するように修正されています。これにより、異なるコンパイラ間での構造体のアライメントとパディングに関する互換性の問題が解決され、特にWindows上の386アーキテクチャやx86アーキテクチャにおけるint64
型のアライメント不一致に起因するバグ(Issue #6833およびIssue #5603)が修正されます。
コミット
commit 6795687427ad79574465ee83b4fa398aa036aa0c
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Dec 10 11:30:12 2013 +1100
cmd/cgo: use __gcc_struct__ for go exported functions
Fixes #6833
R=minux.ma, iant
CC=golang-dev
https://golang.org/cl/35790045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6795687427ad79574465ee83b4fa398aa036aa0c
元コミット内容
cmd/cgo: use __gcc_struct__ for go exported functions
Fixes #6833
R=minux.ma, iant
CC=golang-dev
https://golang.org/cl/35790045
変更の背景
この変更の背景には、Go言語とC言語の間でデータをやり取りする際の、構造体のアライメントとパディングに関する互換性の問題がありました。特に、Goのコンパイラ(6c
/8c
/5c
など)とGCCのようなCコンパイラが、同じ構造体定義に対して異なるメモリレイアウトを生成する可能性がありました。
具体的な問題は、Go Issue #6833とGo Issue #5603で報告されています。
- Go Issue #6833:
cgo
を介してGoからCにエクスポートされた関数が、uint64
のような64ビット整数を引数として受け取る際に、Windows上の386アーキテクチャ(32ビットシステム)で正しく値が渡されないという問題。これは、GoとGCCの間でスタック上の引数のパディング規則が異なるために発生していました。GCCはint64
を8バイト境界にアライメントしようとするのに対し、Goのコンパイラは4バイト境界にアライメントすることがあり、これによりスタックレイアウトがずれ、誤った値が読み取られる可能性がありました。 - Go Issue #5603: GCCのバグ(PR52991)に関連しており、x86アーキテクチャで特定の構造体属性が正しく解釈されない問題。これは、
__attribute__((__packed__))
と__gcc_struct__
の組み合わせが、GCCの最適化によって意図しない動作を引き起こす可能性を示唆していました。
これらの問題は、GoとCの相互運用性(cgo)において、特に異なるアーキテクチャやOS環境でGo関数をCから呼び出す際に、データの破損やクラッシュを引き起こす可能性がありました。このコミットは、これらのアライメントとパディングの不一致を解消し、より堅牢なcgoの相互運用性を実現することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
- Cgo: Go言語とC言語の間で相互にコードを呼び出すためのGoのメカニズムです。GoプログラムからCライブラリの関数を呼び出したり、CプログラムからGoの関数を呼び出したりすることができます。cgoは、GoとCの異なるABI(Application Binary Interface)やデータ表現を橋渡しする役割を担います。
- ABI (Application Binary Interface): オペレーティングシステムとプログラム、またはプログラムの異なるモジュール間で、バイナリレベルでの互換性を定義する一連の規則です。これには、関数呼び出し規約(引数の渡し方、戻り値の受け取り方)、レジスタの使用方法、スタックフレームのレイアウト、データ型のアライメントとパディングなどが含まれます。異なるコンパイラや言語が同じABIに従わない場合、相互運用性に問題が生じます。
- 構造体のアライメントとパディング:
- アライメント (Alignment): メモリ上のデータが特定のバイト境界(例:4バイト境界、8バイト境界)に配置されることを指します。CPUが効率的にデータを読み書きするために重要です。例えば、32ビットシステムでは4バイトのアライメントが一般的で、64ビットシステムでは8バイトのアライメントが一般的です。
- パディング (Padding): 構造体内で、アライメント要件を満たすためにコンパイラが自動的に挿入する未使用のバイトのことです。例えば、4バイトの整数と8バイトの長整数が連続する構造体では、長整数が8バイト境界に配置されるように、その前にパディングが挿入されることがあります。
- 異なるコンパイラが異なるアライメント規則やパディング規則を持つ場合、同じ構造体定義でもメモリ上のサイズやメンバのオフセットが異なり、バイナリ互換性が失われます。
- GCCの属性 (
__attribute__
): GCC(GNU Compiler Collection)が提供する拡張機能で、変数、関数、型などに特別なプロパティを付与するために使用されます。__attribute__((__packed__))
: 構造体のメンバ間にパディングを挿入しないようにコンパイラに指示します。これにより、構造体のサイズが最小化されますが、アライメント要件が満たされない場合があり、パフォーマンスの低下や特定のアーキテクチャでのクラッシュを引き起こす可能性があります。__attribute__((__gcc_struct__))
: GCCのデフォルトの構造体レイアウト規則を使用するようにコンパイラに指示します。これは、特に他のコンパイラ(例:Clang)が異なるデフォルトのレイアウト規則を持つ場合に、GCCとの互換性を確保するために使用されます。この属性は、GCCの特定のバグ(PR52991)の回避策としても機能しました。このバグは、__packed__
属性が適用された構造体で、GCCが誤ったアライメントを適用する可能性がありました。
技術的詳細
このコミットの核心は、GoからCにエクスポートされる関数(//export
ディレクティブで定義されたGo関数)の引数や戻り値をC側で扱うためのプロキシ構造体(Goのコンパイラが生成する内部的な構造体)のメモリレイアウトを、GCCの規則に厳密に合わせる点にあります。
Goのcmd/cgo
ツールは、Go関数をCから呼び出せるようにするために、C言語のスタブコードを生成します。このスタブコードは、Go関数に渡される引数をCの構造体にパックし、Go関数からの戻り値をCの構造体からアンパックする役割を担います。この際、Goのコンパイラ(6c
/8c
/5c
)とGCCの間で、構造体のアライメントとパディングの規則が異なると、C側で期待されるメモリレイアウトとGo側で実際に使用されるメモリレイアウトが食い違い、データの破損が発生します。
特に、32ビットシステム(例:Windows上の386アーキテクチャ)で64ビット整数(int64
やuint64
)を扱う際に問題が顕著でした。Goのコンパイラはint64
を4バイト境界にアライメントすることがありますが、GCCは通常8バイト境界にアライメントしようとします。このアライメントの不一致により、スタック上の引数のオフセットがずれ、C側で誤ったメモリ位置から値が読み取られていました。
このコミットでは、Goが生成するCのスタブコードにおいて、Go関数への引数や戻り値を保持する構造体に対して、__attribute__((__packed__))
に加えて__gcc_struct__
属性を適用するように変更されました。
__attribute__((__packed__))
: これは以前から使用されており、構造体内のパディングを最小限に抑えることで、Goのコンパイラが生成するコンパクトなメモリレイアウトに近づけることを目的としていました。__attribute__((__gcc_struct__))
: この属性の追加が今回の主要な変更点です。これにより、GCCが構造体のレイアウトを決定する際に、そのデフォルトの規則(特にアライメントとパディングに関して)を強制的に使用するようになります。これにより、Goのコンパイラが生成するレイアウトとGCCが期待するレイアウトの間の不一致が解消されます。特に、GCCのバグ(PR52991)の回避策としても機能し、__packed__
属性との組み合わせで発生する可能性のある問題を緩和します。
この変更は、src/cmd/cgo/out.go
内のwriteOutputFunc
関数と新しく追加されたpackedAttribute
関数に集約されています。packedAttribute
関数は、現在のGoアーキテクチャ(amd64
または386
)がGCCのバグの影響を受ける可能性があり、かつコンパイラがClangではない場合にのみ、__gcc_struct__
属性を追加するロジックを含んでいます。これにより、必要な場合にのみこの属性が適用され、他の環境での不必要な影響を避けています。
コアとなるコードの変更箇所
主要な変更は src/cmd/cgo/out.go
ファイルにあります。
-
writeOutputFunc
関数の変更: GoからCにエクスポートされる関数のラッパーを生成する際に、引数や戻り値を保持する構造体のアライメント属性の生成方法が変更されました。--- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -529,15 +529,8 @@ func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) { } // We're trying to write a gcc struct that matches 6c/8c/5c's layout. // Use packed attribute to force no padding in this struct in case - // gcc has different packing requirements. For example, - // on 386 Windows, gcc wants to 8-align int64s, but 8c does not. - // Use __gcc_struct__ to work around http://gcc.gnu.org/PR52991 on x86, - // and http://golang.org/issue/5603. - extraAttr := "" - if !strings.Contains(p.gccBaseCmd()[0], "clang") && (goarch == "amd64" || goarch == "386") { - extraAttr = ", __gcc_struct__" - } - fmt.Fprintf(fgcc, "\t%s __attribute__((__packed__%v)) *a = v;\n", ctype, extraAttr) + // gcc has different packing requirements. + fmt.Fprintf(fgcc, "\t%s %v *a = v;\n", ctype, p.packedAttribute()) fmt.Fprintf(fgcc, "\t") if t := n.FuncType.Result; t != nil { fmt.Fprintf(fgcc, "a->r = ")
__attribute__((__packed__%v))
の部分がp.packedAttribute()
の呼び出しに置き換えられました。これにより、属性の生成ロジックが新しいヘルパー関数にカプセル化されます。 -
packedAttribute
関数の追加: 構造体のアライメント属性文字列を生成するための新しいメソッドpackedAttribute
がPackage
型に追加されました。--- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -618,6 +611,19 @@ func (p *Package) writeGccgoOutputFunc(fgcc *os.File, n *Name) { fmt.Fprintf(fgcc, "\n") } +// packedAttribute returns host compiler struct attribute that will be +// used to match 6c/8c/5c's struct layout. For example, on 386 Windows, +// gcc wants to 8-align int64s, but 8c does not. +// Use __gcc_struct__ to work around http://gcc.gnu.org/PR52991 on x86, +// and http://golang.org/issue/5603. +func (p *Package) packedAttribute() string { + s := "__attribute__((__packed__" + if !strings.Contains(p.gccBaseCmd()[0], "clang") && (goarch == "amd64" || goarch == "386") { + s += ", __gcc_struct__" + } + return s + "))" +} + // Write out the various stubs we need to support functions exported // from Go so that they are callable from C. func (p *Package) writeExports(fgo2, fc, fm *os.File) { @@ -727,7 +733,7 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) { fmt.Fprintf(fgcc, "extern void _cgoexp%s_%s(void *, int);\\n", cPrefix, exp.ExpName) fmt.Fprintf(fgcc, "\\n%s\\n", s) fmt.Fprintf(fgcc, "{\\n") - fmt.Fprintf(fgcc, "\t%s __attribute__((packed)) a;\\n", ctype) + fmt.Fprintf(fgcc, "\t%s %v a;\\n", ctype, p.packedAttribute())\n if gccResult != "void" && (len(fntype.Results.List) > 1 || len(fntype.Results.List[0].Names) > 1) { fmt.Fprintf(fgcc, "\t%s r;\\n", gccResult) }
この関数は、
__attribute__((__packed__))
を基本とし、コンパイラがClangでなく、かつアーキテクチャがamd64
または386
の場合にのみ, __gcc_struct__
を追加します。 -
テストファイルの追加:
misc/cgo/test/issue6833.go
とmisc/cgo/test/issue6833_c.c
が追加され、この問題が修正されたことを検証するためのテストケースが導入されました。issue6833.go
: Go関数GoIssue6833Func
を定義し、//export
ディレクティブでCから呼び出せるようにしています。この関数はuint
とuint64
を引数に取り、uint64
を返します。test6833
関数内でCの関数issue6833Func
を呼び出し、Go関数が正しく呼び出されるか検証します。issue6833_c.c
: C関数issue6833Func
を定義し、GoからエクスポートされたGoIssue6833Func
を呼び出します。
コアとなるコードの解説
src/cmd/cgo/out.go
の変更は、Goのcgo
ツールがC言語のスタブコードを生成する際の、構造体定義の方法を改善しています。
以前のコードでは、Goのコンパイラ(6c
/8c
/5c
)のメモリレイアウトに合わせるために、Cの構造体に__attribute__((__packed__))
属性を適用していました。しかし、これだけではGCCが特定の状況(特に386 Windowsでのint64
のアライメントや、GCCのPR52991バグ)で異なるアライメント規則を適用してしまう問題がありました。
新しいpackedAttribute
関数は、この問題を解決するために導入されました。
func (p *Package) packedAttribute() string {
s := "__attribute__((__packed__"
if !strings.Contains(p.gccBaseCmd()[0], "clang") && (goarch == "amd64" || goarch == "386") {
s += ", __gcc_struct__"
}
return s + "))"
}
この関数は、まず基本的な__attribute__((__packed__))
文字列を構築します。
次に、条件分岐によって__gcc_struct__
属性を追加するかどうかを決定します。
!strings.Contains(p.gccBaseCmd()[0], "clang")
: 使用しているCコンパイラがClangではないことを確認します。ClangはGCCとは異なるデフォルトの構造体レイアウト規則を持つ可能性があり、またGCCの特定のバグの影響を受けないため、この属性は不要です。(goarch == "amd64" || goarch == "386")
: 問題が報告されていたアーキテクチャ(x86およびx64)に限定してこの属性を適用します。これにより、他のアーキテクチャでの不必要な変更を避けます。
このロジックにより、Goが生成するCのスタブコード内の構造体は、GCCが期待する厳密なメモリレイアウトに従うようになります。これにより、GoとCの間でuint64
のようなデータ型が正しくやり取りされ、アライメントの不一致によるデータの破損が防止されます。
writeOutputFunc
およびwriteExports
関数内でp.packedAttribute()
が呼び出されることで、Goからエクスポートされる関数がCから呼び出される際の引数・戻り値の受け渡しメカニズムが、より堅牢でクロスコンパイラ互換性のあるものになります。
テストケース misc/cgo/test/issue6833.go
と misc/cgo/test/issue6833_c.c
は、この修正が正しく機能することを確認するために設計されています。Go関数がuint
とuint64
の引数を受け取り、uint64
を返すという、アライメント問題が発生しやすいシナリオを再現し、CからGo関数を呼び出した結果が期待通りであることを検証しています。
関連リンク
- Go Issue #6833: https://github.com/golang/go/issues/6833
- Go Issue #5603: https://github.com/golang/go/issues/5603
- GCC Bugzilla – Bug 52991: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 (このコミットで参照されているGCCのバグ)
- Go CL 35790045: https://golang.org/cl/35790045 (このコミットに対応するGoのコードレビュー)
参考にした情報源リンク
- Go言語の公式ドキュメント (cgoに関する情報): https://go.dev/blog/c-go-cgo
- GCCの
__attribute__
に関するドキュメント: https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html - データアライメントとパディングに関する一般的な情報 (C言語の文脈): https://en.wikipedia.org/wiki/Data_structure_alignment
- ABIに関する一般的な情報: https://en.wikipedia.org/wiki/Application_binary_interface