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

[インデックス 14453] ファイルの概要

このコミットは、Go言語のCgoツールにおけるbool型のメモリ配置(アライメント)に関するバグ修正です。具体的には、Cgoが生成するコードにおいて、bool型が誤ったアライメントを持つことで、C関数呼び出し時に引数が正しく渡されない問題(Issue 4417)を解決します。

コミット

commit dd01e9281d681c18f6e70bf032d622603bec6a67
Author: Vladimir Nikishenko <vova616@gmail.com>
Date:   Wed Nov 21 13:04:38 2012 -0800

    cmd/cgo: fix alignment of bool.
    Fixes #4417.
    
    R=golang-dev, iant, minux.ma, bradfitz
    CC=golang-dev, vova616
    https://golang.org/cl/6782097

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/dd01e9281d681c18f6e70bf032d622603bec6a67

元コミット内容

このコミットは、cmd/cgoツールにおけるbool型のアライメント問題を修正します。これにより、GoとCの間でbool型の値が正しくやり取りされるようになります。この修正は、GoのIssue 4417を解決するものです。

変更の背景

Go言語は、C言語のコードをGoプログラムから呼び出すためのcgoツールを提供しています。cgoは、GoとCの間のデータ型変換や関数呼び出しの橋渡しを行います。このコミットが行われた当時、cgobool型を扱う際に、そのメモリ上のアライメント(配置)が正しく設定されていませんでした。

具体的には、cgobool型に対してポインタサイズ(例えば32ビットシステムでは4バイト、64ビットシステムでは8バイト)のアライメントを適用しようとしていました。しかし、C言語の_Bool型(またはstdbool.hで定義されるbool型)は通常1バイトのサイズを持ち、アライメントも1バイトであることが一般的です。この不一致により、GoからC関数を呼び出す際に、bool型の引数の後に続く引数が期待されるメモリ位置に配置されず、結果としてC関数が誤った値を受け取ってしまうというバグが発生していました(Issue 4417)。

この問題は、特に複数のbool型引数やbool型と他の型の引数が混在するC関数をGoから呼び出す場合に顕著に現れ、プログラムの誤動作を引き起こす可能性がありました。

前提知識の解説

Cgo

Cgoは、GoプログラムからC言語の関数を呼び出したり、C言語のコード内でGoの関数を使用したりするためのGoのツールです。GoとCの間の相互運用性を提供し、既存のCライブラリをGoから利用することを可能にします。Cgoは、Goのソースコード内にCのコードを直接記述できる特殊な構文(import "C")を提供し、ビルド時にGoとCのコードをリンクします。

メモリのアライメントとパディング

コンピュータのメモリは、バイト単位でアドレス指定されますが、CPUは通常、特定のデータ型を特定のバイト境界(アライメント)に配置することを効率的とします。例えば、4バイトの整数は4の倍数のアドレスに配置されると、CPUは一度のメモリアクセスでその整数全体を読み書きできるため、パフォーマンスが向上します。

  • アライメント (Alignment): データがメモリ上で配置される際の、特定のバイト境界への制約です。例えば、「4バイトアライメント」とは、データのアドレスが4の倍数でなければならないことを意味します。
  • パディング (Padding): アライメント要件を満たすために、構造体などのメンバ間に挿入される未使用のバイトのことです。例えば、1バイトのcharと4バイトのintが続く構造体では、charの後に3バイトのパディングが挿入され、intが4バイト境界に配置されるように調整されることがあります。

アライメントが正しくないと、CPUがデータを効率的に読み書きできなかったり、最悪の場合、アライメント違反エラーが発生してプログラムがクラッシュしたりすることがあります。特に、異なる言語(GoとC)間でデータをやり取りする際には、それぞれの言語が持つアライメント規則を正しく理解し、適切に処理することが不可欠です。

dwarf.Typecgoの型変換

Goのコンパイラやツールチェーンは、プログラムのデバッグ情報や型情報を扱うためにDWARF (Debugging With Attributed Record Formats) 形式を利用することがあります。dwarf.Typeは、このDWARF形式で表現される型情報を抽象化したものです。

cgoツールは、Goの型とCの型の間で変換を行う際に、これらの型情報(サイズ、アライメントなど)を考慮します。src/cmd/cgo/gcc.goファイルは、cgoがCの型情報を解析し、Goの型システムにマッピングする際のロジックを含んでいます。このファイル内のtypeConv構造体は、CのDWARF型をGoの内部表現に変換する役割を担っています。

技術的詳細

このバグは、src/cmd/cgo/gcc.go内のtypeConv構造体のTypeメソッドが、Cのdwarf.BoolType(Cのbool型に対応)のアライメントを誤って設定していたことに起因します。

元のコードでは、dwarf.BoolTypeのアライメントをc.ptrSize(ポインタのサイズ、例えば64ビットシステムでは8バイト)に設定していました。これは、Goのbool型が内部的に1バイトとして扱われることが多いものの、Goのコンパイラが特定の状況でbool型をワードサイズ(ポインタサイズ)にパディングすることがあるため、その慣習に合わせたものかもしれません。しかし、C言語のbool型(_Boolまたはstdbool.hbool)は、標準で1バイトのサイズを持ち、アライメントも1バイトです。

GoからC関数を呼び出す際、cgoはGoの引数をCのABI(Application Binary Interface)に従ってメモリに配置します。もしbool型がGo側で8バイトアライメントとして扱われ、C側で1バイトアライメントとして期待される場合、引数のメモリレイアウトにずれが生じます。

