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

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

このコミットは、Go言語のツールチェインにおいて、ARMアーキテクチャ向けのコンパイル時に使用されるGOARM環境変数の扱いを改善するものです。具体的には、リンカであるcmd/5lGOARMの値を埋め込み、さらにcmd/distツールが実行環境のARMプロセッサの浮動小数点ユニット(FPU)のサポートレベル(VFPv1またはVFPv3)を自動検出して、最適なGOARMの値を設定するように変更しています。これにより、ユーザーが手動でGOARMを設定する手間を省き、より適切なバイナリが生成されるようになります。

コミット

commit 77e42e2108740eefb6eafb630a524dec019ea656
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Mon Oct 22 14:26:36 2012 +0800

    lib9, cmd/dist, cmd/5l: embed GOARM into cmd/5l and auto detect GOARM
    
    R=rsc, dave
    CC=golang-dev
    https://golang.org/cl/6638043

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

https://github.com/golang/go/commit/77e42e2108740eefb6eafb630a524dec019ea656

元コミット内容

lib9, cmd/dist, cmd/5l: embed GOARM into cmd/5l and auto detect GOARM

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

変更の背景

Go言語は、様々なアーキテクチャに対応していますが、特にARMアーキテクチャは多様なプロセッサバージョンと浮動小数点ユニット(FPU)のバリエーションが存在します。これまで、GoプログラムをARM向けにコンパイルする際、ユーザーはGOARM環境変数を手動で設定する必要がありました。このGOARMは、ターゲットとなるARMプロセッサのバージョンやFPUの有無・種類に応じて、コンパイラが生成するコードを最適化するために使われます。

しかし、ユーザーが自身の環境に最適なGOARMの値を常に把握しているとは限りません。誤った設定は、パフォーマンスの低下や、最悪の場合、実行できないバイナリの生成につながる可能性がありました。このコミットの背景には、このようなユーザーの手間を減らし、GoツールチェインがよりインテリジェントにARM環境を検出し、適切なコンパイル設定を自動的に適用できるようにするという目的があります。これにより、Go言語のARMサポートの利便性と堅牢性が向上します。

前提知識の解説

ARMアーキテクチャとGOARM

ARMは、モバイルデバイスや組み込みシステムで広く利用されているCPUアーキテクチャです。ARMプロセッサには、ARMv5、ARMv6、ARMv7など、複数のバージョンが存在し、それぞれ異なる命令セットや機能セットを持っています。

GOARMは、Go言語のコンパイラが32ビットARMアーキテクチャ(GOARCH=arm)向けにコンパイルする際に使用される環境変数です。この変数は、GoコンパイラがどのARMアーキテクチャバージョンに最適化されたバイナリを生成するかを指示します。主な値とその意味は以下の通りです。

  • GOARM=5: ARMv5などの古いARMアーキテクチャ向け。これらのプロセッサはハードウェア浮動小数点ユニット(FPU)を持たないことが多く、ソフトウェアエミュレーションによる浮動小数点演算が行われます。
  • GOARM=6: ARMv6命令セットをターゲットとします。Raspberry Pi Zeroなどのデバイスで利用されます。
  • GOARM=7: ARMv7などの新しいARMアーキテクチャ向け。スマートフォン、タブレット、Raspberry Pi 3/4などで広く使われています。これらのプロセッサは通常、ハードウェアFPU(VFPv3など)を搭載しています。

GOARMの値は、特に浮動小数点演算の処理方法に影響を与えます。ハードウェアFPUが利用できる場合、計算は高速になりますが、FPUを持たないプロセッサでFPU命令を含むバイナリを実行しようとすると、不正な命令エラーが発生します。

浮動小数点ユニット (FPU) と VFP

FPUは、浮動小数点演算を高速に実行するためのプロセッサ内の専用回路です。ARMアーキテクチャでは、VFP (Vector Floating Point) と呼ばれるFPUが広く採用されています。

  • VFPv1: 比較的初期のVFPバージョン。基本的な浮動小数点演算をサポートします。
  • VFPv3: VFPv1よりも新しいバージョンで、より多くのレジスタや命令、そして倍精度浮動小数点演算のサポートが強化されています。

Goツールチェインは、ターゲットのARMプロセッサがどのVFPバージョンをサポートしているかを検出し、それに応じて最適なGOARMの値を決定することで、パフォーマンスと互換性のバランスを取ろうとします。

シグナルハンドリングとsetjmp/longjmp

