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

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

このコミットは、Go言語のデバッグ情報パーサーである debug/dwarf パッケージが、macOS上でClangコンパイラによって生成されたDWARFデバッグ情報の特定のエンコーディングを正しく処理できないバグを修正するものです。特に、cgoを使用する際にこの問題が発生し、GoプログラムがCコードのデバッグ情報を正確に解釈できない事態を招いていました。

コミット

commit 0965459bd908fdbd0ffc6a6cb82d58bd0091fc0a
Author: Russ Cox <rsc@golang.org>
Date:   Wed Oct 9 11:08:22 2013 -0400

    debug/dwarf: handle surprising clang encoding
    
    Fixes a bug in cgo on OS X using clang.
    See golang.org/issue/6472 for details.
    
    Fixes #6472.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/14575043

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

https://github.com/golang/go/commit/0965459bd908fdbd0ffc6a6cb82d58bd0091fc0a

元コミット内容

debug/dwarf: handle surprising clang encoding

Fixes a bug in cgo on OS X using clang.
See golang.org/issue/6472 for details.

Fixes #6472.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/14575043

変更の背景

この変更は、Goのcgo機能がmacOS上でClangコンパイラと組み合わせて使用された際に発生するバグ(Go issue #6472)を修正するために行われました。具体的には、Clangが生成するDWARFデバッグ情報の一部が、Goのdebug/dwarfパッケージの想定と異なる形式でエンコードされており、これによりデバッグ情報のパースが失敗していました。

問題の根本は、DWARFの構造体定義において、Clangが通常とは異なる方法でネストされた構造体を表現していたことにあります。Goのdebug/dwarfパッケージは、DWARFエントリを線形に読み進めることを前提としていましたが、Clangは特定の複合型(この場合は構造体の配列内にネストされた構造体)を表現する際に、親エントリの直下にその子エントリを配置せず、間に余分なDWARFエントリ(例えば、DW_TAG_array_typeのようなタグを持つエントリ)を挿入することがありました。これにより、Goのパーサーが期待する子エントリを見つけられず、デバッグ情報の解釈に失敗していました。

このバグは、特にcgoを介してC言語の構造体をGo側で利用する際に顕在化し、デバッガが構造体のメンバーにアクセスできないなどの問題を引き起こしていました。このコミットは、debug/dwarfパッケージがこのような「驚くべき」Clangのエンコーディングパターンを許容し、正しくスキップして目的の子エントリに到達できるようにすることで、互換性を向上させることを目的としています。

前提知識の解説

DWARF (Debugging With Arbitrary Record Formats)

DWARFは、プログラムのソースコードとコンパイルされたバイナリコードとの間のマッピングを記述するための標準的なデバッグ情報フォーマットです。主にUnix系システムで広く使用されており、GDBなどのデバッガがプログラムの実行中に変数名、型情報、関数名、ソースコードの行番号などを表示するために利用します。

DWARF情報は、コンパイル時にバイナリファイル(実行可能ファイルやライブラリ)の.debug_info.debug_abbrev.debug_lineなどのセクションに埋め込まれます。

  • DIE (Debugging Information Entry): DWARF情報の基本的な単位で、プログラムのエンティティ(変数、関数、型、スコープなど)を記述します。各DIEはタグ(DW_TAG_variable, DW_TAG_subprogram, DW_TAG_structure_typeなど)と属性(名前、型、メモリ位置など)を持ちます。
  • 階層構造: DIEは親子関係を持つことができ、例えば構造体型を表すDIEは、そのメンバーを表すDIEを子として持ちます。この階層構造は、デバッグ情報の論理的な関連性を表現するために重要です。

cgo

cgoは、GoプログラムからC言語のコードを呼び出す(またはその逆)ためのGoの機能です。これにより、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。cgoを使用すると、GoコンパイラはCコードをコンパイルし、GoとCの間のインターフェースを生成します。このプロセスには、Cコンパイラ(macOSでは通常Clang)が関与し、その際に生成されるデバッグ情報がGoのdebug/dwarfパッケージによって解釈される必要があります。

Clang

Clangは、LLVMプロジェクトの一部として開発されているC、C++、Objective-C、Objective-C++コンパイラのフロントエンドです。macOSのXcodeのデフォルトコンパイラとして広く採用されています。Clangは、標準に準拠したコード生成と優れた診断機能で知られていますが、DWARFデバッグ情報の生成方法には、他のコンパイラ(例えばGCC)とは異なる、あるいはより複雑なパターンが存在することがあります。このコミットで問題となったのは、Clangが特定の複合型(構造体の配列内の構造体)のDWARF表現において、Goのdebug/dwarfパッケージが予期しない中間エントリを挿入する挙動でした。

技術的詳細

このバグは、debug/dwarfパッケージがDWARFエントリの階層構造をパースする際の想定と、Clangが生成する実際のDWARF構造とのミスマッチに起因します。

通常、DWARFでは、親のDIE(例えば、構造体型を表すDW_TAG_structure_type)の直下には、そのメンバーを表すDIE(例えば、DW_TAG_member)が配置されることが期待されます。しかし、Clangは、特定の複雑な型(例: struct { struct { int x; } y[16]; } z; のような構造体)を表現する際に、DW_TAG_structure_typeのDIEの直下に、配列型を表すDW_TAG_array_typeのDIEを挿入し、そのDW_TAG_array_typeのDIEの子として、配列の要素型(この場合はネストされた構造体)のDIEを配置することがありました。

Goのdebug/dwarfパッケージのTypeメソッドは、EntryChildrenフィールドがtrueの場合、その子エントリを線形に読み進めるnext()ヘルパー関数を使用していました。このnext()関数は、単に次のDIEを読み込み、それが子エントリであると仮定していました。Clangの「驚くべきエンコーディング」では、このnext()が期待するDW_TAG_memberではなく、DW_TAG_array_typeのような中間的なDIEを返してしまうため、Goのパーサーは正しい構造体メンバーを見つけられず、デバッグ情報の解釈に失敗していました。

このコミットの修正は、next()関数を改良し、親エントリの直接の子ではない複合エントリ(kid.Childrentrueであるようなエントリ)をスキップするように変更することで、この問題を解決しています。具体的には、nextDepthというカウンタを導入し、kid.ChildrentrueのDIEを読み込んだ際にnextDepthをインクリメントし、kid.Tag == 0(NULLエントリ、複合エントリの終わりを示す)を読み込んだ際にnextDepthをデクリメントします。nextDepthが0より大きい間は、読み込んだDIEが現在の親の直接の子ではないと判断し、スキップして次のDIEを読み込み続けます。これにより、GoのパーサーはClangが挿入する中間的なDIEを無視し、最終的に目的の直接の子エントリに到達できるようになります。

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

src/pkg/debug/dwarf/type.go ファイルの (d *Data) Type(off Offset) (Type, error) メソッド内の next() ヘルパー関数が変更されています。

--- a/src/pkg/debug/dwarf/type.go
+++ b/src/pkg/debug/dwarf/type.go
@@ -271,24 +271,43 @@ func (d *Data) Type(off Offset) (Type, error) {
 	// d.Type recursively, to handle circular types correctly.
 	var typ Type
 
+	nextDepth := 0
+
 	// Get next child; set err if error happens.
 	next := func() *Entry {
 		if !e.Children {
 			return nil
 		}
 -		kid, err1 := r.Next()
 -		if err1 != nil {
 -			err = err1
 -			return nil
 -		}
 -		if kid == nil {
 -			err = DecodeError{"info", r.b.off, "unexpected end of DWARF entries"}
 -			return nil
 -		}
 -		if kid.Tag == 0 {
 -			return nil
 +		// Only return direct children.
 +		// Skip over composite entries that happen to be nested
 +		// inside this one. Most DWARF generators wouldn't generate
 +		// such a thing, but clang does.
 +		// See golang.org/issue/6472.
 +		for {
 +			kid, err1 := r.Next()
 +			if err1 != nil {
 +				err = err1
 +				return nil
 +			}
 +			if kid == nil {
 +				err = DecodeError{"info", r.b.off, "unexpected end of DWARF entries"}
 +				return nil
 +			}
 +			if kid.Tag == 0 {
 +				if nextDepth > 0 {
 +					nextDepth--
 +					continue
 +				}
 +				return nil
 +			}
 +			if kid.Children {
 +				nextDepth++
 +			}
 +			if nextDepth > 0 {
 +				continue
 +			}
 +			return kid
 +		}
 -		return kid
  	}
 
  	// Get Type referred to by Entry's AttrType field.

また、misc/cgo/test/issue6472.go に新しいテストファイルが追加されています。

// Copyright 2013 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.

package cgotest

/*
typedef struct
{
        struct
        {
            int x;
        } y[16];
} z;
*/
import "C"

func test6472() {
	// nothing to run, just make sure this compiles
	s := new(C.z)
	println(s.y[0].x)
}

コアとなるコードの解説

変更の核心は、src/pkg/debug/dwarf/type.go 内の next() ヘルパー関数にあります。

変更前: 変更前のnext()関数は、単純に次のDWARFエントリを読み込み、それが現在のエントリの直接の子であると仮定していました。kid.Tag == 0(NULLエントリ)に遭遇した場合、それは子エントリの終わりを示し、nilを返していました。このシンプルなロジックは、ほとんどのDWARFジェネレータが生成する「素直な」階層構造には対応できましたが、Clangが生成するような、親と直接の子の間に余分な複合エントリが挿入されるケースには対応できませんでした。

変更後: 変更後のnext()関数は、forループとnextDepthカウンタを導入することで、より堅牢なパースロジックを実現しています。

  1. nextDepthの導入: nextDepthは、現在読み込んでいるDIEが、現在の親エントリの直接の子ではなく、その中にネストされた複合エントリである場合に、そのネストの深さを追跡します。初期値は0です。
  2. ループ処理: for {} ループが導入され、目的の直接の子エントリが見つかるまで、またはエラーが発生するまで、DIEの読み込みを続けます。
  3. エラーハンドリングとNULLエントリ: r.Next()でエラーが発生した場合や、kid == nil(予期せぬDWARFエントリの終端)の場合の処理は変更されていません。
  4. kid.Tag == 0 (NULLエントリ) の処理:
    • nextDepth > 0 の場合: これは、現在処理中の複合エントリの終わりを示します。nextDepthをデクリメントし、ループをcontinueして次のDIEを読み込みます。これにより、ネストされた複合エントリをスキップして、その外側の階層に戻ることができます。
    • nextDepth == 0 の場合: これは、現在の親エントリの直接の子の終わり、または現在の階層レベルの終わりを示します。この場合、nilを返して子エントリの読み込みを終了します。
  5. kid.Children (複合エントリ) の処理:
    • kid.Childrentrueの場合: これは、読み込んだDIEが自身も子を持つ複合エントリであることを示します。このDIEが現在の親の直接の子ではない(つまり、中間的なDIEである)可能性があるため、nextDepthをインクリメントします。
  6. 直接の子の判定と返却:
    • nextDepth > 0 の場合: これは、現在読み込んだDIEが、現在の親の直接の子ではなく、ネストされた複合エントリの一部であることを意味します。このDIEはスキップされるべきなので、continueして次のDIEを読み込みます。
    • nextDepth == 0 の場合: これは、現在読み込んだDIEが、現在の親の直接の子であることを意味します。このDIEが目的のものであるため、return kidで返却されます。

この修正により、debug/dwarfパッケージは、Clangが生成するような、親と直接の子の間に余分な複合エントリが挿入されるDWARF構造を正しくパースできるようになり、cgoとClangの組み合わせにおけるデバッグ情報の問題が解決されました。

misc/cgo/test/issue6472.go は、このバグを再現するための最小限のC構造体定義を含むcgoテストケースです。このテストは、コンパイルが成功し、C.z型のs.y[0].xにアクセスできることを確認するもので、修正が正しく適用されたことを検証します。

関連リンク

参考にした情報源リンク