カーネルモジュールとeBPFアプリを書いてみた

LinuxカーネルモジュールとeBPFアプリケーションを作ってみた。 とりあえずやってみたのは次のこと。

  • カーネルモジュールの開発環境を準備する
  • イベントフックを試してみる(カーネルモジュール, eBPF with bcc-python)

カーネルモジュールの開発環境を準備する

使った環境はこちら。ちょっと古い。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 2つためした
$ cat /etc/issue
Ubuntu 16.04.7 LTS \n \l
$ uname -a
Linux ubuntu-xenial 4.4.0-194-generic #226-Ubuntu SMP Wed Oct 21 10:19:36 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/issue
Ubuntu 18.04.5 LTS \n \l
$ uname -a
Linux test-k3s 4.15.0-124-generic #127-Ubuntu SMP Fri Nov 6 10:54:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# カーネルモジュール開発環境の準備で紹介されていたpkg
$ apt-get install \
  gawk wget git diffstat unzip texinfo \
  gcc-multilib chrpath socat xterm lzop kmod libsdl1.2-dev \
  build-essential flex libelf-dev libncurses5-dev

# カーネルビルドに必要なもの
$ apt-get install \
  build-essential bc bison flex libssl-dev \
  libelf-dev libssl-dev libncurses5-dev

参考にしたのはこのブログと動かしながらゼロから学ぶLinuxカーネルの教科書の2つ。

イベントフック

ドキュメントにeBPFで対応してると書かれているイベントはCで12、Pythonで7

  • kprobe
  • kretprobe
  • tracepoint
  • uprobe
  • uretprobe
  • USDT
  • raw_tracepoint

eBPFのkprobeはカーネル内のjprobeっぽいフックポイントになっている。カーネルモジュールではkprobeは任意のアドレスをフックできる。一方でjprobe, kretprobeはkprobeで実装された高水準のフックポイントだ。 それぞれ関数のエントリポイント, 関数からのリターンをフックする。

uprobe, uretprobeはユーザープロセスのシンボルに対して使えるプローブになっている。

tracepointは事前にカーネルに組み込まれたフックポイント。同様にUSDTは事前にユーザープログラムに組み込まれたフックポイントになっている。

eBPFとは関係なく他にもイベントフックポイントはあるようだった。

  • kprobe
    • jprobe
    • kretprobe
  • uprobe
  • hw_breakpoint
  • Tracepoint
  • USDT

jprobe(kprobe in eBPF)について

jprobe in カーネルモジュール

jprobeを使うためにすること。

  1. jprobeを使えるカーネル内のシンボルを確認する
  2. 対象の関数と同じスキーマのイベントハンドラを実装する
  3. jprobe構造体を定義する
  4. register_jprobe(&jprobe_instance) を呼び出してjprobe構造体をイベントハンドラとして登録する
  5. unregister_jprobe(&jprobe_instance) を呼び出してイベントハンドラを解除する
1
2
3
4
5
sudo cat /proc/kallsyms | less
....
0000000000004800 A unsafe_stack_register_backup
0000000000004840 A cpu_debug_store
....

jprobeを使うカーネルモジュールのサンプルは下のようになる。jprobe_return/0の呼び出しが必要とのこと

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static ssize_t trace_etn_write(struct file *filp, const char __user *buf, 
                               size_t count, loff_t *f_pos)
{
	/* Always end with a call to jprobe_return(). */
	jprobe_return();
	return 0;
}

static struct jprobe jprobe_instance = {
	.entry			= trace_etn_write,
	.kp = {
		.symbol_name	= "etn_write",
	},
};

static int __init jprobe_init(void)
{
	int ret;

	ret = register_jprobe(&jprobe_instance);
	if (ret < 0) {
		return -1;
	}
	return 0;
}

static void __exit jprobe_exit(void)
{
	unregister_jprobe(&jprobe_instance);
}

module_init(jprobe_init)
module_exit(jprobe_exit)

ref. https://gist.github.com/dzeban/a19c711d6b6b1d72e594

kprobe in eBPF

前に使ったサンプルコードに attach_kretprobe() の呼び出しを加えている。

eBPFの開発環境については下のパッケージインストールしました。

1
2
3
4
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
$ echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list
$ sudo apt-get update
$ sudo apt-get install python3-bcc bcc-tools libbcc-examples linux-headers-$(uname -r)

BPFインスタンスの attach_kprobe/2 メソッドを使い event のハンドラとして fn_name を登録して使います。

ここでBPFのハンドラが受け取る *ctx の中身はkprobeの場合は struct pt_regs が入っているらしい

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from bcc import BPF
import time

bpf_code = """
int trace_clone(void *ctx) {
   bpf_trace_printk("Hello: clone(2)\\n");
   return 0;
}
int trace_fork(void *ctx) {
    bpf_trace_printk("Hello: fork(2)\\n");
    return 0;
 }
int trace_openat(void *ctx) {
   bpf_trace_printk("Hello: openat(2)\\n");
   return 0;
}
int trace_open(void *ctx) {
   bpf_trace_printk("Hello: open(2)\\n");
   return 0;
}
int trace_close(void *ctx) {
   bpf_trace_printk("Hello: close(2)\\n");
   return 0;
}
"""

bpf = BPF(text=bpf_code)
for name in ["close", "open", "openat", "clone", "fork"]:
    syscall_name = bpf.get_syscall_fnname(name)
    print(syscall_name)
    bpf.attach_kprobe(event = syscall_name, fn_name = "trace_{}".format(name))
    bpf.attach_kretprobe(event = syscall_name, fn_name = "trace_{}".format(name))
bpf.trace_print()

なんとなくサンプルコードを走らせたり入門的な記事を拾い読みして感じをつかんだと思う。

次は下を考えている。

  • 公式資料やコミュニティへのリンクを集める
  • bpfオブジェクトを自力でビルドして bpf(2) で利用したりubpf上で走らせるなどを試す
  • 他のイベントをフックする
  • 特にeBPF, XDPの実例に目を通したり使ってみる
  • オライリーのLinuxデバイスドライバを読み通す
  • デバイスファイルを提供するなどカーネルモジュールの実装練習
  • カーネルモジュールやカーネルをQEMUでデバッグ

参考資料

comments powered by Disqus