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

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

このコミットは、Go言語のリンカ(cmd/ld)の一部である src/cmd/ld/data.c ファイルに対する変更です。data.c は、リンカがシンボルやリロケーション(再配置)情報を処理する際に使用するデータ構造や関数を定義しているファイルと考えられます。具体的には、relocsym 関数内でリロケーションオフセットの計算に関するバグが修正されています。

コミット

commit d727d147c0c724e9d6489db86925dc61a5ddfd91
Author: Rob Pike <r@golang.org>
Date:   Wed May 1 17:00:21 2013 -0700

    cmd/ld: fix another unsigned value causing bugs on Plan 9
    "The usual conversions" bite again.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/9103044

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

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

元コミット内容

cmd/ld: fix another unsigned value causing bugs on Plan 9
"The usual conversions" bite again.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/9103044

変更の背景

このコミットは、Go言語のリンカがPlan 9オペレーティングシステム上で動作する際に発生していたバグを修正するものです。コミットメッセージにある「"The usual conversions" bite again.」という記述から、C言語の「通常の算術変換 (Usual Arithmetic Conversions)」が原因で、符号なしの値が予期せぬ挙動を引き起こしていたことが示唆されます。

具体的には、src/cmd/ld/data.c 内の relocsym 関数におけるオフセット計算において、uchar 型(符号なし文字型)の r->sizint32 型の変数と混合された際に、Plan 9の8cコンパイラが誤った型推論を行い、結果として不正な値が生成されていました。この問題は、GoのリンカがC言語で書かれており、各ホストシステム(この場合はPlan 9)のCコンパイラによってコンパイルされるという特性に起因しています。他のシステム(例えばGCCを使用するLinuxなど)では問題が発生しなかったため、Plan 9の8cコンパイラの特定の挙動が原因であることが特定されました。

前提知識の解説

Plan 9 8c コンパイラ

Plan 9 8cコンパイラは、Bell Labsが開発した分散オペレーティングシステムであるPlan 9 for Bell Labsのために、Intel 386アーキテクチャ向けに設計されたCコンパイラです。

  • アーキテクチャ固有のコンパイラ: Plan 9は、サポートするハードウェアアーキテクチャごとに異なるCコンパイラとリンカを提供しています。例えば、AMD64には6c、ARMには5c、Intel 386には8cが使われます。
  • 統合されたコンパイルプロセス: 従来のCコンパイラが複数のパス(プリプロセッサ、コンパイラ、アセンブラ、リンカ)を別々に実行するのに対し、Plan 9のコンパイラ(8c)はこれらの多くの役割を単一のプログラムに統合しています。プリプロセッサ、字句解析器、パーサ、コードジェネレータ、ローカルオプティマイザ、およびアセンブラの前半部分の機能を兼ね備えています。
  • 2段階のビルドプロセス: コンパイルプロセスは通常2つの主要なステップを含みます。
    1. コンパイラ(例: 8c)がCソースファイルを受け取り、オブジェクトファイル(例: 386ターゲットの場合はhello-p9.8)を生成します。
    2. 別のローダプログラム(例: 386の場合は8l)がオブジェクトファイルとライブラリを結合して最終的な実行可能バイナリを作成します。ローダはアセンブラの後半部分の役割、グローバル最適化、およびリンクも処理します。
  • C言語の方言: Plan 9コンパイラによって実装されるC言語は、1989年のANSI C標準に基づいていますが、いくつかの変更と拡張が含まれています。

このコミットの文脈では、GoのリンカのCコードがPlan 9上でコンパイルされる際に、この8cコンパイラの特定のバグが問題を引き起こしたという点が重要です。

C言語の「通常の算術変換 (Usual Arithmetic Conversions)」

C言語において、異なる算術型のオペランドが式中で使用される場合、コンパイラは自動的に「通常の算術変換」と呼ばれる一連のルールを適用します。これは、演算を実行する前にオペランドを共通の型に変換し、データの整合性を確保し、予期せぬ結果を防ぐことを目的とした暗黙的な型変換の一種です。

変換プロセスは以下のステップで進行します。

  1. 整数昇格 (Integer Promotions):

    • charshort intenum型、ビットフィールドなどのより小さい整数型は昇格されます。
    • 元の型のすべての値がintで表現できる場合、値はintに変換されます。
    • そうでない場合、unsigned intに変換されます。
    • この昇格は、他の通常の算術変換の前に発生します。
  2. 型階層 (Type Hierarchy): 整数昇格後もオペランドが異なる型を持つ場合、それらは事前に定義された階層の中で「最も高い」型に変換されます。変換は通常、より低い精度/サイズからより高い精度/サイズへと進みます。

    • いずれかのオペランドがlong doubleの場合、もう一方はlong doubleに変換されます。
    • そうでなければ、いずれかのオペランドがdoubleの場合、もう一方はdoubleに変換されます。
    • そうでなければ、いずれかのオペランドがfloatの場合、もう一方はfloatに変換されます。
    • そうでなければ(昇格後、両方が整数型の場合):
      • いずれかのオペランドがunsigned long long intの場合、もう一方はunsigned long long intに変換されます。
      • そうでなければ、いずれかのオペランドがlong long intの場合、もう一方はlong long intに変換されます。
      • そうでなければ、いずれかのオペランドがunsigned long intの場合、もう一方はunsigned long intに変換されます。
      • そうでなければ、一方のオペランドがlong intで、もう一方がunsigned intの場合、両方ともunsigned long intに変換されます。
      • そうでなければ、いずれかのオペランドがlong intの場合、もう一方はlong intに変換されます。
      • そうでなければ、いずれかのオペランドがunsigned intの場合、もう一方はunsigned intに変換されます。
      • そうでなければ、両方のオペランドはintに変換されます。

