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

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

このコミットは、Goランタイムにおける64ビットシステムでのヒープメモリマッピングの柔軟性を向上させるものです。特に、SysReserveシステムコールがヒープ領域を予約する際のアドレス選択ロジックが変更され、AddressSanitizerのような外部ツールとの競合を避けるための改善が施されています。また、この変更を検証するための新しいCGOテストが追加されています。

コミット

commit a8ad859c30c8d4c30c38ac41d858c9030d025ddd
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jun 12 18:47:16 2013 +0400

    runtime: more flexible heap memory mapping on 64-bits
    Fixes #5641.
    
    R=golang-dev, dave, daniel.morsing, iant
    CC=golang-dev, kcc
    https://golang.org/cl/10126044

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

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

元コミット内容

runtime: more flexible heap memory mapping on 64-bits Fixes #5641.

このコミットは、64ビットシステムにおけるGoランタイムのヒープメモリマッピングをより柔軟にすることを目的としています。また、Issue #5641を修正します。

変更の背景

Goランタイムは、効率的なメモリ管理のために、起動時に広大な仮想アドレス空間をヒープとして予約します。64ビットシステムでは、この予約されるアドレス空間の開始アドレスが重要になります。以前のバージョンでは、0x00c0000000000x00c0ULL<<32)という特定のアドレス範囲を優先的に使用しようとしていました。

しかし、この固定されたアドレス選択には問題がありました。特に、AddressSanitizer (ASan) のようなメモリデバッグツールは、独自の目的のために特定のアドレス範囲を予約することがあります。コミットメッセージに記載されているように、「0x00c0 causes conflicts with AddressSanitizer which reserves all memory up to 0x0100.」とあり、0x00c0から始まるアドレスがAddressSanitizerが予約する0x0100までのメモリと競合し、Goプログラムが正常に動作しない問題が発生していました。

この競合を解決し、Goランタイムがより多様な環境(特にメモリデバッグツールが使用される環境)で安定して動作できるようにするために、ヒープ予約アドレスの選択に柔軟性を持たせる必要がありました。

前提知識の解説

仮想メモリとヒープ

現代のオペレーティングシステムでは、各プロセスは独自の仮想アドレス空間を持ちます。これにより、物理メモリの配置を気にすることなく、プログラムは連続したメモリ空間を利用できるかのように動作します。Goランタイムは、この仮想アドレス空間の一部を「ヒープ」として管理し、プログラムが動的にメモリを確保する際に使用します。

SysReserveシステムコール

SysReserveは、GoランタイムがOSに対して特定の仮想アドレス範囲を予約するために使用する内部関数です。これは、実際に物理メモリを割り当てるわけではなく、単にそのアドレス範囲が将来的に使用されることをOSに伝えるものです。これにより、Goランタイムは広大なヒープ領域を事前に確保し、必要に応じてその中から物理メモリをコミット(実際に使用可能にする)することができます。

AddressSanitizer (ASan)

AddressSanitizer (ASan) は、Googleによって開発された高速なメモリエラー検出ツールです。コンパイル時にコードに計測(instrumentation)を挿入することで、以下のようなメモリ関連のエラーを検出します。

  • Use-after-free (解放済みメモリの使用)
  • Heap buffer overflow/underflow (ヒープバッファのオーバーフロー/アンダーフロー)
  • Stack buffer overflow/underflow (スタックバッファのオーバーフロー/アンダーフロー)
  • Global buffer overflow/underflow (グローバルバッファのオーバーフロー/アンダーフロー)
  • Use-after-return (リターン後のスタックメモリ使用)
  • Use-after-scope (スコープ外のスタックメモリ使用)
  • Double-free (二重解放)
  • Invalid free (不正な解放)

ASanは、検出のためにシャドウメモリと呼ばれる特別なメモリ領域を使用します。このシャドウメモリは、アプリケーションの仮想アドレス空間の一部を占有し、ASanが監視するメモリ領域の状態を記録します。ASanが特定のアドレス範囲を予約するのは、このシャドウメモリの配置と効率的な動作のためです。

mmapシステムコール

mmapはUnix系システムで利用可能なシステムコールで、ファイルやデバイス、または匿名メモリ領域をプロセスの仮想アドレス空間にマッピングするために使用されます。このコミットのテストコードでは、mmapを使ってGoランタイムが通常ヒープをマッピングするアドレスを意図的に占有し、Goランタイムのヒープ予約ロジックが正しくフォールバックするかを検証しています。

  • MAP_PRIVATE: マッピングがプロセスプライベートであることを示します。
  • MAP_ANONYMOUS: ファイルではなく、匿名メモリ領域をマッピングすることを示します。
  • MAP_FIXED: 指定されたアドレスに正確にマッピングしようとします。そのアドレスが利用できない場合、mmapは失敗します。

