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

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

このコミットは、Go言語のランタイムデバッグを支援するGDB (GNU Debugger) のPretty Printerスクリプトである src/pkg/runtime/runtime-gdb.py に対する変更です。主な目的は、GDBがGoのデータ構造(特にスライスとインターフェース)を表示する際に、破損したデータに遭遇した場合の堅牢性を向上させるためのサニティチェックを追加することです。これにより、デバッガがクラッシュしたり、誤った情報を表示したりするのを防ぎます。

コミット

commit fb2706113f36452f7e1e514be1949c0cdae46835
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Feb 29 16:42:25 2012 +0100

    pkg/runtime: 2 sanity checks in the runtime-gdb.py prettyprinters.
    
    Don't try to print obviously corrupt slices or interfaces.
    Doesn't actually solve 3047 or 2818, but seems a good idea anyway.
    
    R=rsc, bsiegert
    CC=golang-dev
    https://golang.org/cl/5708061

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

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

元コミット内容

pkg/runtime: runtime-gdb.py の prettyprinter に2つのサニティチェックを追加。

明らかに破損したスライスやインターフェースをプリントしようとしない。 これは実際には問題3047や2818を解決するものではないが、いずれにせよ良いアイデアだと思われる。

変更の背景

Go言語のプログラムをGDBでデバッグする際、Goの内部データ構造(スライス、インターフェース、マップ、チャネルなど)はC言語の構造体として表現されます。GDBはこれらの構造体をそのまま表示すると、Goのセマンティクスとは異なる低レベルな情報しか得られません。そこで、Goプロジェクトは runtime-gdb.py というPythonスクリプトを提供し、GDBのPretty Printer機能を利用して、これらのGoのデータ構造をより人間が理解しやすい形式(例えば、スライスを [elem1, elem2] のように)で表示できるようにしています。

しかし、Goのランタイムやプログラムのバグ、あるいはメモリ破損などによって、これらの内部データ構造が不正な状態になることがあります。例えば、スライスの len (長さ) が cap (容量) を超える、あるいはインターフェースの型情報が循環参照を起こすなどです。このような不正なデータにPretty Printerが遭遇した場合、スクリプトがクラッシュしたり、無限ループに陥ったりする問題が発生していました。コミットメッセージで言及されている「問題3047」と「問題2818」は、それぞれスライスとインターフェースのPretty Printerが不正なデータでクラッシュする具体的なケースを指しています。

このコミットの背景は、これらのクラッシュを防ぎ、デバッグ体験を向上させることにあります。根本的なバグ(データ破損の原因)を解決するのではなく、Pretty Printer側で不正なデータを検出し、安全に処理(表示をスキップするなど)することで、デバッガの安定性を高めることが目的です。

前提知識の解説

GDB (GNU Debugger)

GDBは、Unix系システムで広く使われているコマンドラインベースのデバッガです。C, C++, Go, Fortranなど多くのプログラミング言語に対応しており、プログラムの実行を一時停止させたり、変数の値を検査したり、メモリの内容を調べたりすることができます。

GDB Pretty Printers

GDBのPretty Printerは、デバッグ対象のプログラムが使用するカスタムデータ型を、GDBの標準的な表示形式よりも人間が理解しやすい形式で表示するための機能です。Pythonスクリプトで実装され、GDBにロードされます。Go言語の場合、スライス、インターフェース、マップ、チャネルなどのGo固有の型は、GDBから見ると単なるC言語の構造体として扱われるため、Pretty Printerが必須となります。runtime-gdb.py はGoのランタイムが提供する公式のPretty Printerスクリプトです。

Go言語のスライス (Slice)

Goのスライスは、配列の一部を参照する軽量なデータ構造です。内部的には、以下の3つの要素で構成されます。

  1. ポインタ (Pointer): スライスが参照する基底配列の先頭要素へのポインタ。
  2. 長さ (Length, len): スライスに含まれる要素の数。
  3. 容量 (Capacity, cap): スライスの基底配列の、ポインタから始まる部分の最大容量。スライスを拡張できる上限を示します。

