ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

RubyKaigi takeoutでRuby to BPF compilerの話をした話

Rubyアドベントカレンダー(カレンダー2)の2日目です。

qiita.com

今見たらカレンダー2も半分以上埋まってて嬉しい... Ruby is alive now!!!

ということで、RubyKaigi takeout 2021の振り返りも兼ねて作ったものの話です。

rubykaigi.org

Rucy: A Ruby Compiler

github.com

RucyはRubyスクリプトコンパイルして直接特定のフォーマットのバイナリを吐き出すツールです。今のところBPFのバイナリにのみ対応しています。

ちなみに "Ru"by "C"ompiler -> RuC -> Rucy という経緯で、Rucyという綴りがそれなりにRubyと空目する点含めて(?) 気に入った名付けです。

Rucyについて把握する前にBPFの説明からしないといけないのですが、要するにLinuxの機能の一つで、カーネルの中で安全かつ高速にユーザの作成したプログラムを動作させる仕組みです。雑にいうとフィルタリングが得意(Packet Filterですから)なので、高速にiptables的なものを作ったり、あるいはカーネルの関数呼び出しの情報を集めてパフォーマンス計測する、などに使えます。

@chikuwait さんによるスライドがわかりやすいので詳細はそちらに譲ります。

speakerdeck.com

ポイントは、「ユーザの作成したプログラム」はBPFのためのバイトコード*1である必要がある点です。このバイトコードを含むバイナリは今のところ clang または gcc を用い、C言語ソースコードbpf ターゲットでコンパイルすることで作成するのが普通です。

そのBPFのバイナリをカーネルにロードさせたり、あるいは実行した結果を何かしらの形で取り出す加工するプログラムはどの言語でも書けます。したがってRubyが得意ならRubyで書いて構いません*2

一方でその「バイナリ自体」もRubyのコードを書くだけで作れたら嬉しいだろうということで、Rucyを開発してみることにしました。

ちなみにこのBPFのためのバイナリはELF形式(リロケータブルファイル扱い)で、text領域にBPFバイトコードを保持するような形になります。一時期ELFファイルのことをしつこく調べてたのはRucyのためです。。

udzura.hatenablog.jp

Rucy を支える技術

スライド にもあるのですが、ざっくり「Rubyスクリプト」→「mrubyのバイトコード」→「BPFのバイトコード」→「ELFバイナリ」という経路で変換しています。

f:id:udzura:20211201224105p:plain

「Rubyスクリプト」→「mrubyのバイトコード」 の箇所は当然というか、mrbcの中で使われているCの当該関数を再利用しています。

ただ、その先の 「mrubyのバイトコード」→「BPFのバイトコード」 の部分までC言語で書くほどC言語を信用できなかったため、このPassはRustを用いて変換ロジックを実装しています。なので、実はmrubyのバイトコードを命令単位に区切る処理は自分で再実装していたりします。

また、mrubyのVMレジスタたくさん使える)とBPFのVMレジスタ10個まで、一部は用途が指定)は、同じレジスタVMとはいえ思想が色々違い、思った以上に「翻訳」は苦労しました。

とはいえPoCレベルは動作するようになり、発表では言及していませんでしたが、最低限の文字列の埋め込みや、 BPF_CALL のサポート、つまりBPFヘルパー関数呼び出しも動くようにしています。つまり bpf_trace_printk を使ってフィルタした結果をユーザスペースにフィードバックしたりできるようになっています。ここで燃え尽きたといえばそうですが...。

「BPFのバイトコード」→「ELFバイナリ」 の部分はlibelfを用いています。libelfはあまりドキュメントがなかったので苦労しましたが、ここもとりあえず動いてるのできっと大丈夫。

前提知識が多く苦労はしましたが、とはいえそれぞれのPassはありものを使ったり実装上の関心ごとを減らすことができたので、全体としてRubyKaigiまでになんとなく動くものが完成しました。あとRustのFFIは素晴らしい。

