ローファイ日記

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

BPFバイナリはどのようなELF形式か(1) - 最低限の情報とは?

主に9月に開かれる某Kaigiの関係とか、色々があり、C言語以外の言語でBPFバイナリを作ることができないか模索しています*1。某Slackなどで相談させてもらっていますが、こんな感じ(資料後半)。

docs.google.com

その予備調査として、BPFバイナリとしてlibbpfが取り扱えるELF形式のバイナリがどのようなものか調べています。

これ、なんかドキュメントがあるような気もせんでもないですが、まあぼく自身ELF形式に詳しくないこともあり実際のファイルを眺めながら調べた結果*2を残しとこうと思います。


前回の記事で利用した、cgroup v2のデバイスフィルタに利用するBPFプログラムを、

udzura.hatenablog.jp

以下のコマンドでビルドします。

$ clang -O1 -c -target bpf dev_cgroup.c -o dev_cgroup.o

まずは dev_cgroup.oreadelf -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: RELMachine: 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 File

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 でバイナリを取得し、対応する箇所を確認しましょう。

f:id:udzura:20210701230936p:plain

背景を赤くした、 .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 メンバで指定する必要があります。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が埋まっている様子。

f:id:udzura:20210701233050p:plain
青背

.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", ... >

データはバイナリではこの位置です。

f:id:udzura:20210701233434p:plain

このバイナリ部分には、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番地始まりにするため)が、赤背景に対応していますね。

f:id:udzura:20210701233752p:plain

シンボルの名前 st_name は、相変わらずstrtabから引いています。シンボル名のテーブルに使うstrtabのインデックスとしてセクションヘッダの sh_link = 1 が使われています。別々のstrtabにすることも可能なのでしょう。

41 = 2 * 16 + 9 です。

f:id:udzura:20210701234135p:plain

: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) バイト目は何かというと、

f:id:udzura:20210701234948p:plain

ということで、 llvm-objdump -d などでレーベルの情報が表示されます。

f:id:udzura:20210701235041p:plain

ディスアセンブルした際には便利ですが、このレーベルの情報も実は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:調べるプロセス自体を残すことは、後学のためにも悪くないでしょう