Goの仕様では、常に 0 <= len <= cap が保証されます。もし len > cap となるようなスライスが存在する場合、それはメモリ破損やランタイムのバグによって不正な状態になったことを意味します。

Go言語のインターフェース (Interface)

Goのインターフェースは、メソッドの集合を定義する型です。インターフェース型の変数は、内部的に以下の2つの要素で構成されます。

  1. 型情報 (Type, _type または tab): インターフェースに格納されている具体的な値の型 (_typeeface の場合、tabiface の場合)。
  2. データ (Data, data): インターフェースに格納されている具体的な値。

Goのランタイムは、リフレクションのために型情報を内部的に管理しています。この型情報は、runtime._typeruntime.commonType といった構造体で表現されます。Pretty Printerはこれらの内部構造を解析して、インターフェースの動的な型と値を表示します。型情報が破損している場合、例えば型定義が循環参照を起こしているような場合、Pretty Printerが無限ループに陥る可能性があります。

efaceiface

Goのインターフェースは、格納する値がポインタ型であるか否かによって、内部表現が異なります。

  • eface (empty interface): interface{} 型のインターフェース。値がポインタ型でなくても格納できます。内部的には typedata の2つのポインタで構成されます。
  • iface (non-empty interface): io.Reader のような、特定のメソッドを持つインターフェース。内部的には tab (型とメソッドのテーブルへのポインタ) と data (値へのポインタ) で構成されます。

Pretty Printerは、これらの内部構造を区別して解析する必要があります。

技術的詳細

このコミットは、src/pkg/runtime/runtime-gdb.py スクリプト内の以下のPretty Printerクラスとヘルパー関数にサニティチェックを追加しています。

  1. SliceTypePrinter クラス:

    • children メソッドは、スライスの要素を列挙するために使用されます。
    • 追加されたサニティチェック: if self.val["len"] > self.val["cap"]:
      • Goのスライスの不変条件である len <= cap が破られている場合、スライスは破損していると判断し、要素の列挙を中止します (return)。これにより、不正なメモリ領域へのアクセスやGDBのクラッシュを防ぎます。
  2. インターフェース関連のヘルパー関数 (iface_dtype, iface_commontype):

    • iface_dtype(obj) は、インターフェースに格納されている動的な値のGDB型をデコードする関数です。
    • iface_commontype(obj) は新しく導入されたヘルパー関数で、インターフェースの型情報から runtime.commonType 構造体を取得します。
    • iface_commontype 内に追加されたサニティチェック:
      • tt = go_type_ptr['_type'].cast(_rtp_type).dereference()['_type']
      • if tt != tt.cast(_rtp_type).dereference()['_type']:
        • これは、Goのリフレクション型記述が循環参照に陥っているかどうかをチェックするものです。_type フィールドが自分自身を指している場合、それは有効な型記述の終端を示しますが、もし不正な循環参照がある場合、無限ループを防ぐために None を返します。
    • iface_dtype は、iface_commontype の結果が None の場合、または lookup_type (GDBの型ルックアップ) が失敗した場合に None を返すように変更されました。これにより、不正な型情報を持つインターフェースの処理が安全になります。
  3. IfacePrinter クラス:

    • to_string メソッドは、インターフェースの文字列表現を生成します。
    • 変更点: if dtype is None:
      • iface_dtypeNone を返した場合(型情報が不正な場合)、以前はエラーになる可能性がありましたが、この変更により、動的な型名と生のデータ値を使って「<bad dynamic type>」のような、より有用なエラーメッセージを表示するようになりました。これにより、デバッガがクラッシュすることなく、問題のあるインターフェースを特定しやすくなります。
  4. GoIfaceCmd クラス (GDBコマンド go iface):

    • GDBの go iface <variable> コマンドの実装です。
    • 追加されたチェック: if obj['data'] == 0: dtype = "nil"
      • インターフェースのデータポインタが 0 (nil) の場合、明示的に dtype"nil" と設定します。これにより、nilインターフェースがより正確に表示されます。
    • if not dtype:if dtype is None: に変更し、iface_dtype の新しい戻り値のセマンティクスに合わせました。

