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

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

このコミットは、Go言語のリンカ (cmd/ld) において、Mach-O (macOS/iOS実行ファイル形式) ファイルにおけるゼロフィルセクションのサポートを追加するものです。具体的には、S_ZEROFILL フラグを持つセクションが正しく処理され、メモリがゼロで初期化されるようにリンカの挙動を修正しています。

コミット

commit c5444a89373c492b819d41c9d0047089e0bc04be
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Sun Sep 2 04:50:58 2012 +0800

    cmd/ld: support zero-filled section for Mach-O files
    
    R=golang-dev, r, dave, rsc
    CC=golang-dev
    https://golang.org/cl/6492069

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

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

元コミット内容

cmd/ld: support zero-filled section for Mach-O files

R=golang-dev, r, dave, rsc
CC=golang-dev
https://golang.org/cl/6492069

変更の背景

この変更の背景には、Mach-O 実行ファイル形式における特定のセクションの取り扱いに関するリンカの不完全な実装がありました。Mach-O ファイルでは、プログラムの実行時にメモリにロードされるセクションには様々な種類があります。その中でも、S_ZEROFILL フラグを持つセクションは、ファイル内には実際のデータを持たず、実行時にリンカやOSによってゼロで初期化されることが期待されます。これは、初期値がゼロであるグローバル変数や静的変数などを効率的に扱うために使用されます。

Go言語のリンカが Mach-O ファイルを生成する際、この S_ZEROFILL セクションを適切に処理できていなかった可能性があります。具体的には、ゼロフィルされるべきセクションに対して、ファイル内の既存のデータ領域を指すポインタを割り当ててしまうという問題があったと考えられます。これにより、本来ゼロであるべきメモリ領域に意図しないデータが残ってしまう、あるいはメモリが適切に確保されないといったバグが発生する可能性がありました。

このコミットは、このような問題を解決し、Go言語でビルドされた Mach-O 実行ファイルが、macOS や iOS 環境で期待通りに動作するようにするために導入されました。

前提知識の解説

Mach-O (Mach Object)

Mach-O は、macOS、iOS、watchOS、tvOS などの Apple 製オペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、共有ライブラリ、およびコアダンプのファイル形式です。ELF (Executable and Linkable Format) や PE (Portable Executable) と同様に、プログラムのコード、データ、メタデータなどを構造化して格納します。

Mach-O ファイルは、以下の主要なコンポーネントで構成されます。

  • ヘッダ (Header): ファイルの種類 (実行可能ファイル、ライブラリなど)、CPU アーキテクチャ、ロードコマンドの数とサイズなどの基本的な情報を含みます。
  • ロードコマンド (Load Commands): OS がプログラムをメモリにロードし、実行するために必要な命令のリストです。これには、セグメントの定義、シンボルテーブル、ダイナミックリンカ情報などが含まれます。
  • セグメント (Segments): 実行可能ファイルの論理的なブロックであり、メモリ保護やページングの単位となります。各セグメントは1つ以上のセクションを含みます。一般的なセグメントには、__TEXT (コードと読み取り専用データ) や __DATA (読み書き可能なデータ) などがあります。
  • セクション (Sections): セグメント内のより細かい論理的なブロックです。各セクションは特定の種類のデータやコードを格納します。例えば、__text セクションは実行可能なコードを、__data セクションは初期化された読み書き可能なデータを格納します。

セクションフラグと S_ZEROFILL

Mach-O のセクションには、その特性を示すためのフラグが設定されます。これらのフラグは、セクションがどのようにメモリにロードされ、どのように扱われるべきかをリンカやOSに指示します。

S_ZEROFILL (または SECTION_ATTRIBUTES_ZEROFILL) は、セクションフラグの一つで、そのセクションが実行時にゼロで初期化されるべきであることを示します。このフラグが設定されたセクションは、ファイル内には実際のデータを含みません。これにより、実行ファイルのサイズを不必要に大きくすることなく、大きなゼロ初期化されたデータ領域を確保できます。例えば、初期値がゼロの大きな配列や構造体などがこのタイプのセクションに配置されることがあります。

リンカは、S_ZEROFILL フラグを持つセクションを見つけると、そのセクションのサイズ分のメモリを確保し、その内容をすべてゼロで埋める必要があります。もしリンカがこの処理を怠ると、そのメモリ領域には不定な値が残ったり、他のセデータが誤って上書きされたりする可能性があります。

技術的詳細

このコミットは、Go言語のリンカ (src/cmd/ld/ldmacho.c) における Mach-O ファイルのセクション処理ロジックを修正しています。

