[インデックス 12259] ファイルの概要
このコミットは、Go言語のランタイムプロファイリングツール pprof
がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line
および objdump
というGo独自のツールの追加、そしてpprof
がこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。
コミット
commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 28 16:18:24 2012 -0500
runtime/pprof: support OS X CPU profiling
Work around profiling kernel bug with signal masks.
Still broken on 64-bit Snow Leopard kernel,
but I think we can ignore that one and let people
upgrade to Lion.
Add new trivial tools addr2line and objdump to take
the place of the GNU tools of the same name, since
those are not installed on OS X.
Adapt pprof to invoke 'go tool addr2line' and
'go tool objdump' if the system tools do not exist.
Clean up disassembly of base register on amd64.
Fixes #2008.
R=golang-dev, bradfitz, mikioh.mikioh, r, iant
CC=golang-dev
https://golang.org/cl/5697066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be
元コミット内容
このコミットは、Goのプロファイリングツール pprof
がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。
主な変更点:
- macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
- Go独自の
addr2line
とobjdump
ツールの追加: macOSにはGNUのaddr2line
やobjdump
が標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2line
およびgo tool objdump
として利用されます。 pprof
の適応:pprof
スクリプトが、システムにaddr2line
やobjdump
が存在しない場合に、新しく追加されたgo tool
バージョンを自動的に使用するように変更されました。- amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
- 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。
ファイル変更の概要:
misc/pprof
:addr2line
とobjdump
のフォールバックロジックを追加。src/cmd/addr2line/main.c
:addr2line
ツールのC言語実装を新規追加。src/cmd/objdump/main.c
:objdump
ツールのC言語実装を新規追加。src/cmd/dist/build.c
:addr2line
とobjdump
をビルドプロセスに追加。src/libmach/8db.c
: amd64の逆アセンブルロジックを修正。src/pkg/runtime/*
: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROF
の挙動を制御するための変更。runtime·setprof
関数の追加と、notesleep
,notetsleep
,entersyscall
,exitsyscall
,resetcpuprofiler
,semasleep
などの関数でのsetprof
の呼び出し。src/pkg/runtime/pprof/pprof.go
: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。src/pkg/runtime/pprof/pprof_test.go
: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。
変更の背景
このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。
-
macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用される
SIGPROF
シグナルの配信に関するバグが存在しました。通常、SIGPROF
はCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。 -
GNUツールの非存在:
pprof
ツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2line
やobjdump
といった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。
これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2line
とobjdump
を導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。
-
CPUプロファイリング:
- 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
- サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
SIGPROF
シグナル: Unix系システムでは、ITIMER_PROF
タイマーとSIGPROF
シグナルがCPUプロファイリングによく利用されます。ITIMER_PROF
は、プロセスが消費したCPU時間に基づいて定期的にSIGPROF
シグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
-
pprof
ツール:- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
pprof
形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。 pprof
は、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2line
やobjdump
のような外部ツールが利用されます。
- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
-
addr2line
:- GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
-
objdump
:- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
pprof
では、プロファイルされたコードの逆アセンブル表示に利用されます。
- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
-
シグナルとシグナルマスク:
- シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
- シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。
SIG_BLOCK
はシグナルをブロックし、SIG_UNBLOCK
はブロックを解除します。
-
Goランタイム:
- Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
-
Goのビルドシステム (
src/cmd/dist
):- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
addr2line
やobjdump
)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。
- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。
技術的詳細
このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。
1. macOSカーネルのプロファイリングバグへの回避策
コミットメッセージとsrc/pkg/runtime/pprof/pprof.go
のコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROF
シグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROF
が誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。
Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。
runtime·setprof(bool on)
関数の導入: この新しい関数は、現在のスレッドがSIGPROF
シグナルを受信できるかどうかを制御します。on
がtrue
の場合(プロファイリングを有効にする場合)、SIGPROF
がシグナルマスクからSIG_UNBLOCK
されます。on
がfalse
の場合(プロファイリングを一時停止する場合)、SIGPROF
がシグナルマスクにSIG_BLOCK
されます。
- ブロッキングシステムコール中のプロファイリング一時停止:
runtime·notesleep
(セマフォ待機),runtime·notetsleep
(タイムアウト付きセマフォ待機),runtime·entersyscall
(システムコール開始),runtime·exitsyscall
(システムコール終了),runtime·semasleep
(セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)
を呼び出してSIGPROF
をブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)
を呼び出してSIGPROF
のブロックを解除するようにしました。- このアプローチにより、カーネルは
SIGPROF
をスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
runtime·resetcpuprofiler
の変更: CPUプロファイラのリセット関数も、SIGPROF
のハンドリングをruntime·setprof
に委譲するように変更されました。
ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROF
が配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。
2. Go独自のaddr2line
とobjdump
ツールの追加
macOSにはGNU Binutilsのaddr2line
やobjdump
が標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。
src/cmd/addr2line/main.c
:- このファイルは、
addr2line
のGoバージョンをC言語で実装しています。 - Goの内部ライブラリである
libmach
(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。 - 標準入力からメモリアドレスを読み込み、対応する関数名と
ファイル:行番号
の形式で標準出力に出力します。これは、pprof
がプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
- このファイルは、
src/cmd/objdump/main.c
:- このファイルは、
objdump
のGoバージョンをC言語で実装しています。 - これも
libmach
を利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。 pprof
がコードの逆アセンブル表示を行う際に利用されます。
- このファイルは、
src/cmd/dist/build.c
の変更:- Goのビルドシステムに、これらの新しいツール(
cmd/addr2line
とcmd/objdump
)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go tool
コマンドを通じて利用可能になります。
- Goのビルドシステムに、これらの新しいツール(
3. pprof
ツールの適応
既存のmisc/pprof
スクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。
- フォールバックロジック:
pprof
スクリプトは、addr2line
やobjdump
コマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")
のようなコマンドでチェックします。 go tool
の利用: もしシステムツールが見つからない場合、pprof
は自動的にgo tool addr2line
やgo tool objdump
にフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。
4. その他の改善
src/libmach/8db.c
の修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。src/pkg/runtime/signal_darwin_386.c
およびsrc/pkg/runtime/signal_darwin_amd64.c
の変更:SIGPROF
ハンドラ内で、gp != m->g0 && gp != m->gsignal
という条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0
やgsignal
)に配信された場合にはruntime·sigprof
を呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。
これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。
-
misc/pprof
:Disassemble
サブルーチン内で、objdump
が見つからない場合にgo tool objdump
にフォールバックするロジックが追加されました。MapToSymbols
サブルーチン内で、addr2line
が見つからない場合にgo tool addr2line
にフォールバックするロジックが追加されました。
--- a/misc/pprof +++ b/misc/pprof @@ -1234,6 +1234,13 @@ sub Disassemble { my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " . "--start-address=0x$start_addr " . "--stop-address=0x$end_addr $prog"); + + if (system("$objdump --help >/dev/null 2>&1") != 0) { + # objdump must not exist. Fall back to go tool objdump. + $objdump = "go tool objdump"; + $cmd = "$objdump $prog 0x$start_addr 0x$end_addr"; + } + open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n"); my @result = (); my $filename = ""; @@ -4391,6 +4398,12 @@ sub MapToSymbols { $cmd = "$addr2line --demangle -f -C -e $image"; } + if (system("$addr2line --help >/dev/null 2>&1") != 0) { + # addr2line must not exist. Fall back to go tool addr2line. + $addr2line = "go tool addr2line"; + $cmd = "$addr2line $image"; + } + # If "addr2line" isn't installed on the system at all, just use # nm to get what info we can (function names, but not line numbers). if (system("$addr2line --help >/dev/null 2>&1") != 0) {
-
src/cmd/addr2line/main.c
(新規ファイル):- Go独自の
addr2line
ツールのC言語実装。crackhdr
,syminit
,findsym
,fileline
といったlibmach
の関数を使用して、バイナリからシンボル情報を取得します。
// 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. /* * addr2line simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd; char *p; uvlong pc; Symbol s; Fhdr fhdr; Biobuf bin, bout; char file[1024]; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); Binit(&bin, 0, OREAD); Binit(&bout, 1, OWRITE); for(;;) { p = Brdline(&bin, '\n'); if(p == nil) break; p[Blinelen(&bin)-1] = '\0'; pc = strtoull(p, 0, 16); if(!findsym(pc, CTEXT, &s)) s.name = "??"; if(!fileline(file, sizeof file, pc)) strcpy(file, "??:0"); Bprint(&bout, "%s\n%s\n", s.name, file); } Bflush(&bout); exits(0); }
- Go独自の
-
src/cmd/objdump/main.c
(新規ファイル):- Go独自の
objdump
ツールのC言語実装。crackhdr
,syminit
,loadmap
,machdata->das
,machdata->instsize
といったlibmach
の関数を使用して、バイナリを逆アセンブルします。
// 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. /* * objdump simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd, n; uvlong pc, start, stop; Fhdr fhdr; Biobuf bout; char buf[1024]; Map *text; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); text = loadmap(nil, fd, &fhdr); if(text == nil) sysfatal("loadmap: %r"); Binit(&bout, 1, OWRITE); for(pc=start; pc<stop; ) { if(fileline(buf, sizeof buf, pc)) Bprint(&bout, "%s\n", buf); buf[0] = '\0'; machdata->das(text, pc, 0, buf, sizeof buf); Bprint(&bout, " %llx: %s\n", pc, buf); n = machdata->instsize(text, pc); if(n <= 0) break; pc += n; } Bflush(&bout); exits(0); }
- Go独自の
-
src/pkg/runtime/runtime.h
:runtime·setprof
関数の宣言が追加されました。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -730,3 +730,13 @@ bool runtime·showframe(Func*); void runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*); uintptr runtime·memlimit(void); + +// If appropriate, ask the operating system to control whether this +// thread should receive profiling signals. This is only necessary on OS X. +// An operating system should not deliver a profiling signal to a +// thread that is not actually executing (what good is that?), but that's +// what OS X prefers to do. When profiling is turned on, we mask +// away the profiling signal when threads go to sleep, so that OS X +// is forced to deliver the signal to a thread that's actually running. +// This is a no-op on other systems. +void runtime·setprof(bool);
-
src/pkg/runtime/thread_darwin.c
:runtime·setprof
関数の実装が追加されました。これはSIGPROF
シグナルをブロックまたはアンブロックするためにruntime·sigprocmask
を呼び出します。runtime·semasleep
内でruntime·setprof
が呼び出され、セマフォ待機中にプロファイリングを一時停止します。runtime·minit
内で、プロファイリングが有効な場合にSIGPROF
をアンブロックするように初期シグナルマスクを設定します。
--- a/src/pkg/runtime/thread_darwin.c +++ b/src/pkg/runtime/thread_darwin.c @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[]; static Sigset sigset_all = ~(Sigset)0; static Sigset sigset_none; +static Sigset sigset_prof = 1<<(SIGPROF-1); static void unimplemented(int8 *name) @@ -23,7 +24,14 @@ unimplemented(int8 *name) int32 runtime·semasleep(int64 ns) { -\treturn runtime·mach_semacquire(m->waitsema, ns); +\tint32 v; +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\ +\tv = runtime·mach_semacquire(m->waitsema, ns);\ +\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\ +\treturn v; } void @@ -98,7 +106,11 @@ runtime·minit(void) // Initialize signal handling. m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\ -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil); +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\ +\telse +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\ } // Mach IPC, to get at semaphores @@ -434,3 +446,34 @@ runtime·memlimit(void) // the limit. return 0; } + +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF +// signal is not guaranteed to be sent to the thread that was executing to +// cause it to expire. It can and often does go to a sleeping thread, which is +// not interesting for our profile. This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225. +// To work around this bug, we disable receipt of the profiling signal on +// a thread while in blocking system calls. This forces the kernel to deliver +// the profiling signal to an executing thread. +// +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel. +// In that configuration, the kernel appears to want to deliver SIGPROF to the +// sleeping threads regardless of signal mask and, worse, does not deliver +// the signal until the thread wakes up on its own. +// +// If necessary, we can switch to using ITIMER_REAL for OS X and handle +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver +// to all the running threads. SIGALRM does not appear to be affected by +// the 64-bit Snow Leopard bug. However, as of this writing Mountain Lion +// is in preview, making Snow Leopard two versions old, so it is unclear how +// much effort we need to spend on one buggy kernel. + +// Control whether profiling signal can be delivered to this thread. +void +runtime·setprof(bool on) +{ +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+}
-
src/pkg/runtime/proc.c
:runtime·entersyscall
とruntime·exitsyscall
内でruntime·setprof
が呼び出され、システムコール中にプロファイリングを一時停止します。
--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -916,6 +915,9 @@ runtime·entersyscall(void) { uint32 v; +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n // Leave SP around for gc and traceback. runtime·gosave(&g->sched); g->gcsp = g->sched.sp; @@ -979,6 +981,9 @@ runtime·exitsyscall(void) // Garbage collector isn't running (since we are), // so okay to clear gcstack. g->gcstack = nil; +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\ return; }
これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprof
ツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。
コアとなるコードの解説
ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。
1. misc/pprof
におけるフォールバックロジック
if (system("$objdump --help >/dev/null 2>&1") != 0) {
# objdump must not exist. Fall back to go tool objdump.
$objdump = "go tool objdump";
$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
}
このPerlコードスニペットは、pprof
スクリプトがobjdump
コマンドを呼び出す前に実行されるチェックです。
system("$objdump --help >/dev/null 2>&1") != 0
: これは、現在のシステムパスでobjdump
コマンドが見つかるかどうか、または実行可能であるかどうかをテストします。$objdump --help
:objdump
コマンドに--help
オプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。>/dev/null 2>&1
: 標準出力と標準エラー出力を/dev/null
にリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。system(...) != 0
:system
関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0
であれば成功(コマンドが見つかり、実行できた)、0
以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
# objdump must not exist. Fall back to go tool objdump.
: コマンドが見つからなかった場合のコメントです。$objdump = "go tool objdump";
:objdump
コマンドのパスを、Goツールチェーンに含まれるgo tool objdump
に再設定します。$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
: 実際に実行するコマンド文字列を、新しいobjdump
パスとGoツールに合わせた引数形式で再構築します。
同様のロジックがaddr2line
に対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。
2. src/cmd/addr2line/main.c
とsrc/cmd/objdump/main.c
の新規追加
これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2line
とobjdump
の実装です。C言語で書かれており、Goの内部ライブラリであるlibmach
を利用しています。
libmach
の利用:libmach
は、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。crackhdr(fd, &fhdr)
: ファイルディスクリプタfd
から実行可能ファイルのヘッダ情報を解析し、fhdr
構造体に格納します。syminit(fd, &fhdr)
: シンボルテーブルを初期化し、シンボル情報をメモリにロードします。findsym(pc, CTEXT, &s)
: 指定されたプログラムカウンタpc
に対応する関数シンボルs
を検索します。fileline(file, sizeof file, pc)
: 指定されたプログラムカウンタpc
に対応するソースファイル名と行番号を取得します。machdata->das(text, pc, 0, buf, sizeof buf)
: 指定されたテキストセクションtext
内のプログラムカウンタpc
から命令を逆アセンブルし、結果をbuf
に格納します。machdata->instsize(text, pc)
: 指定されたプログラムカウンタpc
にある命令のサイズを返します。
これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprof
がプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。
3. runtime·setprof(bool on)
関数の導入と利用
// src/pkg/runtime/runtime.h
void runtime·setprof(bool);
// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
if(on)
runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
else
runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
runtime·setprof
の役割: この関数は、現在のスレッドがSIGPROF
シグナルを受信できるかどうかを制御します。on
がtrue
の場合、SIGPROF
はシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。on
がfalse
の場合、SIGPROF
はシグナルマスクに追加され、スレッドはシグナルをブロックします。sigset_prof
:SIGPROF
シグナルのみを含むシグナルセットです。runtime·sigprocmask
: Unix系システムコールsigprocmask
のGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCK
は指定されたシグナルをマスクから削除し、SIG_BLOCK
は追加します。
この関数は、Goランタイムの様々な場所で呼び出されます。
-
src/pkg/runtime/lock_futex.c
とsrc/pkg/runtime/lock_sema.c
:runtime·notesleep
(セマフォ待機) やruntime·notetsleep
(タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)
を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)
を呼び出します。- これにより、スレッドがスリープしている間は
SIGPROF
がブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
-
src/pkg/runtime/proc.c
:runtime·entersyscall
(システムコールに入る前) とruntime·exitsyscall
(システムコールから出る後) でruntime·setprof
が呼び出されます。- システムコール中はスレッドがカーネルモードで実行されるため、この間も
SIGPROF
をブロックすることで、プロファイリングの精度を維持しようとします。
このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。
4. src/pkg/runtime/signal_darwin_386.c
およびsrc/pkg/runtime/signal_darwin_amd64.c
のSIGPROF
ハンドラ
if(sig == SIGPROF) {
if(gp != m->g0 && gp != m->gsignal)
runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
return;
}
このコードは、SIGPROF
シグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。
if(gp != m->g0 && gp != m->gsignal)
: この条件は非常に重要です。gp
は現在のゴルーチン(Goの軽量スレッド)へのポインタです。m->g0
: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。m->gsignal
: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
- この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
- 目的: Goランタイムの内部ゴルーチンが
SIGPROF
を受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。
これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。
関連リンク
- Go Issue #2008: runtime/pprof: support OS X CPU profiling
- Go Change List 5697066: runtime/pprof: support OS X CPU profiling
- Apple Bug Report #9177434 (Goのコメントで言及):
- 直接的な公開リンクは見つかりませんが、Goのソースコードコメントで言及されています。
- http://code.google.com/p/go/source/detail?r=35b716c94225 (これはGoの古いSVNリポジトリの変更履歴へのリンクで、Appleのバグ報告に関する詳細なコメントが含まれています。)
参考にした情報源リンク
- Go Issue #2008: https://github.com/golang/go/issues/2008
- Go Change List 5697066: https://golang.org/cl/5697066
- GNU Binutils - addr2line: https://sourceware.org/binutils/docs/binutils/addr2line.html
- GNU Binutils - objdump: https://sourceware.org/binutils/docs/binutils/objdump.html
- Go pprof documentation: https://pkg.go.dev/runtime/pprof
- Go blog: Profiling Go Programs: https://go.dev/blog/pprof
- Unix signals (general concept): https://en.wikipedia.org/wiki/Signal_(IPC)
- sigprocmask(2) - Linux man page: https://man7.org/linux/man-pages/man2/sigprocmask.2.html
- ITIMER_PROF: https://man7.org/linux/man-pages/man2/setitimer.2.html
- Go source code comments (specifically in
src/pkg/runtime/thread_darwin.c
andsrc/pkg/runtime/pprof/pprof.go
related to Apple Bug Report #9177434): - Go
libmach
(internal library for Mach-O parsing): (具体的な公開ドキュメントは少ないが、Goのソースコード内で利用されている)src/libmach/
ディレクトリ内のファイル群
これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。 I have completed the request. I have read the commit data, performed web searches for context, and generated a comprehensive technical explanation in Markdown format, adhering to all specified sections and requirements. The output is printed to standard output only, as requested.# [インデックス 12259] ファイルの概要
このコミットは、Go言語のランタイムプロファイリングツール pprof
がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line
および objdump
というGo独自のツールの追加、そしてpprof
がこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。
コミット
commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 28 16:18:24 2012 -0500
runtime/pprof: support OS X CPU profiling
Work around profiling kernel bug with signal masks.
Still broken on 64-bit Snow Leopard kernel,
but I think we can ignore that one and let people
upgrade to Lion.
Add new trivial tools addr2line and objdump to take
the place of the GNU tools of the same name, since
those are not installed on OS X.
Adapt pprof to invoke 'go tool addr2line' and
'go tool objdump' if the system tools do not exist.
Clean up disassembly of base register on amd64.
Fixes #2008.
R=golang-dev, bradfitz, mikioh.mikioh, r, iant
CC=golang-dev
https://golang.org/cl/5697066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be
元コミット内容
このコミットは、Goのプロファイリングツール pprof
がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。
主な変更点:
- macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
- Go独自の
addr2line
とobjdump
ツールの追加: macOSにはGNUのaddr2line
やobjdump
が標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2line
およびgo tool objdump
として利用されます。 pprof
の適応:pprof
スクリプトが、システムにaddr2line
やobjdump
が存在しない場合に、新しく追加されたgo tool
バージョンを自動的に使用するように変更されました。- amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
- 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。
ファイル変更の概要:
misc/pprof
:addr2line
とobjdump
のフォールバックロジックを追加。src/cmd/addr2line/main.c
:addr2line
ツールのC言語実装を新規追加。src/cmd/objdump/main.c
:objdump
ツールのC言語実装を新規追加。src/cmd/dist/build.c
:addr2line
とobjdump
をビルドプロセスに追加。src/libmach/8db.c
: amd64の逆アセンブルロジックを修正。src/pkg/runtime/*
: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROF
の挙動を制御するための変更。runtime·setprof
関数の追加と、notesleep
,notetsleep
,entersyscall
,exitsyscall
,resetcpuprofiler
,semasleep
などの関数でのsetprof
の呼び出し。src/pkg/runtime/pprof/pprof.go
: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。src/pkg/runtime/pprof/pprof_test.go
: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。
変更の背景
このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。
-
macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用される
SIGPROF
シグナルの配信に関するバグが存在しました。通常、SIGPROF
はCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。 -
GNUツールの非存在:
pprof
ツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2line
やobjdump
といった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。
これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2line
とobjdump
を導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。
-
CPUプロファイリング:
- 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
- サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
SIGPROF
シグナル: Unix系システムでは、ITIMER_PROF
タイマーとSIGPROF
シグナルがCPUプロファイリングによく利用されます。ITIMER_PROF
は、プロセスが消費したCPU時間に基づいて定期的にSIGPROF
シグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
-
pprof
ツール:- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
pprof
形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。 pprof
は、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2line
やobjdump
のような外部ツールが利用されます。
- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
-
addr2line
:- GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
-
objdump
:- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
pprof
では、プロファイルされたコードの逆アセンブル表示に利用されます。
- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
-
シグナルとシグナルマスク:
- シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
- シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。
SIG_BLOCK
はシグナルをブロックし、SIG_UNBLOCK
はブロックを解除します。
-
Goランタイム:
- Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
-
Goのビルドシステム (
src/cmd/dist
):- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
addr2line
やobjdump
)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。
- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。
技術的詳細
このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。
1. macOSカーネルのプロファイリングバグへの回避策
コミットメッセージとsrc/pkg/runtime/pprof/pprof.go
のコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROF
シグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROF
が誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。
Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。
runtime·setprof(bool on)
関数の導入: この新しい関数は、現在のスレッドがSIGPROF
シグナルを受信できるかどうかを制御します。on
がtrue
の場合(プロファイリングを有効にする場合)、SIGPROF
がシグナルマスクからSIG_UNBLOCK
されます。on
がfalse
の場合(プロファイリングを一時停止する場合)、SIGPROF
がシグナルマスクにSIG_BLOCK
されます。
- ブロッキングシステムコール中のプロファイリング一時停止:
runtime·notesleep
(セマフォ待機),runtime·notetsleep
(タイムアウト付きセマフォ待機),runtime·entersyscall
(システムコール開始),runtime·exitsyscall
(システムコール終了),runtime·semasleep
(セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)
を呼び出してSIGPROF
をブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)
を呼び出してSIGPROF
のブロックを解除するようにしました。- このアプローチにより、カーネルは
SIGPROF
をスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
runtime·resetcpuprofiler
の変更: CPUプロファイラのリセット関数も、SIGPROF
のハンドリングをruntime·setprof
に委譲するように変更されました。
ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROF
が配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。
2. Go独自のaddr2line
とobjdump
ツールの追加
macOSにはGNU Binutilsのaddr2line
やobjdump
が標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。
src/cmd/addr2line/main.c
:- このファイルは、
addr2line
のGoバージョンをC言語で実装しています。 - Goの内部ライブラリである
libmach
(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。 - 標準入力からメモリアドレスを読み込み、対応する関数名と
ファイル:行番号
の形式で標準出力に出力します。これは、pprof
がプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
- このファイルは、
src/cmd/objdump/main.c
:- このファイルは、
objdump
のGoバージョンをC言語で実装しています。 - これも
libmach
を利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。 pprof
がコードの逆アセンブル表示を行う際に利用されます。
- このファイルは、
src/cmd/dist/build.c
の変更:- Goのビルドシステムに、これらの新しいツール(
cmd/addr2line
とcmd/objdump
)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go tool
コマンドを通じて利用可能になります。
- Goのビルドシステムに、これらの新しいツール(
3. pprof
ツールの適応
既存のmisc/pprof
スクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。
- フォールバックロジック:
pprof
スクリプトは、addr2line
やobjdump
コマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")
のようなコマンドでチェックします。 go tool
の利用: もしシステムツールが見つからない場合、pprof
は自動的にgo tool addr2line
やgo tool objdump
にフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。
4. その他の改善
src/libmach/8db.c
の修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。src/pkg/runtime/signal_darwin_386.c
およびsrc/pkg/runtime/signal_darwin_amd64.c
の変更:SIGPROF
ハンドラ内で、gp != m->g0 && gp != m->gsignal
という条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0
やgsignal
)に配信された場合にはruntime·sigprof
を呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。
これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。
-
misc/pprof
:Disassemble
サブルーチン内で、objdump
が見つからない場合にgo tool objdump
にフォールバックするロジックが追加されました。MapToSymbols
サブルーチン内で、addr2line
が見つからない場合にgo tool addr2line
にフォールバックするロジックが追加されました。
--- a/misc/pprof +++ b/misc/pprof @@ -1234,6 +1234,13 @@ sub Disassemble { my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " . "--start-address=0x$start_addr " . "--stop-address=0x$end_addr $prog"); + + if (system("$objdump --help >/dev/null 2>&1") != 0) { + # objdump must not exist. Fall back to go tool objdump. + $objdump = "go tool objdump"; + $cmd = "$objdump $prog 0x$start_addr 0x$end_addr"; + } + open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n"); my @result = (); my $filename = ""; @@ -4391,6 +4398,12 @@ sub MapToSymbols { $cmd = "$addr2line --demangle -f -C -e $image"; } + if (system("$addr2line --help >/dev/null 2>&1") != 0) { + # addr2line must not exist. Fall back to go tool addr2line. + $addr2line = "go tool addr2line"; + $cmd = "$addr2line $image"; + } + # If "addr2line" isn't installed on the system at all, just use # nm to get what info we can (function names, but not line numbers). if (system("$addr2line --help >/dev/null 2>&1") != 0) {
-
src/cmd/addr2line/main.c
(新規ファイル):- Go独自の
addr2line
ツールのC言語実装。crackhdr
,syminit
,findsym
,fileline
といったlibmach
の関数を使用して、バイナリからシンボル情報を取得します。
// 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. /* * addr2line simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd; char *p; uvlong pc; Symbol s; Fhdr fhdr; Biobuf bin, bout; char file[1024]; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); Binit(&bin, 0, OREAD); Binit(&bout, 1, OWRITE); for(;;) { p = Brdline(&bin, '\n'); if(p == nil) break; p[Blinelen(&bin)-1] = '\0'; pc = strtoull(p, 0, 16); if(!findsym(pc, CTEXT, &s)) s.name = "??"; if(!fileline(file, sizeof file, pc)) strcpy(file, "??:0"); Bprint(&bout, "%s\n%s\n", s.name, file); } Bflush(&bout); exits(0); }
- Go独自の
-
src/cmd/objdump/main.c
(新規ファイル):- Go独自の
objdump
ツールのC言語実装。crackhdr
,syminit
,loadmap
,machdata->das
,machdata->instsize
といったlibmach
の関数を使用して、バイナリを逆アセンブルします。
// 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. /* * objdump simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd, n; uvlong pc, start, stop; Fhdr fhdr; Biobuf bout; char buf[1024]; Map *text; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); text = loadmap(nil, fd, &fhdr); if(text == nil) sysfatal("loadmap: %r"); Binit(&bout, 1, OWRITE); for(pc=start; pc<stop; ) { if(fileline(buf, sizeof buf, pc)) Bprint(&bout, "%s\n", buf); buf[0] = '\0'; machdata->das(text, pc, 0, buf, sizeof buf); Bprint(&bout, " %llx: %s\n", pc, buf); n = machdata->instsize(text, pc); if(n <= 0) break; pc += n; } Bflush(&bout); exits(0); }
- Go独自の
-
src/pkg/runtime/runtime.h
:runtime·setprof
関数の宣言が追加されました。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -730,3 +730,13 @@ bool runtime·showframe(Func*); void runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*); uintptr runtime·memlimit(void); + +// If appropriate, ask the operating system to control whether this +// thread should receive profiling signals. This is only necessary on OS X. +// An operating system should not deliver a profiling signal to a +// thread that is not actually executing (what good is that?), but that's +// what OS X prefers to do. When profiling is turned on, we mask +// away the profiling signal when threads go to sleep, so that OS X +// is forced to deliver the signal to a thread that's actually running. +// This is a no-op on other systems. +void runtime·setprof(bool);
-
src/pkg/runtime/thread_darwin.c
:runtime·setprof
関数の実装が追加されました。これはSIGPROF
シグナルをブロックまたはアンブロックするためにruntime·sigprocmask
を呼び出します。runtime·semasleep
内でruntime·setprof
が呼び出され、セマフォ待機中にプロファイリングを一時停止します。runtime·minit
内で、プロファイリングが有効な場合にSIGPROF
をアンブロックするように初期シグナルマスクを設定します。
--- a/src/pkg/runtime/thread_darwin.c +++ b/src/pkg/runtime/thread_darwin.c @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[]; static Sigset sigset_all = ~(Sigset)0; static Sigset sigset_none; +static Sigset sigset_prof = 1<<(SIGPROF-1); static void unimplemented(int8 *name) @@ -23,7 +24,14 @@ unimplemented(int8 *name) int32 runtime·semasleep(int64 ns) { -\treturn runtime·mach_semacquire(m->waitsema, ns); +\tint32 v; +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\tv = runtime·mach_semacquire(m->waitsema, ns);\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\n+\treturn v; } void @@ -98,7 +106,11 @@ runtime·minit(void) // Initialize signal handling. m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\ -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil); +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\ +\telse +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\ } // Mach IPC, to get at semaphores @@ -434,3 +446,34 @@ runtime·memlimit(void) // the limit. return 0; } + +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF +// signal is not guaranteed to be sent to the thread that was executing to +// cause it to expire. It can and often does go to a sleeping thread, which is +// not interesting for our profile. This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225. +// To work around this bug, we disable receipt of the profiling signal on +// a thread while in blocking system calls. This forces the kernel to deliver +// the profiling signal to an executing thread. +// +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel. +// In that configuration, the kernel appears to want to deliver SIGPROF to the +// sleeping threads regardless of signal mask and, worse, does not deliver +// the signal until the thread wakes up on its own. +// +// If necessary, we can switch to using ITIMER_REAL for OS X and handle +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver +// to all the running threads. SIGALRM does not appear to be affected by +// the 64-bit Snow Leopard bug. However, as of this writing Mountain Lion +// is in preview, making Snow Leopard two versions old, so it is unclear how +// much effort we need to spend on one buggy kernel. + +// Control whether profiling signal can be delivered to this thread. +void +runtime·setprof(bool on) +{ +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+}
-
src/pkg/runtime/proc.c
:runtime·entersyscall
とruntime·exitsyscall
内でruntime·setprof
が呼び出され、システムコール中にプロファイリングを一時停止します。
--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -916,6 +915,9 @@ runtime·entersyscall(void) { uint32 v; +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n // Leave SP around for gc and traceback. runtime·gosave(&g->sched); g->gcsp = g->sched.sp; @@ -979,6 +981,9 @@ runtime·exitsyscall(void) // Garbage collector isn't running (since we are), // so okay to clear gcstack. g->gcstack = nil; +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\ return; }
これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprof
ツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。
コアとなるコードの解説
ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。
1. misc/pprof
におけるフォールバックロジック
if (system("$objdump --help >/dev/null 2>&1") != 0) {
# objdump must not exist. Fall back to go tool objdump.
$objdump = "go tool objdump";
$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
}
このPerlコードスニペットは、pprof
スクリプトがobjdump
コマンドを呼び出す前に実行されるチェックです。
system("$objdump --help >/dev/null 2>&1") != 0
: これは、現在のシステムパスでobjdump
コマンドが見つかるかどうか、または実行可能であるかどうかをテストします。$objdump --help
:objdump
コマンドに--help
オプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。>/dev/null 2>&1
: 標準出力と標準エラー出力を/dev/null
にリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。system(...) != 0
:system
関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0
であれば成功(コマンドが見つかり、実行できた)、0
以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
# objdump must not exist. Fall back to go tool objdump.
: コマンドが見つからなかった場合のコメントです。$objdump = "go tool objdump";
:objdump
コマンドのパスを、Goツールチェーンに含まれるgo tool objdump
に再設定します。$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
: 実際に実行するコマンド文字列を、新しいobjdump
パスとGoツールに合わせた引数形式で再構築します。
同様のロジックがaddr2line
に対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。
2. src/cmd/addr2line/main.c
とsrc/cmd/objdump/main.c
の新規追加
これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2line
とobjdump
の実装です。C言語で書かれており、Goの内部ライブラリであるlibmach
を利用しています。
libmach
の利用:libmach
は、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。crackhdr(fd, &fhdr)
: ファイルディスクリプタfd
から実行可能ファイルのヘッダ情報を解析し、fhdr
構造体に格納します。syminit(fd, &fhdr)
: シンボルテーブルを初期化し、シンボル情報をメモリにロードします。findsym(pc, CTEXT, &s)
: 指定されたプログラムカウンタpc
に対応する関数シンボルs
を検索します。fileline(file, sizeof file, pc)
: 指定されたプログラムカウンタpc
に対応するソースファイル名と行番号を取得します。machdata->das(text, pc, 0, buf, sizeof buf)
: 指定されたテキストセクションtext
内のプログラムカウンタpc
から命令を逆アセンブルし、結果をbuf
に格納します。machdata->instsize(text, pc)
: 指定されたプログラムカウンタpc
にある命令のサイズを返します。
これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprof
がプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。
3. runtime·setprof(bool on)
関数の導入と利用
// src/pkg/runtime/runtime.h
void runtime·setprof(bool);
// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
if(on)
runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
else
runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
runtime·setprof
の役割: この関数は、現在のスレッドがSIGPROF
シグナルを受信できるかどうかを制御します。on
がtrue
の場合、SIGPROF
はシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。on
がfalse
の場合、SIGPROF
はシグナルマスクに追加され、スレッドはシグナルをブロックします。sigset_prof
:SIGPROF
シグナルのみを含むシグナルセットです。runtime·sigprocmask
: Unix系システムコールsigprocmask
のGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCK
は指定されたシグナルをマスクから削除し、SIG_BLOCK
は追加します。
この関数は、Goランタイムの様々な場所で呼び出されます。
-
src/pkg/runtime/lock_futex.c
とsrc/pkg/runtime/lock_sema.c
:runtime·notesleep
(セマフォ待機) やruntime·notetsleep
(タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)
を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)
を呼び出します。- これにより、スレッドがスリープしている間は
SIGPROF
がブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
-
src/pkg/runtime/proc.c
:runtime·entersyscall
(システムコールに入る前) とruntime·exitsyscall
(システムコールから出る後) でruntime·setprof
が呼び出されます。- システムコール中はスレッドがカーネルモードで実行されるため、この間も
SIGPROF
をブロックすることで、プロファイリングの精度を維持しようとします。
このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。
4. src/pkg/runtime/signal_darwin_386.c
およびsrc/pkg/runtime/signal_darwin_amd64.c
のSIGPROF
ハンドラ
if(sig == SIGPROF) {
if(gp != m->g0 && gp != m->gsignal)
runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
return;
}
このコードは、SIGPROF
シグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。
if(gp != m->g0 && gp != m->gsignal)
: この条件は非常に重要です。gp
は現在のゴルーチン(Goの軽量スレッド)へのポインタです。m->g0
: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。m->gsignal
: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
- この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
- 目的: Goランタイムの内部ゴルーチンが
SIGPROF
を受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。
これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。
関連リンク
- Go Issue #2008: runtime/pprof: support OS X CPU profiling
- Go Change List 5697066: runtime/pprof: support OS X CPU profiling
- Apple Bug Report #9177434 (Goのコメントで言及):
- 直接的な公開リンクは見つかりませんが、Goのソースコードコメントで言及されています。
- http://code.google.com/p/go/source/detail?r=35b716c94225 (これはGoの古いSVNリポジトリの変更履歴へのリンクで、Appleのバグ報告に関する詳細なコメントが含まれています。)
参考にした情報源リンク
- Go Issue #2008: https://github.com/golang/go/issues/2008
- Go Change List 5697066: https://golang.org/cl/5697066
- GNU Binutils - addr2line: https://sourceware.org/binutils/docs/binutils/addr2line.html
- GNU Binutils - objdump: https://sourceware.org/binutils/docs/binutils/objdump.html
- Go pprof documentation: https://pkg.go.dev/runtime/pprof
- Go blog: Profiling Go Programs: https://go.dev/blog/pprof
- Unix signals (general concept): https://en.wikipedia.org/wiki/Signal_(IPC)
- sigprocmask(2) - Linux man page: https://man7.org/linux/man-pages/man2/sigprocmask.2.html
- ITIMER_PROF: https://man7.org/linux/man-pages/man2/setitimer.2.html
- Go source code comments (specifically in
src/pkg/runtime/thread_darwin.c
andsrc/pkg/runtime/pprof/pprof.go
related to Apple Bug Report #9177434): - Go
libmach
(internal library for Mach-O parsing): (具体的な公開ドキュメントは少ないが、Goのソースコード内で利用されている)src/libmach/
ディレクトリ内のファイル群
これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。