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

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

このコミットは、Goコンパイラのバックエンドであるcmd/6c(ARM 64-bit)とcmd/8c(x86 64-bit)において、スタック上に割り当てられたBiobuf(バイナリI/Oバッファ)がプログラム終了時に適切に解放されず、メモリリークとして検出される問題を修正します。具体的には、Bflush関数の代わりにBterm関数を呼び出すことで、バッファが正しく終了処理されるように変更されています。これにより、Valgrindのようなメモリデバッグツールが誤ってメモリリークを報告するのを防ぎます。

コミット

commit f701e1c32043116448e7227ee598b21e0ce42c7
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Mar 20 23:42:00 2013 +1100

    cmd/6c, cmd/8c: fix stack allocated Biobuf leaking at exit
    
    Fixes #5085.
    
    {6,8}c/swt.c allocates a third Biobuf in automatic memory which is not terminated at the end of the function. This causes the buffer to be 'in use' when the batexit handler fires, confusing valgrind.
    
    Huge thanks to DMorsing for the diagnosis.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/7844044

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

https://github.com/golang/go/commit/f701e1c32043116448e7227ee598b21e00ce42c7

元コミット内容

このコミットは、Go言語のコンパイラツールチェーンの一部であるcmd/6c(ARMアーキテクチャ向けコンパイラ)とcmd/8c(x86アーキテクチャ向けコンパイラ)のswt.cファイルにおける問題を修正するものです。問題は、関数内でスタック上に自動的に割り当てられたBiobuf(バイナリI/Oバッファ)が、関数の終了時に適切に終了処理(terminate)されないことにありました。この未終了のBiobufが、プログラム終了時に実行されるbatexitハンドラによって「使用中」と認識され、Valgrindのようなメモリデバッグツールがこれをメモリリークとして誤って報告していました。このコミットは、Bflushの代わりにBtermを呼び出すことで、この誤検出を解消します。

変更の背景

Go言語のコンパイラは、その内部で様々なI/O操作を行います。特に、コード生成や中間表現の処理において、効率的なバッファリングが重要となります。このコミットが修正する問題は、コンパイラのバックエンド(cmd/6ccmd/8c)が、特定の処理(おそらくスイッチ文のコード生成に関連するswt.cファイル内)で一時的なバイナリI/Oバッファ(Biobuf)をスタック上に確保していたことに起因します。

通常、スタック上に確保されたメモリは、関数が終了すると自動的に解放されます。しかし、BiobufのようなI/Oバッファは、単にメモリが解放されるだけでなく、内部状態のクリーンアップや関連するリソース(ファイルディスクリプタなど)の解放といった「終了処理」が必要となる場合があります。このコミット以前は、Bflush(バッファの内容をフラッシュするが、バッファ自体は終了しない)が呼び出されており、バッファの内部状態が「使用中」のまま残っていました。

この「使用中」の状態が、プログラムが終了する際に実行されるbatexitハンドラ(Goランタイムが提供する、プログラム終了時に登録されたコールバック関数を実行するメカニズム)によって検出されました。batexitハンドラは、プログラムのクリーンアップ処理を担当しますが、この際に未終了のBiobufが存在すると、それがメモリリークやリソースリークとして認識されてしまいます。

特に、Valgrindのような高度なメモリデバッグツールは、このような未解放のリソースや不適切なメモリ状態を厳密にチェックするため、このBiobufの未終了状態を「リーク」として報告していました。これは実際のメモリリークではないものの、デバッグ作業においてノイズとなり、開発者が真のメモリ問題を特定するのを妨げる可能性がありました。

