chikaku

且听风吟

永远是深夜有多好。
github
email

Go 実行時の黒魔法

Go プログラムの起動 で述べたように、現在(実行中にスケジュールされている)ゴルーチンのデータ構造ポインタである *g は TLS に置かれています。少しアセンブリを使って *g を取得することができ(もちろん、これは型付きポインタではなく uintptr です)、ランタイムゴルーチンデータにアクセスできます。また、g/p/m データには関連性があり、他にも多くのことができます。

実際、現在は goidpid のような多くの類似のリポジトリがこの方法を使用しています。しかし、類似のライブラリにはある現象があります。リポジトリのコード内でさまざまな _go1.13.go、_go1.14.go、*_go1.15.go などのファイルを見ることができます。これは、各バージョンのランタイムデータ構造が変更される可能性があり、異なるバージョンに対応するフィールドのオフセットが異なるためです。各バージョンに対してランタイムと一致する構造体を作成し、unsafe.Offsetof を使用してオフセットを取得する必要があります。

反射を使用しているときに、Go ELF の中にこれらのデータ構造のオフセット情報を保存している場所があるはずだと思いました。その後、Go ELF に関連する資料を調べたところ、Go はデバッグ情報を DWARF 形式で保存しており、標準ライブラリは対応するインターフェース debug/dwarf を提供しています。しかし、いくつかの問題があります。まず、go test の際にコンパイルされたバイナリにはデバッグ情報が含まれていないようで、dwarf の読み取りに失敗します。次に、darwin でコンパイルされた ELF が dwarf を読み取ると bad magic number エラーが発生します。Linux のみで快適に使用できます

その後は比較的簡単です。os.Args[0] を使用してバイナリパスを取得し、dwarf を読み取り、構造体名とフィールド名に基づいてシンボル情報を検索することで、ランタイムで任意のデータ構造の任意のフィールドオフセット(ランタイムの g/p/m や他の非エクスポート構造の非エクスポートフィールドを含む)を読み取ることができます。これで、ランタイムデータを取得する魔法を手に入れました!

具体的な実装は tools-go リポジトリの rt パッケージを参照してください。

使用方法#

以下に rt パッケージで実装されたツールのいくつかを示します。

任意の構造体の任意のフィールドオフセット rt.Offsetof#

unsafe.Offsetof と同様に、ここでは構造体名を正しく記述する必要があります。サードパーティライブラリの構造体の場合は、シンボルテーブルを参照できます。

// ランタイム g の goid フィールドのオフセットを取得
offset, _ := rt.Offsetof("runtime.g", "goid")
fmt.Println(offset)
// 152

現在のゴルーチンスタックサイズ rt.GoStackSize#

g の上にはスタック範囲を示す 2 つのフィールドがあるため、差分を使用してスタックサイズを取得できます。

func main() {
    done := make(chan int)
    go func() { done <- demo() }()
    <-done
}

func demo() int {
    size, _ := rt.GoStackSize()
    fmt.Println("初期スタックサイズ", size)

    rec(10000, size)
    return 0
}

func rec(n, size0 int) {
    size, _ := rt.GoStackSize()
    if size != size0 {
        fmt.Println("スタックサイズ")
    }

    if n > 0 { rec(n-1, size) }
}

出力から初期スタックサイズが 4K で、その後毎回 morestack によって倍増することがわかります。

初期スタックサイズ 4096
スタックサイズ 8192
スタックサイズ 16384
スタックサイズ 32768
スタックサイズ 65536
スタックサイズ 131072
スタックサイズ 262144
スタックサイズ 524288
スタックサイズ 1048576
スタックサイズ 2097152

タイマーの総数 rt.NumTimers#

すべてのタイマーは p に登録されているため、p.numTimers を使用して単一の p のタイマー数を取得できます。また、ランタイムはちょうど allp というグローバル変数を提供しており、linkname を使用してすべての p を遍歴できます!

もちろん、この方法はタイマーの状態の変更や遅延削除などにより、データが完全に正確ではない可能性がありますが、相対的な値を推定することができ、負荷テストを行う際には非常に役立ちます。

func main() {
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)

    cnt, _ := rt.NumTimers()
    fmt.Println("タイマー数", cnt)
    // タイマー数 5
}

注意事項 ⚠️⚠️⚠️#

  • パッケージ rt は現在 Linux のみで使用可能です
  • ビルドが必要です! 直接 go run または go test でコンパイルされたバイナリにはデバッグ情報が含まれていないため、dwarf セクションのオフセットをデコードする際にエラーが発生します。リポジトリ内部ではテスト時にあらかじめコンパイルされたバイナリが用意されています。
  • 危険! このライブラリでは直接 *g および allp を操作しています(ロックもリンクされていますが、それがより危険に感じます)。ランタイムにどのような影響を与えるかは不明ですので、テスト環境や負荷テスト環境での使用を試みてください。
  • パフォーマンス ランタイムは一度バイナリを読み取り、構造体フィールドを取得するメソッド内では、いくつかの時間がかかる可能性のあるシンボルの遍歴があります。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。