これらの変更は、GDBのPretty PrinterがGoのランタイムデータ構造を解析する際の堅牢性を高め、デバッグ中のクラッシュや誤った表示を減らすことを目的としています。

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

変更はすべて src/pkg/runtime/runtime-gdb.py ファイル内で行われています。

  1. SliceTypePrinter クラスの children メソッド:

    @@ -58,6 +58,8 @@ class SliceTypePrinter:
     		return str(self.val.type)[6:]  # skip 'struct '
     
     	def children(self):
    +		if self.val["len"] > self.val["cap"]:
    +			return
     		ptr = self.val["array"]
     		for idx in range(self.val["len"]):
     			yield ('[%d]' % idx, (ptr + idx).dereference())
    
  2. iface_dtype および iface_commontype 関数の追加と変更:

    @@ -184,12 +186,10 @@ def lookup_type(name):
     		except:
     			pass
     
    +_rctp_type = gdb.lookup_type("struct runtime.commonType").pointer()
    +_rtp_type = gdb.lookup_type("struct runtime._type").pointer()
     
    -def iface_dtype(obj):
    -	"Decode type of the data field of an eface or iface struct."
    -        # known issue: dtype_name decoded from runtime.commonType is "nested.Foo"
    -        # but the dwarf table lists it as "full/path/to/nested.Foo"
    -
    +def iface_commontype(obj):
     	if is_iface(obj):
     		go_type_ptr = obj['tab']['_type']
     	elif is_eface(obj):
    @@ -197,15 +197,31 @@ def iface_dtype(obj):
     	else:
     		return
     
    -	ct = gdb.lookup_type("struct runtime.commonType").pointer()
    -	dynamic_go_type = go_type_ptr['ptr'].cast(ct).dereference()
    +	# sanity check: reflection type description ends in a loop.
    +	tt = go_type_ptr['_type'].cast(_rtp_type).dereference()['_type']
    +	if tt != tt.cast(_rtp_type).dereference()['_type']:
    +		return
    +	
    +	return go_type_ptr['ptr'].cast(_rctp_type).dereference()
    +	
    +
    +def iface_dtype(obj):
    +	"Decode type of the data field of an eface or iface struct."
    +	# known issue: dtype_name decoded from runtime.commonType is "nested.Foo"
    +	# but the dwarf table lists it as "full/path/to/nested.Foo"
    +
    +	dynamic_go_type = iface_commontype(obj)
    +	if dynamic_go_type is None:
    +		return
     	dtype_name = dynamic_go_type['string'].dereference()['str'].string()
     
     	dynamic_gdb_type = lookup_type(dtype_name)
    -        if dynamic_gdb_type:
    -		type_size = int(dynamic_go_type['size'])
    -                uintptr_size = int(dynamic_go_type['size'].type.sizeof)  # size is itself an uintptr
    -		if type_size > uintptr_size:
    +	if dynamic_gdb_type is None:
    +		return
    +	
    +	type_size = int(dynamic_go_type['size'])
    +	uintptr_size = int(dynamic_go_type['size'].type.sizeof)	 # size is itself an uintptr
    +	if type_size > uintptr_size:
     			dynamic_gdb_type = dynamic_gdb_type.pointer()
     
     	return dynamic_gdb_type
    
  3. iface_dtype_name 関数の変更:

    @@ -213,15 +229,9 @@ def iface_dtype_name(obj):
     def iface_dtype_name(obj):
     	"Decode type name of the data field of an eface or iface struct."
     
    -	if is_iface(obj):
    -		go_type_ptr = obj['tab']['_type']
    -	elif is_eface(obj):
    -		go_type_ptr = obj['_type']
    -	else:
    +	dynamic_go_type = iface_commontype(obj)
    +	if dynamic_go_type is None:
     		return
    -
    -	ct = gdb.lookup_type("struct runtime.commonType").pointer()
    -	dynamic_go_type = go_type_ptr['ptr'].cast(ct).dereference()
     	return dynamic_go_type['string'].dereference()['str'].string()
    
  4. IfacePrinter クラスの to_string メソッドの変更:

    @@ -244,7 +254,7 @@ class IfacePrinter:
     		except:
     			return "<bad dynamic type>"
     
    -                if not dtype:  # trouble looking up, print something reasonable
    +		if dtype is None:  # trouble looking up, print something reasonable
     			return "(%s)%s" % (iface_dtype_dtype_name(self.val), self.val['data'])
     
     		try:
    
  5. GoIfaceCmd クラスの invoke メソッドの変更:

    @@ -403,8 +413,12 @@ class GoIfaceCmd(gdb.Command):
     			except:
     				print "Can't parse ", obj, ": ", e
     				continue
     
    -		dtype = iface_dtype(obj)
    -		if not dtype:
    +		if obj['data'] == 0:
    +			dtype = "nil"
    +		else:
    +			dtype = iface_dtype(obj)
    +			
    +		if dtype is None:
     			print "Not an interface: ", obj.type
     			continue
    

