電通総研 テックブログ

電通総研が運営する技術ブログ

【入門eBPF】カーネルに住む蜂

はじめに

エンタープライズ第一本部、2025 Japan AWS Jr. Champions の佐藤悠です。

私はKubernetesを触る機会が多く、その中でも監視に最近興味を持っています。

監視を実現するセキュリティソリューションの中にTetragonなどが挙げられますが、この監視のベースとなっている技術にeBPFがあります。

このeBPFをEC2インスタンスAmazon Linux上で動かして、その面白さと何が起きているのかを解説します。

Linuxユーザー空間/カーネル空間

まずはeBPFの背景知識としてLinuxのユーザー空間とカーネル空間に関して理解する必要があります

下図のようにLinuxにはユーザー空間とカーネル空間というものが存在しています。

これは一度カーネルを経由させることで物理デバイスへのアクセスを制御するのと共に、プロセスが利用するリソースの一元管理を行い配分をする狙いがあります。

そのためユーザーとカーネルの空間を区切り、システムコールというインターフェースでのみカーネルへアクセスを強制し、アクセス制御とリソース配分を漏れなく実現しているというわけです。

以下はlsコマンドを実行したときに呼び出されているシステムコールです。

さまざまなシステムコールを組み合わせてカーネルにアクセスして、コマンドの実行結果を出力していることが理解できますね。

$strace -c ls
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         7           read
  0.00    0.000000           0         3           write
  0.00    0.000000           0        23           close
  0.00    0.000000           0        30           mmap
  0.00    0.000000           0         7           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         2           ioctl
  0.00    0.000000           0         4           pread64
  0.00    0.000000           0         2         2 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2         2 statfs
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         1           futex
  0.00    0.000000           0         2           getdents64
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0        34        13 openat
  0.00    0.000000           0        22           newfstatat
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           prlimit64
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000           0       151        18 total

ここでプロセスはカーネルを経由するから、アプリケーションの振る舞いはカーネルを見ていれば監視できるのでは?という気づきがあるわけです。

カーネルモジュール

じゃあ「Linuxカーネルにコードの変更をしよう!」と思っても、Linuxの利用されている規模、そもそものコード量などを鑑みると、コミュニティの理解を得て、開発のコストをかけて、アップストリームになるまで待つという気の遠くなる時間がかかる作業となってしまいます。

この問題に対して、Linuxカーネルの変更・拡張のためにモジュールを受け入れるようにできています。

これがカーネルモジュールの概念です。

では、カーネルモジュールを作成してみます。

私はC言語の経験がないので以下の書籍「Linux Device Drivers, 3rd Edition」のChapter2 p16 The Hello world Moduleの項を参考にしました。

参考: https://www.oreilly.com/library/view/linux-device-drivers/0596005903/ch02.html

適当に変更してgogo.cを作成します。

以下のモジュールをカーネルにロードするとgo!go!、アンロードするとbyebye!とログバッファに出力します。

#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
//ロードされたら実行される関数
static int gogo(void)
{
    printk(KERN_ALERT "go!go!\n");
    return 0;
}
//アンロードされたら実行される関数
static void byebye(void)
{
    printk(KERN_ALERT "byebye!\n");
}

module_init(gogo);
module_exit(byebye);

これが今回のモジュールです。

ではロード/アンロードしてみます。

環境

アウトバウンド443ポートの通信のみを許可したサブネット内です。

EC2 インスタンスAmazon linuxを起動し、SessionManagerで接続をしています。

sh-5.2$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.8.20250804"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2029-06-30"

カーネルヘッダのインストール

sudo dnf install gcc make kernel-devel kernel-headers
Last metadata expiration check: 1:37:35 ago on Sat Oct 18 05:49:32 2025.
Package gcc-11.5.0-5.amzn2023.0.4.x86_64 is already installed.
Package make-1:4.3-5.amzn2023.0.2.x86_64 is already installed.
Package kernel-devel-1:6.1.147-172.259.amzn2023.x86_64 is already installed.
Package kernel-headers-1:6.1.147-172.259.amzn2023.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!

ソースコードの保存

