主に9月に開かれる某Kaigiの関係とか、色々があり、C言語以外の言語でBPFバイナリを作ることができないか模索しています*1。某Slackなどで相談させてもらっていますが、こんな感じ(資料後半)。
その予備調査として、BPFバイナリとしてlibbpfが取り扱えるELF形式のバイナリがどのようなものか調べています。
これ、なんかドキュメントがあるような気もせんでもないですが、まあぼく自身ELF形式に詳しくないこともあり実際のファイルを眺めながら調べた結果*2を残しとこうと思います。
前回の記事で利用した、cgroup v2のデバイスフィルタに利用するBPFプログラムを、
以下のコマンドでビルドします。
$ clang -O1 -c -target bpf dev_cgroup.c -o dev_cgroup.o
まずは dev_cgroup.o
の readelf -a
の結果を眺めてみます。
$ readelf -a src/dev_cgroup.o 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: Linux BPF Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 368 (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: 7 Section header string table index: 1
Type: REL
で Machine: Linux BPF
と表示されます。また、プログラムヘッダはありません。セクションヘッダを眺めます。
Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .strtab STRTAB 0000000000000000 00000112 000000000000005e 0000000000000000 0 0 1 [ 2] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 4 [ 3] cgroup/dev PROGBITS 0000000000000000 00000040 0000000000000038 0000000000000000 AX 0 0 8 [ 4] license PROGBITS 0000000000000000 00000078 0000000000000004 0000000000000000 WA 0 0 1 [ 5] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 00000110 0000000000000002 0000000000000000 E 6 0 1 [ 6] .symtab SYMTAB 0000000000000000 00000080 0000000000000090 0000000000000018 1 4 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)
セクションはそんなに多くありません。 llvm-objdump -x
の結果とも比較。
Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 cgroup/dev 00000038 0000000000000000 0000000000000000 00000040 2**3 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 license 00000004 0000000000000000 0000000000000000 00000078 2**0 CONTENTS, ALLOC, LOAD, DATA 3 .llvm_addrsig 00000002 0000000000000000 0000000000000000 00000110 2**0 CONTENTS, READONLY, EXCLUDE
シンボルテーブルはこうです。
Symbol table '.symtab' contains 6 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS dev_cgroup.c 2: 0000000000000028 0 NOTYPE LOCAL DEFAULT 3 LBB0_2 3: 0000000000000030 0 NOTYPE LOCAL DEFAULT 3 LBB0_3 4: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 _license 5: 0000000000000000 56 FUNC GLOBAL DEFAULT 3 bpf_prog1
セクションがそれぞれ何者なのか? というところから。 .strtab
からいきます。
何者なのかを読み込む前にELFファイルのざっくりしたレイアウトを頭に入れておくといいでしょう。以下は、Solarisのマニュアルの引用です。
ELFヘッダー、セクションヘッダー(テーブル)のところには、具体的にはそれぞれ対応する構造体(Elf32|64_Ehdr 構造体、Elf32|64_Shdr 構造体)のバイナリ表現が格納されています。セクションヘッダー「テーブル」の箇所はElf*_Shdr構造体の配列ですね。
その上で今回、Rubyの rbelftools というgemを補助的に用いて、ファイルをパースしてそれぞれのセクション構造体の値に何が入るか取得しています。それも見ながら進めます。ちなみにセクション0には必ずNullSection、ゼロ埋めが入るようなのでスキップします。
f = File.open 'src/dev_cgroup.o' elf = ELFTools::ELFFile.new f #=> #<ELFTools::ELFFile:0x000055782f67d098 @elf_class=64, # @endian=:little, @stream=#<File:src/dev_cgroup.o>> elf.sections[0] #=> #<ELFTools::Sections::NullSection:0x000055782fed00b0 @header= {:sh_name=>0, :sh_type=>0, :sh_flags=>0, :sh_addr=>0, :sh_offset=>0, :sh_size=>0, :sh_link=>0, :sh_info=>0, :sh_addralign=>0, :sh_entsize=>0}, ...> elf.sections[1] #=> #<ELFTools::Sections::StrTabSection:0x000055782ff23e18 @header= {:sh_name=>54, :sh_type=>3, :sh_flags=>0, :sh_addr=>0, :sh_offset=>274, :sh_size=>94, :sh_link=>0, :sh_info=>0, :sh_addralign=>1, :sh_entsize=>0}, ...>
ここで、sh_offset はファイル全体のオフセット、 sh_size はセクション領域のサイズです(readelf/objdumpのどの項目に対応するか確認しながら進めましょう)。データのアライメント sh_addralign は1バイト単位です。
"%08x" % 274 #=> "00000112" 94.divmod 16 # sh_size #=> [5, 14]
hexdump -C
でバイナリを取得し、対応する箇所を確認しましょう。
背景を赤くした、 .cgroup/dev\0...bpf_prog1\0
の箇所がまさにBPFプログラムです。
では名前はどうやって取得しているか? sh_name
の項目に注目すると、
54.divmod 16 #=> [3, 6]
このとき、 .strtab
の先頭から54バイト進んだ箇所、上図の青背景で下線を振った箇所 に .strtab
自体の名前が埋め込まれています。
すなわち .strtab
には、セクションの名前などELFファイルの内部で使う文字列のテーブルがまとめて入っているということがわかります。後述する .symtab
では、シンボルの名前にも使われます。
なお、strtabセクションの位置(インデックス、今回は 1
)を、ELFヘッダ構造体の e_shstrndx
メンバで指定する必要があります*3。readelfの結果で言う Section header string table index
のところです。
elf.header #=> {:e_ident=> {:magic=>"\x7FELF", :ei_class=>2, :ei_data=>1, :ei_version=>1, :ei_osabi=>0, :ei_abiversion=>0, :ei_padding=>"\x00\x00\x00\x00\x00\x00\x00"}, :e_type=>1, # :e_machine=>247, :e_version=>1, :e_entry=>0, :e_phoff=>0, :e_shoff=>368, :e_flags=>0, :e_ehsize=>64, :e_phentsize=>0, :e_phnum=>0, :e_shentsize=>64, :e_shnum=>7, :e_shstrndx=>1}
strtabセクションを自分で定義する際は以下に気をつける、と言うことになります。
- e_shstrndx を正しくする
.strtab
というセクションを作る- sh_name の位置を正しく
- sh_type=3 (
SHT_STRTAB
)
- strtab用のデータを適切に配置する。
残りのセクションヘッダを駆け足で眺めます。
elf.sections[2] => #<ELFTools::Sections::Section:0x0000557577a50540 @header= {:sh_name=>12, :sh_type=>1, # SHT_PROGBITS :sh_flags=>6, # :sh_addr=>0, :sh_offset=>64, # "00000040" :sh_size=>0, :sh_link=>0, :sh_info=>0, :sh_addralign=>4, :sh_entsize=>0}, ...> elf.sections[2].name => ".text" elf.sections[2].data => ""
.text
セクションが一応定義されています。が、sizeがゼロです。ネタバレになりますが、試した結果としては実はこのセクションはなくてもいいです。
elf.sections[3] => #<ELFTools::Sections::Section:0x0000557577a43908 @header= {:sh_name=>1, :sh_type=>1, # SHT_PROGBITS :sh_flags=>6, # SHF_ALLOC|SHF_EXECINSTR :sh_addr=>0, :sh_offset=>64, :sh_size=>56, :sh_link=>0, :sh_info=>0, :sh_addralign=>8, :sh_entsize=>0}, elf.sections[3].name => "cgroup/dev" elf.sections[3].data => "a\x12\x04\x00\x00\x00\x00\x00U\x02\x03\x00..."
BPFのCコードで指定している "cgroup/dev"
セクションで、データとしてeBPFのオペコードが詰まっています。アライメントもeBPFの基本的な命令長である8バイトです。
elf.sections[4] => #<ELFTools::Sections::Section:0x0000563780cf7458 @header= {:sh_name=>33, :sh_type=>1, # SHT_PROGBITS :sh_flags=>3, # SHF_WRITE|SHF_ALLOC :sh_addr=>0, :sh_offset=>120, #=> 00000078 :sh_size=>4, :sh_link=>0, :sh_info=>0, :sh_addralign=>1, :sh_entsize=>0},
ライセンスの情報です。GPLが埋まっている様子。
.llvm_addrsig というセクションは結局役割が不明でした。sh_typeが以下の範囲なのでOS固有の設定のようですが、BPFプログラムに含まれていなくても問題ないようです。
#define SHT_LOOS 0x60000000 #define SHT_HIOS 0x6fffffff
symtabの構造を見てみます。セクションは以下のようになっています。
elf.sections[6] => #<ELFTools::Sections::SymTabSection:0x0000563780db7988 @header= {:sh_name=>62, :sh_type=>2, :sh_flags=>0, :sh_addr=>0, :sh_offset=>128, # 00000080 :sh_size=>144, # = 9 * 16 + 0 = 24 * 6 :sh_link=>1, # https://docs.oracle.com/cd/E19253-01/819-0391/6n2qq2n73/index.html#chapter6-47976 # 関連付けられている文字列テーブルの # セクションヘッダーインデックス。 :sh_info=>4, # シンボルテーブルセクションの # sh_info セクションヘッダーメンバーは、 # 最初の*ローカルではない*シンボルに対する # シンボルテーブルインデックスを保持 :sh_addralign=>8, :sh_entsize=>24}, @name=".symtab", ... >
データはバイナリではこの位置です。
このバイナリ部分には、sh_entsize
(=24)バイトの構造体が sh_size/sh_entsize
(=144/24 = 6)個並んでいます。objdumpのバイナリは8バイトで区切られているのでその3つ分で眺めます。
その構造体はこう言うレイアウトをしています。
typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Half st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym;
一つ目は相変わらずゼロ埋めなので、二つ目からみてみる。
elf.sections[6].symbols[1] => #<ELFTools::Sections::Symbol:0x0000563780cfebe0 @header= {:st_name=>41, :st_info=>4, :st_other=>0, :st_shndx=>65521, :st_value=>0, :st_size=>0}, # ... #define STT_FILE 4 @name="dev_cgroup.c", @stream=#<File:dev_cgroup.o>,...
以下は llvm-objdump -s
での表示です(0番地始まりにするため)が、赤背景に対応していますね。
シンボルの名前 st_name
は、相変わらずstrtabから引いています。シンボル名のテーブルに使うstrtabのインデックスとしてセクションヘッダの sh_link = 1
が使われています。別々のstrtabにすることも可能なのでしょう。
41 = 2 * 16 + 9
です。
:st_shndx=>65521 = 0xfff1
は定数 SHN_ABS
です。結局このシンボルはソースファイル名でした。
他のシンボルを見てみます(一部省略)。先にライセンスとプログラムから。
elf.sections[6].symbols[4] => #<ELFTools::Sections::Symbol:0x0000563780cd2b80 @header={:st_name=>32, :st_info=>17, :st_other=>0, :st_shndx=>4, :st_value=>0, :st_size=>4}, @name="_license",... > elf.sections[6].symbols[5] => #<ELFTools::Sections::Symbol:0x0000563780ca5d60 @header={:st_name=>84, :st_info=>18, :st_other=>0, :st_shndx=>3, :st_value=>0, :st_size=>56}, @name="bpf_prog1", ...>
17 = STB_GLOBAL<<4|STT_OBJECT
, 18 = STB_GLOBAL<<4|STT_FUNC
です。
// elf.h #define STB_LOCAL 0 #define STB_GLOBAL 1 #define STB_WEAK 2 #define STT_NOTYPE 0 #define STT_OBJECT 1 #define STT_FUNC 2 #define STT_SECTION 3 #define STT_FILE 4 #define STT_COMMON 5 #define STT_TLS 6 #define ELF_ST_BIND(x) ((x) >> 4) #define ELF_ST_TYPE(x) (((unsigned int) x) & 0xf)
で、st_shndxはセクションでのindex、st_sizeがセクション側のsh_sizeに対応しています。で、st_value = 0 なので、たとえば bpf_prog1
ならsections[3]のデータの頭から、となる感じです。
残りはレーベルですね。
elf.sections[6].symbols[2] #=> #<ELFTools::Sections::Symbol:0x0000563780ce6d38 @header={:st_name=>77, :st_info=>0, :st_other=>0, :st_shndx=>3, :st_value=>40, :st_size=>0}, @name="LBB0_2",
sections[3] の 40(= 00000028)
バイト目は何かというと、
ということで、 llvm-objdump -d
などでレーベルの情報が表示されます。
ディスアセンブルした際には便利ですが、このレーベルの情報も実はBPFバイナリとしては必須ではないようです。
ここまでをまとめると
- 必要なセクション
- strtab
- BPFプログラムセクション with 正しい名前
- ライセンス
- symtab
- 必要なシンボルテーブル
- BPFプログラムの関数名
- ライセンスの場所
が最低の最低限、のようです。その上でBPFプログラムセクションに、ハンドアセンブルしたBPFプログラムでも埋め込めば、動くはず。
ここまでで長くなったので次回、実際にプログラムしてみます。
*1:Rustならredbpfというものがあります、サンプル見るとクソかっこいい: https://github.com/foniod/redbpf/blob/main/examples/example-probes/src/vfsreadlat/main.rs
*2:調べるプロセス自体を残すことは、後学のためにも悪くないでしょう
*3:追記(2021/08/31): 普通の実行ファイルは セクションヘッダの名前は .shstrtab という名前のSHT_STRTABなセクションを参照するようですが、BPFのオブジェクトは.shstrtabは作られず(少なくともclang -target bpf ではつくられず)、.strtab セクションにセクション名、シンボル名両方の文字列が格納されます。