このコミットでは、ARMプロセッサのFPUサポートを検出するために、特定のFPU命令を「試行」し、それが不正な命令として扱われるかどうかをチェックする手法が用いられています。この際、不正な命令が実行された場合に発生するSIGILL(不正命令シグナル)を捕捉し、プログラムのクラッシュを防ぐために、シグナルハンドリングとsetjmp/longjmpメカニズムが利用されています。

  • SIGILL: プロセッサが認識できない、または現在のモードで実行が許可されていない命令に遭遇したときにOSがプロセスに送信するシグナルです。
  • setjmp / longjmp: C言語の標準ライブラリ関数で、非ローカルジャンプ(関数の呼び出しスタックを飛び越えたジャンプ)を可能にします。setjmpは現在のスタック環境を保存し、longjmpはその保存された環境に復元して実行を再開します。これにより、エラーや例外的な状況から安全に回復することができます。このコミットでは、FPU命令の試行中にSIGILLが発生した場合、シグナルハンドラ内でlongjmpを呼び出すことで、命令の試行箇所から安全に復帰し、FPUの有無を判断しています。

技術的詳細

このコミットの主要な技術的変更点は、GoツールチェインがARM環境のGOARM値を自動検出するメカニズムを導入したことです。

  1. GOARMの埋め込み: src/lib9/goos.cが変更され、getgoarm()関数が追加されました。この関数は、ビルド時に定義されるGOARMマクロ(src/cmd/dist/build.cで定義される)の値を返すように変更されています。これにより、Goのランタイムやツールが自身のビルド時に使用されたGOARMの値をプログラム的に取得できるようになります。

  2. cmd/distによるGOARMの自動検出:

    • src/cmd/dist/arm.cという新しいファイルが追加されました。このファイルには、xgetgoarm()関数が含まれています。
    • xgetgoarm()関数は、現在の実行環境のARMプロセッサがどのVFPバージョンをサポートしているかを検出します。これは、xtryexecfuncというヘルパー関数を使用して、VFPv3専用の命令(vmov.f64 d0, #112に相当するバイトコード0xeeb70b00)と、VFPv1で利用可能な命令(vmov.f64 d0, d0に相当するバイトコード0xeeb00b40)をそれぞれ試行することで行われます。
    • xtryexecfuncは、与えられた関数を実行し、その実行中にSIGILLシグナルが発生したかどうかを検出します。SIGILLが発生した場合(つまり、試行した命令がサポートされていない場合)、xtryexecfuncは0を返し、そうでなければ1を返します。この検出には、setjmplongjmpを用いたシグナルハンドリングが利用されています。
    • xgetgoarm()のロジックは以下の通りです。
      • まずVFPv3命令を試行します。成功すれば"7"(GOARM=7)を返します。
      • VFPv3命令が失敗した場合、次にVFPv1命令を試行します。成功すれば"6"(GOARM=6)を返します。
      • どちらのVFP命令も失敗した場合(つまり、ハードウェアFPUがないか、非常に古いARMプロセッサの場合)、"5"(GOARM=5)を返します。
    • src/cmd/dist/build.cが変更され、init()関数内でGOARM環境変数が設定されていない場合、xgetgoarm()を呼び出して自動検出された値を使用するように変更されました。また、コンパイル時にGOARMの値がCコンパイラのマクロとして渡されるように、install()関数内のコンパイルオプションにもDGOARMが追加されました。
  3. cmd/5lでのGOARMの利用: src/cmd/5l/obj.cが変更され、リンカがGOARM環境変数を取得する際に、従来のgetenv("GOARM")に加えて、新しく追加されたgetgoarm()関数(src/lib9/goos.cで定義)を呼び出すように変更されました。これにより、リンカはビルド時に埋め込まれたGOARMの値、または自動検出されたGOARMの値を優先的に利用できるようになります。

これらの変更により、GoツールチェインはARM環境において、より賢く、自動的に最適なGOARM設定を選択できるようになり、クロスコンパイルや異なるARMデバイス上でのビルドがよりスムーズに行えるようになりました。

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

このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。

  • include/libc.h:

    • extern char* getgoarm(void); の宣言が追加されました。これは、lib9ライブラリで提供されるgetgoarm関数のプロトタイプです。
  • src/cmd/5l/obj.c:

    • リンカ(5l)がGOARM環境変数を取得する箇所が変更されました。以前はgetenv("GOARM")を直接呼び出していましたが、getgoarm()関数を呼び出すように変更されました。これにより、ビルド時に埋め込まれたGOARMの値が利用されるようになります。
  • src/cmd/dist/a.h:

    • char* xgetgoarm(void);int xtryexecfunc(void (*)(void)); の宣言が追加されました。これらは、cmd/distツール内でGOARMの自動検出と命令試行を行うための関数です。
  • src/cmd/dist/arm.c (新規ファイル):

    • xgetgoarm()関数が実装されました。この関数は、VFPv3およびVFPv1命令を試行実行し、成功したかどうかでGOARMの値を("7", "6", "5"のいずれかに)決定します。
    • useVFPv3()useVFPv1()という静的関数が定義され、それぞれ対応するVFP命令のバイトコードをインラインアセンブリで記述しています。
    • xtryexecfunc()関数が実装され、setjmp/longjmpSIGILLシグナルハンドリングを用いて、命令の試行とエラーからの回復を行います。
  • src/cmd/dist/build.c:

    • init()関数内で、GOARM環境変数が設定されていない場合にxgetgoarm()を呼び出して自動検出された値を使用するように変更されました。
    • install()関数内で、Goツールチェインのコンパイル時にGOARMの値がCコンパイラのマクロ(-DGOARM="...")として渡されるように変更されました。
    • cmdenv()関数内で、goarchが"arm"の場合にGOARM環境変数を表示するロジックが追加されました。
  • src/cmd/dist/plan9.c:

    • Plan 9環境向けのxtryexecfunc()のスタブ実装が追加されました。これは常に0を返します。
  • src/cmd/dist/unix.c:

    • Unix系環境向けのxtryexecfunc()の具体的な実装が追加されました。sigjmp_bufsigsetjmp/siglongjmp、そしてSIGILLシグナルハンドラsigillhandを用いて、命令試行中のSIGILLを捕捉し、安全に復帰するメカニズムを提供します。
  • src/cmd/dist/windows.c:

    • Windows環境向けのxtryexecfunc()のスタブ実装が追加されました。これは常に0を返します。
  • src/lib9/goos.c:

    • getgoarm()関数が実装されました。この関数は、ビルド時に定義されたGOARMマクロの値を返します。
  • src/make.bat:

    • Windows向けのビルドスクリプトが変更され、cmd/dist/arm.ccmd/dist/dist.exeのビルドに含められるようになりました。

コアとなるコードの解説

src/cmd/dist/arm.c

このファイルは、ARMアーキテクチャの浮動小数点ユニット(FPU)のサポートレベルを自動検出するロジックの核心です。

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "a.h"

#ifndef __ARMEL__
char *
xgetgoarm(void)
{
	return "6";
}
#else
static void useVFPv3(void);
static void useVFPv1(void);

char *
xgetgoarm(void)
{
	if(xtryexecfunc(useVFPv3))
		return "7";
	else if(xtryexecfunc(useVFPv1))
		return "6";
	return "5";
}

static void
useVFPv3(void)
{
	// try to run VFPv3-only "vmov.f64 d0, #112" instruction
	// we can't use that instruction directly, because we
	// might be compiling with a soft-float only toolchain
	__asm__ __volatile__ (".word 0xeeb70b00");
}

static void
useVFPv1(void)
{
	// try to run "vmov.f64 d0, d0" instruction
	// we can't use that instruction directly, because we
	// might be compiling with a soft-float only toolchain
	__asm__ __volatile__ (".word 0xeeb00b40");
}

#endif
  • #ifndef __ARMEL__ ブロック: ARM環境以外でコンパイルされる場合(例えば、クロスコンパイルのホスト側がARMでない場合)は、デフォルトで"6"を返します。これは、ARM環境でのみFPUの自動検出を行うためです。
  • xgetgoarm(): この関数がFPU検出のメインロジックです。
    • xtryexecfunc(useVFPv3): まず、VFPv3専用の命令(useVFPv3関数内のインラインアセンブリ)を実行しようとします。
      • 成功した場合(xtryexecfuncが1を返す)、その環境はVFPv3をサポートしていると判断し、"7"(GOARM=7)を返します。
    • 失敗した場合(xtryexecfuncが0を返す)、次にxtryexecfunc(useVFPv1)を試行します。
      • 成功した場合、その環境はVFPv1をサポートしていると判断し、"6"(GOARM=6)を返します。
    • どちらも失敗した場合、ハードウェアFPUがないか、非常に古いARMプロセッサであると判断し、"5"(GOARM=5)を返します。
  • useVFPv3()useVFPv1(): これらの関数は、それぞれVFPv3とVFPv1の特定の命令のバイトコードをインラインアセンブリで直接記述しています。
    • ".word 0xeeb70b00": これはVFPv3で導入されたvmov.f64 d0, #112命令のバイトコードです。
    • ".word 0xeeb00b40": これはVFPv1で利用可能なvmov.f64 d0, d0命令のバイトコードです。
    • これらの命令を直接実行することで、OSが不正な命令シグナル(SIGILL)を発生させるかどうかを監視し、FPUの有無を判断します。

src/cmd/dist/unix.c

このファイルは、Unix系システムにおけるxtryexecfuncの実装を提供します。

// ... (省略) ...
#include <setjmp.h>

// ... (省略) ...

sigjmp_buf sigill_jmpbuf;
static void sigillhand(int);
// xtryexecfunc tries to execute function f, if any illegal instruction
// signal received in the course of executing that function, it will
// return 0, otherwise it will return 1.
int
xtryexecfunc(void (*f)(void))
{
	int r;
	r = 0;
	signal(SIGILL, sigillhand); // SIGILLシグナルハンドラを設定
	if(sigsetjmp(sigill_jmpbuf, 1) == 0) { // 現在のコンテキストを保存
		f(); // 関数fを実行
		r = 1; // 正常終了
	}
	signal(SIGILL, SIG_DFL); // シグナルハンドラをデフォルトに戻す
	return r;
}
// SIGILL handler helper
static void
sigillhand(int signum)
{
	USED(signum);
	siglongjmp(sigill_jmpbuf, 1); // 保存されたコンテキストにジャンプ
}
  • sigjmp_buf sigill_jmpbuf;: setjmp/longjmpで使用するバッファです。
  • sigillhand(int signum): SIGILLシグナルが発生したときに呼び出されるハンドラです。このハンドラは、siglongjmp(sigill_jmpbuf, 1)を呼び出すことで、xtryexecfunc内のsigsetjmpが呼び出された場所に戻ります。これにより、不正な命令が実行されてもプログラムがクラッシュすることなく、安全に処理を続行できます。
  • xtryexecfunc(void (*f)(void)):
    1. signal(SIGILL, sigillhand);: SIGILLシグナルが発生した場合にsigillhand関数が呼び出されるように設定します。
    2. if(sigsetjmp(sigill_jmpbuf, 1) == 0): 現在の実行コンテキストをsigill_jmpbufに保存します。sigsetjmpが直接呼び出された場合は0を返します。
    3. f();: 引数として渡された関数(例: useVFPv3useVFPv1)を実行します。
    4. もしf()の実行中に不正な命令が発生しSIGILLが送出されると、sigillhandが呼び出され、siglongjmpによってこのif文のブロックの先頭に戻ります。このとき、sigsetjmpは0以外の値を返すため、f()の後のコードは実行されず、rは0のままになります。
    5. f()が正常に完了した場合、rは1に設定されます。
    6. signal(SIGILL, SIG_DFL);: シグナルハンドラをデフォルトの動作に戻します。
    7. return r;: f()が正常に実行されたか(1)、不正な命令で中断されたか(0)を返します。

src/cmd/5l/obj.c

リンカがGOARMの値を取得する部分の変更です。

// ... (省略) ...
#include "goos.h" // getgoarm()の宣言を含む

// ... (省略) ...

main(int argc, char *argv[])
{
	// ... (省略) ...
	p = getgoarm(); // getenv("GOARM")から変更
	if(p != nil)
		goarm = atoi(p);
	else
		goarm = 5; // デフォルト値
	// ... (省略) ...
}
  • 以前はgetenv("GOARM")を直接呼び出していましたが、getgoarm()関数を呼び出すように変更されました。これにより、src/lib9/goos.cで定義されたgetgoarm()が返す値(ビルド時に埋め込まれたGOARMの値)がリンカに渡されるようになります。これにより、リンカはビルド環境のGOARM設定を自動的に認識し、適切なリンキング処理を行うことができます。

src/lib9/goos.c

GOARMの値をGoのランタイムから取得できるようにする変更です。

// ... (省略) ...

char*
getgoarm(void)
{
	return defgetenv("GOARM", GOARM);
}
  • getgoarm()関数が追加されました。この関数は、GOARMマクロ(ビルド時にCコンパイラによって定義される)の値を返します。defgetenvは、環境変数GOARMが設定されていればその値を、そうでなければGOARMマクロの値を返すヘルパー関数です。これにより、Goのプログラムが自身のビルド時に使用されたGOARMの値をプログラム的に取得できるようになります。

これらの変更により、GoツールチェインはARM環境において、よりインテリジェントにGOARMの値を決定し、適切なバイナリを生成できるようになりました。

関連リンク

参考にした情報源リンク