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

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

このコミットは、Go言語のcmd/cgoツール内のgcc.goファイルに対する変更です。gcc.goは、CgoがC/C++コンパイラ(GCC)と連携して、生成されたオブジェクトファイルからデバッグ情報(DWARF)を読み取るためのロジックを含んでいます。具体的には、Mach-O、ELF、PEといった異なる実行可能ファイルフォーマットからDWARFデータを解析する部分を扱っています。

コミット

このコミットは、cmd/cgoツールにおけるファイルディスクリプタのリークを修正するものです。具体的には、gccDebug関数内で開かれたファイル(Mach-O、ELF、PE形式のファイル)が適切に閉じられていなかった問題を解決します。defer f.Close()を追加することで、ファイルディスクリプタが確実に解放されるようにしています。

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

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

元コミット内容

cmd/cgo: avoid leaking fds

Fixes #5714.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/10386043

変更の背景

この変更の背景には、Goのcmd/cgoツールが一時ファイルを生成し、そこからデバッグ情報(DWARF)を読み取る際に、ファイルディスクリプタ(File Descriptor, FD)が適切に閉じられずにリークするという問題がありました。ファイルディスクリプタのリークは、システムのリソースを枯渇させ、最終的には新しいファイルを開けなくなるなどの問題を引き起こす可能性があります。

具体的には、gccDebug関数内でmacho.Openelf.Openpe.Openといった関数が呼び出され、それぞれがファイルを開いていました。これらの関数はファイルオブジェクトを返しますが、エラーが発生した場合や関数の実行が終了した場合に、明示的にClose()メソッドが呼び出されていませんでした。これにより、開かれたファイルディスクリプタがシステムに残り続け、リークが発生していました。

この問題は、Go issue #5714として報告されており、このコミットはその問題を解決するために行われました。

前提知識の解説

ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケットなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる整数値です。プログラムがファイルを開くと、カーネルは対応するファイルディスクリプタを返し、プログラムはそのディスクリプタを使ってファイルの読み書きなどの操作を行います。ファイルディスクリプタは有限のリソースであり、適切に閉じられないとシステム全体のリソース枯渇につながります。

defer文 (Go言語)

Go言語のdefer文は、そのdefer文を含む関数がリターンする直前に、指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に便利です。deferされた関数は、関数の実行が正常に終了した場合でも、パニック(panic)が発生した場合でも実行されます。

DWARF (Debugging With Attributed Record Formats)

DWARFは、ソースレベルデバッガがプログラムの実行をデバッグするために必要な情報(変数名、型情報、ソースコードの行番号と実行可能コードのアドレスのマッピングなど)を格納するための標準的なデバッグファイルフォーマットです。コンパイラによって生成された実行可能ファイルやライブラリに埋め込まれるか、別のファイルとして提供されます。

Mach-O, ELF, PE

これらは、異なるオペレーティングシステムで使用される実行可能ファイルおよびオブジェクトファイルのフォーマットです。

  • Mach-O (Mach Object): AppleのmacOSやiOSで使用される実行可能ファイルフォーマットです。
  • ELF (Executable and Linkable Format): LinuxやUnix系システムで広く使用される実行可能ファイルフォーマットです。
  • PE (Portable Executable): Microsoft Windowsで使用される実行可能ファイルフォーマットです。

Goのdebug/machodebug/elfdebug/peパッケージは、これらのフォーマットのファイルを解析するための機能を提供します。

cmd/cgo

cmd/cgoは、GoプログラムからC言語のコードを呼び出すためのツールです。Cgoは、GoとCの間の相互運用を可能にし、CのライブラリをGoプログラムから利用できるようにします。Cgoは、GoとCのコードをコンパイルし、リンクするプロセスを管理します。このプロセスの一部として、Cコンパイラ(GCCなど)が生成するオブジェクトファイルからデバッグ情報を読み取る必要があります。

技術的詳細

このコミットの技術的詳細の中心は、Goのdefer文を適切に使用してファイルディスクリプタのリークを防ぐ点にあります。

gccDebug関数は、Cgoが生成した一時的なオブジェクトファイルからDWARFデバッグ情報を抽出するために使用されます。この関数は、実行環境に応じてMach-O、ELF、PEのいずれかのフォーマットでファイルをオープンしようとします。