こういうコンパイラ*3実装のアイデアを思いついたのは、当時個人的に調べていた言語実装やコンパイラの動作の仕組みの知識が大いに役立った感があります。

動作の様子

RubyKaigi takeoutの録画 、あるいはTwitterにアップしたデモなどをどうぞ。

アクセスしたデバイスのmajorが1、minorが9( /dev/urandom )のときだけリジェクトするようなデモです。Cで書いてもそんな変わらんですけど...。

とにかくちゃんと動くバイナリを吐き出すことができます!!

今後

徒然と書きましたが、今後色々やりたいこと自体はあります...。

  • BPFツールとして現実で使える程度の機能を持たせる

いまのところ、BPF Map(perf event map含む)を作ったり、rodata経由でパラメータを注入したり、全てのBPFヘルパー関数をテストしたりということは実現していないので、かなり限られた用途のみになります...。まずこれらの機能を使うとき、どういうBPFバイナリが必要になるかを調べるところからですが、地道に進めていくしかない。

  • RubyだけでBPF CO-RE対応できるようにする

昨今のBPFツールはBPF CO-REという 、カーネルのバージョン差異を吸収しつつどこでも動くようなバイナリフォーマットに固める流れになっています。

Infrared *4 というプロジェクトを立ち上げて、RucyとRubyだけでまずはワンバイナリを作れるようにし、できればRubyエコシステムのパフォーマンス測定などに寄与したいと思ってはいるのですが、壮大な構想でなかなか...。

ひとまずmrubyでlibbpfのbindingを作り、skelをいい感じに生成させれば簡単なツールは書けるようになる、と思います。簡単に言ってみたもののこれだけでも手数は多い。そこからさらにCO-REの情報をELFファイルに埋め込む必要があるので、...。

  • バックエンドを増やす

個人的にWebAssemblyのサポートを入れてみたかったんですが、 MRI to WebAssembly は齋藤さんがやってくださる っぽいので優先度は低くなりました。ほかは、LLVM-IRとか? 面白バイナリフォーマットがあれば知りたいです。

mrubyのバイトコードをベースにすることで色々スキップできたのは事実ですが、そもそもBPFのためのフォーマットではないので無理をしている箇所もあり(特にレジスタの使い方、細かい規約が違っていて大変)、mrubyに頼っている箇所も徐々に自前にできないかと考えています。ただ、この辺の差異を埋めるのは、例えば最適化について私が学習していけばなんとか対処できるレベルかもしれません。いずれにせよ勉強しないといけない範囲は広い。

最後に

Rucy を実装するにあたって、以下に挙げる皆様にお世話になりました。お礼を申し上げます。

  • プログラミング言語処理系が好きな人の集まり の定例オンラインミートアップで構想をお話しする機会を得て、その場で色々ご意見をいただきました。とにかくめちゃくちゃ刺激になるSlackなのでみんな入りましょう。
  • プロトタイプの状態(いや、今も?)をあんちぽさん、力武先生、九州大学の嶋吉先生(サイバーセキュリティセンター)、笠原先生(情報基盤研究開発センター)にご覧いただく機会を得て、色々とアドバイスや感想をいただきました。大変助けになりました。

最近はBPFというか、まず「VMってどうやって作るんだろう?」と思って Crafting Interpreters やらCISPやらLuaの実装やらに手を出してしまって収拾がつかない感がありますが、育てていきたいですね...。

というかこれは「Ruby」カレンダーの記事なのか? まあいいや。

明日は #1 は@hasumikin師匠、 #2 は@Reichardtさんです。

*1:BPFはVMを持ち、特定の命令セットを持っている一種のバイトコード言語です

*2:以前作成した RbBCC はそういうコンセプトです https://github.com/udzura/rbbcc

*3:こうやって説明すると、割と普通にコンパイラしてますね...

*4:=赤外線