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

[インデックス 10720] ファイルの概要

src/cmd/gc/lex.c は、Goコンパイラのフロントエンドの一部であり、字句解析(lexical analysis)を担当するファイルです。字句解析は、ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換するプロセスです。このファイルは、コンパイラがGoのソースコードを理解し、後続のフェーズ(構文解析、型チェック、コード生成など)で処理できるようにするための初期ステップを実行します。

コミット

  • Author: Lucio De Re lucio.dere@gmail.com
  • Date: Mon Dec 12 16:25:31 2011 -0500
  • Commit Message: gc: avoid 0-length C array

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0f4f3c6769bddac4cf417849922c0f68f4bedde2

元コミット内容

gc: avoid 0-length C array

R=golang-dev, ality
CC=golang-dev, rsc
https://golang.org/cl/5467066

変更の背景

このコミットの背景には、C言語における「ゼロ長配列(zero-length array)」の使用に関する潜在的な問題と、GoコンパイラがC言語で記述されているという事実があります。

C言語の標準では、配列のサイズは正の整数でなければなりません。しかし、一部のコンパイラ(特にGCC)は、構造体の最後のメンバーとしてゼロ長配列を非標準の拡張としてサポートしています。これは、可変長データを扱うための一般的なイディオムとして使用されることがあります(例: struct { int len; char data[0]; })。

しかし、この非標準の拡張は移植性の問題を引き起こす可能性があり、また、コンパイラによっては警告やエラーを生成する場合があります。Goコンパイラは、様々なプラットフォームやコンパイラでビルドされる必要があるため、このような非標準のC言語の機能に依存することは避けるべきです。

このコミットは、exper という配列が、要素が一つもない場合にゼロ長配列として扱われる可能性があったため、その状況を回避するために行われました。具体的には、exper 配列の要素数を計算するために使用されていた nelem マクロ(おそらく sizeof(array) / sizeof(array[0]) のようなもの)が、配列が空の場合にゼロ除算を引き起こすか、あるいは未定義の動作を引き起こす可能性がありました。

この変更は、より堅牢で移植性の高いコードにするための保守的な修正であり、Goコンパイラのビルドプロセスにおける潜在的な問題を未然に防ぐことを目的としています。

前提知識の解説

ゼロ長配列 (Zero-Length Array)

C言語において、配列のサイズを0と宣言することです(例: char data[0];)。これは標準Cでは許可されていませんが、GCCなどの一部のコンパイラが拡張としてサポートしています。主に、構造体の末尾に可変長データを格納するためのフレキシブル配列メンバー(Flexible Array Member, C99以降の標準機能)の代替として使用されていました。しかし、標準外の機能であるため、移植性やコンパイラ間の互換性に問題が生じることがあります。

nelem マクロ

C言語のコードベースでよく見られるイディオムで、配列の要素数を計算するためのマクロです。一般的には以下のように定義されます。

#define nelem(x) (sizeof(x)/sizeof((x)[0]))

このマクロは、配列 x の全体のサイズを、その配列の最初の要素のサイズで割ることで、要素数を求めます。しかし、もし配列 x が要素を一つも持たない(つまり、sizeof((x)[0]) がゼロになるような状況、または配列自体が空であるとコンパイラが判断するような状況)場合、ゼロ除算や未定義の動作を引き起こす可能性があります。

Goコンパイラの構造 (gc)

Goコンパイラ(gc)は、Go言語で書かれたプログラムを機械語に変換するツールチェーンの中核です。初期のGoコンパイラはC言語で書かれており、その後Go言語自体で書き直されました。このコミットが行われた2011年時点では、まだC言語で書かれた部分が多く残っていました。コンパイラの一般的なフェーズは以下の通りです。

  1. 字句解析 (Lexical Analysis): ソースコードをトークンに分割します。
  2. 構文解析 (Parsing): トークン列から抽象構文木(AST)を構築します。
  3. 型チェック (Type Checking): ASTの各ノードの型を検証し、型エラーを検出します。
  4. 中間表現 (IR) 生成: ASTをコンパイラ内部の中間表現に変換します。
  5. 最適化 (Optimization): 中間表現を最適化します。
  6. コード生成 (Code Generation): 最適化された中間表現からターゲットアーキテクチャの機械語を生成します。

このコミットは、字句解析に関連する lex.c ファイルの変更であり、コンパイラの初期段階の堅牢性を高めるものです。

技術的詳細

このコミットは、src/cmd/gc/lex.c ファイル内の exper という静的構造体配列の扱いを変更しています。

元のコードでは、exper 配列の要素数を nelem(exper) マクロを使って計算し、ループの終了条件としていました。

static struct {
	char *name;
	int *val;
} exper[] = {
//	{"rune32", &rune32},
};

// ...

for(i=0; i<nelem(exper); i++) {
	// ...
}

もし exper 配列がコメントアウトされた行のように、初期化子リストに要素を一つも持たない場合、C言語の標準では配列のサイズは0になります。この場合、nelem(exper)sizeof(exper) / sizeof(exper[0]) と展開されますが、sizeof(exper[0]) は有効な型サイズを返すものの、sizeof(exper) が0になる可能性があり、結果として nelem が0を返すか、あるいはコンパイラによってはゼロ除算の警告やエラーを引き起こす可能性がありました。

この変更では、exper 配列の初期化子リストに {nil, nil} というダミーのエントリを追加しています。