変更前のコードでは、macho.Open(gccTmp())elf.Open(gccTmp())pe.Open(gccTmp())がそれぞれファイルオブジェクト(*macho.File*elf.File*pe.File)を返していました。これらのファイルオブジェクトは、内部的にファイルディスクリプタを保持しています。しかし、これらのファイルオブジェクトに対してClose()メソッドが明示的に呼び出されていませんでした。

Goのdefer文は、この問題を解決するための理想的なメカニズムです。defer f.Close()f, err := macho.Open(gccTmp())などのファイルオープン直後に追加することで、以下の動作が保証されます。

  1. ファイルが正常にオープンされ、fにファイルオブジェクトが代入される。
  2. defer f.Close()がスケジュールされる。この時点ではf.Close()は実行されず、gccDebug関数がリターンする直前まで待機する。
  3. その後の処理(f.DWARF()の呼び出しなど)が実行される。
  4. gccDebug関数が正常に終了した場合でも、エラー(fatalfによるパニックを含む)が発生した場合でも、deferされたf.Close()が必ず実行される。

これにより、開かれたファイルディスクリプタは、gccDebug関数のスコープを抜ける際に確実に閉じられ、システムリソースへの負担が軽減され、ファイルディスクリプタのリークが防止されます。これは、堅牢なシステムを構築する上で非常に重要なプラクティスです。

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

src/cmd/cgo/gcc.goファイルに以下の3行が追加されました。

--- a/src/cmd/cgo/gcc.go
+++ b/src/cmd/cgo/gcc.go
@@ -716,6 +716,7 @@ func (p *Package) gccDebug(stdin []byte) (*dwarf.Data, binary.ByteOrder, []byte)\
 	runGcc(stdin, p.gccCmd())
 
 	if f, err := macho.Open(gccTmp()); err == nil {
+		defer f.Close()
 		d, err := f.DWARF()
 		if err != nil {
 			fatalf("cannot load DWARF output from %s: %v", gccTmp(), err)
@@ -742,6 +743,7 @@ func (p *Package) gccDebug(stdin []byte) (*dwarf.Data, binary.ByteOrder, []byte)\
 	}
 
 	if f, err := elf.Open(gccTmp()); err == nil {
+		defer f.Close()
 		d, err := f.DWARF()
 		if err != nil {
 			fatalf("cannot load DWARF output from %s: %v", gccTmp(), err)
@@ -768,6 +770,7 @@ func (p *Package) gccDebug(stdin []byte) (*dwarf.Data, binary.ByteOrder, []byte)\
 	}
 
 	if f, err := pe.Open(gccTmp()); err == nil {
+		defer f.Close()
 		d, err := f.DWARF()
 		if err != nil {
 			fatalf("cannot load DWARF output from %s: %v", gccTmp(), err)

コアとなるコードの解説

追加されたdefer f.Close()の各行は、それぞれ異なるファイルフォーマット(Mach-O、ELF、PE)のファイルをオープンした直後に配置されています。

  1. if f, err := macho.Open(gccTmp()); err == nil { defer f.Close() ... }:

    • macho.Open(gccTmp())は、一時ファイルgccTmp()をMach-Oフォーマットとして開こうとします。
    • エラーがなければ、開かれたファイルオブジェクトがfに代入されます。
    • defer f.Close()が直ちに実行をスケジュールします。これにより、このifブロックを含むgccDebug関数が終了する際に、fによって保持されているファイルディスクリプタが確実に閉じられます。
  2. if f, err := elf.Open(gccTmp()); err == nil { defer f.Close() ... }:

    • 同様に、elf.Open(gccTmp())がELFフォーマットのファイルを開きます。
    • エラーがなければ、defer f.Close()がスケジュールされ、ファイルディスクリプタのリークを防ぎます。
  3. if f, err := pe.Open(gccTmp()); err == nil { defer f.Close() ... }:

    • pe.Open(gccTmp())がPEフォーマットのファイルを開きます。
    • エラーがなければ、ここでもdefer f.Close()がスケジュールされ、ファイルディスクリプタが適切に閉じられることを保証します。

これらの変更により、gccDebug関数がどのファイルフォーマットのファイルを正常に開いたとしても、そのファイルディスクリプタは関数の終了時に自動的に解放されるようになり、ファイルディスクリプタのリーク問題が根本的に解決されました。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント: defer文に関する情報
  • Go言語のdebug/macho, debug/elf, debug/peパッケージのドキュメント
  • ファイルディスクリプタに関する一般的なオペレーティングシステムの概念
  • DWARFデバッグフォーマットに関する情報