3K Views
January 18, 25
スライド概要
みなさんは Go で開発しているときにデバッガを利用したことはありますか?
デバッガを利用したことがあっても、その仕組みを理解している方は多くはないかと思います。
本資料では、デバッガの仕組みを説明し、 Go で実装する方法を簡単に解説します。
Go でデバッガを自作する 2025/01/18 Gopher’s Gathering okarin
自己紹介 okarin (Kyota Okabe) 所属 ● 株式会社ハイヤールー Activities ● X: @okarin_dev ● community: Ehime.go 好きなもの ● ブロッコリー ● マラソン 福岡の推しグルメ ● 志成 2
HireRooとは ビッグテック水準の採用を誰でも簡単に。 HireRoo(ハイヤールー) We are hiring! リンクはこちら 3
質問 Go で開発するとき デバッガを使っていますか?🙋
質問 デバッガの仕組みを 知っていますか?🙋
目的 今日の目的 ● 雰囲気でデバッガを理解する ● デバッガを自作したくなる
VSCode を使ったデバッグの例 VSCode でデバッグしている様子
VSCode を使ったデバッグの例 DAP DAP Server Debugger Tracee Program ※DAP: Debug Adapter Protocol 今日話すところ StepIn Request の例 interface StepInRequest extends Request { command: 'stepIn'; arguments: StepInArguments; }
デバッガを支える要素 デバッガを作成するために必要な要素(x86_64 アーキテクチャ) ● ptrace システムコール ● DWARF ● INT3 命令
ptrace ptrace システムコール long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data); __ptrace_request ● ● ● PTRACE_TRACEME ○ プログラム追跡の許可 PTRACE_PEEKDATA ○ メモリの読み込み PTRACE_POKEDATA ○ メモリの書き込み ptrace は追跡しているプログラムを デバッグするために利用する
DWARF DWARF (Debugging With Arbitrary Record Formats) 実行ファイルを dwarfdump して得られる main 関数の情報 DW_TAG_subprogram DW_AT_name DW_AT_low_pc DW_AT_high_pc DW_AT_frame_base DW_AT_decl_file DW_AT_decl_line DW_AT_external main.main 0x004b02a0 0x004b0358 len 0x0001: 0x9c: 0x00000003 /path/to/main.go 0x00000005 yes(1) DWARF によって関数や変数などの詳細がわかる
DWARF DWARF (Debugging With Arbitrary Record Formats) メモリアドレスに対応するファイルと行番号 .debug_line: 0x004b02a0 [ 0x004b02ae [ 0x004b02b2 [ 0x004b02b7 [ 0x004b02bc [ 5, 0] NS uri: "/path/to/main.go" 5, 0] NS PE 6, 0] NS 6, 0] 8, 0] NS DWARF によってメモリアドレス ⇔ 行番号の変換ができる
INT3 命令 INT3 命令 48 89 fe e8 f8 91 ff ff 48 83 c4 68 mov rsi,rdi call 4a9540 <fmt.Printf> add rsp,0x68
INT3 命令 INT3 命令 48 89 fe e8 f8 91 ff ff 48 83 c4 68 mov rsi,rdi call 4a9540 <fmt.Printf> add rsp,0x68 先頭1バイトを 0xcc で上書きする → INT3 命令 48 89 fe cc f8 91 ff ff 48 83 c4 68 mov rsi,rdi int3 add rsp,0x68
INT3 命令 INT3 命令 48 89 fe e8 f8 91 ff ff 48 83 c4 68 mov rsi,rdi call 4a9540 <fmt.Printf> add rsp,0x68 先頭1バイトを 0xcc で上書きする → INT3 命令 PC 48 89 fe cc f8 91 ff ff 48 83 c4 68 mov rsi,rdi int3 add rsp,0x68
INT3 命令 INT3 命令 48 89 fe e8 f8 91 ff ff 48 83 c4 68 mov rsi,rdi call 4a9540 <fmt.Printf> add rsp,0x68 先頭1バイトを 0xcc で上書きする → INT3 命令 PC 48 89 fe cc f8 91 ff ff 48 83 c4 68 mov rsi,rdi int3 add rsp,0x68 INT3 命令によって処理が停止し、 SIGTRAP シグナルを送信する → ブレークポイントとして機能する
デバッガを支える要素 ここまでのおさらい ● ptrace システムコール ○ ● DWARF ○ ● プログラムの追跡、メモリ操作などで使用 関数や変数の情報取得、メモリアドレスと行番号の変換などで使用 INT3 命令 ○ ブレークポイントで使用
Go ここから Go の話 The Go gopher was designed by Renee French. The design is licensed under the Creative Commons 4.0 Attributions license. Read this article for more details: https://go.dev/blog/gopher
デバッガの仕組み おおまかなデバッグの流れ ● 最適化なし、関数のインライン化なしでビルド ● ptrace による制御を許可して実行 ● ブレークポイントの設定 ● Continue など
デバッガの仕組み
最適化なし、関数のインライン化なしでビルド
最適化:不要なデッドコードの削除などによるコンパイル最適化
関数のインライン化:関数呼び出しのオーバーヘッドを削減するためのインライン展開
path := "path/to/program"
name := "__debug__1720159170"
// go build -o __debug__1720159170 -gcflags all="-N -l" path/to/program
cmd := exec.Command("go", "build", "-o", name, "-gcflags", "all=-N -l", path)
if err := cmd.Run(); err != nil { /** error handling */ }
https://pkg.go.dev/cmd/go
https://pkg.go.dev/cmd/compile#hdr-Command_Line
デバッガの仕組み
ptrace による制御を許可して実行
SysProcAttr で ptrace による制御を許可
(内部では fork/exec した後に PTRACE_TRACEME を実行)
cmd := exec.Command(path)
cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
if err := cmd.Start(); err != nil {
// error handling
}
// プロセス ID が cmd.Process.Pid で取得できる
// => ptrace でプロセス ID を指定してデバッグできる
デバッガの仕組み
メモリアドレスを指定したブレークポイントの設定
元々の命令の先頭を 0xcc で書き換える
originalInstruction := make([]byte, 8)
_, err := syscall.PtracePeekData(pid, addr, originalInstruction)
if err != nil { /** error handling */ }
data := binary.LittleEndian.Uint64(originalInstruction)
// data & ^uint64(0xff) => data & 11111111 ... 11111111 00000000
newData := (data & ^uint64(0xff)) | 0xcc
newInstruction := make([]byte, 8)
binary.LittleEndian.PutUint64(newInstruction, newData)
_, err = syscall.PtracePokeData(pid, addr, newInstruction)
if err != nil { /** error handling */ }
デバッガの仕組み 行番号を指定したブレークポイント gosym.Table の LineToPC メソッドで (ファイル名, 行番号)→ プログラムカウンタに変換 あとの処理は同じ
デバッガの仕組み Continue 現在のプログラムカウンタにブレークポイントがある場合は元の命令に戻して1命令実行 syscall.PtraceCont で次のブレークポイントまで処理を進める // 現在のプログラムカウンタにブレークポイントが設定されている場合 if breakpoint.IsEnabled() { breakpoint.Disable() // 元の命令に戻す err := syscall.PtraceSingleStep(pid) // 1命令だけ処理 // error handling } err := syscall.PtraceCont(d.pid, 0) // error handling
デバッガの仕組み さらに詳しく知りたい方へ... いいね❤ ください!! Go でデバッガを自作する
まとめ ● デバッガの仕組みを解説 ○ ptrace, DWARF, INT3 ● Go での実装例を解説 ● デバッガを自作したくなった(?) Ehime.go もぜひ! The Go gopher was designed by Renee French. The design is licensed under the Creative Commons 4.0 Attributions license. Read this article for more details: https://go.dev/blog/gopher