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

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

このコミットは、Go言語のリンカ (cmd/ld) において、出力バイナリを生成する前に既存のファイルを削除する際の挙動を改善するものです。具体的には、出力先が通常のファイルではない場合(例: /dev/null のような特殊ファイル)には、そのファイルを削除しないように変更されました。これにより、特殊ファイルの誤った削除を防ぎ、リンカの堅牢性を向上させています。

コミット

commit ece69f7c2b34d9267f3802cd11c1e5fca84e5474
Author: Mike Andrews <mra@xoba.com>
Date:   Sat Mar 29 09:50:49 2014 -0700

    cmd/ld: don't delete output binary if not "ordinary" file.
    
    e.g., don't delete /dev/null. this fix inspired by gnu libiberty,
    unlink-if-ordinary.c.
    
    Fixes #7563
    
    LGTM=iant
    R=golang-codereviews, iant, 0intro
    CC=golang-codereviews, r
    https://golang.org/cl/76810045

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

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

元コミット内容

cmd/ld: don't delete output binary if not "ordinary" file.

このコミットは、Goリンカが生成する出力バイナリが「通常の」ファイルではない場合、そのファイルを削除しないようにするものです。例えば、/dev/null のような特殊ファイルを削除しないようにします。この修正は、GNU libiberty ライブラリの unlink-if-ordinary.c に触発されています。

Fixes #7563

変更の背景

Goリンカ (cmd/ld) は、新しいバイナリを書き込む前に、同じパスに既存のファイルが存在する場合、それを削除しようとします。これは、Unix系システムにおいて、実行中のバイナリや最近実行されたバイナリに上書きしようとすると問題が発生する可能性があるため、一般的なプラクティスです。しかし、この「削除してから書き込む」というアプローチには問題がありました。

問題は、出力先として /dev/null のような特殊ファイルが指定された場合です。/dev/null は、書き込まれたデータをすべて破棄し、読み込み時には常にEOFを返す特殊なデバイスファイルです。これを通常のファイルと同様に remove() 関数で削除しようとすると、意図しない挙動を引き起こしたり、エラーになったりする可能性があります。リンカは、出力先が通常のファイルであるか特殊ファイルであるかを区別せずに remove() を呼び出していたため、/dev/null などの特殊ファイルを誤って削除しようとしていました。

この問題は、GoのIssue #7563として報告されていた可能性があります(ただし、現在のGoのIssueトラッカーでは直接見つかりませんでした)。このコミットは、この問題を解決し、リンカがより堅牢に動作するようにすることを目的としています。

前提知識の解説

Goリンカ (cmd/ld)

Go言語のビルドプロセスにおいて、cmd/ld はGoプログラムのリンカです。コンパイラによって生成されたオブジェクトファイル(.o ファイル)やアーカイブファイル(.a ファイル)を結合し、実行可能なバイナリファイルを生成する役割を担います。リンカは、プログラムが依存するライブラリのコードを解決し、最終的な実行ファイルに必要なすべてのコードとデータを配置します。

特殊ファイル (例: /dev/null)

Unix系オペレーティングシステムには、通常のデータファイルとは異なる「特殊ファイル」という概念があります。これらは、ハードウェアデバイスやOSの機能へのインターフェースとして機能します。

  • /dev/null: 「ヌルデバイス」または「ビットバケツ」とも呼ばれます。このファイルに書き込まれたデータはすべて破棄され、このファイルから読み込もうとすると即座にEOF(End Of File)が返されます。プログラムの出力を捨てる際や、空の入力を提供する際によく使用されます。
  • /dev/zero: 読み込み時に常にヌルバイト(0x00)を生成します。
  • /dev/random, /dev/urandom: 乱数を生成します。

これらの特殊ファイルは、ファイルシステム上にエントリを持ちますが、ディスク上のストレージを消費する通常のファイルとは根本的に異なります。そのため、通常のファイル操作(削除など)を適用すると、予期せぬ結果を招くことがあります。

remove() 関数

remove() は、C標準ライブラリ (stdio.h) で定義されている関数で、指定されたパスのファイルを削除します。これは、Unix系システムでは unlink() システムコールに、Windowsでは DeleteFile() APIに相当することが多いです。この関数は、通常のファイルを削除するのに適していますが、特殊ファイルを削除しようとすると、エラーを返したり、システムによっては意図しない副作用を引き起こしたりする可能性があります。