既存のコードでは、Mach-O ファイルからセクション情報を読み込んだ後、そのセクションがメモリ内でどこに配置されるべきかを決定し、ファイル内のデータ (dat) からそのセクションのデータへのポインタ (s->p) を設定していました。これは、ファイル内に実際のデータを持つセクション(例: コードセクションや初期化済みデータセクション)にとっては正しい挙動です。

しかし、S_ZEROFILL フラグを持つセクションの場合、ファイル内には対応するデータが存在しません。そのため、既存のロジックでは、s->p がファイル内の無関係なデータ領域を指してしまうか、あるいは未定義の動作を引き起こす可能性がありました。

このコミットでは、以下の変更によってこの問題を解決しています。

  1. S_ZEROFILL フラグのチェック: セクションのフラグ (sect->flags) の下位8ビット (0xff) をチェックし、それが S_ZEROFILL (値が 1) であるかどうかを判定します。
  2. ゼロフィルセクションのメモリ割り当て:
    • もしセクションが S_ZEROFILL であれば、そのセクションのサイズ (s->size) 分のメモリを新たに割り当て (mal(s->size))、そのポインタを s->p に設定します。mal 関数は、割り当てられたメモリをゼロで初期化する Go リンカ内部の関数であると推測されます。これにより、実行時にこのセクションがゼロで初期化されることが保証されます。
    • S_ZEROFILL でない通常のセクションの場合は、これまで通りファイル内のデータ (dat) からオフセットを計算して s->p を設定します。

この修正により、リンカは Mach-O ファイルを生成する際に、S_ZEROFILL セクションを正しく認識し、実行時にゼロで初期化されるべきメモリ領域を適切に確保するようになります。

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

変更は src/cmd/ld/ldmacho.c ファイルの ldmacho 関数内で行われています。

--- a/src/cmd/ld/ldmacho.c
+++ b/src/cmd/ld/ldmacho.c
@@ -573,9 +573,14 @@ ldmacho(Biobuf *f, char *pkg, int64 len, char *pn)\n \t\t\tgoto bad;\n \t\t}\n \t\tfree(name);\n-\t\ts->p = dat + sect->addr - c->seg.vmaddr;\n+\n \t\ts->np = sect->size;\n \t\ts->size = s->np;\n+\t\tif((sect->flags & 0xff) == 1) // S_ZEROFILL\n+\t\t\ts->p = mal(s->size);\n+\t\telse {\n+\t\t\ts->p = dat + sect->addr - c->seg.vmaddr;\n+\t\t}\n \t\t\n \t\tif(strcmp(sect->segname, "__TEXT") == 0) {\n \t\t\tif(strcmp(sect->name, "__text") == 0)\n```

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

変更前のコードでは、以下の行でセクションのデータポインタ `s->p` を設定していました。

```c
s->p = dat + sect->addr - c->seg.vmaddr;

これは、ファイル内のデータ (dat) の開始アドレスから、セクションのアドレス (sect->addr) とセグメントの仮想メモリ開始アドレス (c->seg.vmaddr) の差分をオフセットとして加算することで、セクションのデータがファイル内のどこにあるかを指し示していました。

変更後のコードでは、この行が条件分岐の中に移動し、S_ZEROFILL フラグのチェックが追加されています。

if((sect->flags & 0xff) == 1) // S_ZEROFILL
    s->p = mal(s->size);
else {
    s->p = dat + sect->addr - c->seg.vmaddr;
}
  • sect->flags & 0xff: sect->flags はセクションの属性を示すビットフラグの集合です。& 0xff は、フラグの下位8ビットのみを抽出するビットマスク操作です。Mach-O のセクションタイプは通常、この下位8ビットで表現されます。
  • == 1: Mach-O のセクションタイプ定数において、S_ZEROFILL は通常 1 に定義されています。したがって、この条件はセクションがゼロフィルセクションであるかどうかを判定しています。
  • s->p = mal(s->size);: もしセクションが S_ZEROFILL であれば、mal 関数を呼び出して s->size (セクションのサイズ) 分のメモリを新たに割り当て、そのポインタを s->p に設定します。mal は Go リンカ内部のメモリ割り当て関数で、通常は割り当てたメモリをゼロで初期化します。
  • else { s->p = dat + sect->addr - c->seg.vmaddr; }: S_ZEROFILL でない通常のセクションの場合は、以前と同じロジックでファイル内のデータへのポインタを設定します。

この修正により、リンカは Mach-O ファイルを生成する際に、ゼロフィルセクションに対してはファイルデータへのポインタではなく、新しくゼロ初期化されたメモリ領域へのポインタを割り当てるようになり、Mach-O の仕様に準拠した正しい実行ファイルを生成できるようになります。

関連リンク

参考にした情報源リンク