static struct {
	int *val;
} exper[] = {
//	{"rune32", &rune32},
	{nil, nil}, // <-- 追加された行
};

これにより、exper 配列は常に少なくとも1つの要素を持つことが保証されます。そして、ループの終了条件を nelem(exper) から exper[i].name != nil に変更しています。

// 変更前
for(i=0; i<nelem(exper); i++) {

// 変更後
for(i=0; exper[i].name != nil; i++) {

この変更により、以下の利点が得られます。

  1. ゼロ長配列の回避: exper 配列が物理的にゼロ長になることを防ぎます。これにより、一部のコンパイラが生成する可能性のある警告やエラーを回避し、コードの移植性を向上させます。
  2. 堅牢なループ条件: ループの終了条件を、配列の末尾を示す nil エントリの検出に変更することで、nelem マクロの潜在的な問題を回避します。これは、C言語で固定サイズの配列ではなく、Sentinel値(番兵)で終了を示す配列を扱う際の一般的なパターンです。
  3. 明確な意図: nil エントリを追加することで、配列が動的に拡張される可能性のあるリストとして扱われるという意図がコード上でより明確になります。

この修正は、Goコンパイラのビルドシステムが様々なCコンパイラ環境で安定して動作することを保証するための、細部への配慮を示しています。

コアとなるコードの変更箇所

diff --git a/src/cmd/gc/lex.c b/src/cmd/gc/lex.c
index af6c207c79..8c544f6b92 100644
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -38,6 +38,7 @@ static struct {
  	int *val;
  } exper[] = {
  //	{"rune32", &rune32},\n
+\t{nil, nil},\n
  };
  
  static void
@@ -45,7 +46,7 @@ addexp(char *s)\n
  {\n
  	int i;\n
  	\n
-\tfor(i=0; i<nelem(exper); i++) {\n
+\tfor(i=0; exper[i].name != nil; i++) {\n
  \t\tif(strcmp(exper[i].name, s) == 0) {\n
  \t\t\t*exper[i].val = 1;\n
  \t\t\treturn;\n
@@ -75,7 +76,7 @@ expstring(void)\n
  	static char buf[512];\n
  \n
  	strcpy(buf, "X");\n
-\tfor(i=0; i<nelem(exper); i++)\n
+\tfor(i=0; exper[i].name != nil; i++)\n
  \t\tif(*exper[i].val)\n
  \t\t\tseprint(buf+strlen(buf), buf+sizeof buf, ",%s", exper[i].name);\n
  \tif(strlen(buf) == 1)\n

コアとなるコードの解説

  1. exper 配列の初期化子リストへの {nil, nil} の追加:

    --- a/src/cmd/gc/lex.c
    +++ b/src/cmd/gc/lex.c
    @@ -38,6 +38,7 @@ static struct {
      	int *val;
      } exper[] = {
      //	{"rune32", &rune32},\n
    +\t{nil, nil},\n
      };
    

    experchar *name;int *val; の2つのメンバーを持つ構造体の配列です。この変更により、配列の初期化子リストに {nil, nil} というエントリが追加されました。これは、配列が空である場合にゼロ長配列として扱われることを防ぎ、常に少なくとも1つの要素を持つことを保証します。このダミーエントリは、後続のループで配列の終端を示すマーカーとして機能します。

  2. addexp 関数内のループ条件の変更:

    --- a/src/cmd/gc/lex.c
    +++ b/src/cmd/gc/lex.c
    @@ -45,7 +46,7 @@ addexp(char *s)\n
      {\n
      	int i;\n
      	\n
    -\tfor(i=0; i<nelem(exper); i++) {\n
    +\tfor(i=0; exper[i].name != nil; i++) {\n
      \t\tif(strcmp(exper[i].name, s) == 0) {\n
      \t\t\t*exper[i].val = 1;\n
      \t\t\treturn;\n
    

    addexp 関数内の for ループの条件が i<nelem(exper) から exper[i].name != nil に変更されました。これにより、ループは exper 配列の要素を順に処理し、name メンバーが nil になるまで続行します。これは、配列の終端を明示的なマーカー(この場合は {nil, nil} エントリ)で示す一般的なC言語のイディオムです。これにより、nelem マクロがゼロ長配列に対して引き起こす可能性のある問題を回避します。

  3. expstring 関数内のループ条件の変更:

    --- a/src/cmd/gc/lex.c
    +++ b/src/cmd/gc/lex.c
    @@ -75,7 +76,7 @@ expstring(void)\n
      	static char buf[512];\n
      \n
      	strcpy(buf, "X");\n
    -\tfor(i=0; i<nelem(exper); i++)\n
    +\tfor(i=0; exper[i].name != nil; i++)\n
      \t\tif(*exper[i].val)\n
      \t\t\tseprint(buf+strlen(buf), buf+sizeof buf, ",%s", exper[i].name);\n
      \tif(strlen(buf) == 1)\n
    

    expstring 関数内の for ループも同様に、条件が i<nelem(exper) から exper[i].name != nil に変更されました。これも addexp 関数と同様の理由で、ゼロ長配列の潜在的な問題を回避し、より堅牢なループ処理を実現します。

これらの変更は、GoコンパイラのC言語コードベースにおけるゼロ長配列の使用を回避し、より堅牢で移植性の高いコードにするためのものです。

関連リンク

参考にした情報源リンク