例えば、C.c_bool(true, true, 10, true, false)のようなC関数呼び出しを考えます。 Cの関数シグネチャは static int c_bool(bool a, bool b, int c, bool d, bool e) です。 もしboolが8バイトアライメントと誤って解釈されると、abの間に7バイトのパディングが挿入され、bcの間にもパディングが挿入される可能性があります。これにより、c(整数)が期待される位置に配置されず、C関数がcとして全く異なるメモリ上の値を読み取ってしまうことになります。

このコミットでは、dwarf.BoolTypeのアライメントを明示的に1バイトに設定することで、この問題を解決しています。これにより、cgoはCのbool型をCのABIに準拠した1バイトアライメントで扱うようになり、GoとCの間でのbool型引数の受け渡しが正しく行われるようになります。

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

このコミットでは、主に以下の3つのファイルが変更されています。

  1. misc/cgo/test/cgo_test.go:

    • TestBoolAlign(t *testing.T)という新しいテスト関数が追加され、既存のテストスイートに組み込まれました。
  2. misc/cgo/test/issue4417.go: (新規ファイル)

    • GoとCの相互運用性をテストするための新しいファイルが追加されました。
    • stdbool.hをインクルードし、複数のbool型引数とint型引数を持つC関数c_boolを定義しています。
    • testBoolAlign関数内で、様々なbool値の組み合わせでC.c_boolを呼び出し、int型引数cが正しく返されることを検証しています。
  3. src/cmd/cgo/gcc.go:

    • func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type メソッド内のdwarf.BoolTypeを処理する箇所が変更されました。
    • 変更前: t.Align = c.ptrSize
    • 変更後: t.Align = 1

コアとなるコードの解説

src/cmd/cgo/gcc.goの変更

--- a/src/cmd/cgo/gcc.go
+++ b/src/cmd/cgo/gcc.go
@@ -1078,7 +1078,7 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 
 	case *dwarf.BoolType:
 		t.Go = c.bool
-		t.Align = c.ptrSize
+		t.Align = 1
 
 	case *dwarf.CharType:
 		if t.Size != 1 {

この変更がこのコミットの核心です。src/cmd/cgo/gcc.goは、CgoがCの型情報をGoの型に変換する際のロジックを定義しています。typeConv構造体のTypeメソッドは、DWARF形式で表現されたCの型(dtype)を受け取り、それをGoの内部表現である*Typeに変換します。

case *dwarf.BoolType: のブロックは、Cのbool型を処理する部分です。

  • t.Go = c.bool: これは、Go側でこのCのbool型がどのように表現されるかを示しています。
  • t.Align = c.ptrSize から t.Align = 1 への変更: ここが修正点です。
    • 変更前は、bool型のアライメントをポインタサイズ(c.ptrSize)に設定していました。これは、Goの内部的なパディング規則や、一部のアーキテクチャでの最適化を意図したものかもしれませんが、Cのbool型のアライメント要件とは一致しませんでした。
    • 変更後は、bool型のアライメントを明示的に1バイトに設定しています。これにより、Cのbool型がメモリ上で1バイト境界に配置されることが保証され、CのABIに準拠した正しい引数渡しが可能になります。

この修正により、GoとCの間でbool型の値が正しくやり取りされるようになり、Issue 4417で報告されたような引数のずれによるバグが解消されます。

misc/cgo/test/issue4417.goのテストコード

// run

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue 4417:	cmd/cgo: bool alignment/padding issue.
// bool alignment is wrong and causing wrong arguments when calling functions.
//

package cgotest

/*
#include <stdbool.h>

static int c_bool(bool a, bool b, int c, bool d, bool e)  {
   return c;
}
*/
import "C"
import "testing"

func testBoolAlign(t *testing.T) {
	b := C.c_bool(true, true, 10, true, false)
	if b != 10 {
		t.Fatalf("found %d expected 10\n", b)
	}
	b = C.c_bool(true, true, 5, true, true)
	if b != 5 {
		t.Fatalf("found %d expected 5\n", b)
	}
	b = C.c_bool(true, true, 3, true, false)
	if b != 3 {
		t.Fatalf("found %d expected 3\n", b)
	}
	b = C.c_bool(false, false, 1, true, false)
	if b != 1 {
		t.Fatalf("found %d expected 1\n", b)
	}
	b = C.c_bool(false, true, 200, true, false)
	if b != 200 {
		t.Fatalf("found %d expected 200\n", b)
	}
}

このテストファイルは、問題の再現と修正の検証のために作成されました。

  • Cのコードブロックで、stdbool.hをインクルードし、bool型の引数を複数持つC関数c_boolを定義しています。この関数は、3番目の引数cint型)の値をそのまま返します。
  • GoのtestBoolAlign関数では、C.c_boolを様々なbool値の組み合わせで呼び出しています。
  • 重要なのは、c_bool関数がbool引数の間にint引数cを挟んでいる点です。もしboolのアライメントが正しくないと、abbool引数のパディングによって、cがメモリ上でずれてしまい、c_boolが期待するint値(例えば10)ではなく、別のメモリ上の値を読み取って返してしまうことになります。
  • テストでは、c_boolが常に正しいint値を返すことをアサートしています。これにより、bool型のアライメントが正しく設定され、引数渡しが正常に行われていることを確認できます。

このテストは、修正が正しく機能していることを保証するための重要な役割を果たしています。

関連リンク

参考にした情報源リンク