この問題は、DMorsing氏によって診断され、その情報に基づいてDave Cheney氏が修正を行いました。修正の目的は、Valgrindの誤検出を解消し、コンパイラのメモリ管理の健全性を向上させることでした。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が必要です。

  1. Go言語のコンパイラツールチェーン (cmd/6c, cmd/8c):

    • Go言語のコンパイラは、ソースコードを機械語に変換するプロセスを複数のステージに分けて行います。
    • cmd/6cは、GoのソースコードをARM 64-bitアーキテクチャ(GOARCH=arm64)向けのオブジェクトファイルにコンパイルするバックエンドコンパイラです。
    • cmd/8cは、Goのソースコードをx86 64-bitアーキテクチャ(GOARCH=amd64)向けのオブジェクトファイルにコンパイルするバックエンドコンパイラです。
    • これらのコンパイラは、Go言語自体ではなく、C言語で書かれており、Goの初期のブートストラッププロセスや、Goランタイムの低レベルな部分のコンパイルに使用されていました。現在では、Go言語で書かれたコンパイラ(cmd/compile)が主流ですが、当時の文脈ではこれらのC言語製コンパイラが重要な役割を担っていました。
    • swt.cファイルは、これらのコンパイラ内でスイッチ文(switchステートメント)のコード生成に関連する処理を担っていたと考えられます。
  2. Biobuf:

    • Biobufは、Goコンパイラツールチェーンの内部で使われる、バイナリI/Oのためのバッファ構造体です。C言語で実装されており、ファイルからの読み書きや、メモリ上でのバッファリングを効率的に行うためのものです。
    • 一般的なI/Oバッファと同様に、データを一時的に保持し、まとめて読み書きすることでシステムコール回数を減らし、パフォーマンスを向上させます。
    • Biobufは、内部にバッファポインタ、バッファサイズ、現在の読み書き位置などの状態を保持しています。
  3. BflushBterm:

    • Bflush (Biobufflush操作): バッファに書き込まれたデータを、関連付けられた出力先(ファイルなど)に強制的に書き出す(フラッシュする)関数です。バッファの内容はクリアされますが、Biobuf構造体自体は引き続き使用可能な状態を保ちます。
    • Bterm (Biobufterminate操作): Biobufの終了処理を行う関数です。バッファの内容をフラッシュするだけでなく、Biobufに関連付けられたリソース(例えば、オープンされたファイルディスクリプタ)を解放し、Biobuf構造体を「使用不可」または「終了済み」の状態にします。これは、Biobufがもう必要ない場合に呼び出されるべきです。
  4. batexitハンドラ:

    • batexitは、Goランタイム(またはC言語の標準ライブラリ)が提供する、プログラムが終了する際に実行されるコールバック関数を登録するためのメカニズムです。
    • プログラムの正常終了時(main関数からのリターンやos.Exitの呼び出しなど)に、登録されたハンドラが順次実行されます。
    • これは、リソースのクリーンアップ、ログの書き出し、統計情報の保存など、プログラム終了時に必要な最終処理を行うために使用されます。
    • このコンテキストでは、batexitハンドラが、プログラム終了時にまだ「使用中」とマークされているBiobufを検出し、それをリークと判断していました。
  5. Valgrind:

    • Valgrindは、Linux上で動作するオープンソースのプロファイリングおよびメモリデバッグツールです。
    • 特に、メモリリーク、不正なメモリアクセス、初期化されていないメモリの使用、スレッドの競合状態など、様々な実行時エラーを検出するのに非常に強力です。
    • Valgrindは、プログラムの実行をインターセプトし、メモリ操作を監視することでこれらの問題を検出します。
    • このコミットの文脈では、ValgrindがBiobufの未終了状態をメモリリークとして誤って報告していました。これは、Valgrindがリソースのライフサイクルを厳密に追跡するため、Btermが呼び出されずにBiobufがスコープ外に出ることを「リーク」と解釈したためです。

技術的詳細

問題の核心は、cmd/6ccmd/8cswt.cファイル内のoutcode関数にありました。この関数は、コード生成の一部として一時的なBiobufをスタック上に割り当てていました。C言語において、スタック上に割り当てられたローカル変数は、その変数が宣言されたスコープ(この場合はoutcode関数)を抜けると自動的にメモリが解放されます。しかし、Biobufのような複雑な構造体の場合、単にメモリが解放されるだけでは不十分で、内部的な状態をクリーンアップし、関連するリソースを解放するための明示的な終了処理が必要となることがあります。

以前のコードでは、outcode関数の最後でBflush(&b);が呼び出されていました。Bflushは、バッファの内容をディスクに書き出す(フラッシュする)役割を果たしますが、Biobuf構造体自体を「終了済み」の状態にするわけではありません。そのため、outcode関数が終了し、スタック上のBiobuf変数bのメモリが解放された後も、Biobufの内部状態は「使用中」のままでした。

この「使用中」の状態が、プログラム終了時に実行されるbatexitハンドラによって検出されました。batexitハンドラは、プログラムのライフサイクル全体で管理されるべきリソースのリストを保持しており、終了時にそれらがすべて適切に解放されているかを確認します。BiobufBtermによって明示的に終了されていない場合、batexitハンドラはそれを未解放のリソース、すなわちメモリリークとして誤って解釈し、Valgrindのようなツールがその誤検出を報告していました。