lstat() 関数と struct stat

lstat() は、Unix系システムで利用可能なシステムコール (sys/stat.h) で、指定されたパスのファイルに関する詳細な情報を取得します。この関数は、シンボリックリンクの場合でも、リンク自体ではなく、リンクが指す先のファイルではなく、リンク自体の情報を返します(stat() はリンクが指す先の情報を返します)。

lstat() は、ファイルの情報を struct stat 構造体に格納します。この構造体には、ファイルのサイズ、最終変更時刻、所有者、パーミッションなどの情報が含まれます。特に重要なのは st_mode メンバーで、これはファイルのタイプ(通常のファイル、ディレクトリ、シンボリックリンク、デバイスファイルなど)とパーミッションをエンコードしたビットマスクです。

S_ISREG() マクロ

S_ISREG() は、sys/stat.h で定義されているマクロで、struct statst_mode メンバーが通常のファイル(regular file)を示しているかどうかを判定するために使用されます。このマクロは、st_mode の値が通常のファイルタイプを示すビットパターンと一致する場合に真を返します。これにより、プログラムはファイルが通常のデータファイルであるか、それとも特殊なファイルであるかを区別できます。

libiberty は、GNUプロジェクトの様々なツール(GCC、GDB、Binutilsなど)で共有されるユーティリティ関数のライブラリです。このライブラリは、異なるプラットフォーム間での移植性を高め、共通の機能を提供することを目的としています。

unlink-if-ordinary.c は、libiberty 内のソースファイルの一つで、その名前が示す通り、「通常のファイルである場合にのみ unlink する」というロジックを実装しています。これは、リンカが特殊ファイルを誤って削除しようとする問題に対する一般的な解決策として、GNUツールチェーンで採用されているアプローチです。Goリンカのこのコミットは、この libiberty のアプローチから着想を得て、同様の安全策をGoのビルドプロセスに導入しました。

技術的詳細

