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

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

このコミットは、Go言語のリンカ (cmd/5l, cmd/6l, cmd/8l) が入力として PCDATA および FUNCDATA 命令を受け入れるようにする変更です。これにより、ポータブルなリンカコード (cmd/ld) がこれらの命令を適切に処理できるようになり、コード生成時にこれらを無視するようになります。

コミット

commit 567818224edce362fa040ee89e9597982f2bcdf6
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jul 16 16:23:11 2013 -0400

    cmd/5l, cmd/6l, cmd/8l: accept PCDATA instruction in input
    
    The portable code in cmd/ld already knows how to process it,
    we just have to ignore it during code generation.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/11363043

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

https://github.com/golang/go/commit/567818224edce362fa040ee89e9597982f2bcdf6

元コミット内容

cmd/5l, cmd/6l, cmd/8l: accept PCDATA instruction in input

The portable code in cmd/ld already knows how to process it,
we just have to ignore it during code generation.

変更の背景

Go言語のランタイムは、ガベージコレクションやスタック管理のために、コンパイルされたバイナリに特定のメタデータを含める必要があります。このメタデータは主に PCDATA (Program Counter Data) と FUNCDATA (Function Data) と呼ばれる擬似命令によって埋め込まれます。

このコミットが行われた2013年当時、Goのリンカはアーキテクチャ固有の 5l (ARM), 6l (AMD64), 8l (x86) と、よりポータブルな cmd/ld のコードベースに分かれていました。cmd/ld のポータブルな部分は既に PCDATAFUNCDATA を処理するロジックを持っていましたが、アーキテクチャ固有のリンカ (5l, 6l, 8l) がこれらの命令を入力として認識し、適切に無視する(つまり、実行可能なコードを生成しない)ようにする必要がありました。

この変更の目的は、リンカがこれらのランタイムメタデータ命令を正しく受け入れ、それらがコード生成プロセスに干渉しないようにすることでした。これにより、Goランタイムが依存する重要なメタデータがバイナリに正確に埋め込まれることが保証されます。

前提知識の解説

Goリンカ (cmd/ld, 5l, 6l, 8l)

Go言語のビルドプロセスにおいて、リンカはコンパイルされたGoパッケージとその依存関係を結合して実行可能なバイナリを作成する重要な役割を担います。

  • cmd/ld: これはGoの内部リンカであり、Goのツールチェーンの主要なコンポーネントです。Go 1.5以降、リンカの大部分はGo自身で書かれるようになりましたが、このコミットが作成された時点では、まだC言語で書かれた部分が多く残っていました。cmd/ld は、シンボル解決、再配置、デッドコード削除、DWARFデバッグ情報の生成、そして実行可能ファイルのフォーマット(ELF, PE, Mach-Oなど)の準備を行います。
  • 5l, 6l, 8l: これらはGoの初期のリンカで、それぞれ特定のアーキテクチャに対応していました。
    • 5l: ARMアーキテクチャ用
    • 6l: AMD64 (x86-64) アーキテクチャ用
    • 8l: 386 (x86) アーキテクチャ用 これらのリンカは、対応するコンパイラ (5g, 6g, 8g) とともに、Goの初期のツールチェーンの一部でした。Go 1.5でツールチェーンがGo自身で書き直されるまで、これらはC言語で実装されていました。

PCDATA (Program Counter Data)

PCDATA はGoコンパイラによって生成される擬似命令で、コンパイルされたバイナリにメタデータを埋め込むために使用されます。これはCPUが直接実行する命令ではなく、Goランタイム、特にガベージコレクタがメモリを効率的に管理し、正確なスタックスキャンを実行するために必要な情報を提供します。

  • 形式: PCDATA $INDEX, $VALUE の形式を取り、特定のプログラムカウンタ (PC) の位置で、指定された INDEXVALUE を関連付けます。
  • 用途: 主にガベージコレクションのために、スタック上のポインタの位置や、関数内の特定のPCにおけるランタイムの状態(例えば、プリエンプションが安全なポイントなど)を追跡するために使用されます。
  • データ型: 通常、int32 の値として扱われます。

FUNCDATA (Function Data)