修正は非常にシンプルで、Bflush(&b);の呼び出しをBterm(&b);に置き換えることでした。Btermは、バッファの内容をフラッシュするだけでなく、Biobufに関連するすべての内部リソースを解放し、Biobufを「終了済み」の状態に設定します。これにより、outcode関数が終了する前にBiobufが完全にクリーンアップされ、batexitハンドラがそれを「使用中」と誤認することがなくなりました。結果として、Valgrindの誤検出が解消され、コンパイラのメモリ管理の健全性が向上しました。

この問題は、実際のメモリリークというよりも、リソースのライフサイクル管理の不備と、それを厳密にチェックするツールの挙動のミスマッチによって引き起こされた「誤検出」であったと言えます。しかし、このような誤検出も、開発プロセスにおいては無視できないノイズとなるため、修正の価値は高いです。

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

変更は、src/cmd/6c/swt.csrc/cmd/8c/swt.cの2つのファイルにわたって行われています。両ファイルで同じ変更が適用されています。

--- a/src/cmd/6c/swt.c
+++ b/src/cmd/6c/swt.c
@@ -320,7 +320,7 @@ outcode(void)\
 	zaddr(&b, &p->from, sf);\
 	zaddr(&b, &p->to, st);\
 }\
-	Bflush(&b);\
+	Bterm(&b);\
 	close(f);\
 	firstp = P;\
 	lastp = P;\
--- a/src/cmd/8c/swt.c
+++ b/src/cmd/8c/swt.c
@@ -324,7 +324,7 @@ outcode(void)\
 	zaddr(&b, &p->from, sf);\
 	zaddr(&b, &p->to, st);\
 }\
-	Bflush(&b);\
+	Bterm(&b);\
 	close(f);\
 	firstp = P;\
 	lastp = P;\

具体的には、outcode関数内で、Bflush(&b);という行がBterm(&b);に置き換えられています。

コアとなるコードの解説

変更された行は、outcode関数内で宣言されたBiobuf型のローカル変数bの処理に関するものです。

  • 変更前: Bflush(&b);

    • Bflush関数は、Biobuf構造体bが保持しているバッファの内容を、関連付けられた出力ストリーム(この場合はおそらく生成中のオブジェクトファイル)に書き出します。
    • この操作は、バッファの内容をクリアし、ディスクへの書き込みを保証しますが、Biobuf構造体自体が持つ内部的なリソース(例えば、ファイルディスクリプタや、batexitハンドラに登録された状態)を解放したり、そのライフサイクルを終了させたりするものではありません。
    • 結果として、outcode関数が終了し、bがスコープ外に出ても、Biobufの内部状態は「使用中」のままであり、batexitハンドラがこれを未解放のリソースとして検出していました。
  • 変更後: Bterm(&b);

    • Bterm関数は、「Biobufの終了処理」を行います。これには、Bflushと同様にバッファの内容をフラッシュする機能も含まれますが、それに加えて、Biobufに関連付けられたすべての内部リソースを解放し、Biobufを「終了済み」の状態に設定します。
    • この「終了済み」の状態にすることで、batexitハンドラがプログラム終了時にリソースのリストをチェックした際に、このBiobufが適切にクリーンアップされたものとして認識され、誤ってリークとして報告されることがなくなります。
    • Btermを呼び出すことで、スタック上に割り当てられたBiobufが、そのスコープを抜ける前に完全にクリーンアップされることが保証されます。

この変更は、機能的な出力(生成されるオブジェクトファイルの内容)には影響を与えませんが、コンパイラの内部的なリソース管理の健全性を向上させ、Valgrindのようなデバッグツールによる誤検出を排除するという重要な効果をもたらします。

関連リンク

参考にした情報源リンク

  • Valgrind公式ドキュメント: https://valgrind.org/docs/manual/index.html
  • Go言語のコンパイラに関する一般的な情報 (Goの公式ドキュメントやブログ記事など)
  • C言語におけるスタックとヒープのメモリ管理に関する一般的な知識
  • I/Oバッファリングの概念に関する一般的な知識
  • batexitに関するC言語の標準ライブラリまたはGoランタイムのドキュメント (当時のGoのC言語部分の実装に依存)
  • Go言語の古いコンパイラツールチェーン(6c, 8cなど)に関する情報 (Goのソースコードリポジトリ内のsrc/cmdディレクトリや、当時のGoの設計ドキュメントなど)
  • Go Issue #5085の議論内容 (コミットメッセージに記載されているFixes #5085から辿れる情報)
  • Gerrit Change-ID 7844044のレビューコメント (コミットメッセージに記載されているhttps://golang.org/cl/7844044から辿れる情報)
  • Biobufの定義と関連関数の実装 (Goのソースコードリポジトリ内のsrc/lib9psrc/cmdディレクトリ内の関連ファイル)