---
title: Go言語アプリケーションへのeBPFを用いたゼロコード計装
tags: 
author: [appare45](https://convert.docswell.com/user/appare45)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/3JKW8PGMJD.jpg?width=480
description: Go言語アプリケーションへのeBPFを用いたゼロコード計装 by appare45
published: January 28, 26
canonical: https://convert.docswell.com/s/appare45/ZPGNN1-ebpf-japan-meetup-5
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/3JKW8PGMJD.jpg)

eBPF Japan Meetup #5
Go 言語アプリケーションへの
eBPF を用いたゼロコード計装
id: appare45


# Page. 2

![Page Image](https://bcdn.docswell.com/page/LE312XN275.jpg)

eBPF Japan Meetup #5
自己紹介
• 中村天晴（id: appare45）
• 筑波大学 情報科学類 3 年生
• アプリケーションのゼロコード計装に興味があります
本日の資料はこちら→TBD
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/8EDXZ2W6JG.jpg)

eBPF Japan Meetup #5
作ってわかる OpenTelemetry のゼロコード計装 Go 言語 eBPF 編
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/V7PPD1XZ78.jpg)

eBPF Japan Meetup #5
実現したいこと
• Go 言語のアプリケーションのパフォーマンスをソースコードを変更せずに
調べたい
•「ゼロコード計装」「自動計装」と呼ばれる
‣「計装(Instrumentation)」: アプリケーションに監視用のコードを組み込
むこと
4


# Page. 5

![Page Image](https://bcdn.docswell.com/page/2JV21YLMJQ.jpg)

eBPF Japan Meetup #5
作ったもの
• コマンドを打つだけで指定した Web アプリケーションのパフォーマンスを
OpenTelemetry のトレース形式で送信するツール
• eBPF を使っている
‣ https://github.com/appare45/go-auto-instrument-ebpf
• Go 言語特有の課題がいくつかあったのでご紹介する
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/5EGR29XXJL.jpg)

eBPF Japan Meetup #5
sudo -E ./go-auto-instrument-ebpf ./examples/demo/demo
GET: /items a95b06d292ce2616af51073ae379f944 6d34e11faf7ebe3e
http.route=/items host.name=localhost:8081
http.response.status_code=200 http.request.method=GET
POST: /items 69ddb82a2c4591969efcb61e9944d001 22ea087162a66006
http.route=/items host.name=localhost:8081
http.response.status_code=201 http.request.method=POST
6


# Page. 7