FUNCDATAPCDATA と同様にGoコンパイラによって生成される擬似命令で、関数に関するメタデータをバイナリに埋め込みます。

  • 用途: 主にガベージコレクタが関数のメモリレイアウトを理解するために使用されます。これには、関数引数、ローカル変数、その他のスタック割り当てデータ内のポインタの位置に関する詳細が含まれます。これにより、GCはコレクションサイクル中にライブオブジェクトを正確に識別し、追跡できます。
  • PCDATA との違い: FUNCDATA は通常、他のデータシンボル(例えば、amd64 では8バイト)を指しますが、PCDATA はより小さなテーブル内の int32 オフセット(例えば、4バイト)を参照します。

スタック分割 (Stack Split)

Goのゴルーチンは軽量であり、動的にスタックサイズを増減させることができます。Go 1.4より前のバージョンでは、「セグメント化されたスタック」というアプローチが使用されていました。これは、ゴルーチンのスタック領域が不足した場合に、新しいメモリセグメントを割り当てて既存のスタックにリンクするというものでした。このプロセスは「スタック分割」と呼ばれることがありました。

各関数呼び出しのプロローグには、十分なスタック空間があるかを確認するチェックが含まれていました。もし不足していれば、runtime.morestack を呼び出して新しいスタックセグメントを割り当てました。しかし、この方式には「ホットスプリット問題」という欠点があり、頻繁なスタックの拡張がパフォーマンスオーバーヘッドを引き起こす可能性がありました。

Go 1.4以降、Goランタイムはセグメント化されたスタックから、スタックコピーを伴う連続したスタックモデルに移行しました。これにより、「ホットスプリット問題」は解消されましたが、このコミットが作成された時点では、まだセグメント化されたスタックが使用されており、スタック分割に関連するロジックがリンカに存在していました。

技術的詳細

このコミットの技術的な核心は、Goのリンカが PCDATAFUNCDATA という特殊な擬似命令を、実行可能なコードとしてではなく、ランタイムが利用するメタデータとして正しく認識し、処理することにあります。

Goのガベージコレクタは、プログラムの実行中にどのメモリ領域がまだ使用されているか(ライブであるか)を正確に判断する必要があります。これには、スタック上のポインタを正確に識別することが不可欠です。PCDATAFUNCDATA は、このポインタ情報やその他のランタイム状態に関するメタデータを、コンパイル時にバイナリに埋め込むためのメカニズムを提供します。

リンカの役割は、コンパイラが生成したこれらの擬似命令を、最終的な実行可能ファイルに含める際に、CPUが実行する通常の命令とは異なる方法で扱うことです。具体的には、リンカはこれらの命令をコードとして生成するのではなく、ランタイムが後で読み取れる形式でデータセクションに配置する必要があります。

このコミットでは、特に 5l, 6l, 8l といったアーキテクチャ固有のリンカが、これらの新しい命令タイプを認識し、それらを「ゼロ幅命令」(つまり、実行可能なコードを生成しない命令)として扱うように変更されています。これにより、リンカは PCDATAFUNCDATA を見つけてもエラーを発生させず、かつそれらがバイナリのコードセクションに誤って含まれることを防ぎます。

また、スタック分割に関連するコード (noop.cpass.c) においても、PCDATAFUNCDATA が関わる場合に、引数サイズの計算やアライメントチェックが正しく行われるように修正が加えられています。これは、これらの擬似命令がスタックフレームのレイアウトに影響を与える可能性があるため、リンカがその情報を正確に処理する必要があることを示しています。

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

このコミットでは、主に以下のファイルが変更されています。これらはGoのリンカ (5l, 6l, 8l) の命令テーブル定義と、スタック分割に関連する処理を行うファイルです。

  • src/cmd/5l/noop.c
  • src/cmd/5l/optab.c
  • src/cmd/5l/span.c
  • src/cmd/6l/optab.c
  • src/cmd/6l/pass.c
  • src/cmd/8l/optab.c
  • src/cmd/8l/pass.c