技術的詳細

このコミットの主要な変更点は、src/pkg/runtime/malloc.goc内のruntime·mallocinit関数におけるヒープ領域の予約ロジックです。

以前は、Goランタイムはヒープの開始アドレスとして0x00c0ULL<<32(つまり0x00c000000000)を優先的に使用しようとしていました。これは、デバッグのしやすさや、ガベージコレクタが非ポインタブロック内のビットパターンをポインタと誤認識する可能性を減らすための工夫でした。しかし、この固定アドレスがAddressSanitizerのようなツールと競合する問題が浮上しました。

新しいロジックでは、0x00c0ULL<<32から始まるアドレスを試行するだけでなく、0x00c0ULL<<320x00の部分を0x00から0x7fまで変化させながら、利用可能なアドレスを探すループが導入されました。具体的には、i<<40 | 0x00c0ULL<<32という計算で、i0から0x7fまで変化することで、0x00c000000000, 0x01c000000000, ..., 0x7fc000000000といったアドレスを順に試行します。

これにより、もし0x00c000000000がAddressSanitizerなどによって占有されていたとしても、Goランタイムは他の利用可能なアドレスを見つけてヒープを予約できるようになり、より堅牢性が向上しました。

また、この変更を検証するために、misc/cgo/testasan/main.goという新しいテストファイルが追加されました。このテストはCGOを使用して、Goランタイムが通常ヒープをマッピングするアドレス(0x00c000000000)を意図的にmmapで占有します。さらに、別のスレッドで10マイクロ秒ごとに4KBのメモリをmmapで割り当て続け、仮想アドレス空間を断片化させます。この状況下でGoプログラムが正常に動作し、大量のメモリを割り当てられることを確認することで、新しいヒープ予約ロジックの柔軟性と堅牢性を検証しています。

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

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -330,15 +331,17 @@ runtime·mallocinit(void)\
  // 128 GB (MaxMem) should be big enough for now.
  //
  // The code will work with the reservation at any address, but ask
- // SysReserve to use 0x000000c000000000 if possible.
+ // SysReserve to use 0x0000XXc000000000 if possible (XX=00...7f).
  // Allocating a 128 GB region takes away 37 bits, and the amd64
  // doesn't let us choose the top 17 bits, so that leaves the 11 bits
  // in the middle of 0x00c0 for us to choose.  Choosing 0x00c0 means
- // that the valid memory addresses will begin 0x00c0, 0x00c1, ..., 0x0x00df.
+ // that the valid memory addresses will begin 0x00c0, 0x00c1, ..., 0x00df.
  // In little-endian, that's c0 00, c1 00, ..., df 00. None of those are valid
  // UTF-8 sequences, and they are otherwise as far away from 
- // ff (likely a common byte) as possible. An earlier attempt to use 0x11f8 
- // caused out of memory errors on OS X during thread allocations.
+ // ff (likely a common byte) as possible.  If that fails, we try other 0xXXc0
+ // addresses.  An earlier attempt to use 0x11f8 caused out of memory errors
+ // on OS X during thread allocations.  0x00c0 causes conflicts with
+ // AddressSanitizer which reserves all memory up to 0x0100.
  // These choices are both for debuggability and to reduce the
  // odds of the conservative garbage collector not collecting memory
  // because some non-pointer block of memory had a bit pattern
@@ -353,7 +356,12 @@ runtime·mallocinit(void)\
  	tspans_size = arena_size / PageSize * sizeof(runtime·mheap.spans[0]);
  	// round spans_size to pages
  	tspans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1);\
-\t\tp = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + spans_size + arena_size);\
+\t\tfor(i = 0; i <= 0x7f; i++) {\n+\t\t\tp = (void*)(i<<40 | 0x00c0ULL<<32);\n+\t\t\tp = runtime·SysReserve(p, bitmap_size + spans_size + arena_size);\n+\t\t\tif(p != nil)\n+\t\t\t\tbreak;\n+\t\t}\
  	}\
  	if (p == nil) {\
  	\t// On a 32-bit machine, we can't typically get away

misc/cgo/testasan/main.go

--- /dev/null
+++ b/misc/cgo/testasan/main.go
@@ -0,0 +1,49 @@
+// 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.
+
+package main
+
+/*
+#include <sys/mman.h>
+#include <pthread.h>
+#include <unistd.h>
+
+void ctor(void) __attribute__((constructor));
+static void* thread(void*);
+
+void
+ctor(void)
+{
+	// occupy memory where Go runtime would normally map heap
+	mmap((void*)0x00c000000000, 64<<10, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
+
+	// allocate 4K every 10us
+	pthread_t t;
+	pthread_create(&t, 0, thread, 0);
+}
+
+static void*
+thread(void *p)
+{
+	for(;;) {
+		usleep(10000);
+		mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
+	}
+	return 0;
+}
+*/
+import "C"
+
+import (
+	"time"
+)
+
+func main() {
+	// ensure that we can function normally
+	var v [][]byte
+	for i := 0; i < 1000; i++ {
+		time.Sleep(10 * time.Microsecond)
+		v = append(v, make([]byte, 64<<10))
+	}
+}

src/run.bash

--- a/src/run.bash
+++ b/src/run.bash
@@ -108,6 +108,12 @@ esac
 ./test.bash
 ) || exit $?\n
