[インデックス 14273] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、snprint
関数で%#x
フォーマット指定子を使用して数値0を16進数で出力する際の互換性の問題を修正するものです。具体的には、%#x
が0に対して0x
プレフィックスを付けるかどうかの挙動が、Plan 9のlib9/fmt
とGoのlib9/fmt
(およびANSI Cの過去の推奨事項)で異なっていたため、異なるシステムでコンパイルされたバイナリ間で互換性の問題が生じるのを避けるために、明示的に0x%x
というフォーマットに変更しています。
コミット
commit e4cef96be6f03aa3e7e4979f1d55f8e66289904b
Author: Russ Cox <rsc@golang.org>
Date: Thu Nov 1 12:55:21 2012 -0400
cmd/gc: avoid %#x of 0
Plan 9 and Go's lib9/fmt disagree on whether %#x includes the 0x prefix
when printing 0, because ANSI C gave bad advice long ago.
Avoiding that case makes binaries compiled on different systems compatible.
R=ken2
CC=akumar, golang-dev
https://golang.org/cl/6814066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e4cef96be6f03aa3e7e4979f1d55f8e66289904b
元コミット内容
cmd/gc: avoid %#x of 0
Plan 9 and Go's lib9/fmt disagree on whether %#x includes the 0x prefix
when printing 0, because ANSI C gave bad advice long ago.
Avoiding that case makes binaries compiled on different systems compatible.
変更の背景
この変更の背景には、printf
系の関数における16進数フォーマット指定子%#x
の歴史的な挙動の不一致があります。特に、値が0
の場合に0x
プレフィックスが付与されるかどうかが問題でした。
- ANSI Cの「悪いアドバイス」: 過去のANSI C標準では、
%#x
が0
に対して0x
プレフィックスを付けるべきではないと示唆されていました。これは、0
がそれ自体で16進数であることを明確に示しているため、冗長なプレフィックスは不要であるという考えに基づいています。 - Plan 9とGoの
lib9/fmt
の不一致: Plan 9オペレーティングシステムとその標準ライブラリであるlib9/fmt
は、このANSI Cの「アドバイス」に従い、%#x
で0
をフォーマットする際に0x
プレフィックスを付けませんでした(例:printf("%#x", 0)
の結果が0
)。しかし、Go言語の初期のlib9/fmt
の実装は、この挙動と異なる場合がありました。Goのfmt
パッケージは、Plan 9のfmt
ライブラリにインスパイアされていますが、すべての挙動が完全に一致するわけではありません。 - 互換性の問題: この挙動の不一致は、特にコンパイラのようなツールが生成する出力において問題となります。コンパイラが生成するバイナリや中間コード、デバッグ情報などに、このフォーマットされた文字列が含まれる場合、異なるシステム(例えば、Plan 9の環境とGoの標準的な環境)でコンパイルされたバイナリ間で、文字列の表現が異なってしまい、互換性が損なわれる可能性がありました。具体的には、
esc:
というプレフィックスを持つ文字列が、esc:0
となるかesc:0x0
となるかで、バイナリの比較や解析に影響が出ることが懸念されました。
このコミットは、このような環境間の微妙な差異によって引き起こされる互換性の問題を回避し、Goコンパイラが生成する出力の一貫性を保つことを目的としています。
前提知識の解説
printf
系関数のフォーマット指定子
C言語(およびGo言語のfmt
パッケージなど、Cのprintf
に影響を受けた多くの言語)では、数値を特定の形式で文字列に変換するためにフォーマット指定子を使用します。
%x
: 符号なし整数を小文字の16進数形式で出力します。プレフィックスは付きません。- 例:
printf("%x", 10)
->a
- 例:
printf("%x", 0)
->0
- 例:
%#x
: 符号なし整数を小文字の16進数形式で出力し、非ゼロの値には0x
プレフィックスを付けます。- 例:
printf("%#x", 10)
->0xa
- 問題の核心:
printf("%#x", 0)
の挙動は、C標準のバージョンや実装によって異なりました。- 一部の実装(ANSI Cの初期の解釈やPlan 9の
lib9/fmt
)では、0
に対しては0x
プレフィックスを付けず、0
と出力します。これは、0
がそれ自体で16進数表現として十分であるため、0x0
は冗長であるという考えに基づいています。 - 他の実装(より新しいC標準や一般的なUnix系システムの
printf
)では、0
に対しても0x
プレフィックスを付けて0x0
と出力します。これは、#
フラグが「代替形式」を意味し、常にプレフィックスを付けるべきであるという解釈に基づいています。
- 一部の実装(ANSI Cの初期の解釈やPlan 9の
- 例:
Plan 9とlib9/fmt
- Plan 9: ベル研究所で開発された分散オペレーティングシステムです。Unixの後継として設計され、多くの革新的なアイデアを導入しました。
lib9/fmt
: Plan 9の標準ライブラリの一部で、C言語のprintf
に似たフォーマット機能を提供します。Go言語のfmt
パッケージは、このlib9/fmt
の設計思想と一部の挙動を継承しています。
このコミットは、Goコンパイラが内部的に使用するsnprint
関数(文字列にフォーマットされた出力を書き込む関数)における、この%#x
と0
の挙動の不一致が、生成されるバイナリの互換性に影響を与えることを懸念して行われました。
技術的詳細
このコミットが修正している技術的な問題は、printf
系のフォーマット関数における%#x
指定子の挙動の曖昧さに起因するものです。特に、値が0
の場合に0x
プレフィックスが付与されるかどうかが、異なる環境やライブラリの実装間で一貫していなかった点が問題でした。
Goコンパイラ(cmd/gc
)のsrc/cmd/gc/esc.c
ファイルには、mktag
という関数が存在します。この関数は、エスケープ解析に関連するタグを生成するために、マスク値(mask
)を16進数文字列に変換して使用していました。元のコードでは、snprint(buf, sizeof buf, "esc:%#x", mask);
という形式で文字列を生成していました。
ここで、mask
が0
の場合、
- 一部の環境/ライブラリ(例: Plan 9の
lib9/fmt
の特定の挙動):esc:0
という文字列が生成される。 - 他の環境/ライブラリ(例: 多くの一般的なCライブラリの
printf
):esc:0x0
という文字列が生成される。
この差異は、コンパイラが生成する内部的な文字列表現に影響を与え、結果として異なる環境でコンパイルされたGoのバイナリが、この部分で異なる文字列を持つことになり、バイナリレベルでの互換性が損なわれる可能性がありました。例えば、デバッグ情報やシンボルテーブル、あるいは特定の内部的な識別子としてこの文字列が使用される場合、その表現が異なると問題が生じます。
この問題を解決するために、コミットでは%#x
の使用を避け、代わりに0x%x
という明示的なフォーマットを使用するように変更しました。
snprint(buf, sizeof buf, "esc:0x%x", mask);
この変更により、mask
が0
の場合でも、常にesc:0x0
という文字列が生成されるようになります。これにより、0x
プレフィックスの有無に関する曖昧さがなくなり、Goコンパイラが生成する出力がどのシステムでコンパイルされても一貫したものとなり、バイナリの互換性が保証されます。
これは、単なる表示上の問題ではなく、コンパイラが生成する成果物の一貫性と移植性を確保するための重要な修正です。特に、Goのようなクロスプラットフォームを重視する言語において、ビルド環境の差異が最終的なバイナリに影響を与えることは避けるべきであり、このような細かな部分まで配慮することで、堅牢なシステムが構築されます。
コアとなるコードの変更箇所
変更はsrc/cmd/gc/esc.c
ファイルの一箇所のみです。
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -233,7 +233,7 @@ mktag(int mask)
if(mask < nelem(tags) && tags[mask] != nil)
return tags[mask];
- snprint(buf, sizeof buf, "esc:%#x", mask);
+ snprint(buf, sizeof buf, "esc:0x%x", mask);
s = strlit(buf);
if(mask < nelem(tags))
tags[mask] = s;
コアとなるコードの解説
変更されたコードは、src/cmd/gc/esc.c
内のmktag
関数の一部です。
// esc.c
// ...
char buf[100]; // 仮のバッファ宣言、実際のコードでは適切に定義されているはず
// ...
mktag(int mask) {
// ...
if(mask < nelem(tags) && tags[mask] != nil)
return tags[mask];
// 変更前:
// snprint(buf, sizeof buf, "esc:%#x", mask);
// 変更後:
snprint(buf, sizeof buf, "esc:0x%x", mask);
s = strlit(buf);
if(mask < nelem(tags))
tags[mask] = s;
// ...
}
mktag(int mask)
: この関数は、Goコンパイラの内部でエスケープ解析(変数がヒープに割り当てられるべきか、スタックに割り当てられるべきかを決定するプロセス)に関連する「タグ」を生成するために使用されます。mask
引数は、タグの識別子として機能する整数値です。snprint(buf, sizeof buf, "esc:%#x", mask);
(変更前):snprint
は、指定されたバッファ(buf
)に、フォーマットされた文字列を安全に書き込む関数です。sizeof buf
はバッファの最大サイズを指定し、バッファオーバーフローを防ぎます。- フォーマット文字列
"esc:%#x"
は、"esc:"
というリテラル文字列の後に、mask
の値を16進数(%x
)で、かつ代替形式(#
)で出力することを指示しています。 - 前述の通り、
mask
が0
の場合に、この%#x
が0
と出力されるか0x0
と出力されるかが、環境によって異なっていました。
snprint(buf, sizeof buf, "esc:0x%x", mask);
(変更後):- この行が修正後のコードです。
- フォーマット文字列が
"esc:0x%x"
に変更されています。 - これにより、
0x
プレフィックスがフォーマット文字列に直接埋め込まれ、mask
の値は単純な16進数(%x
)として出力されます。 - この変更により、
mask
が0
の場合でも、snprint
は常に"esc:0x0"
という文字列を生成するようになります。例えば、mask
が10
(16進数でa
)であれば、"esc:0xa"
が生成されます。 - この修正は、
%#x
の曖昧な挙動を回避し、0
の場合の出力形式を明示的に固定することで、異なるビルド環境間でのバイナリ互換性を確保しています。
この変更は、Goコンパイラの内部的な文字列生成ロジックにおける、小さくも重要な修正であり、Go言語のクロスプラットフォーム性とビルドの一貫性を高めることに貢献しています。
関連リンク
- Gerrit Change-Id:
https://golang.org/cl/6814066
(GoプロジェクトのコードレビューシステムであるGerritの変更リンク)
参考にした情報源リンク
printf
format string - Wikipedia: https://en.wikipedia.org/wiki/Printf_format_string- Plan 9 from Bell Labs: https://9p.io/plan9/
- Go
fmt
package documentation: https://pkg.go.dev/fmt - (内部情報源) Go issue trackerやメーリングリストでの議論(具体的なリンクはコミットメッセージにはないが、
ANSI C gave bad advice long ago
という記述から、この問題が過去に議論されたことを示唆している) - (一般的なC言語の
printf
の挙動に関する知識) - (Goコンパイラの内部構造に関する一般的な知識)