[インデックス 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 int
ulong
:unsigned long
uchar
:unsigned char
ushort
: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の補数表現に関する情報