ローファイ日記

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

BPFバイナリはどのようなELF形式か(2) - mrubyのDSLを書いたらELFを吐く

あらすじ

udzura.hatenablog.jp

前回の記事で、以下のような構成要素がBPFプログラムに最低限必要な情報だと結論づけました。

    必要なセクション
        strtab
        BPFプログラムセクション with 正しい名前
        ライセンス
        symtab
    必要なシンボルテーブル
        BPFプログラムの関数名
        ライセンスの場所

今回は実際にバイナリを作ってロードしてみます。

ツールを作っている話

RubyKaigi takeout 2021 で話すと思うんですが(まだトーク詳細はアップされていません)、 Rucy という名前の、Pure RubyスクリプトだけでBPFのバイナリツールが作成できるSDKを開発しています(CO-REにできるかはまだ調査中です)。

github.com

ご存知の通り RbBCC を作っていたのですが、昨今のBPFツールの潮流の変化に対応して新しいプロジェクトを始めたということです。例えば:

pingcap.com

その下準備の一つとして、RubyスクリプトからBPFバイナリが作成できる必要があります。ちなみにRustの場合、例えば RedBPF のようなものがあります。参考:

medium.com

Rucy の進捗: Ruby DSLからELFファイルを作る

今のところ、「Ruby DSLからELFファイルを作る」ことができるようになっています。

冒頭の必要なセクションとシンボルテーブルを定義するDSLは以下のような感じ。

include Rucy

ELFFile.define do |elf|
  elf.header do |e|
    e.type ET_REL
    e.machine EM_BPF

    e.section do |scn|
      scn.name ".strtab"
      scn.type SectionType::STRTAB
    end

    e.section do |scn|
      scn.name "license"
      scn.symname "__license"
      scn.type SectionType::LICENSE
      scn.data "GPL\x00"
    end

    e.section do |scn|
      scn.name "cgroup/dev"
      scn.symname "my_prog"
      scn.type SectionType::PROG
      scn.data "\xb7\x00\x00\x00\x00\x00\x00\x00" +
               "\x95\x00\x00\x00\x00\x00\x00\x00"
    end

    e.section do |scn|
      scn.name ".symtab"
      scn.type SectionType::SYMTAB
    end
  end
end

真ん中の謎のバイナリ、つまりBPFのIRが気になる感じです。ここのコンパイラはまだ完成していないので、いったんハンドアセンブルしたものです。

rucyのワークスペースrucy-elfgen というものがあるので、以下のようにサンプルコードを走らせます。

rucy-elfgen$ cargo run --example elf_dsl -- \
               examples/elf_dsl.rb ~/test.bin

これで ~/test.bin にELFフォーマットのバイナリができる。

$ llvm-readelf -a ~/test.bin | head -25
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           EM_BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          216 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         5
  Section header string table index: 1
There are 5 section headers, starting at offset 0xd8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 000040 000036 00      0   0  1
  [ 2] license           PROGBITS        0000000000000000 000076 000004 00  WA  0   0  1
  [ 3] cgroup/dev        PROGBITS        0000000000000000 000080 000010 00  AX  0   0  8
  [ 4] .symtab           SYMTAB          0000000000000000 000090 000048 18      1   1  8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

Elf file type is REL (Relocatable file)
Entry point 0x0
There are 0 program headers, starting at offset 0

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align

 Section to Segment mapping:
  Segment Sections...
   None   .strtab license cgroup/dev .symtab 

There are no relocations in this file.

Symbol table '.symtab' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     1: 0000000000000000     4 OBJECT  GLOBAL DEFAULT     2 __license
     2: 0000000000000000    16 FUNC    GLOBAL DEFAULT     3 my_prog
There are no section groups in this file.

DSL での定義と見比べてください。正しいフォーマットのELF形式です。

また、BPFプログラムも正常に組み込まれています。

$ llvm-objdump -d ~/test.bin

/home/vagrant/test.bin: file format elf64-bpf


Disassembly of section cgroup/dev:

0000000000000000 <my_prog>:
       0:       b7 00 00 00 00 00 00 00 r0 = 0
       1:       95 00 00 00 00 00 00 00 exit

手製BPFプログラムをロードする

udzura.hatenablog.jp

この先日の実験の際と同じプログラムを(ファイル名だけ変えて)走らせてみます。

$ sudo mkdir /sys/fs/cgroup/unified/test-bpf-based-device-cgroup
$ sudo ./load_dev_cgroup
$ sudo bpftool prog
...
29: cgroup_device  name my_prog  tag a04f5eef06a7f555  gpl
        loaded_at 2021-08-12T09:12:35+0000  uid 0
        xlated 16B  jited 37B  memlock 4096B

ロードに成功しました。今回は、実質どんなデバイスにも retrun 0 を返すようなプログラムのため、どんなデバイスにもアクセスできないはずです。

$ echo $$ | sudo tee /sys/fs/cgroup/unified/test-bpf-based-device-cgroup/cgroup.procs 
1858

$ head -c 10 /dev/urandom | hexdump -C
head: cannot open '/dev/urandom' for reading: Operation not permitted

フィルタリングに成功しているようです。 $ echo $$ | sudo tee /sys/fs/cgroup/unified/cgroup.procs のように当該グループから外れれば、元のように /dev/urandom にアクセス可能な状態に戻ります。


ということで(rodataやリロケーションなど、考慮すべきことは他にもたくさんありますが...) Ruby スクリプトからvalidなBPFバイナリを作成できるようになりました。

あとは、中身のBPFのinsnをRubyのコードから生成できるようになればよかったですね、という感じなので、引き続きやっていくしかない。

9月中旬にそんなお話ができるよう、引き続き調査を続けます。