本セッションの登壇者
セッション動画
では「BPFの歩き方」と題して発表させていただきます。
株式会社ミラティブでインフラエンジニアをしている近藤といいます。仕事では日々Goを書きながらミドルウェアのログやiostatと向き合っている感じです。今日、ほかの皆さんは本当にちゃんとカーネルソースコードの読み方を解説されていましたが、私はほぼカーネルのコードを読まないというか、基本的には最後の手段にしているので、今日はソースコードの読み方ではなく、別のものの読み方の話をします。
今日の流れは、BPFの話から始めます。最小限、BPFがどんな感じで動くかという話と、バイナリコードのリーディングをします。
What is BPF?
BPFとは何ぞやというのが今回のテーマです。
LinuxにはBPFという機能が少し前から入っておりまして、何をするものかひと言でいうと、Linuxカーネルの機能を使ったコードをなるべく高速かつ安全に動かす技術です。
具体的にどういうときにBPFが欲しいかというと、カーネルの機能を使う場合は基本的にシステムコールを使いますが、システムコールで公開されていないような機能を使いたい、あるいは内容を見たいという場合、基本的にはカーネルのモジュールを書いたり、カーネルにパッチを当てたりすることになります。しかし、カーネルモジュールをバグらせると最悪kernel panicを起こして、Linuxごと落ちることがあり得ます。なので、システムコールで普通に呼ぶ場合と、カーネルモジュールみたいな深い機能を使う間のプログラムを書きたいときに使えるのがBPFです。
なので、BPFとはカーネルの中で動くバイナリプログラムだと考えてください。図をebpf.ioから引用していますが、プログラムを上の方で作って、システムでカーネルの中に読み込ませて、中で動かして取り出すという流れです。
BPF自体はバイトコードです。カーネルの中にはBPFを評価するVMが存在しています。このVMはレジスタマシンで、基本的には64ビットの固定長で倍のビットです。命令には基本的なALUとかLOADとかの系統があって、汎用レジスタが10個 + フレームポインタR10という仕様になっています。皆さんバイトコードマシンが好きだと思うので、ちょっと触れておきました。
このBPFの肝心な使われ方として、最初はtcpdump (libpcap)の中でパケットフィルタをするために使われていたのですが、現在はカーネルの中の関数呼び出しや、イベントのフック、セキュリティ周りなど、汎用的に使われています。その他、この番号のデバイスにアクセスを許可する/拒否するということもやったりしています。
サンプル ‐ デバイスアクセス制御にBPFを適用する
さっそく軽くコードを見ていきたいと思います。
あらためて、BPFを少し知っている方にとっては用途としてネットワーキング周りの機能とか、もしくはBPFによるトレースみたいなのが思いつくかと思うんですが、今回はdeviceアクセスのフックという謎の機能についてお話をします。
先ほど ten_forwardさんのお話にあったcgroupという機能があります。cgroupの機能のひとつに、/dev/の下の/dev/zeroや/dev/urandomに対するアクセス制限をするという機能が存在しています。v1ではテキストのリストで許可/拒否をやっていましたが、v2ではデバイスに触る瞬間にBPFのプログラムを動かして、その中でアクセスOK/NGを判断します。たとえば1秒間に10回だけ/dev/zeroを触るということができます。
BPFのイメージをつかむために簡単なプログラミングをしていきましょう。C言語のコードですが、これは皆さんも読めると思います。majorが1かつminorが9、これがurandomのデバイスなんですけど、これがNGであれば0、それ以外は1を返すというCの関数ですね。
これをclangで-target bpf
としてコンパイルすると、device.oというバイナリができます。
これをbpf toolというツールを経由してcgroupにロードすると、アタッチされます。たとえば今回はsample1というcgroupにアタッチされています。
そのsample1にアタッチしたDockerコンテナを作成すると、その中ではrootであってもdev/urandom、major 1番、minor 9番のデバイスには触れないことが確認できます。もちろん普通のコンテナを立ち上げた場合は、cgroupが関係ないので触れます。
バイナリをobjdumpで眺めてみる
突然出てきたこのdevice.oとは何かという話ですが、バイナリをそろそろ眺めてみましょう。普通のオブジェクトダンプだと対応しないこともあるので、llvm-objdump
というコマンドを使うと、デバイスの中身が見えます。
いちおうディスアセンブラも存在するので使ってみると、このようになんとなくわかります。
ソースコードを読みたい方はアセンブラもそれなりに読めると思いますが、ちょっとだけBPFのバイトコードを解説すると、4行目のオフセット3のところはr1=*(u32*)(r1+8)
となっています。
これは、先頭の「61」が命令の種類で、ポイントをロードするワイドキャラのサイズという命令で、「11」がレジスタ側、dstとsrcです。「08 00」はオフセットを表わして、R1にあるアドレスから8バイト進んだところの値をderefしてロードするという意味です。
次の4行目を見ると、JEQ命令で、「01」なのでR1の値だけを比較してジャンプします。後半の4バイト「09 00 00 00」で即値のところを使って、R1の値が9であれば1個オフセットを飛ばすということをやっています。
Cでいうとこのif条件部分の話でした。
BPFはバイナリ、バイナリはカッコいい!
バイナリを直接編集する方法は省略します。バイナリを編集すると別のデバイスを制限できます。
まとめると、「BPFはバイナリで、かっこいい」ということです。
ということで、皆さんにはBPFを身近に感じていただけたと思います。皆さんもぜひ触ってみてください。ありがとうございました。