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

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

このコミットは、Go言語のコンパイラ(cmd/5ccmd/5g)、リンカ(cmd/5l)、およびリンカライブラリ(liblink)に、Google Native Client (NaCl) のARMアーキテクチャサポートを追加するものです。これにより、GoプログラムをNaCl環境のARMプロセッサ上で実行できるようになります。

コミット

commit 783bcba84d253227950fa005bf8214a6350d1104
Author: Shenghou Ma <minux@golang.org>
Date:   Thu Jul 10 15:14:37 2014 -0400

    cmd/5c, cmd/5g, cmd/5l, liblink: nacl/arm support
    
    LGTM=dave, rsc
    R=rsc, iant, dave
    CC=golang-codereviews
    https://golang.org/cl/108360043

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

https://github.com/golang/go/commit/783bcba84d253227950fa005bf8214a6350d1104

元コミット内容

cmd/5c, cmd/5g, cmd/5l, liblink に Native Client (NaCl) の ARM サポートを追加します。

変更の背景

この変更の背景には、Go言語の実行環境を多様なプラットフォームに拡大するというGoプロジェクトの継続的な取り組みがあります。特に、Google Native Client (NaCl) は、ウェブブラウザ内でネイティブコードを安全に実行するためのサンドボックス技術であり、Goプログラムをウェブアプリケーションの一部として利用可能にするための重要なターゲットプラットフォームでした。

当初、GoはNaClのx86アーキテクチャ(32-bitおよび64-bit)をサポートしていましたが、ARMベースのデバイス(例えばChromebooksやAndroidデバイス)の普及に伴い、NaCl環境でのARMサポートの需要が高まりました。このコミットは、GoコンパイラとリンカがARMアーキテクチャ向けのNaClバイナリを生成できるようにすることで、このギャップを埋めることを目的としています。

NaCl環境は、セキュリティ上の理由から、通常のネイティブコードとは異なる厳格な制約(例えば、特定のシステムコールへのアクセス制限、コードの整列要件、間接ジャンプの制限など)を課します。これらの制約に対応するためには、コンパイラとリンカレベルでの特別な処理が必要となります。このコミットは、GoのツールチェーンがこれらのNaCl/ARM固有の制約を適切に処理し、安全かつ効率的なコードを生成するための基盤を構築します。

前提知識の解説

Google Native Client (NaCl)

Google Native Client (NaCl) は、ウェブブラウザ内でネイティブコード(C/C++など)を安全に実行するためのサンドボックス技術です。主な目的は、ウェブアプリケーションのパフォーマンスを向上させつつ、セキュリティを維持することです。NaClは、以下の特徴を持ちます。

  • サンドボックス化: 実行されるコードは厳重に隔離された環境で動作し、ホストシステムへの不正なアクセスを防ぎます。
  • 検証: 実行前にバイナリコードがセキュリティポリシーに準拠しているか検証されます。これには、特定の命令の使用制限や、コードの整列(バンドル化)要件などが含まれます。
  • アーキテクチャ独立性: Portable Native Client (PNaCl) を使用することで、異なるCPUアーキテクチャ(x86, ARMなど)間でバイナリの移植性を実現します。ただし、このコミットはPNaClではなく、特定のアーキテクチャ(ARM)向けのNaClバイナリ生成に関するものです。

Goコンパイラとリンカの役割

Go言語のビルドプロセスでは、以下の主要なツールが関与します。

  • cmd/5c (Go ARMコンパイラ): GoソースコードをARMアセンブリコードにコンパイルします。5c5はARMアーキテクチャを指します(歴史的な経緯による)。
  • cmd/5g (Go ARMコンパイラ): GoソースコードをARMアセンブリコードにコンパイルします。5c5gはGoの初期のコンパイラ設計の名残で、それぞれC言語で書かれたフロントエンドとGo言語で書かれたバックエンドを指すことがありますが、文脈によっては両方ともARMコンパイラを指します。このコミットでは、両者がARMコード生成に関連する変更を受けています。
  • cmd/5l (Go ARMリンカ): コンパイルされたオブジェクトファイルとライブラリを結合し、実行可能なバイナリを生成します。この際、ターゲットプラットフォーム(この場合はNaCl/ARM)固有のフォーマットや制約(例:ELFフォーマットの調整、セクションのアドレス配置)を考慮します。
  • liblink (リンカライブラリ): リンカの共通ロジックやアーキテクチャ固有のアセンブリ生成ルーチンを提供するライブラリです。cmd/5lはこのライブラリを利用して、最終的なバイナリを生成します。

ARMアーキテクチャと命令セット

