[インデックス 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->siz
が int32
型の変数と混合された際に、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つの主要なステップを含みます。
- コンパイラ(例:
8c
)がCソースファイルを受け取り、オブジェクトファイル(例: 386ターゲットの場合はhello-p9.8
)を生成します。 - 別のローダプログラム(例: 386の場合は
8l
)がオブジェクトファイルとライブラリを結合して最終的な実行可能バイナリを作成します。ローダはアセンブラの後半部分の役割、グローバル最適化、およびリンクも処理します。
- コンパイラ(例:
- C言語の方言: Plan 9コンパイラによって実装されるC言語は、1989年のANSI C標準に基づいていますが、いくつかの変更と拡張が含まれています。
このコミットの文脈では、GoのリンカのCコードがPlan 9上でコンパイルされる際に、この8cコンパイラの特定のバグが問題を引き起こしたという点が重要です。
C言語の「通常の算術変換 (Usual Arithmetic Conversions)」
C言語において、異なる算術型のオペランドが式中で使用される場合、コンパイラは自動的に「通常の算術変換」と呼ばれる一連のルールを適用します。これは、演算を実行する前にオペランドを共通の型に変換し、データの整合性を確保し、予期せぬ結果を防ぐことを目的とした暗黙的な型変換の一種です。
変換プロセスは以下のステップで進行します。
-
整数昇格 (Integer Promotions):
char
、short int
、enum
型、ビットフィールドなどのより小さい整数型は昇格されます。- 元の型のすべての値が
int
で表現できる場合、値はint
に変換されます。 - そうでない場合、
unsigned int
に変換されます。 - この昇格は、他の通常の算術変換の前に発生します。
-
型階層 (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
からfloat
、int
から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コンパイラが「通常の算術変換」を誤って適用し、uchar
がuint32
として扱われた後、int32
との演算で符号拡張が正しく行われなかった、あるいは予期せぬ符号拡張が発生した可能性が考えられます。
技術的詳細
このバグは、src/cmd/ld/data.c
内の relocsym
関数における以下の行で発生していました。
o += r->add - (s->value + r->off + r->siz);
ここで、s->value
と r->off
は int32
型であると推測されますが、r->siz
は uchar
型(符号なし文字型)です。C言語の「通常の算術変換」のルールに従えば、uchar
型の r->siz
は、式中でより大きい整数型(この場合はint32
)と演算される際に、まずint
に昇格されるべきです。しかし、Plan 9の8cコンパイラは、このuchar
をuint32
として誤って扱い、その結果、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コンパイラは、
uchar
をuint32
として扱い、式全体をuint32
として評価していました。これにより、o
への加算時に不正な値(特に符号拡張の問題)が発生していました。
-
変更後:
s->value + r->off + (int32)r->siz
r->siz
が明示的にint32
にキャストされます。- これにより、
uchar
がint32
に昇格される際に、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
参考にした情報源リンク
- Plan 9 from Bell Labs - Wikipedia
- Plan 9 from Bell Labs - 8c, 6c, 5c, 4c - C compilers
- C Language: Usual arithmetic conversions - GeeksforGeeks
- C Language: Type Conversion - GeeksforGeeks
- Sign Extension in C - GeeksforGeeks
- What is sign extension? - Stack Overflow
- Implicit Type Conversion in C - Tutorialspoint
- C Standard: Integer Promotions - C99 Standard (6.3.1.1) (PDF, 6.3.1.1 Integer promotions)
- C Standard: Usual arithmetic conversions - C99 Standard (6.3.1.8) (PDF, 6.3.1.8 Usual arithmetic conversions)