[インデックス 1593] ファイルの概要
このコミットは、Go言語の初期のコンパイラツールチェーンの一部であったsrc/cmd/6g/list.c
ファイルにおける、出力フォーマットに関するバグ修正です。具体的には、snprintf
関数を用いた数値の文字列変換において、型キャストが不足していたために発生していた問題を解決しています。
コミット
- コミットハッシュ:
122ed3e9888e70218235a0f6c7a2b40f12d2bd0a
- 作者: Russ Cox rsc@golang.org
- コミット日時: Fri Jan 30 14:54:49 2009 -0800
- 変更ファイル:
src/cmd/6g/list.c
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/122ed3e9888e70218235a0f6c7a2b40f12d2bd0a
元コミット内容
print format bug
R=ken
OCL=23965
CL=23965
変更の背景
このコミットは、Go言語の初期開発段階におけるコンパイラ(特に6g
、AMD64アーキテクチャ向けのGoコンパイラ)のバグ修正です。当時のGoコンパイラは、Plan 9由来のCコンパイラツールチェーン(6c
, 6g
, 6l
など)をベースにしていました。
src/cmd/6g/list.c
は、コンパイラのリスト出力(アセンブリコードの表示など)に関連する部分であり、数値のフォーマット表示に問題がありました。具体的には、snprintf
関数を使用して64ビット整数(long long
)を%lud
フォーマット指定子(unsigned long
用)で出力しようとした際に、適切な型キャストが行われていなかったため、予期せぬ値が出力される可能性がありました。これは、C言語における型変換のルールと、可変引数関数の引数プロモーションに関する潜在的な問題に起因します。
このバグは、コンパイラのデバッグ出力や診断メッセージの正確性に影響を与える可能性があり、早期に修正する必要がありました。
前提知識の解説
Go言語の初期コンパイラ (6g
)
Go言語の初期のコンパイラは、現在のGoツールチェーンとは異なり、C言語で書かれたPlan 9のコンパイラ群(8c
, 6c
, 5c
など)をベースにしていました。
6g
: AMD64 (x86-64) アーキテクチャ向けのGoコンパイラ。6c
: AMD64 (x86-64) アーキテクチャ向けのCコンパイラ。6l
: AMD64 (x86-64) アーキテクチャ向けのリンカ。
これらのコンパイラは、Go言語のソースコードを機械語に変換する役割を担っていました。src/cmd/6g/list.c
は、6g
コンパイラの一部として、コンパイル過程で生成される中間表現やアセンブリコードのリスト表示を担当していました。
snprintf
関数
snprintf
はC標準ライブラリの関数で、指定されたフォーマット文字列に従ってデータを整形し、指定されたバッファに書き込む関数です。
int snprintf(char *str, size_t size, const char *format, ...);
str
: 結果を書き込むバッファへのポインタ。size
: バッファの最大サイズ(ヌル終端文字を含む)。format
: フォーマット文字列。%d
,%s
,%f
などのフォーマット指定子を含む。...
: フォーマット指定子に対応する可変引数。
snprintf
は、バッファオーバーフローを防ぐためにsize
引数で書き込みサイズを制限できる点が特徴です。
フォーマット指定子と型の一致
snprintf
のような可変引数関数では、フォーマット指定子と対応する引数の型が厳密に一致していることが非常に重要です。
%lud
:unsigned long
型の引数に対応します。lu
はlong unsigned
を意味し、d
は10進数を意味します。long long
: C99で導入された64ビット整数型です。
もしフォーマット指定子と引数の型が一致しない場合、未定義動作(Undefined Behavior)を引き起こす可能性があります。これは、コンパイラが引数をどのように解釈するかが保証されず、予期せぬ値が出力されたり、プログラムがクラッシュしたりする原因となります。特に、異なるサイズの整数型を渡す場合に問題が発生しやすいです。
型キャスト ((ulong)
)
C言語では、明示的な型キャストを行うことで、ある型の値を別の型として扱わせることができます。
例: (unsigned long)d1
これは、変数d1
の値をunsigned long
型として解釈するようにコンパイラに指示します。このコミットでは、long long
型の値を%lud
フォーマット指定子に対応させるために、明示的にunsigned long
型にキャストしています。
技術的詳細
このバグは、src/cmd/6g/list.c
内のDconv
関数で発生していました。この関数は、Goコンパイラが内部的に使用するデータ構造(おそらくアセンブリ命令のオペランドなど)を文字列に変換する役割を担っています。
問題の箇所は、a->offset
という64ビット整数(long long
型)の値を2つの32ビット部分(d1
, d2
)に分割し、これらをsnprintf
で$%lud-%lud
というフォーマットで出力しようとしていた点です。
元のコード:
snprint(str, sizeof(str), "$%lud-%lud", d1, d2);
ここで、d1
とd2
はそれぞれa->offset
から抽出された32ビット値ですが、これらがどのような型で宣言されていたか、そしてsnprintf
に渡される際にどのようにプロモーションされるかが問題でした。もしd1
やd2
がlong long
型、あるいはint
型など、unsigned long
とは異なる型であった場合、%lud
フォーマット指定子との不一致が生じます。
C言語の可変引数関数呼び出し規約では、整数型は通常int
またはlong
にプロモーションされます。しかし、long long
型はlong
型よりも大きい場合があり、%lud
が期待するunsigned long
型とサイズや符号が異なる場合に、メモリ上の解釈がずれて誤った値が出力される可能性があります。
修正は、d1
とd2
を明示的にulong
(unsigned long
のtypedef、または単にunsigned long
)にキャストすることで、この型不一致の問題を解消しています。
修正後のコード:
snprint(str, sizeof(str), "$%lud-%lud", (ulong)d1, (ulong)d2);
これにより、snprintf
はunsigned long
型の引数を受け取ることが保証され、フォーマット指定子との整合性が保たれ、正しい数値が文字列として出力されるようになります。これは、C言語における安全なプログラミング慣習の一つであり、可変引数関数を使用する際には特に注意が必要です。
コアとなるコードの変更箇所
--- a/src/cmd/6g/list.c
+++ b/src/cmd/6g/list.c
@@ -124,7 +124,7 @@ Dconv(Fmt *fp)
if(fp->flags & FmtLong) {
d1 = a->offset & 0xffffffffLL;
d2 = (a->offset>>32) & 0xffffffffLL;
- snprint(str, sizeof(str), "$%lud-%lud", d1, d2);
+ snprint(str, sizeof(str), "$%lud-%lud", (ulong)d1, (ulong)d2);
break;
}
snprint(str, sizeof(str), "$%lld", a->offset);
コアとなるコードの解説
変更はsrc/cmd/6g/list.c
ファイルのDconv
関数内にあります。
Dconv(Fmt *fp)
: この関数は、おそらくGoコンパイラの内部的なフォーマット処理ルーチンの一部であり、Fmt
構造体ポインタfp
を通じてフォーマットフラグや状態を受け取ります。if(fp->flags & FmtLong)
: これは、特定のフォーマットフラグFmtLong
が設定されている場合に、64ビットのオフセット値を処理するブロックであることを示しています。d1 = a->offset & 0xffffffffLL;
:a->offset
(おそらくlong long
型)の下位32ビットをd1
に抽出しています。0xffffffffLL
は64ビットのマスクです。d2 = (a->offset>>32) & 0xffffffffLL;
:a->offset
を32ビット右シフトし、上位32ビットをd2
に抽出しています。snprint(str, sizeof(str), "$%lud-%lud", d1, d2);
(変更前):str
: 結果の文字列を格納するバッファ。sizeof(str)
: バッファのサイズ。"$%lud-%lud"
: フォーマット文字列。2つのunsigned long
型の10進数を期待しています。d1
,d2
: ここで、d1
とd2
の型がunsigned long
と一致しない場合に問題が発生していました。
snprint(str, sizeof(str), "$%lud-%lud", (ulong)d1, (ulong)d2);
(変更後):(ulong)d1
,(ulong)d2
:d1
とd2
の値を明示的にulong
型(unsigned long
のエイリアスまたは直接unsigned long
)にキャストしています。これにより、snprintf
に渡される引数の型がフォーマット指定子%lud
と完全に一致することが保証され、未定義動作が回避されます。
この修正は、C言語における型安全性の重要性、特に可変引数関数を使用する際の引数の型とフォーマット指定子の一致の必要性を示しています。
関連リンク
- Go言語の歴史と初期のコンパイラツールチェーンに関する情報:
- The Go Programming Language (Official Website): https://go.dev/
- Go Blog - Go's first decade: https://go.dev/blog/go10years
- C言語の
snprintf
関数とフォーマット指定子に関するドキュメント:- cppreference.com -
snprintf
: https://en.cppreference.com/w/c/io/fprintf (fprintfファミリーの一部としてsnprintfも解説されています)
- cppreference.com -
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/122ed3e9888e70218235a0f6c7a2b40f12d2bd0a
- Web検索結果(Goコンパイラの歴史、
6g
の役割、C言語のsnprintf
と型変換に関する一般的な情報)