![Page Image](https://bcdn.docswell.com/page/4JQVPW857P.jpg)

eBPF Japan Meetup #5
OpenTelemetry 対応サービスでビジュアライズできる
7


# Page. 8

![Page Image](https://bcdn.docswell.com/page/K74MYK2VJ1.jpg)

eBPF Japan Meetup #5
様々なゼロコード計装の手法
• 実行時にコードを差し替える（モンキーパッチ）
‣ ランタイムがある言語であれば可能
• コンパイル時にコードを差し替える
• リンク時にコードを差し替える
‣ 動的リンクされていれば可能
• eBPF を用いて観測する
8


# Page. 9

![Page Image](https://bcdn.docswell.com/page/LJ186XM4EG.jpg)

eBPF Japan Meetup #5
Go 言語でのゼロコード計装
• コンパイル時にコードを差し替える
‣ Alibaba Loongsuite Go Agent
‣ Datadog Orchestrion
• eBPF を用いて計装する
‣ OpenTelemetry eBPF Instrument（Grafana Beyla）
‣ OpenTelemetry Go Auto Instrument
OpenTelemetry Go Auto Instrument で使われている手法を実装
9


# Page. 10

![Page Image](https://bcdn.docswell.com/page/GJWZWN3ZJ2.jpg)

eBPF Japan Meetup #5
今回のゴール:
• Go 言語で作られた Web アプリケーションに計装する
‣ 特にオプションを付けずにコンパイルしたもの
• アプリケーションのバイナリに手を加えない
• リクエストが来たときに、以下の情報を取得する
‣ リクエストの URL パス
‣ HTTP メソッド
‣ 処理にかかった時間（レスポンスタイム）
‣ ステータスコード
10


# Page. 11

![Page Image](https://bcdn.docswell.com/page/4EZ153VL73.jpg)

eBPF Japan Meetup #5
環境
• MacOS で起動した Docker コンテナ内で動作
• Arm64 アーキテクチャ
• eBPF で計装するアプリケーションも Go 言語で実装
‣ eBPF プログラムは C 言語で実装
‣ bpf2go を使用して C の eBPF プログラムから Go 言語向けのバインディ
ングを生成
11


# Page. 12

![Page Image](https://bcdn.docswell.com/page/Y76L9X5MJV.jpg)

eBPF Japan Meetup #5
手法
• eBPF の Uprobe を用いてアプリケーション中の関数の開始と終了で情報を
収集する
• 今回は HTTP リクエストを処理する関数を観測する（後述）
12


# Page. 13

![Page Image](https://bcdn.docswell.com/page/G751NXYQE4.jpg)

eBPF Japan Meetup #5
関数の開始と終了にフックする
• eBPF をアプリケーション中の関数にフックできる
• トレースポイント
‣ 関数の開始は Uprobe
‣ 関数の終了は Uretprobe
• Uprobe・Uretprobe ではメモリやレジスタの状態を取得できる
‣ メモリやレジスタを書き換えることもできる
13


# Page. 14

![Page Image](https://bcdn.docswell.com/page/9J215XGW7R.jpg)

eBPF Japan Meetup #5
int __attribute__ ((noinline)) hello(int i) {
printf(&quot;Hello, %d!\n&quot;, i);
return i * 2;
}
int main() {
for (int i = 0; i &lt; 5; i++) {
hello(i);
}
return 0;
}
14


# Page. 15

![Page Image](https://bcdn.docswell.com/page/DEYZK8V97M.jpg)

eBPF Japan Meetup #5
bpftrace を使ってトレース
バイナリと関数名（シンボル名）を指定してフックする
uprobe:./main:hello {
printf(&quot;uprobe: called with %d\n&quot;, reg(&quot;r0&quot;));
}
uretprobe:./main:hello {
printf(&quot;uretprobe: return %d\n&quot;, reg(&quot;r0&quot;));
}
15


# Page. 16

![Page Image](https://bcdn.docswell.com/page/VJN3RQMDJ8.jpg)

eBPF Japan Meetup #5
bpftrace を使ってトレース
$ ./main
Hello, 0!
Hello, 1!
Hello, 2!
Hello, 3!
Hello, 4!
$ sudo bpftrace trace.bt
Attaching 2 probes...
uprobe: called with 0
uretprobe: return 0
uprobe: called with 1
uretprobe: return 2
uprobe: called with 2
uretprobe: return 4
...
16


# Page. 17

![Page Image](https://bcdn.docswell.com/page/YE99MDN8E3.jpg)

eBPF Japan Meetup #5
Go 言語特有の問題
C 言語と異なる部分がいくつかありそれぞれに対応する必要がある
1. 並行処理におけるスレッドと関数のコンテキストの不一致
2. Uretprobe を使うとプログラムがクラッシュする
3. 関数の引数の取得
17


# Page. 18

![Page Image](https://bcdn.docswell.com/page/GE89L3KZ7D.jpg)

eBPF Japan Meetup #5
Go の並行処理
• 非同期処理ごとに Goroutine という軽量スレッドを作る
‣ このとき確保されるスタックが小さい
‣ スタックが不足するとランタイムが拡張する
• Go 言語のランタイムが Goroutine を OS スレッドに割り当てる
‣ ランタイムはアプリケーションのバイナリ中に埋め込まれる
• M:N スケジューリング
‣ 非同期処理と OS スレッドが一致しない
18


# Page. 19

![Page Image](https://bcdn.docswell.com/page/LELWLVZ1JR.jpg)

eBPF Japan Meetup #5
eBPF と Goroutine
• eBPF プログラムからスレッドを取得しても Goroutine を特定できない
• 同じ Goroutine の途中でも異なるスレッドで実行されることがある
‣ Uprobe で取得した情報と Uretprobe で取得した情報を紐づけられない
19


# Page. 20

![Page Image](https://bcdn.docswell.com/page/4JM9MGV5EW.jpg)

eBPF Japan Meetup #5
Goroutine
Goroutine
Goroutine
Goroutine
Goroutine
Go言語ランタイム
OS
Goroutine
Thread 1
Thread 2
Thread 3
Thread 4
eBPF
CPU
20


# Page. 21

![Page Image](https://bcdn.docswell.com/page/PJRGVZ2ZE9.jpg)

eBPF Japan Meetup #5
Goroutine
Goroutine
Goroutine
Goroutine
Goroutine
Go言語ランタイム
OS
Goroutine
Thread 1
Thread 2
Thread 3
Thread 4
eBPF
CPU
21


# Page. 22

![Page Image](https://bcdn.docswell.com/page/PEXXZ9D1EX.jpg)

eBPF Japan Meetup #5
Goroutine の見分け方
• Goroutine ごとに管理用の構造体 g が作られる
• CPU 実行時には R28 レジスタに g 構造体のポインタが保存されている
22


# Page. 23

![Page Image](https://bcdn.docswell.com/page/3EKW8PXMED.jpg)

eBPF Japan Meetup #5
解決策: レジスタから Goroutine を特定する
• 関数呼び出しごとに g 構造体のアドレスを取得する
• g のアドレスを eBPF マップのキーとして利用する
‣ 関数の開始時と終了時で同じ Goroutine の情報を取得できる
• eBPF ではレジスタの値を取得できるので、R28 レジスタを取得する
#define goroutine_id(ctx) (__PT_REGS_CAST(ctx)-&gt;regs[28])
23


# Page. 24

![Page Image](https://bcdn.docswell.com/page/L7312X52J5.jpg)

eBPF Japan Meetup #5
Go 言語特有の問題
1. 並行処理におけるスレッドと関数のコンテキストの不一致
2. Uretprobe を使うとプログラムがクラッシュする
3. 関数の引数の取得
24


# Page. 25

![Page Image](https://bcdn.docswell.com/page/87DXZ2D6EG.jpg)

eBPF Japan Meetup #5
Uretprobe が使えない
$ ./main-go
runtime: g 8: unexpected return pc for Hello, 4!
Hello, 2!
Hello, 3!
Hello, 0!
main.hello called from 0xfffffffff000
stack: frame={sp:0x4000100f50, fp:0x4000100fb0}
stack=[0x4000100000,0x4000101000)
...
25


# Page. 26

![Page Image](https://bcdn.docswell.com/page/VJPPD16ZJ8.jpg)

eBPF Japan Meetup #5
0x0000004000100ff0: 0x0000000000000000
fatal error: unknown caller pc
0x0000000000000000
runtime stack:
runtime.throw({0xd56f8?, 0x64c30?})
runtime.(*unwinder).next(0x4000069dc8)
/usr/local/go/src/runtime/traceback.go:470 +0x2a0
fp=0x4000069d80 sp=0x4000069cf0 pc=0x6c9d0
...
runtime: g 8: unexpected return pc for main.hello called from
0xfffffffff000
26


# Page. 27

![Page Image](https://bcdn.docswell.com/page/2EV21Y3MEQ.jpg)

eBPF Japan Meetup #5
Uprobe のしくみ
• Uprobe は対象プログラムのロード時に関数の開始位置にある命令を割り込み
に置き換える
‣ 割り込み発生時に eBPF プログラムをカーネル中で実行する
‣ eBPF プログラムが終了したら、元の命令を再度実行する
27


# Page. 28

![Page Image](https://bcdn.docswell.com/page/57GR293XEL.jpg)

eBPF Japan Meetup #5
Uprobe 設定前と設定後の比較
- stp
x29, x30, [sp, #...
+ brk
#0x5
mov
x29, sp
str
w0, [sp, #28]
ldr
w1, [sp, #28]
adrp
x0, 0xaaaaaaaa0000
add
x0, x0, #0x830
bl
0xaaaaaaaa0650
&lt;printf@plt&gt;
ldr
lsl
ldp
ret
w0, [sp, #28]
w0, w0, #1
x29, x30, [sp],...
28


# Page. 29

![Page Image](https://bcdn.docswell.com/page/4EQVPW55JP.jpg)

eBPF Japan Meetup #5
Uretprobe のしくみ
• Uretprobe は Uprobe と同様関数の開始位置の命令を書き換える
• 関数の開始時割り込みを発生させてリターンアドレスを eBPF のエントリポ
イントへ書き換える
• 関数終了時は eBPF プログラム実行後に元のリターンアドレスに戻る
29


# Page. 30

![Page Image](https://bcdn.docswell.com/page/KJ4MYK9VE1.jpg)

eBPF Japan Meetup #5
Uretprobe 設定後の hello 関数
Uprobe を設定したときのバイナリは全く同じ
brk
#0x5
mov
x29, sp
str
w0, [sp, #28]
ldr
w1, [sp, #28]
adrp
x0, 0xaaaaaaaa0000
add
x0, x0, #0x830
bl
0xaaaaaaaa0650
&lt;printf@plt&gt;
ldr
lsl
ldp
ret
w0, [sp, #28]
w0, w0, #1
x29, x30, [sp], #32
30


# Page. 31

![Page Image](https://bcdn.docswell.com/page/LE186XK47G.jpg)

eBPF Japan Meetup #5
Backtrace が違う
hello 関数の 2 命令目で Backtrace を取得すると Return Address が不可解
なものになっている
Uretprobe なし
#0 0x0000aaaaaaaa07ac in
hello ()
#1 0x0000aaaaaaaa07ec in
main ()
Uretprobe 設定後
#0 0x0000aaaaaaaa07ac in
hello ()
#1 0x0000fffff7ff3000 in ??
()
31


# Page. 32

![Page Image](https://bcdn.docswell.com/page/GEWZWNDZE2.jpg)

eBPF Japan Meetup #5
メモリマップからアドレスを調べると…
Start Addr
End Addr
Size
Perms File
...
0x0000fffff7ff3000 0x0000fffff7ff4000 0x1000
--xp [uprobes]
...
Offset
0x0
リスト 2: メモリマップ
32


# Page. 33

![Page Image](https://bcdn.docswell.com/page/47Z153GLJ3.jpg)

eBPF Japan Meetup #5
Go ではなぜ Uretprobe が使えないのか
• スタック拡張が関係している
‣ Go ではランタイムが Goroutine に割り当てたスタックを拡張する
• Uretprobe はスタックのリターンアドレスを書き換える
• スタック拡張時にプログラムがクラッシュする
33


# Page. 34

![Page Image](https://bcdn.docswell.com/page/YJ6L9XYMEV.jpg)

eBPF Japan Meetup #5
スタック拡張時になぜクラッシュするのか
Go のランタイムはスタック拡張時
1. 新しいスタック領域を確保する
2. 元のスタックをコピーする
3. スタック内で参照されているアドレスを更新する
• このときフレームごとに関数の呼び出し元をさかのぼる(unwind)
• uretprobe がリターンアドレスを書き換えると呼び出し元の関数を特定
できずプログラムがクラッシュする
34


# Page. 35

![Page Image](https://bcdn.docswell.com/page/GJ51NXGQ74.jpg)

eBPF Japan Meetup #5
解決策: Uretprobe の代わりに Uprobe を使う
• Uprobe は命令を書き換えるだけなのでスタックを変更しない
• Uprobe はバイナリ上の任意の箇所に設定可能
• 関数の終了位置に Uprobe を設定することで、関数終了を検知できる
35


# Page. 36

![Page Image](https://bcdn.docswell.com/page/LE312XRZ75.jpg)

eBPF Japan Meetup #5
関数の終了位置を知るには
1. バイナリを逆アセンブルする
2. ret 命令の位置を特定する
3. ret 命令のある場所すべてに Uprobe を設定する
36


# Page. 37

![Page Image](https://bcdn.docswell.com/page/8EDXZ2Y4JG.jpg)

eBPF Japan Meetup #5
関数の終了位置を知るには
• 現時点で Go 言語のコンパイラは Tail Call Optimization を行わない
• すべての関数終了位置に ret 命令が存在する
https://github.com/iovisor/bcc/issues/1320#issuecomment-407927542
37


# Page. 38

![Page Image](https://bcdn.docswell.com/page/V7PPD16V78.jpg)

eBPF Japan Meetup #5
const arm64RetInstructionSize = 4
retOffsets := make([]uint64, 0)
for i := 0; i &lt; len(readBuf); i += arm64RetInstructionSize {
instruction, err := arm64asm.Decode(readBuf[i:])
if err != nil {
continue
}
if instruction.Op == arm64asm.RET {
retOffsets = append(retOffsets, offset+uint64(i))
}
}
リスト 3: ret 命令のオフセットを特定する
38


# Page. 39

![Page Image](https://bcdn.docswell.com/page/2JV21Y3RJQ.jpg)

eBPF Japan Meetup #5
for _, retOffset := range retOffsets {
_, err := ex.Uprobe(&quot;&quot;, end, &amp;link.UprobeOptions{Address:
retOffset})
if err != nil {
return nil, err
}
}
リスト 4: すべての関数終了位置に Uprobe を設定する
39


# Page. 40

![Page Image](https://bcdn.docswell.com/page/5EGR2936JL.jpg)

eBPF Japan Meetup #5
Go 言語特有の問題
1. 並行処理におけるスレッドと関数のコンテキストの不一致
2. Uretprobe を使うとプログラムがクラッシュする
3. 関数の引数の取得
40


# Page. 41

![Page Image](https://bcdn.docswell.com/page/4JQVPW527P.jpg)

eBPF Japan Meetup #5
関数の引数を取得したい
• リクエストの URL やレスポンスのステータスコードを取得したい
• これらは関数の引数に含まれる
41


# Page. 42

![Page Image](https://bcdn.docswell.com/page/K74MYK9PJ1.jpg)

eBPF Japan Meetup #5
Go 言語における関数の引数
• Go 言語では関数の引数はレジスタで渡される
‣ 構造体などはアドレスがレジスタで渡される
• インターフェイス型は 2 つの値（型情報とデータポインタ）で渡される
42


# Page. 43

![Page Image](https://bcdn.docswell.com/page/LJ186XKXEG.jpg)

eBPF Japan Meetup #5
net/http.serverHandler.ServeHTTP
• リクエストを受け終わってから実際のハンドラを呼び出す関数
• この関数に計装する
func (sh *serverHandler) ServeHTTP(rw ResponseWriter, req
*Request)
43


# Page. 44

![Page Image](https://bcdn.docswell.com/page/GJWZWNDKJ2.jpg)

eBPF Japan Meetup #5
func (sh *serverHandler) ServeHTTP(rw ResponseWriter, req
*Request)
• rw
ResponseWriter: レスポンスを書き込むためのインターフェイス
‣ 実体は net/http.response 構造体
‣ ステータスコードを取得したい
• req
*Request: リクエスト情報を持つ構造体
‣ URL パスやメソッドなどを取得したい
44


# Page. 45

![Page Image](https://bcdn.docswell.com/page/4EZ153GN73.jpg)

eBPF Japan Meetup #5
net/http.serverHandler.ServeHTTP
引数の配置は以下のようになる
func (h *serverHandler) ServeHTTP(w ResponseWriter, r *Request )
レジスタ
R0
R1
R2
R3
R4
R5
45


# Page. 46

![Page Image](https://bcdn.docswell.com/page/Y76L9XY9JV.jpg)

eBPF Japan Meetup #5
Go 言語における構造体
• eBPF では構造体の中身はポインタからフィールドまでのオフセットを使っ
て取得する
• 構造体は Go 言語独自のメモリレイアウトを持つ
‣ メモリレイアウトはコンパイル時に決定される
46


# Page. 47

![Page Image](https://bcdn.docswell.com/page/G751NXGDE4.jpg)

eBPF Japan Meetup #5
eBPF でステータスコードを取得
• 関数開始時 ResponseWriter のポインタ（3 番目のレジスタ）を BPF Map
に保存
struct event event = {0};
__u64 key = goroutine_id(ctx);
event.resp_ptr = (__u64)PT_REGS_PARM3(ctx);
bpf_map_update_elem(&amp;traces, &amp;key, &amp;event, BPF_ANY)
47


# Page. 48

![Page Image](https://bcdn.docswell.com/page/9J215XDM7R.jpg)

eBPF Japan Meetup #5
関数終了時に
1. 関数開始時に保存した ResponseWriter のポインタを BPF マップから取得
struct event {0};
__u64 key = goroutine_id(ctx);
event = bpf_map_lookup_elem(&amp;traces, &amp;key);
void *resp = (void *)event-&gt;resp_ptr;
48


# Page. 49

![Page Image](https://bcdn.docswell.com/page/DEYZK82P7M.jpg)

eBPF Japan Meetup #5
2. response 構造体の StatusCode のオフセット
(net_http_response_status_offset)を使ってステータスコードを読み取
る
int status_code;
bpf_probe_read(
&amp;status_code,
sizeof(status_code),
resp + net_http_response_status_offset
);
49


# Page. 50

![Page Image](https://bcdn.docswell.com/page/VJN3RQ8MJ8.jpg)

eBPF Japan Meetup #5
フィールドのオフセット
• フィールドのオフセットはコンパイル時に決まる
‣ 言語のバージョンなどで変わりうる
‣ フィールドの順序が変わったりもする
• DWARF（デバッグ情報）にメモリレイアウトの情報が含まれる
• eBPF にはロード時にグローバル変数を通じてオフセットを渡す
50


# Page. 51

![Page Image](https://bcdn.docswell.com/page/YE99MDKWE3.jpg)

eBPF Japan Meetup #5
eBPF ロード前に DWARF を解析して eBPF プログラムに渡す
// DWARF解析
off := analyzer.StructFieldOffset(&quot;net/http.response&quot;,
&quot;status&quot;)
// eBPFプログラムにセット
objs.NetHttpResponseStatusOffset.Set(uint32(off))
volatile __u32 net_http_response_status_offset;
51


# Page. 52

![Page Image](https://bcdn.docswell.com/page/GE89L3RR7D.jpg)

eBPF Japan Meetup #5
最後に: BPF マップから情報を取得
• 取得した文字列はバイト列になっているので、
ユーザプログラム側で Go 言語の文字列に変換する
• 時刻は unix.CLOCK_MONOTONIC で取得されているので、
ユーザプログラム側でミリ秒に変換する
GET: /items a95b06d292ce2616af51073ae379f944 6d34e11faf7ebe3e
http.route=/items host.name=localhost:8081
http.response.status_code=200 http.request.method=GET
52


# Page. 53

![Page Image](https://bcdn.docswell.com/page/LELWLVK2JR.jpg)

eBPF Japan Meetup #5
全体の流れ
eBPF プログラムをロードするまで
1. 計装対象から DWARF をもとに構造体のフィールドオフセットを取得
2. 計装対象の関数を逆アセンブルし、関数の終了位置を特定
2. eBPF プログラムをロードし、Uprobe を設定
53


# Page. 54

![Page Image](https://bcdn.docswell.com/page/4JM9MG29EW.jpg)

eBPF Japan Meetup #5
計装対象プログラム実行後
1. 関数開始時に引数・リクエストの情報を eBPF マップに保存
2. 関数終了時にステータスコード・処理時間を eBPF マップに保存
3. ユーザプログラム側で eBPF マップから情報を取得
54


# Page. 55

![Page Image](https://bcdn.docswell.com/page/PJRGVZM9E9.jpg)

eBPF Japan Meetup #5
Go 言語での eBPF ゼロコード計装まとめ
• Goroutine によりスレッドと非同期処理が紐づかない
‣ 関数呼び出しごとに Goroutine を取得して紐づける
• Uretprobe が利用できない
‣ なぜなら Uretprobe がリターンアドレスを書き換えるから
‣ バイナリを逆アセンブルして関数終了位置に Uprobe を設定する
• 関数の引数は Go 言語独自のメモリレイアウトに対応する必要がある
以上を気をつければ簡単にユーザアプリケーションへの計装が可能
55


