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

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

このコミットは、Go言語のビルドシステムとランタイムにおいて、NetBSD/ARMアーキテクチャでのCgoサポートを追加するものです。これにより、GoプログラムがC言語のコードを呼び出す機能が、NetBSDオペレーティングシステムのARMプロセッサ上で利用可能になります。

コミット

commit 2fdd60b9b66f26720ea571345785285d4037dce5
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Feb 13 01:06:52 2013 +0800

    go/build, runtime/cgo: cgo support for NetBSD/ARM
    
    R=golang-dev, rsc, dave
    CC=golang-dev
    https://golang.org/cl/7229082

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

https://github.com/golang/go/commit/2fdd60b9b66f26720ea571345785285d4037dce5

元コミット内容

go/build, runtime/cgo: cgo support for NetBSD/ARM

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

変更の背景

Go言語は、その設計思想としてクロスプラットフォーム対応を重視しています。Cgoは、GoプログラムからC言語の関数を呼び出すためのメカニズムを提供し、既存のCライブラリとの連携や、特定のプラットフォームの低レベル機能へのアクセスを可能にします。

このコミットが作成された背景には、Go言語がNetBSDオペレーティングシステムのARMアーキテクチャ上でCgoをサポートする必要性がありました。NetBSDは、移植性の高さで知られるUNIX系OSであり、ARMはモバイルデバイスや組み込みシステムで広く利用されるプロセッサアーキテクチャです。これらの組み合わせでCgoを機能させるためには、GoのビルドシステムがNetBSD/ARMをCgoが有効なプラットフォームとして認識し、かつGoランタイムがCgo呼び出しを適切に処理するためのプラットフォーム固有のコード(特にスレッド管理やレジスタの保存・復元)を提供する必要がありました。

特に、Goランタイムの重要な要素であるg (goroutine) と m (machine/OSスレッド) ポインタの管理は、Cgo呼び出し時に複雑になります。Cコードが実行されている間、Goランタイムはこれらのポインタを安全に保存し、CコードからGoコードに戻る際に正確に復元する必要があります。外部のCコードが予期せぬシグナル(例: SIGSEGV)を発生させた場合でも、Goランタイムがクラッシュしないように、これらのポインタを堅牢に管理するメカニズムが求められました。このコミットは、NetBSD/ARM環境におけるこれらの課題を解決することを目的としています。

前提知識の解説

  • Cgo: Go言語の機能の一つで、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりすることを可能にします。これにより、既存のCライブラリをGoプロジェクトで再利用したり、OSの低レベルAPIにアクセスしたりできます。
  • NetBSD: オープンソースのUNIX系オペレーティングシステムの一つで、非常に高い移植性を持つことで知られています。多くの異なるハードウェアアーキテクチャで動作します。
  • ARMアーキテクチャ: Advanced RISC Machineの略で、主にモバイルデバイス、組み込みシステム、IoTデバイスなどで広く使用されているRISC(Reduced Instruction Set Computer)ベースのプロセッサアーキテクチャです。低消費電力と高性能を両立しています。
  • TLS (Thread Local Storage): スレッドローカルストレージは、各スレッドが独自のデータコピーを持つことができるメモリ領域です。グローバル変数とは異なり、TLSに格納されたデータはスレッド間で共有されず、各スレッドが独立してアクセスできます。Cgoのコンテキストでは、Goランタイムのgmポインタをスレッドごとに安全に保存・復元するために利用されます。
  • pthread (POSIX Threads): POSIXスレッドは、UNIX系オペレーティングシステムにおけるスレッドAPIの標準です。マルチスレッドプログラミングにおいて、スレッドの生成、同期、管理などを行うための関数を提供します。Cgoは、GoランタイムがOSスレッドを管理する際にpthreadを使用することがあります。
  • SIGSEGV (Segmentation Fault): セグメンテーション違反は、プログラムがアクセスを許可されていないメモリ領域にアクセスしようとしたときに発生するシグナルです。CgoでCコードを実行中にこのようなエラーが発生した場合、Goランタイムが適切に処理しないと、プログラム全体がクラッシュする可能性があります。
  • Goランタイムの gm:
    • g (goroutine): Go言語の軽量スレッドであるゴルーチンを表す構造体へのポインタです。各ゴルーチンは独自のスタックを持ち、Goランタイムによってスケジューリングされます。
    • m (machine/OS thread): オペレーティングシステムのスレッドを表す構造体へのポインタです。Goランタイムは、mを介してOSスレッドを管理し、その上でゴルーチンを実行します。 Cgo呼び出しでは、GoランタイムがCコードに制御を渡す際に、現在のgmのコンテキストを保存し、Cコードの実行が完了した後にそれらを復元する必要があります。
  • AEABI (ARM Embedded Application Binary Interface): ARMプロセッサ上で動作するソフトウェアのためのバイナリインターフェース標準です。関数呼び出し規約、データ型のアライメント、レジスタの使用方法などを定義しており、異なるコンパイラやツールチェーンで生成されたコード間の互換性を保証します。__aeabi_read_tpは、AEABIの一部として定義される可能性のある、スレッドポインタ(Thread Pointer)を読み取るための関数です。