ARMは、モバイルデバイスや組み込みシステムで広く使用されているRISC(Reduced Instruction Set Computer)アーキテクチャです。NaCl環境では、ARM命令セットのサブセットが許可され、特定の命令(例:システムコール命令)は制限されます。また、NaClのサンドボックスモデルでは、コードの実行が特定の「バンドル」に整列されている必要があります。これは、間接ジャンプのターゲットを制限し、コードの検証を容易にするためです。

技術的詳細

このコミットは、GoツールチェーンがNaCl/ARM環境の特殊な要件を満たすように、複数のコンポーネントにわたる変更を導入しています。

  1. NaCl固有のコード生成の調整:

    • src/cmd/5c/peep.c および src/cmd/5g/peep.c: 命令の最適化(peephole optimization)を行う部分で、naclフラグが導入されています。これにより、NaCl環境では特定のメモリアドレッシングモード(D_SHIFTなど)が制限されるため、それらの使用を避けるようになります。
    • src/cmd/5c/reg.c および src/cmd/5g/reg.c: レジスタ割り当てのロジックが変更され、mod/divランタイムルーチンがレジスタR12を破壊する可能性があるため、R12が使用済みとしてマークされるようになります。これはNaCl環境でのレジスタ使用規約に合わせた調整と考えられます。
    • src/cmd/5c/swt.c および src/cmd/5g/cgen.c, src/cmd/5g/ggen.c: naclフラグに基づいて、特定の最適化(例:スイッチ文の直接ジャンプテーブル生成、MOVW命令の最適化、duffzeroduffcopyのような最適化されたメモリ操作ルーチンの使用)を無効化しています。これは、NaClのコード検証器がこれらの最適化によって生成される特定の命令パターンを許可しないため、またはコードの整列要件に違反する可能性があるためです。
  2. リンカのNaCl/ARMサポート:

    • src/cmd/5l/5.out.h: 新しい命令タイプ ADATABUNDLEADATABUNDLEEND が追加されています。これらはNaClのコードバンドル整列要件に対応するためのものです。
    • src/cmd/5l/asm.c および src/cmd/5l/obj.c: リンカがELF形式のNaClバイナリを生成する際に、Hnacl(Host Native Client)タイプを認識し、NaCl固有のヘッダサイズ(HEADR)、関数整列(funcalign)、テキストセクションとデータセクションの初期アドレス(INITTEXT, INITDAT)を設定するようになります。これは、NaClバイナリが特定のメモリレイアウトと整列要件を持つためです。
    • src/liblink/asm5.c: このファイルが最も広範な変更を受けており、NaCl/ARM固有のアセンブリ生成ロジックが実装されています。
      • asmoutnacl関数が導入され、NaCl環境での命令アセンブルを担当します。この関数は、命令のサイズを計算し、必要に応じてパディングを追加してコードを16バイト境界に整列させます。
      • AUNDEF(未定義命令)やAPLD(プリロード命令)のような特定の命令が、NaCl環境では異なる命令(例:NACL_INSTR_ARM_ABORT_NOW)に置き換えられます。
      • AB(無条件分岐)やABL(関数呼び出し)命令が、NaClの制約(間接ジャンプの制限)に対応するために、レジスタを介した分岐に変換される場合があります。特に、runtime.deferreturnのような特定の関数呼び出しでは、コードの整列を保証するためにPC(プログラムカウンタ)の調整が行われます。
      • MOVW.W x(R13), PCのようなスタックポインタ(R13)からのロードとPCへの書き込みを伴う命令(関数からのリターンによく使われる)が、NaClの制約に合わせて複数の命令に展開されます。これは、PCへの直接書き込みが制限されるため、R12レジスタを介した間接ジャンプに変換されます。
      • 非常に大きなフレームサイズを持つ関数プロローグ(MOVW.W R14,-100004(R13)のような命令)が、ADD命令とMOVW命令の2つに分割されます。これは、単一の命令で表現できない大きなオフセットを扱うためです。
      • MOVW Rx, X(Ry)のようなロード/ストア命令で、レジスタRyR9R13でない場合に、アドレス計算のためにR11レジスタを一時的に使用するよう命令が分割されることがあります。これは、NaCl環境でのレジスタ使用の制約や、特定のメモリアドレッシングモードの制限に対応するためです。
      • ADATABUNDLEADATABUNDLEEND命令が、リテラルプール(定数データが配置される領域)の整列を保証するために使用されます。ADATABUNDLEは新しいデータバンドルの開始を示し、ADATABUNDLEENDはパディングを追加して次の命令を16バイト境界に整列させます。
    • src/liblink/sym.c: TLS(Thread Local Storage)オフセットの初期化にARMアーキテクチャ('5')のケースが追加されています。