具体的な変更点は以下の通りです。

  1. optab.c ファイル群 (5l, 6l, 8l):

    • Optab optab[] 配列に APCDATAAFUNCDATA 命令が追加されています。これにより、リンカがこれらの命令を認識できるようになります。
    • 6l8loptab.c では、yfuncdataypcdata という新しい uchar 配列が定義され、AFUNCDATAAPCDATA のオペランドタイプが Yi32 (32ビット整数) や Ym (メモリ) として指定されています。これは、これらの命令がどのような引数を取るかをリンカに伝えます。
  2. span.c ファイル群 (5l):

    • span 関数内で、命令のサイズがゼロの場合の診断メッセージの条件が変更されています。m == 0 && (p->as != AFUNCDATA && p->as != APCDATA) となり、AFUNCDATAAPCDATA はサイズがゼロであっても診断メッセージを出さないように修正されています。これは、これらの命令が実行可能なコードを生成しないため、サイズがゼロであることが期待されるためです。
    • buildop 関数に AFUNCDATAAPCDATAcase 文に追加され、これらの命令が特別な処理を必要としない(つまり、そのまま無視される)ことを示しています。
  3. noop.c (5l) および pass.c (6l, 8l) ファイル群:

    • スタック分割に関連するロジック (noop.cnoops 関数、pass.cdostkoff 関数) で、cursym->text->to.offset2 の値が arg 変数に代入され、その値が 1 の場合は 0 に設定されるという特殊なマーカー処理が追加されています。
    • arg のアライメント (arg&3 または arg&7) がチェックされ、ミスアライメントがある場合に診断メッセージ (diag("misaligned argument size in stack split")) が出力されるようになっています。
    • p->from.offsetarg が設定されるようになり、スタック分割時の引数サイズが正確に反映されるようになっています。

コアとなるコードの解説

このコミットのコード変更は、Goリンカが PCDATAFUNCDATA というランタイムメタデータ命令を適切に処理するための基盤を確立しています。

  • optab.c の変更:

    • Optab はリンカが認識する命令のテーブルです。ここに APCDATAAFUNCDATA を追加することで、リンカはこれらの命令を構文的に有効なものとして扱えるようになります。
    • yfuncdataypcdata の定義は、これらの擬似命令がどのような種類のオペランド(引数)を持つかをリンカに伝えます。例えば、Yi32 は32ビット整数、Ym はメモリ参照を意味します。これにより、リンカはこれらの命令の構造を理解し、パースできるようになります。Zpseudo は、これらの命令が擬似命令であり、直接機械語に変換されるものではないことを示しています。
  • span.c の変更:

    • span 関数は、命令のサイズを計算し、コード生成を行うリンカの重要な部分です。m == 0 のチェックは、通常、リンカが命令のサイズを計算できなかった場合にエラーを報告するためのものです。しかし、AFUNCDATAAPCDATA は実行可能なコードを生成しないため、そのサイズは論理的にゼロです。したがって、これらの命令に対しては m == 0 のチェックをスキップすることで、誤ったエラー報告を防ぎます。
    • buildop 関数は、命令の種類に基づいて特定の処理を行うためのものです。AFUNCDATAAPCDATAbreak 文に追加されているのは、これらの命令が特別なコード生成ロジックを必要とせず、単に無視されるか、あるいは後続のパスでメタデータとして処理されることを意味します。
  • noop.c および pass.c の変更:

    • これらのファイルは、Goのスタック分割(当時の実装)に関連するロジックを含んでいます。cursym->text->to.offset2 は、関数の引数サイズやスタックフレームのオフセットに関連する情報を含んでいる可能性があります。
    • arg = 1 の特殊なマーカー処理は、特定のケース(例えば、引数サイズがゼロであることを示すためのコンパイラからの特別なシグナル)をリンカが認識し、適切に 0 として扱うためのものです。
    • アライメントチェック (arg&3arg&7) は、スタック上のデータが特定のバイト境界に整列していることを保証するために重要です。ミスアライメントはパフォーマンスの問題や、場合によってはクラッシュを引き起こす可能性があります。リンカがこのチェックを行うことで、生成されるバイナリの健全性が保たれます。
    • p->from.offset = arg; の変更は、スタック分割のロジックにおいて、引数サイズが正確にリンカの内部表現に反映されるようにするためのものです。これにより、ランタイムがスタックフレームを正しく解釈できるようになります。

これらの変更は、Goのコンパイラとリンカ、そしてランタイムが連携して、ガベージコレクションやスタック管理に必要なメタデータを効率的かつ正確に処理するための重要なステップでした。

関連リンク

参考にした情報源リンク