薄いブログ

技術の雑多なことを書く場所

Apple Silicon だと go tool pprof の Disassemble が動かない問題とその対応

まとめ

macOS ARM64 では -no_pie オプションが使えなくなっていて、現状だと go tool pprof で Source / Disassemble が使えない。

プロファイリング対象のバイナリ自身でプロファイルを書き換えるライブラリを作成し Source / Disassemble ができるようになった。

github.com

現状では macOS ARM64 のみ対応している。

背景

go-json の性能改善をしていて benchmark の cpuprofile を pprof で見ようとしたところ Source の機能が使えないことがわかった。

github.com

原因調査

バージョン起因の問題であるか確認するために go 1.17, 1.18, 1.19 でも試したが同じ結果だった。

次に環境の問題であるか確認するために linux で試したところ問題は発生しなかった。そのため環境に依存する問題であることがわかった。

なぜ正常に動作しないのかを理解するために cmd/pprof をデバッガを使いながら確認したところシンボルは存在しているがアドレスが異なることがわかった。

また実行するたびにアドレスが変わることもわかった。

ASLR と PIE の具体的な違いを理解していないので ASLR の問題かと思ったが直近有効になったものではなかった。

ASLR について調査していたときに以下の issue を見つけ PIE について思い出した。

OS X requires disabling Position Independent Executable · Issue #565 · gperftools/gperftools · GitHub

PIE について調査すると macOS ARM64 では no_pie が使えなくなっていることがわかった。

cmd/link: pass darwin/amd64-specific flags only on AMD64 · golang/go@612b119 · GitHub

つまり no_pie が使えないため PIE が有効になっていて pprof の Source / Disassemble が動かないということがわかった。

対応

runtime/pprof は SIGPROF を受け取るたびに goroutine の pc を取得し link register をたどって unwind している。

sample としてコールスタックの pc をそのまま書き込んでしまう。

その処理を修正するのは難しいので書き込まれたプロファイルを書き換える方針で考えた。

書き換えるためには実行時の text segment のアドレスが必要なためプロファイリング対象のバイナリ自身でプロファイルを書き換えなければならない。

go:linkname を使って非公開の関数を呼び出して text segment のアドレスを取得し、バイナリに含まれている runtime.text を用いて pc を変換する。

これを行うライブラリを作成し公開した。

github.com

非公開の関数を使っているため、将来的には動かなくなる可能性がある。また runtime.text の値が固定であるように見えたのでハードコードしている。

より良い方法があれば教えてもらいたいし、PR を作ってもらえるとより嬉しい。

個人的なユースケースだと go test -bench -cpuprofile でプロファイリングすることが多いのでユーティリティ関数を提供している。

package nopieprofile_test

import (
    "log"
    "os"
    "testing"

    "github.com/orisano/nopieprofile"
)

func TestMain(m *testing.M) {
    code := m.Run()
    if err := nopieprofile.RewriteTestProfile(); err != nil {
        log.Printf("warn: failed to rewrite test profile: %v", err)
    }
    os.Exit(code)
}

上記のようにすると -cpuprofile の先が書き換えられるようになる。

pprof の Source / Disassemble は非常に便利なので使えるようになって嬉しい。