これらの変更は、Goのコンパイラとリンカが、NaClの厳格なサンドボックスモデルとARMアーキテクチャの特性の両方に対応できるようにするためのものです。特に、コードの整列、間接ジャンプの制限、特定の命令の使用制限といったNaCl固有の要件を満たすための複雑なアセンブリ生成ロジックがliblink/asm5.cに集約されています。

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

このコミットのコアとなる変更は、主にsrc/liblink/asm5.cに集中しています。特に、asmoutnacl関数の追加と、span5関数におけるNaCl固有の命令アセンブルロジックの導入が重要です。

src/liblink/asm5.c の変更点

  • asmoutnacl 関数の追加: この関数は、NaCl環境向けにARM命令をアセンブルする主要なロジックを含んでいます。命令のサイズ計算、16バイト境界へのパディング、特定の命令のNaCl互換な代替命令への変換(例: AUNDEF -> NACL_INSTR_ARM_ABORT_NOW)、分岐命令の変換(レジスタを介した分岐)、スタックポインタ操作を伴うリターン命令の展開、大きなオフセットを持つロード/ストア命令の分割などを行います。

    static int
    asmoutnacl(Link *ctxt, int32 origPC, Prog *p, Optab *o, int32 *out)
    {
        // ... (詳細な実装は上記コミット内容を参照)
    }
    
  • span5 関数における asmoutnacl の呼び出し: span5関数は、Goのリンカにおけるコードレイアウトと命令のサイズ計算を行う主要な関数です。この関数内で、ctxt->headtype == Hnacl(ターゲットがNaClの場合)の条件分岐が追加され、通常のoplookの代わりにasmoutnaclを呼び出して命令のサイズを計算し、必要に応じてPC(プログラムカウンタ)を調整するようになります。これにより、NaClのコード整列要件が満たされます。

    void
    span5(Link *ctxt, LSym *cursym)
    {
        // ...
        for(op = p, p = p->link; p != nil || ctxt->blitrl != nil; op = p, p = p->link) {
            // ...
            if(ctxt->headtype != Hnacl) {
                m = o->size;
            } else {
                m = asmoutnacl(ctxt, c, p, o, nil); // サイズ計算とPC調整
                c = p->pc; // asmoutnaclがPCを変更する可能性を考慮
                o = oplook(ctxt, p); // asmoutnaclがpを変更する可能性を考慮
            }
            // ...
        }
        // ...
    }
    
  • リテラルプール(Literal Pool)の管理: checkpoolおよびaddpool関数が変更され、NaCl環境でのリテラルプールの整列(16バイト境界)を保証するために、ADATABUNDLEおよびADATABUNDLEEND命令が挿入されるようになります。

    // src/liblink/asm5.c
    static int
    checkpool(Link *ctxt, Prog *p, int sz)
    {
        // ...
        if(ctxt->headtype == Hnacl && pool.size % 16 != 0) {
            // ... ADATABUNDLEEND を挿入して整列
        }
        // ...
    }
    
    // src/liblink/asm5.c
    void
    addpool(Link *ctxt, Prog *p, Addr *a)
    {
        // ...
        if(ctxt->headtype == Hnacl && pool.size%16 == 0) {
            // ... ADATABUNDLE を挿入して新しいデータバンドルを開始
        }
        // ...
    }
    

その他の主要な変更箇所

  • src/cmd/5c/peep.c, src/cmd/5g/peep.c: naclフラグに基づくメモリアドレッシングモードの制限。
  • src/cmd/5c/reg.c, src/cmd/5g/reg.c: mod/divルーチンによるR12レジスタの破壊を考慮したレジスタ割り当て。
  • src/cmd/5l/5.out.h: ADATABUNDLE, ADATABUNDLEEND 命令の定義追加。
  • src/cmd/5l/obj.c: Hnaclターゲットに対するリンカの初期設定(HEADR, funcalign, INITTEXT, INITDAT, INITRND)。

コアとなるコードの解説

asmoutnacl 関数

asmoutnacl関数は、NaCl/ARMバイナリ生成の心臓部です。NaClのサンドボックスは、セキュリティと検証の目的で、実行されるコードに非常に厳格な制約を課します。特に、ARMアーキテクチャでは、命令が特定の「バンドル」に整列されている必要があり、間接ジャンプのターゲットも制限されます。

