[インデックス 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の変更リンク)
参考にした情報源リンク
printfformat string - Wikipedia: https://en.wikipedia.org/wiki/Printf_format_string- Plan 9 from Bell Labs: https://9p.io/plan9/
- Go
fmtpackage documentation: https://pkg.go.dev/fmt - (内部情報源) Go issue trackerやメーリングリストでの議論(具体的なリンクはコミットメッセージにはないが、
ANSI C gave bad advice long agoという記述から、この問題が過去に議論されたことを示唆している) - (一般的なC言語の
printfの挙動に関する知識) - (Goコンパイラの内部構造に関する一般的な知識)