+[ "$CGO_ENABLED" != 1 ] ||
+[ "$GOHOSTOS-$GOARCH" != linux-amd64 ] ||
+(xcd ../misc/cgo/testasan
+go run main.go
+) || exit $?\n
+\n
 (xcd ../doc/progs
 time ./run
 ) || exit $?\n

コアとなるコードの解説

src/pkg/runtime/malloc.gocの変更

  • uint64 i; の追加: ループカウンタとして使用されるi変数が追加されました。
  • ヒープ予約ロジックの変更:
    • 以前はruntime·SysReserve((void*)(0x00c0ULL<<32), ...)のように、0x00c0ULL<<32という固定のアドレスをSysReserveに渡していました。
    • 変更後、for(i = 0; i <= 0x7f; i++)というループが導入されました。
    • ループ内でp = (void*)(i<<40 | 0x00c0ULL<<32);という計算が行われます。
      • i<<40: i0から0x7f(127)まで変化します。iを40ビット左シフトすることで、64ビットアドレスの上位ビット(特に40ビット目から46ビット目)を操作します。
      • 0x00c0ULL<<32: これは0x00c000000000というアドレスを表します。
      • | (ビットOR演算): これにより、iの値に応じて0x00c0000000000x00の部分が0x00から0x7fまで変化するアドレスが生成されます。例えば、i=0のときは0x00c000000000i=1のときは0x01c000000000、...、i=0x7fのときは0x7fc000000000となります。
    • 生成されたアドレスpruntime·SysReserveに渡し、予約が成功した場合はループを抜けます。これにより、GoランタイムはAddressSanitizerなどによって特定のアドレスが占有されている場合でも、他の利用可能なアドレスを見つけてヒープを予約できるようになります。

misc/cgo/testasan/main.goの追加

このファイルは、新しいヒープ予約ロジックをテストするためのCGOプログラムです。

  • CGOの利用: C言語のコードをGoプログラムに埋め込み、Cの標準ライブラリ関数(mmap, pthread_create, usleep)を呼び出しています。
  • ctor関数:
    • __attribute__((constructor))により、この関数はGoプログラムのmain関数が実行される前に呼び出されます。
    • mmap((void*)0x00c000000000, 64<<10, ..., MAP_FIXED, ...): Goランタイムが通常ヒープをマッピングするアドレス0x00c000000000に、意図的に64KBのメモリをマッピングして占有します。これにより、Goランタイムがこのアドレスを使用できない状況をシミュレートします。
    • pthread_create(&t, 0, thread, 0): 新しいスレッドを生成し、thread関数を実行させます。
  • thread関数:
    • 無限ループ内でusleep(10000)(10ミリ秒待機)とmmap(0, 4096, ...)(4KBの匿名メモリを割り当て)を繰り返します。
    • mmap(0, ...)は、OSに任意のアドレスにメモリを割り当てるように要求します。これにより、仮想アドレス空間が断片化され、Goランタイムが連続した大きなヒープ領域を見つけるのが難しくなる状況をシミュレートします。
  • Goのmain関数:
    • for i := 0; i < 1000; i++ { ... v = append(v, make([]byte, 64<<10)) }: 1000回ループし、各ループで64KBのバイトスライスを割り当てます。これにより、Goランタイムが大量のメモリを動的に確保できることを確認します。
    • このテストは、CGOで意図的にGoランタイムのヒープ予約を妨害し、さらに仮想アドレス空間を断片化させた状況でも、Goランタイムが正常にヒープを確保し、プログラムが動作し続けることを検証します。

src/run.bashの変更

  • misc/cgo/testasanディレクトリに移動し、go run main.goを実行するテストステップが追加されました。
  • このテストは、CGO_ENABLED1であり、かつGOHOSTOS-GOARCHlinux-amd64の場合にのみ実行されます。これは、CGOテストが特定の環境に依存するためです。

これらの変更により、Goランタイムは64ビットシステムにおいて、より多様な環境(特にAddressSanitizerのようなツールが使用される環境)でヒープを柔軟にマッピングできるようになり、堅牢性が向上しました。

関連リンク

参考にした情報源リンク