技術的詳細

このコミットの主要な技術的課題は、NetBSD/ARM環境でCgoがGoランタイムのgmポインタを安全に管理することです。特に、外部のCコードがSIGSEGVのようなシグナルをトリガーした場合でも、Goランタイムがこれらのポインタを正しく認識し、クラッシュを防ぐ必要があります。

この解決策として、TLS (Thread Local Storage) を利用してgmポインタを保存・復元するアプローチが採用されています。ARMアーキテクチャでは、通常、スレッドポインタを保持するための専用レジスタ(例えば、TPIDRURWレジスタ)や、mrc p15命令のようなシステム制御コプロセッサ命令を使用してTLS領域にアクセスします。

コミットで追加されたsrc/pkg/runtime/cgo/gcc_netbsd_arm.cファイルには、以下の重要なアセンブリ関数が含まれています。

  • __aeabi_read_tp: この関数は、ARMのシステム制御コプロセッサ(CP15)のレジスタc13, c0, 3(通常はTLSベースアドレスを保持する)を読み取り、スレッドポインタ(TP)を取得します。この関数はr0レジスタのみを破壊することが許されており、AEABIの規約に従っています。_lwp_getprivateシステムコール(NetBSDの軽量プロセスプライベートデータ取得)も使用されており、これはTLS領域へのアクセス方法の一つです。
  • cgo_tls_get_gm: この関数は、__aeabi_read_tpを呼び出してスレッドポインタを取得した後、そのポインタからオフセット812に格納されているg (R10) と m (R9) ポインタを読み込み、それぞれのレジスタにロードします。
  • cgo_tls_set_gm: cgo_tls_get_gmと同様にスレッドポインタを取得した後、現在のR10R9レジスタに格納されているgmポインタを、スレッドポインタからのオフセット812に書き込みます。

これらのアセンブリ関数は、GoランタイムがCgo呼び出しを行う前後に、現在のゴルーチンとOSスレッドのコンテキスト(gmポインタ)をTLSに保存し、Cコードから戻った後に復元するために使用されます。これにより、Cコードの実行中にGoランタイムのコンテキストが破壊されることを防ぎ、堅牢性を高めます。