この関数は、Goコンパイラが生成した抽象的な命令(Prog構造体で表現される)を受け取り、それを実際のARMマシンコードに変換する際に、以下のNaCl固有の処理を行います。

  1. 命令の整列とパディング: NaClでは、命令は16バイト境界に整列された「バンドル」内に配置される必要があります。asmoutnaclは、命令のPC(プログラムカウンタ)を調整し、必要に応じてNOP(No Operation)命令や特別なパディング命令を挿入して、次の命令が正しい境界から始まるようにします。これは、p->pc = (p->pc+15) & ~15; のようなコードで実現されます。
  2. 特殊命令の変換:
    • AUNDEF (未定義命令) は、NaCl環境では NACL_INSTR_ARM_ABORT_NOW (UDF #0xEDE0) という特別な命令に変換されます。これは、未定義の動作が発生した場合に安全にプログラムを終了させるためのNaCl固有のメカニズムです。
    • APLD (プリロード命令) は、NaClでは許可されないため、MOVW R1, R1 (NOPに相当) に変換されます。
  3. 分岐命令の処理:
    • AB (無条件分岐) や ABL (関数呼び出し) のような分岐命令は、NaClの制約により、直接的なPC相対分岐が制限される場合があります。asmoutnaclは、これらの命令をレジスタを介した間接分岐(例: BIC $0xc000000f, RxBX Rx の組み合わせ)に変換することがあります。これは、NaClのコード検証器が間接ジャンプのターゲットをより厳密にチェックできるようにするためです。
    • 特に、runtime.deferreturnのようなGoランタイムの重要な関数への呼び出しでは、コードの整列を保証するために、呼び出し命令のPCが厳密に調整されます。
  4. スタックポインタ操作とリターン命令の展開: Goの関数からのリターンでは、スタックポインタ(ARMのR13)からのロードとPCへの書き込み(MOVW.W x(R13), PC)がよく使われます。NaClではPCへの直接書き込みが制限されるため、この命令は複数の命令に展開されます。具体的には、ロードされた値が一時的にR12レジスタに格納され、その後R12を介して間接的にPCにジャンプします。これにより、NaClのセキュリティモデルに準拠しつつ、Goのリターンメカニズムをエミュレートします。
  5. 大きなオフセットを持つロード/ストア命令の分割: ARM命令は、ロード/ストア命令のオフセット値に制限があります。Goコンパイラが非常に大きなスタックフレームを持つ関数を生成する場合、スタックポインタからのオフセットが命令の許容範囲を超えることがあります。asmoutnaclは、このような命令をADD命令とMOVW命令の2つに分割することで対応します。例えば、MOVW.W R14,-100004(R13)ADD $-100004, R13MOVW R14, 0(R13) に変換されます。
  6. レジスタ使用の調整: 特定のロード/ストア命令で、アドレス計算に使用されるレジスタがR9R13でない場合、R11レジスタを一時的に使用してアドレスを計算し、その後ロード/ストアを実行するように命令が分割されることがあります。これは、NaCl環境でのレジスタ使用の制約や、特定のメモリアドレッシングモードの制限に対応するためです。

リテラルプールとデータバンドル

NaClでは、コードとデータが混在するセクション(特にリテラルプール)も16バイト境界に整列された「データバンドル」として扱われる必要があります。ADATABUNDLEADATABUNDLEEND命令は、この目的のために導入されました。

  • ADATABUNDLE: 新しいデータバンドルの開始を示し、その後のデータが16バイト境界から始まるようにします。
  • ADATABUNDLEEND: データバンドルの終端を示し、必要に応じてパディングを挿入して次のデータまたはコードが16バイト境界に整列するようにします。

これらの命令は、リンカがリテラルプールを配置する際に、NaClの整列要件を満たすように自動的に挿入されます。これにより、GoプログラムがNaCl環境で正しくロードされ、検証されることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/5c, src/cmd/5g, src/cmd/5l, src/liblink ディレクトリ)
  • Google Native Client のドキュメント (特にARMアーキテクチャとサンドボックスの制約に関する情報)
  • ARMアーキテクチャリファレンスマニュアル (命令セットとアドレッシングモードの理解のため)
  • Go言語のコンパイラとリンカの内部構造に関する一般的な知識
  • Go言語のコードレビューシステム (Gerrit) のCL (Change-list) ページ
  • Go言語のメーリングリストやフォーラムでの関連する議論 (もしあれば)
  • https://developer.chrome.com/docs/native-client/nacl-and-pnacl/
  • https://go.dev/doc/install/source#go1.4 (Go 1.4 リリースノート、NaClサポートに関する言及がある可能性)
  • https://go.dev/doc/go1.3 (Go 1.3 リリースノート、このコミットがGo 1.3または1.4の期間に属するため)