第688回と第690回では、
Python版BCCの問題点
これまで紹介していたBPF Compiler Collection
実行環境でBPFオブジェクトをビルドする必要があるこの方法にはいくつかの問題点が存在します。
- 実行環境にコンパイラをインストールする必要がある
- 実行環境にカーネルのヘッダーファイルが必要になる
- 実行時にコンパイルという重い処理が走る
この問題は日常的に開発に利用しているデスクトップ環境やサーバーならそこまで影響はありません。しかしながらプロダクション用途となると話は別です。セキュティ的にもメンテナンス的にもできる限り余計なものはインストールしたくないでしょう。障害の事前検知手段としてeBPFを使いたいと考えたときに、
対応策として、
- Clang等でBPFオブジェクトをコンパイルし、
別途ロードする - sysfsの機能を用いてトレーシングする
- bpftrace等の別のツールを使う
- BPF CO-REによりポータブルなバイナリを生成する
Clang等でBPFオブジェクトをコンパイルし、別途ロードする
Clang等を使えばBPFオブジェクトをコンパイルできます。これをさらにカーネルにロードしてしまえば、
sysfsの機能を用いてトレーシングする
2番目の選択肢としてカーネルのsysfsにあるトレース機能を使ってトレーシングを行う方法が考えられます。この方法ならシステムに追加でソフトウェアをインストールしなくても、trace_
でしか取得できないことでしょう。つまりユーザーランド側はtrace_
の出力結果を解析する必要があります。表示するデータの量は変更可能ではあるのですが、
bpftrace等の別のツールを使う
より高機能なトレーシングツールとしてbpftraceが存在します。内部的にやっていることはBCCと同じでその場でコンパイルすることになるのですが、
BPF CO-REによりポータブルなバイナリを生成する
最後の方法が現在注目されている、
BPF CO-REもbpftraceと同じくカーネルのBTFを活用します。BTFは端的に言うとBPFプログラムを動かすために必要な、
つまり
結局何を使うべきか
このように、
今回例をあげたもの中だと、
今回はsysfsとbpftraceを使う方法を紹介します。BPF CO-REについては次回以降に解説予定です。
今一度execveをトレースする
まずは最初に第690回で解説した、execve()
をトレースするPython版BCC向けのコードexecve.
)
#!/usr/bin/python3
from bcc import BPF
bpf_text="""
#include <linux/sched.h>
struct data_t {
u32 pid;
u32 ppid;
char comm[TASK_COMM_LEN];
char fname[128];
};
BPF_PERF_OUTPUT(events);
int syscall__execve(struct pt_regs *ctx, const char __user *filename)
{
struct data_t data = {};
struct task_struct *task;
data.pid = bpf_get_current_pid_tgid() >> 32;
task = (struct task_struct *)bpf_get_current_task();
data.ppid = task->real_parent->tgid;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_user(data.fname, sizeof(data.fname), (void *)filename);
events.perf_submit(ctx, &data, sizeof(struct data_t));
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="syscall__execve")
print("PID PPID COMM FNAME")
def print_event(cpu, data, size):
event = b["events"].event(data)
print("{:<8} {:<8} {:16} {}".format(event.pid, event.ppid, event.comm.decode(), event.fname.decode()))
b["events"].open_perf_buffer(print_event)
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
BCC
$ sudo apt instal python3-bpfcc $ sudo python3 execve.py (中略) PID PPID COMM FNAME 2822958 5480 tmux: server /bin/sh 2822960 2822958 sh /usr/bin/byobu-status 2822959 5480 tmux: server /bin/sh 2822961 2822960 byobu-status /usr/bin/sed 2822963 2822959 sh /usr/bin/byobu-status 2822964 2822960 byobu-status /usr/bin/tmux
sysfsの機能を用いてトレーシングする
シンプルなトレーシングなら余計なツールをインストールすることなく、execve()
システムコールの呼び出しを記録する方法を実現してみましょう。ちなみにこの節の説明はbpftraceの利用とは関係ないため、
/sys/kernel/debug/tracingの活用
まずはsysfsを利用したトレーシングの基本からです。単純にexecve()
システムコールの呼び出しを記録するだけなら、
$ sudo mkdir /sys/kernel/debug/tracing/instances/execve $ echo 'sys_enter_execve' | sudo tee -a /sys/kernel/debug/tracing/instances/execve/set_event $ echo 1 | sudo tee /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/enable
まず、/sys/
」sudo -i
」
/sys/
以下に任意の名前のディレクトリを作ることで、set_
にはイベントトレーサーを記録できます。ここではシステムコールのsys_
」/sys/
」
最後に、
$ echo 1 | sudo tee /sys/kernel/debug/tracing/instances/execve/free_buffer $ sudo cat /sys/kernel/debug/tracing/instances/execve/trace_pipe sh-666208 [005] .... 1313488.957135: sys_execve(filename: 561bf66f3280, argv: 561bf57cd8c8, envp: 561bf66f3078) byobu-status-666209 [002] .... 1313488.958351: sys_execve(filename: 55d8463f94d0, argv: 55d846404450, envp: 55d8463f92c8) tmux: client-666211 [004] .... 1313488.959675: sys_execve(filename: 55d846406910, argv: 55d8464065f0, envp: 55d846406708) (後略)
イベントごとの出力フォーマットは次の方法で確認できます。このあたりの書式や使い方はカーネルドキュメントの
$ sudo cat /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/format name: sys_enter_execve ID: 707 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:int __syscall_nr; offset:8; size:4; signed:1; field:const char * filename; offset:16; size:8; signed:0; field:const char *const * argv; offset:24; size:8; signed:0; field:const char *const * envp; offset:32; size:8; signed:0; print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))
この出力フォーマットを見る限り、
作成したイベントを止め、
$ echo 0 | sudo tee /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/enable $ echo '!sys_enter_execve' | sudo tee -a /sys/kernel/debug/tracing/instances/execve/set_event $ sudo rmdir /sys/kernel/debug/tracing/instances/execve
「/sys/
」
システムコール以外をトレースしてファイル名を表示する
せっかくなのでファイル名を表示させてみましょう。この場合、
$ grep execve /proc/kallsyms 0000000000000000 t audit_log_execve_info 0000000000000000 t __do_execve_file.isra.0 0000000000000000 T __ia32_compat_sys_execve 0000000000000000 T __ia32_compat_sys_execveat 0000000000000000 T __ia32_sys_execve 0000000000000000 T __ia32_sys_execveat 0000000000000000 T __x32_compat_sys_execve 0000000000000000 T __x64_sys_execve 0000000000000000 T __x64_sys_execveat 0000000000000000 T __x32_compat_sys_execveat 0000000000000000 T do_execve_file 0000000000000000 T do_execve 0000000000000000 T do_execveat 0000000000000000 d event_exit__execveat 0000000000000000 d event_enter__execveat (後略)
第690回でも少し触れたように、/proc/
で確認できます。ちなみに最初のアドレスフィールドは管理者権限でアクセスしないと表示できません。2番目のフィールドはnmコマンドのマニュアルを参照してください。たとえばTだとテキストセクションにあるシンボルを意味し、
システムコールのシンボルは環境に依存し、__
」__
、do_
あたりが使えます。
$ echo 'p:execve __do_execve_file.isra.0 comm=$comm file=+0(+0($arg2)):string' | \ sudo tee /sys/kernel/debug/tracing/kprobe_events $ sudo cat /sys/kernel/debug/tracing/events/kprobes/execve/format name: execve ID: 1641 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:unsigned long __probe_ip; offset:8; size:8; signed:0; field:__data_loc char[] comm; offset:16; size:4; signed:1; field:__data_loc char[] file; offset:20; size:4; signed:1; print fmt: "(%lx) comm=\"%s\" file=\"%s\"", REC->__probe_ip, __get_str(comm), __get_str(file)
kprobe_
に対象となる関数とそのときに出力する内容を設定します。具体的な書式はkprobe_
」
最初にp:
」execve
」/sys/
以下にイベント名のディレクトリが作られます。今後はそのディレクトリ以下のファイルを操作することになります。
「__
」+offs
」
「NAME=引数
」comm=$comm
」$comm
」kprobe_
」file=+0(+0($arg2)):string
」__
に渡されたファイル名を表示する部分です。まず、__
の定義を見てみましょう。
static int __do_execve_file(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags, struct file *file)
ここで欲しいのは2番目の引数にあります。N番目の引数は$argN
」+0($argN)
」+OFFS(ADDR)
」-OFFS(ADDR)
」+uOFFS(ADDR)
」
しかしながら2番目の引数の型であるstruct filename
」
struct filename {
const char *name; /* pointer to actual string */
const __user char *uptr; /* original userland pointer */
int refcnt;
struct audit_names *aname;
const char iname[];
};
つまり文字列を取得するためには、+0(+0($arg2))
」:string
」
さて実際にトレーサーを起動してみましょう。次のようにenableに1を書くことでトレーサーを起動できます。あとはシステムコール版と使い方は同じです。
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/kprobes/execve/enable $ echo 1 | sudo tee /sys/kernel/debug/tracing/free_buffer $ sudo cat /sys/kernel/debug/tracing/trace_pipe cat-980 [000] .... 4396.341029: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/cat" <...>-981 [000] .... 4399.065705: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/cat" bash-982 [000] .... 4401.941474: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/ls" bash-983 [000] .... 4404.274790: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/touch"
無事にコマンド名と実行しょうとしているファイル名が表示されるようになりました。また、
$ echo 0 | sudo tee /sys/kernel/debug/tracing/events/kprobes/execve/enable $ echo '' | sudo tee /sys/kernel/debug/tracing/kprobe_events
上記の場合、kprove_
をすべて消してしまうので注意してください。
ちなみにシステムコールから先の関数として何が呼ばれるかはカーネルのバージョンやコンパイラのバージョンに依存します。たとえばUbuntu 21.do_
を呼び出していることがわかります。よってより新しいカーネルだと次のようなコマンドになります。
$ echo 'p:execve do_execveat_common comm=$comm file=+0(+0($arg2)):string' | \ sudo tee /sys/kernel/debug/tracing/kprobe_events
このようにsysfsを利用したトレーシングは、
bpftraceを用いてトレーシングする
ソフトウェアの依存関係を少なくトレーシングを行いたいならbpftraceも選択肢に入ってきます。パッケージのインストールは必要であるものの、
bpftraceパッケージはUbuntuリポジトリにもあるものの、
$ sudo snap install bpftrace $ sudo snap connect bpftrace:system-trace
たとえばexecve()
システムコールの呼び出し時に、
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s %s\n", comm, str(args->filename)); }' tmux: server /bin/sh byobu-status /usr/bin/sed sh /usr/bin/byobu-status byobu-status /usr/bin/tmux
説明が不要なぐらいとても簡単ですね。-e
」
bpftraceの書式の簡単な説明
まずはイベントやトレースポイントなどのプローブ対象の文字列を書きます。-l
」
$ sudo bpftrace -l '*execve*' kprobe:__do_execve_file.isra.0 kprobe:__ia32_compat_sys_execve kprobe:__ia32_compat_sys_execveat kprobe:__ia32_sys_execve kprobe:__ia32_sys_execveat kprobe:__x32_compat_sys_execve kprobe:__x32_compat_sys_execveat kprobe:__x64_sys_execve kprobe:__x64_sys_execveat kprobe:audit_log_execve_info kprobe:do_execve kprobe:do_execve_file kprobe:do_execveat tracepoint:kprobes:execve tracepoint:syscalls:sys_enter_execve tracepoint:syscalls:sys_enter_execveat tracepoint:syscalls:sys_exit_execve tracepoint:syscalls:sys_exit_execveat
システムコールとしてはtracepoint:syscalls:sys_
」kprobe:__
」
ちなみにUbuntu 20.
$ sudo bpftrace -lv 'tracepoint:syscalls:sys_enter_execve' ERROR: Permission denied: /proc/sys/kernel/randomize_va_space tracepoint:syscalls:sys_enter_execve int __syscall_nr const char * filename const char *const * argv const char *const * envp
最初のエラーは、/proc/
の読み込みに失敗したときに表示されます。snapパッケージの場合はこのファイルへのアクセスは許容されていないようです。実害はありませんが、BPFTRACE_
を明示的に指定しておくと良いでしょう。
eBPFの処理部分はargs->NAME
」comm
」char *
」str()
で文字列として使えるようになります。ただし設定しない限り、
出力結果をわかりやすくする
せっかくなので、
$ sudo bpftrace -e '#include <linux/sched.h> BEGIN { printf("PID PPID COMM FNAME\n") } tracepoint:syscalls:sys_enter_execve { $task = (struct task_struct *)curtask; printf("%-8d %-8d %-16s %s\n", pid, $task->real_parent->tgid, comm, str(args->filename)); }' Attaching 2 probes... PID PPID COMM FNAME 1968127 5480 tmux: server /bin/sh 1968128 5480 tmux: server /bin/sh 1968129 1968127 sh /usr/bin/byobu-status 1968130 1968129 byobu-status /usr/bin/sed 1968131 1968128 sh /usr/bin/byobu-status 1968133 1968129 byobu-status /usr/bin/tmux
こちらもほぼ説明が不要なシンプルなコードですね。ポイントは次のとおりです。
task_
構造体のために、struct linux/
ヘッダーをインクルードsched. h BEGIN
イベントでトレース開始時にヘッダーを出力- 組み込み変数である
curtask
でカレントタスクの情報を取得し$task
として保存 - 同じく組み込み変数である
pid
でスレッドグループのIDを取得 - カレントタスクから
「 $task->real_
」parent->tgid で親プロセスのスレッドグループIDを取得
このようにbpftraceを使えば、
$ cat <<'EOF' > execve.bt #include <linux/sched.h> BEGIN { printf("PID PPID COMM FNAME\n") } tracepoint:syscalls:sys_enter_execve { $task = (struct task_struct *)curtask; printf("%-8d %-8d %-16s %s\n", pid, $task->real_parent->tgid, comm, str(args->filename)); } EOF
このように
$ bpftrace execve.bt Attaching 2 probes... PID PPID COMM FNAME 2228 852 bash /usr/bin/ls
つまりbpftraceは再利用性も十分確保されているのです。おそらくトレーシング目的であれば、