前回の続きです。
今回はlibbpf-toolsにあるexecsnoopのコマンドライン部分をRustに移植する。まず、Rustのworkspaceを使って以下のように execsnoop
クレートを作成した。 bpf/
ディレクトリにBPFのプログラムを置く。
. ├── Cargo.toml ├── LICENSE ├── README.md └── execsnoop/ ├── Cargo.toml └── src ├── bpf/ └── main.rs
bpf/
に配置する、 execsnoop.bpf.c
は以下をベースにコピーしてくればいいが、 execsnoop.h
で定義された構造体や定数は同じファイルに展開しておく。
合わせて、コンパイルできるように同じディレクトリに vmlinux.h
を置いておく。本来はカーネルビルド時にできるものを置いておく感じなのだろうか? いったん以下のファイルと同じものをコピーした。
この段階でまずBPFプログラムだけコンパイルし、オブジェクトを作る。また、skelも作る。
$ cd execsnoop $ cargo libbpf build $ ls -l ../target/bpf/ total 992 -rw-r--r-- 1 vagrant vagrant 1015216 Jan 7 10:52 execsnoop.bpf.o $ cargo libbpf gen Warning: unrecognized map: .maps Warning: unrecognized map: license $ ls -l src/bpf/ total 2692 -rw-r--r-- 1 vagrant vagrant 3887 Jan 5 13:21 execsnoop.bpf.c -rw-r--r-- 1 vagrant vagrant 6622 Jan 7 10:52 execsnoop.skel.rs -rw-r--r-- 1 vagrant vagrant 189 Jan 7 10:52 mod.rs -rw-r--r-- 1 vagrant vagrant 2734479 Jan 5 11:02 vmlinux.h
Rustプログラム側ではこのskelを用いて、コマンドからBPFプログラムをアタッチ、Perf Bufferから出てくるデータを表示すればOK。 ... ところでexecsnoopの元の実装は、フォーマット系のオプションがややこしいため一旦全部出す、パラメータ系だけサポートする、という感じにする。
実装と説明のコメント
最終的にこういう感じの実装。まずは全体を。
// Rust port of execsnoop.c // See also: https://github.com/iovisor/bcc/blob/master/libbpf-tools/execsnoop.c use core::mem; use core::time::Duration; use std::str; use std::convert::TryFrom; use chrono::Local; use anyhow::Result; use libbpf_rs::PerfBufferBuilder; use plain::Plain; use structopt::StructOpt; #[macro_use] extern crate lazy_static; mod bpf; use bpf::*; // オプション用の構造体をStructOptで定義 #[derive(Debug, StructOpt)] struct Command { /// Trace this UID only #[structopt(short = "u", default_value = "-1", value_name = "UID")] uid: i32, /// Include failed exec()s #[structopt(short = "x")] fails: bool, /// Maximum number of arguments parsed and displayed #[structopt(long = "max-args", default_value = "20", value_name = "MAX_ARGS")] max_args: i32, } // Perf Bufferから送られるイベントを受け取るための構造体 // Cのレイアウトにする。詳細は後述 #[repr(C)] #[derive(Default)] struct Event { pub comm: [u8; 16], pub pid: i32, pub tgid: i32, pub ppid: i32, pub uid: i32, pub retval: i32, pub args_count: i32, pub args_size: u32, } unsafe impl Plain for Event {} // 経過時間を計測するためのタイマーを、lazy_staticでstaticに生成する。 mod timer { lazy_static! { pub static ref TIMER: std::time::Instant = { std::time::Instant::now() }; } } // perfのイベントハンドラ fn handle_event(_cpu: i32, data: &[u8]) { let now = Local::now(); // 上記の timer::TIMER は、こうやって呼び出すたびに、起動時からの経過Durationを返してくれる。 // BPF toolsでは頻出のパターンなので便利。 let elap = timer::TIMER.elapsed().as_nanos() as f32 / (1000*1000*1000) as f32; let mut event: Event = Event::default(); let event_size = mem::size_of_val(&event); // この辺の Event のアンパックや args の扱いは後述... plain::copy_from_bytes(&mut event, data).expect("Data buffer was too short"); let comm = str::from_utf8(&event.comm).unwrap().trim_end_matches('\0'); let args: Vec<&str> = str::from_utf8(&data[event_size..]).unwrap().trim_end_matches('\0').split('\0').collect(); // 表示部分 println!( "{:8} {:<8.3} {:<6} {:16} {:<6} {:<6} {:3} {:?}", now.format("%H:%M:%S"), elap, event.uid, comm, event.pid, event.ppid, event.retval, args ); } // こちらはPerf Bufferのイベントロスト時のフック fn handle_lost_events(cpu: i32, count: u64) { eprintln!("Lost {} events on CPU {}", count, cpu); } // main // なお、下の println! はこの警告に対応すると逆に意図が分かりづらいので、無視指定。 #[allow(clippy::print_literal)] fn main() -> Result<()> { let opts: Command = Command::from_args(); // Builder の生成とオープン let mut skel_builder: ExecsnoopSkelBuilder = ExecsnoopSkelBuilder::default(); let mut open_skel: OpenExecsnoopSkel = skel_builder.open()?; // パラメータを埋め込み if opts.uid >= 0 { let uid: u32 = TryFrom::try_from(opts.uid)?; open_skel.rodata().targ_uid = uid; } else { open_skel.rodata().targ_uid = u32::MAX; } if opts.fails { open_skel.rodata().ignore_failed = 0 } else { open_skel.rodata().ignore_failed = 1 } open_skel.rodata().max_args = opts.max_args; // ロードとアタッチ let mut skel = open_skel.load()?; skel.attach()?; // ヘッダを表示。この辺は、BCCなんかと同じノリ。 println!( "{:8} {:8} {:6} {:16} {:6} {:6} {:3} {:}", "TIME", "TIME(s)", "UID", "PCOMM", "PID", "PPID", "RET", "ARGS" ); // lazy_staticなタイマーをここで初期化するため、ダミーで elapsed() を呼ぶ。 timer::TIMER.elapsed(); // To initialize static timer // Perf Bufferのオープン let perf = PerfBufferBuilder::new(skel.maps().events()) .sample_cb(handle_event) .lost_cb(handle_lost_events) .build()?; // あとはloopでポーリング loop { perf.poll(Duration::from_millis(100))?; } }
細かい箇所の説明
Skel の利用とパラメータの変更
bpfのプログラムから生成されるSkelは、大体以下のような感じで使うことになる。
let mut builder = HogeSkelBuilder::default(); let mut open_skel = builder.open()?; open_skel.rodata().foo_arg = opts.foo_arg; //... let mut skel = open_skel.load()?; skel.attach()?;
この際、ロード時のオプションを渡す箇所は、BPFプログラム側の const
な変数に対応して生成される。execsnoopなら以下。
const volatile bool ignore_failed = true; const volatile uid_t targ_uid = INVALID_UID; const volatile int max_args = DEFAULT_MAXARGS; // 実はなぜかstatic const struct event empty_event = {} も // 可変なパラメータ扱いになってしまうが、触れないこと。
この定義を検知してskelで以下のような構造体を生成する。
#[derive(Debug, Copy, Clone)] #[repr(C)] pub struct rodata { pub ignore_failed: u8, pub targ_uid: u32, pub max_args: i32, pub empty_event: event, }
openしたskelのメソッド rodata()
からこの構造体にアクセスできるためそこで値を埋めていけばOK。
Perf Bufferから戻ってきたデータの扱い
今回のexecsnoopでは、以下のC構造体に対応するRustの構造体に対し、 plain クレート経由でデータをアンパックしてあげれば利用できる。
#define TASK_COMM_LEN 16 #define ARGSIZE 128 #define TOTAL_MAX_ARGS 60 #define FULL_MAX_ARGS_ARR (TOTAL_MAX_ARGS * ARGSIZE) struct event { char comm[TASK_COMM_LEN]; pid_t pid; pid_t tgid; pid_t ppid; uid_t uid; int retval; int args_count; unsigned int args_size; char args[FULL_MAX_ARGS_ARR]; };
ただ、このレイアウトにそのまま対応するRustの構造体を定義しようとして、以下のように記述すると難儀する。
#[repr(C)] #[derive(Default)] struct Event { pub comm: [u8; 16], pub pid: i32, pub tgid: i32, pub ppid: i32, pub uid: i32, pub retval: i32, pub args_count: i32, pub args_size: u32, pub args: [u8; 7680], }
まず pub args: [u8; 7680]
という定義が長すぎるようで、 Default
を注釈できない。このままではインスタンスの初期化に難儀する羽目になる。
さらにいうと、実は、今回はargsは可変長のような扱いになるようで、 handle_event(_cpu: i32, data: &[u8])
に渡ってくる data
の長さがまちまちになってしまう。したがって、plainの仕様により、もし Event::default()
で確保したサイズより短いサイズの data が来てしまった場合、 plain::copy_from_bytes
は失敗する。
今回は後ろの args
メンバはアンパックする範囲に含めず、 args_size
までをアンパックして利用することにした。逆に plain::copy_from_bytes
のコピー先、 Event::default()
構造体のサイズよりもデータのほうが長い場合は、データの後ろを無視するだけなので問題がない。
#[repr(C)] #[derive(Default)] struct Event { pub comm: [u8; 16], pub pid: i32, pub tgid: i32, pub ppid: i32, pub uid: i32, pub retval: i32, pub args_count: i32, pub args_size: u32, } unsafe impl Plain for Event {}
残りの args
は以下のように切り出して生の [u8]
のスライスとしてアクセスできる。
let mut event: Event = Event::default(); let event_size = mem::size_of_val(&event); let raw_args = &data[event_size..];
当然というか、これは \0
区切りのバイト列であるので、あとは以下のように扱えばよろしい。
let args: Vec<&str> = str::from_utf8(raw_args) .unwrap() .trim_end_matches('\0') .split('\0') .collect();
実際の動作
ビルドすると以下のように、普通に execsnoop として動作する。 -u
や -x
、 --max-args
も動作します。
$ sudo ../target/debug/execsnoop --help execsnoop 0.1.0 USAGE: execsnoop [FLAGS] [OPTIONS] FLAGS: -x Include failed exec()s -h, --help Prints help information -V, --version Prints version information OPTIONS: --max-args <MAX_ARGS> Maximum number of arguments parsed and displayed [default: 20] -u <UID> Trace this UID only [default: -1]
まとめ
本記事で、RustでのBPF (CO-RE) toolの実装例として、execsnoopの実装をポートした。また、RustとBPFでどのようにパラメータやデータをやり取りするかの留意点も書いた。
BPFプログラムは既存のものを利用したため、今後はいくつか簡単でもオリジナルのツールを実装していきたい。
今回の実装はここにアップしてます: