eBPFを使用してLinuxシステムコールをトレースする

# はじめに
eBPF（Extended Berkeley Packet Filter）は、Linuxカーネルで動作するプログラムを効果的に実行できる仕組みです。この記事では、eBPFを使用してLinuxシステムコールをトレースする方法を紹介します。

## システムコール一覧
Linuxのシステムコール一覧です。
|システムコール  |説明  |
|---|---|
|accept|新しい接続を受け入れる。通常、サーバーソケットで使用されます。|
|bind|ソケットにアドレスを関連付ける。|
|close|ファイルディスクリプタやソケットなどの資源を解放する。| 
|connect|クライアントがサーバーに接続するために使用される。|
|creat|ファイルを作成する。新しいファイルが作成され、既存のファイルが更新される可能性があります。|
|dup|ファイルディスクリプタを複製する。|
|execve|新しいプログラムを実行する。|
|fcntl|ファイルディスクリプタの操作を行う。|
|fork|新しいプロセスを作成する。|
|fstat|ファイルのステータスを取得する。|
|listen|ソケットが接続待ち状態になるように設定する。|
|lstat|ファイルのステータスを取得するが、シンボリックリンクの場合はリンク先ではなくリンク自体のステータスを取得する。|
|open|ファイルを開く。存在しない場合は新しいファイルを作成する。|
|openat|相対パスでファイルを開く。openと似ていますが、ディレクトリファイルディスクリプタを指定することができます。|
|poll|ファイルディスクリプタの状態をポーリングする。|
|read|ファイルからデータを読み取る。|
|recv|ソケットからデータを受け取る。|
|recvfrom|ソケットからデータを受け取り、送信元の情報も取得する。|
|send|ソケットにデータを送信する。|
|sendto|ソケットにデータを送信し、送信先の情報を指定する。|
|socket|新しいソケットを作成する。|
|stat|ファイルのステータスを取得する。lstatと異なり、シンボリックリンクの場合はリンク先のステータスを取得する。|
|write|ファイルにデータを書き込む。|
# eBPFプログラムの定義
まず、eBPFプログラムを定義します。以下は、特定のシステムコールが呼ばれた際にメッセージを出力する簡単なプログラムの例です。

```c
int syscall_accept(void *ctx){
    bpf_trace_printk("syscall_accept!");
    return 0;
}

// 他のシステムコールについても同様に定義
```
# BPFオブジェクトの定義
次に、BPFオブジェクトを定義し、システムコールに紐づけます。
```python
from bcc import BPF

program = """
// 上記で定義したeBPFプログラム
"""

b = BPF(text=program)

# 各システムコールに対してkprobeをアタッチ
# 例：acceptシステムコール
syscall_accept = b.get_syscall_fnname("accept")
b.attach_kprobe(event=syscall_accept, fn_name="syscall_accept")
```
# トレースの開始
最後に、トレースを開始します。
```python
b.trace_print(fmt="TASK={0} PID={1} MESSAGE={5}")
```
# 結果例
```plaintext
TASK=b'example' PID=123 MESSAGE=b'syscall_accept!'
```
このようにして、特定のシステムコールが呼ばれるたびにメッセージが出力されます。

# ソースコード全文
以下は、eBPFプログラムの完全なソースコードです。Linuxを起動しているだけで、read、writeのトレースが大量に出力されますので、read、writeをkprobeにアタッチする処理はコメントアウトしています。
```python:SysCallTrc.py
#!/usr/bin/python
from bcc import BPF

# eBPFプログラムの定義
program = r"""
int syscall_accept(void *ctx){
    bpf_trace_printk("syscall_accept!");
    return 0;
}

int syscall_bind(void *ctx){
    bpf_trace_printk("syscall_bind!");
    return 0;
}

int syscall_close(void *ctx){
    bpf_trace_printk("syscall_close!");
    return 0;
}

int syscall_connect(void *ctx){
    bpf_trace_printk("syscall_connect!");
    return 0;
}

int syscall_creat(void *ctx){
    bpf_trace_printk("syscall_creat!");
    return 0;
}

int syscall_dup(void *ctx){
    bpf_trace_printk("syscall_dup!");
    return 0;
}

int syscall_execve(void *ctx){
    bpf_trace_printk("syscall_execve!");
    return 0;
}

int syscall_fcntl(void *ctx){
    bpf_trace_printk("syscall_fcntl!");
    return 0;
}

int syscall_fork(void *ctx){
    bpf_trace_printk("syscall_fork!");
    return 0;
}

int syscall_fstat(void *ctx){
    bpf_trace_printk("syscall_fstat!");
    return 0;
}

int syscall_listen(void *ctx){
    bpf_trace_printk("syscall_listen!");
    return 0;
}

int syscall_lstat(void *ctx){
    bpf_trace_printk("syscall_lstat!");
    return 0;
}

int syscall_open(void *ctx){
    bpf_trace_printk("syscall_open!");
    return 0;
}

int syscall_openat(void *ctx){
    bpf_trace_printk("syscall_openat!");
    return 0;
}

int syscall_poll(void *ctx){
    bpf_trace_printk("syscall_poll!");
    return 0;
}

int syscall_read(void *ctx){
    bpf_trace_printk("syscall_read!");
    return 0;
}

int syscall_recv(void *ctx){
    bpf_trace_printk("syscall_recv!");
    return 0;
}

int syscall_recvfrom(void *ctx){
    bpf_trace_printk("syscall_recvfrom!");
    return 0;
}

int syscall_send(void *ctx){
    bpf_trace_printk("syscall_send!");
    return 0;
}

int syscall_sendto(void *ctx){
    bpf_trace_printk("syscall_sendto!");
    return 0;
}

int syscall_socket(void *ctx){
    bpf_trace_printk("syscall_socket!");
    return 0;
}

int syscall_stat(void *ctx){
    bpf_trace_printk("syscall_stat!");
    return 0;
}

int syscall_write(void *ctx){
    bpf_trace_printk("syscall_write!");
    return 0;
}

"""

# BPFオブジェクトの定義
b = BPF(text=program)

# システムコールのkprobeをアタッチ
syscall_accept = b.get_syscall_fnname("accept")
syscall_bind   = b.get_syscall_fnname("bind")
syscall_close  = b.get_syscall_fnname("close")
syscall_connect = b.get_syscall_fnname("connect")
syscall_creat = b.get_syscall_fnname("creat")
syscall_dup = b.get_syscall_fnname("dup")
syscall_execve = b.get_syscall_fnname("execve")
syscall_fcntl = b.get_syscall_fnname("fcntl")
syscall_fork = b.get_syscall_fnname("fork")
syscall_fstat = b.get_syscall_fnname("fstat")
syscall_listen = b.get_syscall_fnname("listen")
syscall_lstat = b.get_syscall_fnname("lstat")
syscall_open = b.get_syscall_fnname("open")
syscall_openat = b.get_syscall_fnname("openat")
syscall_poll = b.get_syscall_fnname("poll")
syscall_read = b.get_syscall_fnname("read")
syscall_recv = b.get_syscall_fnname("recv")
syscall_recvfrom = b.get_syscall_fnname("recvfrom")
syscall_send = b.get_syscall_fnname("send")
syscall_sendto = b.get_syscall_fnname("sendto")
syscall_socket = b.get_syscall_fnname("socket")
syscall_stat = b.get_syscall_fnname("stat")
syscall_write = b.get_syscall_fnname("write")

b.attach_kprobe(event=syscall_accept, fn_name="syscall_accept")
b.attach_kprobe(event=syscall_bind, fn_name="syscall_bind")
b.attach_kprobe(event=syscall_close, fn_name="syscall_close")
b.attach_kprobe(event=syscall_connect, fn_name="syscall_connect")
b.attach_kprobe(event=syscall_creat, fn_name="syscall_creat")
b.attach_kprobe(event=syscall_dup, fn_name="syscall_dup")
b.attach_kprobe(event=syscall_execve, fn_name="syscall_execve")
b.attach_kprobe(event=syscall_fcntl, fn_name="syscall_fcntl")
b.attach_kprobe(event=syscall_fork, fn_name="syscall_fork")
b.attach_kprobe(event=syscall_fstat, fn_name="syscall_fstat")
b.attach_kprobe(event=syscall_listen, fn_name="syscall_listen")
b.attach_kprobe(event=syscall_lstat, fn_name="syscall_lstat")
b.attach_kprobe(event=syscall_open, fn_name="syscall_open")
b.attach_kprobe(event=syscall_openat, fn_name="syscall_openat")
b.attach_kprobe(event=syscall_poll, fn_name="syscall_poll")
#b.attach_kprobe(event=syscall_read, fn_name="syscall_read")
b.attach_kprobe(event=syscall_recv, fn_name="syscall_recv")
b.attach_kprobe(event=syscall_recvfrom, fn_name="syscall_recvfrom")
b.attach_kprobe(event=syscall_send, fn_name="syscall_send")
b.attach_kprobe(event=syscall_sendto, fn_name="syscall_sendto")
b.attach_kprobe(event=syscall_socket, fn_name="syscall_socket")
b.attach_kprobe(event=syscall_stat, fn_name="syscall_stat")
#b.attach_kprobe(event=syscall_write, fn_name="syscall_write")

# トレースの開始
b.trace_print(fmt="TASK={0} PID={1} MESSAGE={5}")
```

# まとめ
eBPFを使用することで、システムコールのトレースが容易になります。他にも様々な情報を取得することが可能なので、必要に応じて拡張して利用することができます。

このサンプルプログラムの完全なソースコードは、[GitHubリポジトリ](https://github.com/EsperNyanko/eBPF_SysCallTrc) で公開しています。ぜひご覧ください。