このコミットの技術的な核心は、Goリンカが outfile (出力バイナリのパス) を削除する前に、そのファイルが本当に「通常のファイル」であるかどうかを確認するロジックを追加した点にあります。

  1. 既存の削除ロジック: 元々、libinit() 関数内で、リンカは出力ファイルを書き込む前に remove(outfile); を呼び出して既存のファイルを削除していました。これは、Unix系システムで実行中のバイナリを上書きしようとすると問題が発生する可能性があるためです。また、Windows 7では remove() の後に create() が失敗するケースがあったため、Windows環境ではこの remove() は無効化されていました (#ifndef _WIN32)。

  2. 問題点: この既存のロジックでは、outfile/dev/null のような特殊ファイルであった場合でも、無条件に remove() が呼び出されていました。これは、特殊ファイルを削除しようとすることになり、意図しないエラーやシステムの不安定性を引き起こす可能性がありました。

  3. 修正の導入: このコミットでは、以下の変更が加えられました。

    • sys/stat.h のインクルード: lstat() 関数と struct statS_ISREG() マクロを使用するために、sys/stat.h ヘッダファイルがインクルードされました。ただし、Windows (_WIN32) と Plan 9 (PLAN9) ではこれらの機能が利用できないため、条件付きコンパイル (#if !(defined(_WIN32) || defined(PLAN9))) で囲まれています。
    • ファイルタイプのチェック: remove(outfile) の呼び出しの前に、lstat(outfile, &st) を使用して outfile のファイル情報を取得します。lstat() が成功し(戻り値が0)、かつ S_ISREG(st.st_mode) が真を返す場合、つまり outfile が通常のファイルである場合にのみ remove(outfile) が実行されるようになりました。
    • コメントの追加: S_ISREG() が Plan 9 に存在しないこと、およびこの修正が unlink-if-ordinary.c に触発されたものであることを示すコメントが追加されました。
  4. 効果: この変更により、リンカは /dev/null のような特殊ファイルを出力先として指定された場合でも、それらを誤って削除しようとすることがなくなりました。リンカは、出力先が通常のファイルである場合にのみ、既存のファイルを安全に削除するようになります。これにより、リンカの堅牢性が向上し、予期せぬエラーを防ぐことができます。

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

src/cmd/ld/lib.c ファイルにおける変更点です。

--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -37,6 +37,9 @@
 #include	"../../pkg/runtime/funcdata.h"
 
 #include	<ar.h>
+#if !(defined(_WIN32) || defined(PLAN9))
+#include	<sys/stat.h>
+#endif
 
 enum
 {
@@ -106,8 +109,13 @@ libinit(void)\n 	// Unix doesn't like it when we write to a running (or, sometimes,\n 	// recently run) binary, so remove the output file before writing it.\n 	// On Windows 7, remove() can force the following create() to fail.\n-#ifndef _WIN32\n-#tremove(outfile);\n+\t// S_ISREG() does not exist on Plan 9.\n+#if !(defined(_WIN32) || defined(PLAN9))\n+\t{\n+\t\tstruct stat st;\n+\t\tif(lstat(outfile, &st) == 0 && S_ISREG(st.st_mode))\n+\t\t\tremove(outfile);\n+\t}\n #endif\n \tcout = create(outfile, 1, 0775);\n \tif(cout < 0) {\n```

## コアとなるコードの解説

変更は `src/cmd/ld/lib.c` の `libinit()` 関数内で行われています。

1.  **ヘッダの追加**:
    ```c
    #if !(defined(_WIN32) || defined(PLAN9))
    #include	<sys/stat.h>
    #endif
    ```
    このブロックは、Windows (`_WIN32`) と Plan 9 (`PLAN9`) 以外のシステムでのみ `<sys/stat.h>` をインクルードするようにしています。これは、`lstat` 関数や `struct stat`、`S_ISREG` マクロがこれらのプラットフォームでは利用できないためです。

2.  **削除ロジックの変更**:
    ```c
    -#ifndef _WIN32
    -#tremove(outfile);\
    +\t// S_ISREG() does not exist on Plan 9.
    +#if !(defined(_WIN32) || defined(PLAN9))
    +\t{\
    +\t\tstruct stat st;\
    +\t\tif(lstat(outfile, &st) == 0 && S_ISREG(st.st_mode))\
    +\t\t\tremove(outfile);\
    +\t}\
    #endif
    ```
    *   元のコードでは、Windows以外のシステムで無条件に `remove(outfile)` を呼び出していました。
    *   新しいコードでは、まず `// S_ISREG() does not exist on Plan 9.` というコメントが追加され、Plan 9 での `S_ISREG()` の非互換性について言及しています。
    *   その下の `#if !(defined(_WIN32) || defined(PLAN9))` ブロックは、WindowsとPlan 9以外のシステムでのみ実行されるコードです。
    *   このブロック内で、`struct stat st;` を宣言し、`lstat(outfile, &st)` を呼び出して `outfile` のファイル情報を取得します。
    *   `if(lstat(outfile, &st) == 0 && S_ISREG(st.st_mode))` という条件文が追加されました。
        *   `lstat(outfile, &st) == 0`: `lstat` 関数が成功し、ファイル情報が正しく取得できたことを確認します。
        *   `S_ISREG(st.st_mode)`: 取得したファイル情報 (`st.st_mode`) が通常のファイル(regular file)であることを確認します。
    *   この両方の条件が真である場合にのみ、`remove(outfile);` が実行されます。これにより、`/dev/null` のような特殊ファイルは `S_ISREG()` のチェックで除外され、削除されなくなります。

この変更により、リンカは出力先が通常のファイルである場合にのみ、既存のファイルを安全に削除するようになり、特殊ファイルの誤削除を防ぐことができます。

## 関連リンク

*   **Go Issue #7563**: コミットメッセージに `Fixes #7563` と記載されていますが、現在のGoのIssueトラッカーでは直接この番号のIssueは見つかりませんでした。これは、古いIssueトラッカーの番号であるか、または内部的な参照番号である可能性があります。しかし、コミットメッセージから問題の内容は明確に読み取れます。

## 参考にした情報源リンク

*   [Go GitHub Commit: ece69f7c2b34d9267f3802cd11c1e5fca84e5474](https://github.com/golang/go/commit/ece69f7c2b34d9267f3802cd11c1e5fca84e5474)
*   GNU `libiberty` (特に `unlink-if-ordinary.c` の概念): GNUプロジェクトのドキュメントやソースコードリポジトリで詳細を確認できます。
*   Unix系システムのファイルシステムと特殊ファイルに関する一般的なドキュメント。
*   C標準ライブラリの `remove()` 関数、および `sys/stat.h` の `lstat()`、`struct stat`、`S_ISREG()` に関するドキュメント。