演算の結果は、この共通の昇格された型になります。

重要な考慮事項:

  • データ損失: 暗黙的な変換はデータの整合性を維持することを目的としていますが、より大きな型からより小さな型への変換(例: doubleからfloatintからchar)は、精度損失や切り捨てにつながる可能性があります。
  • 符号付き vs. 符号なし: 符号付き型と符号なし型を混在させる場合、特別な注意が必要です。符号付きの値が暗黙的に符号なし型に変換されると、非常に大きな正の数になる可能性があり、予期せぬ動作につながることがあります。

符号拡張 (Sign Extension)

Cプログラミングにおいて、符号拡張とは、符号付き整数のビット数を増やしながら、その符号と値を保持するプロセスです。これは通常、より小さい符号付き整数型がより大きい符号付き整数型に変換されるときに発生します(例: char(8ビット)がint(通常32ビット)に代入される場合)。

主な原則は、元の符号付き数値の値を維持することです。ほとんどのシステムで符号付き整数に広く使用されている2の補数表現では、最上位ビット(MSB)が符号を示します(正の場合は0、負の場合は1)。

動作の仕組みは以下の通りです。

  • 正の数の場合: MSBは0です。拡張時には、新しい上位ビットはゼロで埋められます。これはゼロ拡張とも呼ばれます。
  • 負の数の場合: MSBは1です。拡張時には、新しい上位ビットは1で埋められます。これにより、2の補数値が同じに保たれます。

C標準は、符号付き整数型がより広い符号付き整数型に変換される場合、値が保持されることを保証しています。これは暗黙的に符号拡張が発生することを意味します。

このコミットの文脈では、uchar(符号なし)がint32(符号付き)と混合された際に、Plan 9の8cコンパイラが「通常の算術変換」を誤って適用し、ucharuint32として扱われた後、int32との演算で符号拡張が正しく行われなかった、あるいは予期せぬ符号拡張が発生した可能性が考えられます。

技術的詳細

このバグは、src/cmd/ld/data.c 内の relocsym 関数における以下の行で発生していました。

o += r->add - (s->value + r->off + r->siz);

ここで、s->valuer->offint32 型であると推測されますが、r->sizuchar 型(符号なし文字型)です。C言語の「通常の算術変換」のルールに従えば、uchar 型の r->siz は、式中でより大きい整数型(この場合はint32)と演算される際に、まずintに昇格されるべきです。しかし、Plan 9の8cコンパイラは、このucharuint32として誤って扱い、その結果、s->value + r->off + r->siz という式全体がuint32型として評価されていました。

uint32型として評価された値が、その後int32型のoから減算される際に、符号なしと符号付きの混合演算となり、特にr->sizの値が大きい場合(例えば、ucharの最大値に近い場合)、uint32の大きな正の値がint32の負の値として解釈されるなど、予期せぬ結果(オーバーフローや不正なオフセット計算)を引き起こしていました。これは、特に負のオフセットが必要な場合に問題となります。

他のシステム(例えばGCC)では、この式はC標準に従って正しくint32として評価されるため、問題は発生しませんでした。このため、問題はPlan 9の8cコンパイラの特定のバグ、すなわち「通常の算術変換」の誤った実装に起因すると結論付けられました。

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

--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -247,7 +247,13 @@ relocsym(Sym *s)
 		o = 0;
 		if(r->sym)
 			o += symaddr(r->sym);
-		o += r->add - (s->value + r->off + r->siz);
+		// NOTE: The (int32) cast on the next line works around a bug in Plan 9's 8c
+		// compiler. The expression s->value + r->off + r->siz is int32 + int32 +
+		// uchar, and Plan 9 8c incorrectly treats the expression as type uint32
+		// instead of int32, causing incorrect values when sign extended for adding
+		// to o. The bug only occurs on Plan 9, because this C program is compiled by
+		// the standard host compiler (gcc on most other systems).
+		o += r->add - (s->value + r->off + (int32)r->siz);
 		break;
 	case D_SIZE:
 		o = r->sym->size + r->add;

コアとなるコードの解説

変更は、relocsym 関数内の以下の行にあります。

-		o += r->add - (s->value + r->off + r->siz);
+		o += r->add - (s->value + r->off + (int32)r->siz);

修正は、r->siz に明示的に (int32) キャストを追加することです。

  • 変更前: s->value + r->off + r->siz

    • s->value (int32) + r->off (int32) + r->siz (uchar)
    • Plan 9の8cコンパイラは、ucharuint32として扱い、式全体をuint32として評価していました。これにより、oへの加算時に不正な値(特に符号拡張の問題)が発生していました。
  • 変更後: s->value + r->off + (int32)r->siz

    • r->siz が明示的に int32 にキャストされます。
    • これにより、ucharint32に昇格される際に、C標準に則った正しい符号拡張が行われます。
    • 結果として、s->value + r->off + (int32)r->siz という式全体が正しくint32型として評価されるようになります。
    • この明示的なキャストは、Plan 9の8cコンパイラのバグを回避し、リンカが期待する正しいオフセット値を計算できるようにします。

コミットメッセージのコメントにもあるように、このキャストはPlan 9の8cコンパイラのバグに対するワークアラウンドであり、他のコンパイラ(GCCなど)ではこのような問題は発生しないため、通常は不要なキャストです。しかし、Goのリンカは様々なプラットフォームで動作する必要があるため、このような特定のコンパイラの挙動に対応するための修正が必要でした。

関連リンク

  • Go Gerrit Change-Id: https://golang.org/cl/9103044

参考にした情報源リンク