Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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型の引数に対応します。lulong 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);

ここで、d1d2はそれぞれa->offsetから抽出された32ビット値ですが、これらがどのような型で宣言されていたか、そしてsnprintfに渡される際にどのようにプロモーションされるかが問題でした。もしd1d2long long型、あるいはint型など、unsigned longとは異なる型であった場合、%ludフォーマット指定子との不一致が生じます。

C言語の可変引数関数呼び出し規約では、整数型は通常intまたはlongにプロモーションされます。しかし、long long型はlong型よりも大きい場合があり、%ludが期待するunsigned long型とサイズや符号が異なる場合に、メモリ上の解釈がずれて誤った値が出力される可能性があります。

修正は、d1d2を明示的にulongunsigned longのtypedef、または単にunsigned long)にキャストすることで、この型不一致の問題を解消しています。

修正後のコード:

snprint(str, sizeof(str), "$%lud-%lud", (ulong)d1, (ulong)d2);

これにより、snprintfunsigned 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: ここで、d1d2の型がunsigned longと一致しない場合に問題が発生していました。
  • snprint(str, sizeof(str), "$%lud-%lud", (ulong)d1, (ulong)d2); (変更後):
    • (ulong)d1, (ulong)d2: d1d2の値を明示的にulong型(unsigned longのエイリアスまたは直接unsigned long)にキャストしています。これにより、snprintfに渡される引数の型がフォーマット指定子%ludと完全に一致することが保証され、未定義動作が回避されます。

この修正は、C言語における型安全性の重要性、特に可変引数関数を使用する際の引数の型とフォーマット指定子の一致の必要性を示しています。

関連リンク

  • Go言語の歴史と初期のコンパイラツールチェーンに関する情報:
  • C言語のsnprintf関数とフォーマット指定子に関するドキュメント:

参考にした情報源リンク