[インデックス 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ビットシステムでは、この予約されるアドレス空間の開始アドレスが重要になります。以前のバージョンでは、0x00c000000000
(0x00c0ULL<<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<<32
の0x00
の部分を0x00
から0x7f
まで変化させながら、利用可能なアドレスを探すループが導入されました。具体的には、i<<40 | 0x00c0ULL<<32
という計算で、i
が0
から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
:i
は0
から0x7f
(127)まで変化します。i
を40ビット左シフトすることで、64ビットアドレスの上位ビット(特に40ビット目から46ビット目)を操作します。0x00c0ULL<<32
: これは0x00c000000000
というアドレスを表します。|
(ビットOR演算): これにより、i
の値に応じて0x00c000000000
の0x00
の部分が0x00
から0x7f
まで変化するアドレスが生成されます。例えば、i=0
のときは0x00c000000000
、i=1
のときは0x01c000000000
、...、i=0x7f
のときは0x7fc000000000
となります。
- 生成されたアドレス
p
をruntime·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_ENABLED
が1
であり、かつGOHOSTOS-GOARCH
がlinux-amd64
の場合にのみ実行されます。これは、CGOテストが特定の環境に依存するためです。
これらの変更により、Goランタイムは64ビットシステムにおいて、より多様な環境(特にAddressSanitizerのようなツールが使用される環境)でヒープを柔軟にマッピングできるようになり、堅牢性が向上しました。
関連リンク
- Go Issue #5641: https://github.com/golang/go/issues/5641
- Go CL 10126044: https://golang.org/cl/10126044
参考にした情報源リンク
- AddressSanitizer: https://clang.llvm.org/docs/AddressSanitizer.html
mmap
man page: https://man7.org/linux/man-pages/man2/mmap.2.html- Go runtime memory allocation (general concepts): https://go.dev/doc/articles/go_mem (これは一般的な情報源であり、特定のコミット内容に直接関連するものではありませんが、背景知識として有用です。)
- Go source code (for context on
malloc.goc
andSysReserve
): https://github.com/golang/go pthread_create
man page: https://man7.org/linux/man-pages/man3/pthread_create.3.htmlusleep
man page: https://man7.org/linux/man-pages/man3/usleep.3.html