[インデックス 16681] ファイルの概要
このコミットは、Go言語のランタイムの一部として使用されていたlib9ライブラリのfmtパッケージにおける、符号付き整数(signed integers)の出力に関するバグ修正です。具体的には、以前の変更によって失われた符号付き整数の正しい表示(符号拡張)を復元することを目的としています。
コミット
commit 0a4fc122de2d0c5da7e89feb7c079e602612a2fd
Author: Russ Cox <rsc@golang.org>
Date: Sun Jun 30 19:53:36 2013 -0400
lib9: restore printing of signed integers
A casualty of https://golang.org/cl/10195044.
If x is an 32-bit int and u is a 64-bit ulong,
u = (uint)x // converts to uint before extension, so zero fills
u = (ulong)x // sign-extends
TBR=iant, r
CC=golang-dev
https://golang.org/cl/10814043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a4fc122de2d0c5da7e89feb7c079e602612a2fd
元コミット内容
このコミットの元々の内容は、lib9ライブラリのsrc/lib9/fmt/dofmt.cファイルにおいて、符号付き整数をフォーマットする際の型変換を修正し、符号拡張(sign-extension)が正しく行われるようにすることです。以前の変更(https://golang.org/cl/10195044)によって、符号付き整数が符号なし型にキャストされる際にゼロフィル(zero-fill)されてしまい、負の値が正しく表示されない問題が発生していました。このコミットは、その問題を修正し、符号付き整数の表示を復元します。
変更の背景
この変更は、https://golang.org/cl/10195044という以前の変更によって引き起こされた回帰(regression)を修正するために行われました。この以前の変更は、おそらく特定の最適化やコードのクリーンアップを目的としていたと考えられますが、その副作用として、lib9のフォーマットルーチンが符号付き整数を正しく扱えなくなってしまいました。
具体的には、C言語における型変換の挙動が問題でした。符号付き整数をより大きな符号なし整数型にキャストする際、コンパイラによってはゼロフィル(上位ビットを0で埋める)を行うことがあります。しかし、負の数を正しく表現するためには、符号拡張(最上位ビットの値を上位ビットにコピーする)が必要です。例えば、8ビットのchar型で-1が0xFFと表現される場合、これを16ビットのunsigned shortにキャストすると、ゼロフィルでは0x00FFとなりますが、符号拡張では0xFFFFとなります。後者が-1の正しい表現です。
この問題により、Goランタイムが内部的に使用するlib9のフォーマット機能において、負の整数が誤った値として表示される可能性がありました。このコミットは、この重要な表示の正確性を復元するために導入されました。
前提知識の解説
-
lib9:lib9は、Plan 9オペレーティングシステムに由来するC言語のライブラリ群です。Go言語の初期のランタイムやツールチェーンの一部では、このlib9のコードが移植されて使用されていました。特に、低レベルのシステムコールや文字列操作、フォーマットなどのユーティリティ機能が含まれています。src/lib9/fmt/dofmt.cは、C言語のprintfに似たフォーマット機能を提供するfmtパッケージの一部です。 -
fmtパッケージ (lib9):lib9のfmtパッケージは、C言語におけるprintfのような書式付き出力機能を提供します。dofmt.cファイルは、このフォーマット処理の核心部分であり、様々なデータ型を文字列に変換して出力するためのロジックを含んでいます。 -
va_arg: C言語の標準ライブラリで提供されるマクロで、可変引数リスト(stdarg.hで定義されるva_list、va_start、va_endと共に使用)から引数を取得するために使われます。dofmt.cのようなフォーマット関数では、printfのように引数の型と数を実行時に決定する必要があるため、va_argが頻繁に利用されます。 -
符号付き整数と符号なし整数:
- 符号付き整数 (signed integer): 正の値、負の値、ゼロを表現できる整数型です。通常、最上位ビット(MSB)が符号ビットとして使用され、0なら正、1なら負を示します(2の補数表現が一般的)。
- 符号なし整数 (unsigned integer): 負の値を表現できず、ゼロと正の値のみを表現できる整数型です。すべてのビットが数値の大きさを表すために使用されます。
-
型変換 (Type Casting): あるデータ型の値を別のデータ型に変換することです。C言語では、明示的なキャスト(例:
(int)x)と暗黙的なキャストがあります。型変換の際には、元の値が新しい型でどのように表現されるかが重要になります。 -
符号拡張 (Sign Extension): 符号付き整数を、よりビット幅の広い型に変換する際に、元の値の符号を維持するために行われる操作です。元の値の最上位ビット(符号ビット)を、新しい型の追加された上位ビットにコピーします。例えば、8ビットの
char型で-1(2の補数で11111111)を16ビットのshort型に変換する場合、符号拡張を行うと1111111111111111(-1)となります。 -
ゼロフィル (Zero Filling): 符号なし整数、または符号付き整数を符号なし整数に変換する際に、よりビット幅の広い型に変換する際に行われる操作です。新しい型の追加された上位ビットをすべて
0で埋めます。例えば、8ビットのchar型で-1(11111111)を16ビットのunsigned short型に変換する場合、ゼロフィルを行うと0000000011111111(255)となります。 -
uint,ulong,uchar,ushort: これらはC言語における符号なし整数型を表す型エイリアスまたはマクロである可能性が高いです。uint:unsigned intulong:unsigned longuchar:unsigned charushort:unsigned shortこれらは通常、特定のビット幅を持つ符号なし整数型として定義されます。
技術的詳細
このコミットの技術的な核心は、C言語における整数型変換のセマンティクス、特に符号付き整数から符号なし整数への変換と、それによる符号拡張とゼロフィルの違いにあります。
dofmt.cのコードは、va_argを使用して可変引数リストから整数値を取得し、それをuというulong型の変数に格納しています。このu変数は、最終的に様々な基数(10進数、16進数など)で数値を文字列に変換するために使用されます。
問題は、以前の変更で、符号付き整数を扱う際に、中間的にuchar、ushort、uintといった符号なしのより小さい、または同じサイズの型にキャストしていた点です。
例えば、元のコードでは以下のようになっていました。
// 8ビットの場合
u = (uchar)(char)va_arg(f->args, int);
// 16ビットの場合
u = (ushort)(short)va_arg(f->args, int);
// 32ビットの場合
u = (uint)va_arg(f->args, int);
ここで、va_arg(f->args, int)は、スタックからint型の値を取得します。intは通常32ビットの符号付き整数です。
-
u = (uchar)(char)va_arg(f->args, int);の問題:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(char)にキャストすると、下位8ビットが保持され、0xFF(-1)となる。(uchar)にキャストすると、0xFFは符号なしの255となる。- この
255が最終的にulongのuに代入されると、0x00000000000000FFとなり、元の-1という情報が失われる。
-
u = (ushort)(short)va_arg(f->args, int);の問題:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(short)にキャストすると、下位16ビットが保持され、0xFFFF(-1)となる。(ushort)にキャストすると、0xFFFFは符号なしの65535となる。- この
65535が最終的にulongのuに代入されると、0x000000000000FFFFとなり、元の-1という情報が失われる。
-
u = (uint)va_arg(f->args, int);の問題:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(uint)にキャストすると、0xFFFFFFFFは符号なしの4294967295となる。- この
4294967295が最終的にulongのuに代入されると、0x00000000FFFFFFFFとなり、元の-1という情報が失われる。
このコミットでは、これらのキャストをulongに直接行うように変更しています。
// 8ビットの場合
u = (ulong)(char)va_arg(f->args, int);
// 16ビットの場合
u = (ulong)(short)va_arg(f->args, int);
// 32ビットの場合
u = (ulong)va_arg(f->args, int);
この変更により、int、char、shortといった符号付き型から直接ulong(符号なしのより大きな型)にキャストされるため、C言語の標準的な挙動として符号拡張が行われます。
-
u = (ulong)(char)va_arg(f->args, int);の修正後:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(char)にキャストすると、下位8ビットが保持され、0xFF(-1)となる。(ulong)にキャストすると、charの-1がulongに符号拡張され、0xFFFFFFFFFFFFFFFF(-1)となる。
-
u = (ulong)(short)va_arg(f->args, int);の修正後:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(short)にキャストすると、下位16ビットが保持され、0xFFFF(-1)となる。(ulong)にキャストすると、shortの-1がulongに符号拡張され、0xFFFFFFFFFFFFFFFF(-1)となる。
-
u = (ulong)va_arg(f->args, int);の修正後:va_arg(f->args, int)でint(例えば-1、0xFFFFFFFF)を取得。(ulong)にキャストすると、intの-1がulongに符号拡張され、0xFFFFFFFFFFFFFFFF(-1)となる。
このように、ulongへの直接キャストにより、元の符号付きの値が正しく保持され、フォーマットルーチンが負の数を正しく処理できるようになります。
コアとなるコードの変更箇所
変更はsrc/lib9/fmt/dofmt.cファイルに集中しています。
--- a/src/lib9/fmt/dofmt.c
+++ b/src/lib9/fmt/dofmt.c
@@ -387,17 +387,17 @@ __ifmt(Fmt *f)
if(fl & FmtByte){
if(fl & FmtUnsigned)
u = (uchar)va_arg(f->args, int);
else
- u = (uchar)(char)va_arg(f->args, int);
+ u = (ulong)(char)va_arg(f->args, int);
}else if(fl & FmtShort){
if(fl & FmtUnsigned)
u = (ushort)va_arg(f->args, int);
else
- u = (ushort)(short)va_arg(f->args, int);
+ u = (ulong)(short)va_arg(f->args, int);
}else{
if(fl & FmtUnsigned)
u = va_arg(f->args, uint);
else
- u = (uint)va_arg(f->args, int);
+ u = (ulong)va_arg(f->args, int);
}
conv = "0123456789abcdef";
grouping = "\4"; /* for hex, octal etc. (undefined by spec but nice) */
コアとなるコードの解説
このコードスニペットは、__ifmt関数の一部であり、これはlib9のフォーマットルーチンにおいて整数値を処理する部分です。Fmt *fはフォーマットの状態を保持する構造体へのポインタで、f->argsは可変引数リストです。flはフォーマットフラグ(例: FmtUnsignedは符号なしとして扱うことを示す)を含んでいます。
変更された3つのブロックは、それぞれ異なるサイズの整数(8ビット、16ビット、32ビット)を符号付きとして扱う場合の型変換を修正しています。
-
if(fl & FmtByte)ブロック:- これは、引数が8ビットのバイトとして扱われるべき場合です。
elseブロック(符号付きの場合)で、元のコードはu = (uchar)(char)va_arg(f->args, int);となっていました。- 修正後は
u = (ulong)(char)va_arg(f->args, int);となっています。 intからcharへのキャストで8ビットに切り詰められ、そのchar(符号付き)が直接ulong(符号なし、64ビット)にキャストされることで、符号拡張が保証されます。
-
else if(fl & FmtShort)ブロック:- これは、引数が16ビットのショートとして扱われるべき場合です。
elseブロック(符号付きの場合)で、元のコードはu = (ushort)(short)va_arg(f->args, int);となっていました。- 修正後は
u = (ulong)(short)va_arg(f->args, int);となっています。 intからshortへのキャストで16ビットに切り詰められ、そのshort(符号付き)が直接ulongにキャストされることで、符号拡張が保証されます。
-
elseブロック:- これは、引数がデフォルトの
intサイズ(通常32ビット)として扱われるべき場合です。 elseブロック(符号付きの場合)で、元のコードはu = (uint)va_arg(f->args, int);となっていました。- 修正後は
u = (ulong)va_arg(f->args, int);となっています。 int(符号付き)が直接ulongにキャストされることで、符号拡張が保証されます。
- これは、引数がデフォルトの
これらの変更により、va_argから取得した符号付き整数値が、最終的なulong型の変数uに代入される際に、常に正しい符号拡張が行われるようになり、負の値が正しくフォーマットされるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/0a4fc122de2d0c5da7e89feb7c079e602612a2fd
- Go CL 10814043 (このコミットに対応するGoのコードレビューシステムのエントリ): https://golang.org/cl/10814043
- Go CL 10195044 (この問題を引き起こした以前の変更): https://golang.org/cl/10195044
参考にした情報源リンク
- C言語の型変換と符号拡張/ゼロフィルに関する一般的な情報源(例: C標準のセクション、Cプログラミングの教科書、オンラインのC言語リファレンス)
- Go言語の初期のランタイムにおける
lib9の使用に関する情報(Goの歴史や設計に関するドキュメント、初期のGoソースコードの分析など) stdarg.hとva_argに関するC言語のドキュメント- 2の補数表現に関する情報