コアとなるコードの解説

スライスに対するサニティチェック

SliceTypePrinterchildren メソッドは、スライスの要素をイテレートしてGDBに表示するためのものです。Goのスライスは lencap という2つの重要なフィールドを持ちます。len はスライスが現在保持している要素の数、cap は基底配列の容量を示します。Goの言語仕様では 0 <= len <= cap が常に真であると保証されています。

追加されたコード if self.val["len"] > self.val["cap"]: は、この不変条件が破られているかどうかをチェックします。もし lencap を超えている場合、それはスライスがメモリ破損などの原因で不正な状態にあることを意味します。このような状況で要素をイテレートしようとすると、未定義のメモリ領域にアクセスしたり、GDBがクラッシュしたりする可能性があります。このチェックにより、不正なスライスが検出された場合は、それ以上の処理を行わずに return し、安全に処理を終了します。これにより、デバッガの安定性が向上します。

インターフェース型情報の堅牢化

インターフェースのPretty Printerは、インターフェースが保持する動的な型情報を正確にデコードする必要があります。この型情報はGoランタイムの内部構造 (runtime._type, runtime.commonType) に格納されており、リフレクションメカニズムの基盤となります。

新しく導入された iface_commontype 関数は、インターフェースから runtime.commonType 構造体へのポインタを取得する共通ロジックをカプセル化しています。この関数内で最も重要な変更は、リフレクション型記述の循環参照を検出するサニティチェックです。

tt = go_type_ptr['_type'].cast(_rtp_type).dereference()['_type'] if tt != tt.cast(_rtp_type).dereference()['_type']:

このコードは、型情報が不正な循環参照を形成していないかを確認します。Goの型システムでは、型記述は通常、最終的に自己参照する形で終端します。しかし、もし不正な循環参照が存在する場合、Pretty Printerが無限ループに陥る可能性があります。このチェックは、そのような不正な状態を検出し、None を返すことで安全に処理を中断します。

iface_dtype 関数は、この iface_commontype を利用するように変更され、None が返された場合には自身も None を返すようになりました。これにより、不正な型情報を持つインターフェースに対して、Pretty Printerがクラッシュすることなく、より適切なフォールバック処理(例えば、生のデータ値を表示する)を実行できるようになります。

また、GoIfaceCmd では、インターフェースの data フィールドが 0 (nil) の場合に明示的に "nil" と表示するロジックが追加されました。これは、nilインターフェースの表示をより明確にするための改善です。

これらの変更は、Goの内部データ構造の複雑さと、デバッグ時に発生しうるメモリ破損やランタイムのバグといったエッジケースを考慮し、GDBのPretty Printerがより堅牢に動作するように設計されています。

関連リンク

参考にした情報源リンク