#先ほどのファイルをコピペ
sh-5.2$ sudo vim gogo.c
sh-5.2$ cat gogo.c
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
//ロードされたら実行される関数
static int gogo(void)
{
    printk(KERN_ALERT "go!go!\n");
    return 0;
}
//アンロードされたら実行される関数
static void byebye(void)
{
    printk(KERN_ALERT "byebye!\n");
}

module_init(gogo);
module_exit(byebye);

Makefileの作成

同じディレクトリでMakefileを作成

sh-5.2$ sudo vim Makefile
sh-5.2$ sudo cat Makefile
obj-m += gogo.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

モジュールのビルド

make

gogo.koが出来ました。

モジュールのロード

※私にはこの末尾のログは紫に光って見えています。

sh-5.2$ sudo insmod gogo.ko
sh-5.2$ sudo dmesg | tail
...
[ 6689.540149] gogo: loading out-of-tree module taints kernel.
[ 6689.540977] gogo: module verification failed: signature and/or required key missing - tainting kernel
[ 6689.542802] go!go!

モジュールのリスト

lsmodで確認しました。gogoモジュールがあります。

sh-5.2$ lsmod
Module                  Size  Used by
gogo                   16384  0
・・・

モジュールのアンロード

sh-5.2$ sudo rmmod gogo
sh-5.2$ sudo dmesg | tail
...
[ 6689.540149] gogo: loading out-of-tree module taints kernel.
[ 6689.540977] gogo: module verification failed: signature and/or required key missing - tainting kernel
[ 6689.542802] go!go!
[ 6999.290169] byebye!

感想

こんなに手軽にカーネルが拡張できるんですね。

ところで、カーネルモジュールをロードしたタイミングで以下のログが出ています。

loading out-of-tree module taints kernel.

ツリー外(公式外)のモジュールをロードしたため、カーネルが汚染されました。(翻訳:ChatGPT)

まあ自作のモジュールなので、メッセージの通りですね。

ただ、安易なモジュールのロードって自作や第三者の提供、問わずカーネルが壊れる可能性がありリスクが大きそうだと思えてきます。

ようやく、eBPF

そんなカーネルモジュールの代わりに登場するのがeBPFです。

eBPFはカーネルソースコードの改変や、カーネルモジュールのロードを必要とせずに、安全かつ効率的なカーネルの機能拡張に利用されています。

参考:https://ebpf.io/ja/what-is-ebpf/

この安全な拡張は主にeBPF Verifierによって保障されています

eBPF Verifier

eBPF プログラムはクラッシュしたりシステムに悪影響を及ぼしたりしません。

プログラムは常に実行が完了します(つまり、プログラムは無限ループに陥って処理が実行し続けられることはありません)。

参考:https://ebpf.io/ja/what-is-ebpf/

このeBPF Verifier(検証器)でチェックしている内容は多岐にわたっているため、私が調査した範囲で代表的な項目を以下に列挙します。

呼び出せるカーネル関数はかなり限定的

基本的には安全性が検証されたヘルパー関数しか使えません。

ヘルパー関数でも関係外の関数を呼ぼうとするとエラーとなります。

※BPFプログラムタイプの概念

Stepの制限

100万Stepの指示数に以内の時に限り許可することで、CPUを占有しないようにしています

メモリアクセスの制限

アクセス可能な範囲外のメモリに触れません。

※eBPF mapという概念を使用してカーネルとユーザー空間でデータのやり取りをします。

NULLポインタ参照がないか

   ( ・∀・)   | | ガッ
  と    )    | |
    Y /ノ    人
     / )    <  >__Λ∩
   _/し' //. V`Д´)/
  (_フ彡        /

これ以上にも、もっとたくさんの検証事があって安全性を保障しています。

ここに踏み込むと、分からないことだらけになってしまうので、「さまざまな検証を経て、カーネル空間での実行を許される」としておきます。

eBPFでHello World

ではeBPFプログラムの実装をします。

これは以下の書籍「入門eBPF」2章 p15 eBPFの「Hello World」を参考にしています。

参考:https://www.oreilly.co.jp/books/9784814400560/

BCCPythonライブラリでC言語で書いたeBPFコードをバイトコードへ変換し、カーネル空間にロードさせます。

#!/usr/bin/python
from bcc import BPF

program = r"""
int hello(void *ctx){
    bpf_trace_printk("Hello World!\\n");
    return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

b.trace_print()

それぞれ分解しながら解説します。

以下のコードがカーネルにロードされるeBPFコードです。

program = r"""
int hello(void *ctx){
    bpf_trace_printk("Hello World!\\n");
    return 0;
}
"""

eBPFのプログラム作成時には、先述のヘルパー関数の1つ、bpf_trace_printk()を用いて出力していることが分かります。

このC言語コンパイルして動作するようにします。

b = BPF(text=program)

以上のように書くことで、BPFオブジェクトの生成時に引数に渡すことでBCCコンパイルさせます。

syscall = b.get_syscall_fnname("execve")

次のコードでバイナリのパスを渡して実行するシステムコール「execve()」の関数名を取得します。

b.attach_kprobe(event=syscall, fn_name="hello")

ここでexecve()に関数helloをアタッチします。

kprobeとはLinuxカーネルの任意の関数や命令の実行時に動的にフックして処理を挿入できる仕組みです

このプログラムがカーネルにロードされると、execve()が実行された際にはhello Worldがtrace_pipe(ユーザー空間から取得可能)に出力されます。

b.trace_print()

最後に、トレースデータをPythonが実行されている標準出力へ出力します。

実際に動かしてみます。

環境

カーネルモジュールの検証時と同様

ファイルの作成

sh-5.2$ sudo vim hello.py
sh-5.2$ cat hello.py
#!/usr/bin/python
from bcc import BPF

program = r"""
int hello(void *ctx){
    bpf_trace_printk("Hello World!\\n");
    return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

b.trace_print()

実行

#実行権限の付与
h-5.2$ sudo chmod +x hello.py
sh-5.2$ sudo ./hello.py

ちなみにeBPFは原則特権ユーザーでしか実行できないので、./hello.pyの実行だと以下のようになります

sh-5.2$ ./hello.py
bpf: Failed to load program: Operation not permitted

Traceback (most recent call last):
  File "/home/ssm-user/./hello.py", line 13, in <module>
    b.attach_kprobe(event=syscall, fn_name="hello")
  File "/usr/lib/python3.9/site-packages/bcc/__init__.py", line 841, in attach_kprobe
    fn = self.load_func(fn_name, BPF.KPROBE)
  File "/usr/lib/python3.9/site-packages/bcc/__init__.py", line 523, in load_func
    raise Exception("Need super-user privileges to run")
Exception: Need super-user privileges to run

結果

sh-5.2$ sudo ./hello.py
b'           <...>-46202   [001] ...21 84764.107466: bpf_trace_printk: Hello World!'
b'           <...>-46203   [001] ...21 84823.020341: bpf_trace_printk: Hello World!'

何も操作してなくても裏でexecve()って呼ばれているんですね。

別の接続を開いてls等のコマンドを打つと、Hello World!が同時に出力されるのが分かります。

感想

このように特定のシステムコールに紐づけて、監視できると分かりました。

まあ、今回の実験では何かが実行されたという情報しかないですが...

そしてTetragonへ...

このようにeBPFはカーネルで動作できるので、実行環境で監視の範囲を広げられるように感じます。

ただ当然ですが、毎回監視のためのコードを書くのは辛いものがあります。

そこで、はじめに例にあげたTetragonのようなeBPFをベースにしたサービスを利用するんですね。

ここまでくれば、Kubernetesではよくある監視のサイドカーコンテナ利用に対して、eBPFという切り口で監視を提供するときに拡張される範囲がわかってくると思います。

メインコンテナに対して同じボリュームを共有し、localhostで接続可能なサイドカーコンテナとして実行する監視(例:Datadogのサイドカーインジェクション)に加えて、ノードにDaemonSetsとしてデプロイされることで、カーネルで実行された実行イベントまで監視の目を広げることができますね。

参考:https://tetragon.io/docs/getting-started/execution/

本記事ではeBPFの紹介なので、Tetragonの詳細の解説は省略します。

おわりに

eBPFではよく蜂を見かけますが、これはeBeeという名前です。

私たちは一緒に働いてくれる仲間を募集しています!

電通総研 キャリア採用サイト 電通総研 新卒採用サイト

執筆:@sato.yu
レビュー:@akutsu.masahiro
Shodoで執筆されました