また、xinitcgo関数は、初期スレッドのgmポインタをTLSに保存するためにcgo_tls_set_gm()を呼び出します。libcgo_sys_thread_start関数は、新しいOSスレッドをpthread_createで作成し、そのスレッドでGoのコードを実行するためのエントリポイントとしてthreadentryを設定します。threadentry内では、crosscall_arm2を呼び出してGoの関数を実行します。スタックガードの調整も行われ、スタックオーバーフローからの保護を強化しています。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/go/build/build.go
    • cgoEnabledマップに "netbsd/arm": true, のエントリが追加されました。これにより、GoのビルドシステムがNetBSD/ARMプラットフォームでCgoを有効にするようになります。
  2. src/pkg/runtime/cgo/gcc_netbsd_arm.c
    • このファイルは新規作成されました。NetBSD/ARM環境でのCgoサポートに必要なプラットフォーム固有のCコードとアセンブリコードが含まれています。
      • TLS (Thread Local Storage) を利用してGoランタイムのg (goroutine) と m (machine/OSスレッド) ポインタを保存・復元するためのアセンブリ関数 (__aeabi_read_tp, cgo_tls_get_gm, cgo_tls_set_gm)。
      • GoランタイムがCgo呼び出しを行う際に使用するcgo_load_gmcgo_save_gm関数ポインタの初期化。
      • 初期スレッドのCgo初期化を行うxinitcgo関数。
      • 新しいOSスレッドを生成し、Goランタイムのコンテキストで実行するためのlibcgo_sys_thread_start関数とthreadentry関数。

コアとなるコードの解説

src/pkg/go/build/build.go の変更

var cgoEnabled = map[string]bool{
	// ... 既存のエントリ ...
	"linux/arm":     true,
	"netbsd/386":    true,
	"netbsd/amd64":  true,
	"netbsd/arm":    true, // 追加された行
	"openbsd/386":   true,
	"openbsd/amd64": true,
	"windows/386":   true,
	// ...
}

この変更は、Goのビルドシステムに対して、netbsd/armというターゲットプラットフォームがCgoをサポートしていることを明示的に伝えます。これにより、GoコンパイラはNetBSD/ARM向けにCgoを有効にしたバイナリを生成できるようになります。

src/pkg/runtime/cgo/gcc_netbsd_arm.c の新規追加

このファイルは、NetBSD/ARM環境におけるCgoの低レベルな実装を提供します。

// Copyright 2013 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 <sys/types.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include "libcgo.h"

static void *threadentry(void*);

// We have to resort to TLS variable to save g(R10) and
// m(R9). One reason is that external code might trigger
// SIGSEGV, and our runtime.sigtramp don't even know we
// are in external code, and will continue to use R10/R9,
// this might as well result in another SIGSEGV.
// Note: all three functions will clobber R0, and the last
// two can be called from 5c ABI code.
void __aeabi_read_tp(void) __attribute__((naked));
void cgo_tls_set_gm(void) __attribute__((naked));
void cgo_tls_get_gm(void) __attribute__((naked));

void __aeabi_read_tp(void) {
	// this function is only allowed to clobber r0
	__asm__ __volatile__ (
		"mrc p15, 0, r0, c13, c0, 3\n\t" // Read TPIDRURW (Thread ID Register, User Read/Write)
		"cmp r0, #0\n\t"
		"movne pc, lr\n\t"
		"push {r1,r2,r3,r12}\n\t"
		"svc 0x00a0013c\n\t" // _lwp_getprivate (NetBSD specific system call for thread private data)
		"pop {r1,r2,r3,r12}\n\t"
		"mov pc, lr\n\t"
	);
}

// g (R10) at 8(TP), m (R9) at 12(TP)
void cgo_tls_get_gm(void) {
	__asm__ __volatile__ (
		"push {lr}\n\t"
		"bl __aeabi_read_tp\n\t"
		"ldr r10, [r0, #8]\n\t" // Load g from TP + 8 into R10
		"ldr r9, [r0, #12]\n\t" // Load m from TP + 12 into R9
		"pop {pc}\n\t"
	);
}

void cgo_tls_set_gm(void) {
	__asm__ __volatile__ (
		"push {lr}\n\t"
		"bl __aeabi_read_tp\n\t"
		"str r10, [r0, #8]\n\t" // Store g from R10 to TP + 8
		"str r9, [r0, #12]\n\t" // Store m from R9 to TP + 12
		"pop {pc}\n\t"
	);
}

// both cgo_tls_{get,set}_gm can be called from runtime
void (*cgo_load_gm)(void) = cgo_tls_get_gm;
void (*cgo_save_gm)(void) = cgo_tls_set_gm;

static void
xinitcgo(G *g)
{
	pthread_attr_t attr;
	size_t size;
	cgo_tls_set_gm(); // save g and m for the initial thread

	pthread_attr_init(&attr);
	pthread_attr_getstacksize(&attr, &size);
	g->stackguard = (uintptr)&attr - size + 4096;
	pthread_attr_destroy(&attr);
}

void (*initcgo)(G*) = xinitcgo;

void
libcgo_sys_thread_start(ThreadStart *ts)
{
	pthread_attr_t attr;
	sigset_t ign, oset;
	pthread_t p;
	size_t size;
	int err;

	sigfillset(&ign);
	sigprocmask(SIG_SETMASK, &ign, &oset); // Block all signals temporarily

	pthread_attr_init(&attr);
	pthread_attr_getstacksize(&attr, &size);
	ts->g->stackguard = size; // Set stackguard for the new goroutine
	err = pthread_create(&p, &attr, threadentry, ts); // Create new OS thread

	sigprocmask(SIG_SETMASK, &oset, nil); // Restore signal mask

	if (err != 0) {
		fprintf(stderr, "runtime/cgo: pthread_create failed: %s\n", strerror(err));
		abort();
	}
}

extern void crosscall_arm2(void (*fn)(void), void *g, void *m);
static void*
threadentry(void *v)
{
	ThreadStart ts;

	ts = *(ThreadStart*)v;
	free(v);

	ts.g->stackbase = (uintptr)&ts;

	/*
	 * libcgo_sys_thread_start set stackguard to stack size;
	 * change to actual guard pointer.
	 */
	ts.g->stackguard = (uintptr)&ts - ts.g->stackguard + 4096 * 2; // Adjust stackguard

	crosscall_arm2(ts.fn, (void *)ts.g, (void *)ts.m); // Call Go function
	return nil;
}
  • TLSへのgmの保存・復元:
    • __aeabi_read_tpは、ARMのシステムレジスタ(CP15のC13レジスタ)からスレッドポインタ(Thread Pointer)を読み取ります。これは、各スレッドが自身のプライベートデータにアクセスするための基点となるアドレスです。NetBSD固有の_lwp_getprivateシステムコールも使用され、TLS領域へのアクセスを補助します。
    • cgo_tls_get_gmcgo_tls_set_gmは、このスレッドポインタを基点として、オフセット812にGoランタイムのgmポインタをそれぞれロード(取得)またはストア(保存)します。Goランタイムは、Cgo呼び出しを行う際にこれらの関数を呼び出し、Goのコンテキストを安全に退避・復元します。これにより、Cコードの実行中にGoランタイムの重要なポインタが破壊されることを防ぎます。
  • スレッドの初期化と管理:
    • xinitcgoは、Goプログラムの初期スレッドが起動する際に呼び出され、そのスレッドのgmポインタをTLSに保存します。また、スタックガードの設定も行い、スタックオーバーフローを検出できるようにします。
    • libcgo_sys_thread_startは、Goランタイムが新しいOSスレッドを必要とする際に呼び出されます。pthread_createを使用して新しいスレッドを作成し、そのスレッドの開始ルーチンとしてthreadentryを設定します。スレッド作成中にシグナルがブロックされることで、競合状態を防ぎます。
    • threadentryは、新しく作成されたOSスレッドのエントリポイントです。GoランタイムのThreadStart構造体から必要な情報を取得し、スタックベースとスタックガードを適切に設定します。最終的に、crosscall_arm2を呼び出して、Goランタイムが指定したGo関数を新しいOSスレッド上で実行できるようにします。

これらの変更により、NetBSD/ARM環境でCgoが安定して動作するための基盤が確立され、GoプログラムがC言語のライブラリやシステムコールをより堅牢に利用できるようになりました。

関連リンク

参考にした情報源リンク