Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Perlで始めるeBPF: 自作Loaderの作り方 / Getting started w...

Perlで始めるeBPF: 自作Loaderの作り方 / Getting started with eBPF in Perl_How to create your own Loader

YAPC::Hakodate 2024で発表しました。
cf. https://fortee.jp/yapc-hakodate-2024/proposal/2c24d2e4-f488-414f-ae3d-1df24180867b

Perl製のeBPF Loaderはこちらです。
https://github.com/takehaya/Sys-Ebpf

CPANはこちらです。
https://metacpan.org/pod/Sys::Ebpf

Takeru Hayasaka

October 05, 2024
Tweet

More Decks by Takeru Hayasaka

Other Decks in Technology

Transcript

  1. Agenda • eBPFの基礎 ◦ 概要と歴史 ◦ 動作の仕組みとアーキテクチャ • PerlでeBPF Loaderを自作する

    ◦ 解説 ◦ デモ ▪ オブザーバービリティの例: kprobe ▪ パケット処理の例: XDP • まとめと今後の展望
  2. この発表で話すこと・話さないこと - 🙆eBPFに主軸をおいて話します - 簡単な仕組み - ユースケース - Perlでローダーを作るって何するんですか?みたいな話 -

    🙅BTFに関しては話しません - 🙅kprobeの仕組みなどについても話しません - 🙅Netlinkについての仕組みについても話しません - 🙅XDPとかのパケット処理の仕組みも(あまり)話しません
  3. 自己紹介 • 早坂 彪流 (Hayasaka Takeru|@takemioIO) • さくらインターネット に所属 現在は

    BBSakura Networksへ出向中 ◦ 前職はゲーム会社でゲーム機のファームを書いていた ◦ モバイルコアの研究開発・運用に従事 ▪ eBPF を使った開発を業務でやってます(パケット処理) • 一言: 発表当日の10/5は自分の26歳の誕生日です。 裏番組でやってるU25支援にギリギリ応募できなかった... ◦ 社会人4年目はもう若者ではないのかもしれない ...orz • YAPC初参加です ◦ 学生支援が通ってたけどコロナ時代で無くなってました ...(YAPC kyoto…) • なお、昨日初めてCPANにuploadするぐらいにはPerl初心者 ◦ CPANアカウントは TAKEMIO でやってます
  4. PR: eBPF Japan Meetup 第二回やります - 正式名:Cloud Native Community Japan

    - eBPF Japan Meetup #2 - eBPF ユーザー会みたいなものを CNCFでやってたりしてます - 次回第二回が12/6に さくらインターネット東京支社で 開催予定です(会場スポンサーをしてます - eBPFに興味が出てきました! みたいな人がいたらぜひご参加ください cf. https://ebpf.connpass.com/
  5. eBPF(extended Berkeley Packet Filter)とは? - Linuxカーネルに対する拡張を楽に書いて、動的にロードさせる仕組み - つまりカーネル空間で動作する拡張プログラムを用意できる仕組み - 言語仕様としてはRISC型仮想マシンとして表現される

    - 主に以下の用途で利用される - セキュリティ: seccomp, LSM… - オブザーバビリティ: kprobe, uprobe, retprobe, tracepoint, fentry… - ネットワーク: XDP, TC, TCP-BPF, cgroup_skb… - その他: CPUスケジューラー … cf. https://ebpf.io/what-is-ebpf/ 誤解を恐れずにいうと, eBPFを使えば • 手軽にkernelの拡張ができる • Kernel内部で実行された関数の結果を ほぼ何でも取れる • 100GbEを超える速度のパケット処理 が手軽にできるようになる と言ったことが出来る嬉しさがある
  6. プロダクトユースケース - セキュリティ - Isovalent, Cisco/ Tetragon(Podセキュリティイベント監視、ContainerRuntimeポリシー...) - Sysdig/ Falco(Teragonと同様にセキュリティ監視)

    - オブザーバビリティ - Isovalent, Cisco/ Hubble(Metrics, Service map, flow log…) - ネットワーク - Isovalent, Cisco / Cilium(KubernetesのCNIやセキュリティ) - Meta / Katran(L4Loadbrancer) - Cloudflare / Gatebot(DDoS Mitigation), Unimog(L4Loadbrancer) - LINEヤフー / L4LB, SRv6 - MIXI / StaticNAT, PayloadCutterなど - さくらインターネット&BBSakura Networks /パケット交換機(PGW-U) - 弊社でも使ってます:)
  7. - Linuxにはカーネルモジュールと呼ばれる機能がある - これを使えばカーネル空間で動く独自の拡張を書くことができる - eBPFで実装できる機能は基本的はカーネルモジュールで作れる機能と同じ - しかし以下の点が異なる - 1.

    安全性: Verifier による検証でプログラマのミスを未然に防ぐ - メモリアクセス違反でカーネルがハングすることを防いだり、 メモリリークや無限ループに対する事前検証をVerifierが行ってくれる - 2. 後方互換性: カーネルモジュールが後方互換性を保証しないのに対し、 eBPFはAPI Interface経由で動作するため後方互換性が保証される - 更に仮想マシンなのでCPUアーキテクチャに依存しない、ポータビリティもある - なので頑張ってLinuxのアップストリームに入れる必要もない - カーネルメンテナと議論する必要もなくなり、開発アジリティも改善する KernelModule(従来の仕組み)と何が違う?
  8. 歴史:最初期 - 1992: 「The BSD Packet Filter: A New Architecture

    for User-level Packet Capture」のパケットキャプチャを効率化するアイディアが始まり - classic BPF (cBPF)と呼ばれ、LinuxにおいてLinuxSocketFilter(LSF)という 名前になって入れられている。libpcap(tcpdump)での利用が知られている。 $ sudo tcpdump -d "ip proto \tcp" (000) ldh [12] ; read EtherType (001) jeq #0x800 jt 2 jf 5 ; if (is_ipv4){goto 2}else{goto 5} (002) ldb [23] ; read IPv4Proto (003) jeq #0x6 jt 4 jf 5 ; if(is_tcp){goto 4}else{goto 5} (004) ret #262144 ; パケットキャプチャするようにして exit (005) ret #0 ; 何もせず exit tcpdumpで利用される、cBPFアセンブリの例 IPv4で尚且つTCPのパケットをフィルタできるバイトコードを示している
  9. - 2013: 「[PATCH net-next] extended BPF」をAlexei StarovoitovがLKML に投稿してBPFの拡張を提案する - この段階でパケット処理やSeccompなど汎用的な仕組みを目指していたことがMLから読める

    - Alexei自身は、IOVisorの元となってるPLUMgridの開発者で、 後にXDPの初期パッチを投稿していたりしてる - cf. PLUMgridによるLinuxへの開発の様子(主にeBPFとNetwork関連に力を入れてる) - cf. 最初期のXDPのパッチ(Jul 2016) - 2024現在、約32年の時を経て、BPFは拡張されまくった結果 パケットフィルタに留まらない、いろいろ便利な機能として育った 歴史:約20年後、eBPFが生まれる
  10. - eBPFのプログラムは右のような スタイルのC言語で書くことになる - clangのバックエンドにeBPFバイト コードを吐き出す仕組みがあり、 このプログラムを食わせることで eBPFバイトコード(ELFファイル) を取得できる -

    最近だとgccでコンパイルしたり、 Rustでもかけたりするらしい... eBPFプログラミング #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; cf. Get started with XDP/Task 2: Drop specific packets with XDP
  11. eBPFプログラミング - 右図はIPv6パケットをDropするコード例 - 1. SECマクロをつけることで エントリポイントを指定する - Cのmainに相当するものを自分で指定する -

    どこにhookを設定するかによって、 SECマクロと関数の引数の中身が変更される - 2. 安全性を保つためにデータを読むたび に境界値チェックをしている #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; cf. Get started with XDP/Task 2: Drop specific packets with XDP 1 2
  12. eBPF Loader - eBPFバイトコード等をBPFシステムコールに渡せるようにするモノ - eBPF Mapとの読み書きの中継もするのでユーザー空間とのインターフェースみたいなモノ - 今日はここを自作します!!!!(後で詳しく解説予定) -

    よく知られているものはこの辺 - libbpf(C): 本家大元、これが一番実装されてる。 - cilium/ebpf(Go):pure-goで頑張っててえらい。libbpfと比べると機能が足りないが... - libbpf-rs(Rust): libbpfのrust wrapper, libbpf-sysというFFIバインディングでやってる - Aya(Rust): pure-rustで頑張っててえらい。 - BCC(C,Python,Go,lua): 初めて勉強する時はこれがわかりやすい方かもしれない。 今回はPerl
  13. - bpf syscallでロードされたプログラムはVerifierにより二段階でチェック - 一段階目は全ての分岐をトレースした大雑把なもの - DAGをみることで無限ループの回避 - 最大命令長(100万命令)を超えてしまわないかのチェック -

    不正なジャンプがないかの確認 - 内部的にはcheck_cfgで深さ優先探索して実現してる - 二段階目は制約の上でレジスタの演算ができるなどの細かい確認 - 例えばスカラー値をポインタとしてメモリに対して読み書きできたら、 任意のアドレス空間の読み書きができてしまい大変危ない。 - バッファオーバーフローしないかのチェックもここでやっている - 型や定数の追跡をすることでこれらを実現している - 具体的にはbpf_reg_typeやbpf_reg_stateに定義してある eBPF Verifier
  14. - Verifierを通過したBPFプログラムは安全と仮定し、パフォーマンスのために ネイティブマシンコードに変換される - ここでeBPFバイトコードからArchごとのネイティブコードに変換されるため、 eBPFはアーキテクチャ非依存で展開することができる - そもそもネイティブで動いてくれないと、実行速度に難があるので・・・ - 実際にはdo_jitというカーネル内部の関数でJITコンパイルされる

    - バイトコードからの変換なので結構愚直で素朴な実装で書かれてる JIT Compiler case BPF_ALU | BPF_ADD | BPF_X: case (色々なcase条件を中略...): case BPF_ALU64 | BPF_XOR | BPF_X: maybe_emit_mod(&prog, dst_reg, src_reg, BPF_CLASS(insn->code) == BPF_ALU64); b2 = simple_alu_opcodes[BPF_OP(insn->code)]; EMIT2(b2, add_2reg(0xC0, dst_reg, src_reg)); break; cf. https://elixir.bootlin.com/linux/v6.11/source/arch/x86/net/bpf_jit_comp.c#L1379 dst_reg ^= src_reg 相当のコード
  15. eBPF Attach & Hook - カーネルにロードしたら、実行したい場所にアタッチする必要がある - 何かのイベントをトリガーにeBPFのプログラムが実行される - Hookの例

    - Socket: socketに対してのIOをhookしてフィルタ - kprobe, kretprobe: カーネルの関数呼び出し・返り値をトレース - TC, XDP: NICに対してのIOをhookして読み書き - アタッチ可能なタイプがbpf_attach_typeに定義されている - Linux v6.11現在で58のattach pointがある
  16. eBPF Map - eBPFプログラムとユーザー空間から読み書きできるKVストア - 様々なMap Typeが存在している - Hash, Array

    - Trie(IPアドレスのプレフィックスマッチなどに使う) - Per-CPU (Hash|Array): RWでロックしないように割り込みCPU毎でデータを持ちたい時に使う - LRU, CPUMAP, QUEUE etc… - 例えば、IPアドレスのブラックリストをeBPF Mapで持っていれば、 それを参照することでカーネルレベルでパケットを落とせる。 このブラックリストはユーザー空間から更新可能である
  17. 今回資料の参考としてこちらを公開中 - あくまで参考です。 - Sys::Ebpf という拙作のライブラリとして実装済み - Pure-Perlにこだわって制作しました(XSを利用してません!) - お陰でXDPにアタッチするのに使うNetlinkのコール部分とかもスクラッチ!😇

    - https://github.com/takehaya/Sys-Ebpf - 本発表のコードに関して詳しくはこちらを併せて参照ください - コードの形式に関して @AnaTofuZ さんがレビューをくれたのでこの場でお礼申し上げます:) - よければStarお待ちしてます!
  18. eBPF Loaderを自作するというのはどういうことか? - Loaderはカーネルとユーザー空間のインターフェースの役割を持つ - 実装するべき項目をざっくり挙げると... - ELFバイナリをパースして、実行に必要な情報を取り出す - リロケーションでeBPF

    MapのFD(File Descriptor)を埋め込む - bpf(2) syscallを使ってeBPFプログラムをロードする - bpf(2) syscallを使ってeBPF Mapを読み書きする - つまりこれらをPerlで実装すればOK! - 今回触れないところ諦めたところ - BTFのパースとそれのコール... - おかげでCO-REとかも非対応... - とりあえずELFパーサーを書けという話から始まる Perl Lib Loader
  19. 説明用にこのプログラムをコンパイルして利用 - なんかファイルが開かれたら カウンターが増えるコード - kprobeにAttachするコードで sys_openに対してhookする #include <linux/bpf.h> #include

    <linux/ptrace.h> #include <bpf/bpf_helpers.h> struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; SEC("kprobe/sys_open") int kprobe_sysopen() { __u32 key = 1; __u64 initval = 1, *valp; valp = bpf_map_lookup_elem(&kprobe_map, &key); if (!valp){ bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY); return 0; } __sync_fetch_and_add(valp, 1); return 0; } char LICENSE[] SEC("license") = "GPL";
  20. readelfして構造を見る - readelfコマンドを使うと ELFバイナリの中身が見える - 右図はセクション情報の抜粋 - 必要な情報は以下の4つ - kprobe/sys_open

    - .relkprobe/sys_open - license - maps 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 00000250 0000000000000093 0000000000000000 0 0 1 [ 2] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 4 [ 3] kprobe/sys_open PROGBITS 0000000000000000 00000040 00000000000000b0 0000000000000000 AX 0 0 8 [ 4] .relkprobe/s[...] REL 0000000000000000 00000220 0000000000000020 0000000000000010 I 9 3 8 [ 5] license PROGBITS 0000000000000000 000000f0 0000000000000011 0000000000000000 WA 0 0 1 [ 6] maps PROGBITS 0000000000000000 00000104 0000000000000014 0000000000000000 WA 0 0 4 [ 7] .eh_frame PROGBITS 0000000000000000 00000118 0000000000000030 0000000000000000 A 0 0 8 [ 8] .rel.eh_frame REL 0000000000000000 00000240 0000000000000010 0000000000000010 I 9 7 8 [ 9] .symtab SYMTAB 0000000000000000 00000148 00000000000000d8 0000000000000018 1 5 8
  21. 必要な情報をざっくり解説 - kprobe/sys_open: BPFのプログラム本体 - .relkprobe/sys_open: リロケーション情報(実行時に決定するもの) - license: ライセンス情報

    - char _license[] SEC("license") = "GPL"; みたいなやつのことである - printkやその他eBPFで動かすhelper関数を使うためにはGPLライセンスかどうかを 検証されたうえで使う必要があるため、明示的に指定しないとVerifierに怒られる - eBPF プログラムを実行するときに利用する - maps: eBPF Map本体の情報 Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map  .relkprobe/sys_openのリロケーション情報の例
  22. PerlでELFファイルをパースする(ELFヘッダ) - まずはELFヘッダをパース - 今回は struct Elf64_Ehdr を 参考に実装 -

    ELF Fileをreadをして、 substrで文字列のように切りだし て、unpackでPerlのデータ構造に 変換することで実現 - unpack: 指定されたバイナリデータを テンプレート文字列に合わせてPerlの データ構造に変換する組み込み関数 my $byte_offset = 0; my $byte_range = 16;# ELFヘッダは16バイト # e_identをパース my ( $magic, $class, $endian, $version, $abi, $abi_version ) = unpack( 'A4C3A5C2', substr( $data, $byte_offset, $byte_offset + $byte_range ) ); $byte_offset += $byte_range; $byte_range = 32; # 細かいのは中略 ... # ELFファイルのサイズなどを取得 my ($e_type, $e_machine, $e_version, $e_entry,$e_phoff, $e_shoff, $e_flags, $e_ehsize, $e_phentsize, $e_phnum, $e_shentsize, $e_shnum, $e_shstrndx)= unpack( 'S S L Q Q Q L S S S S S S', substr( $data, $byte_offset, $byte_offset + $byte_range ) ); cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Elf/Parser.pm#L60
  23. 参考: Elf64_Ehdr - ELFヘッダーの 定義が書かれた 構造体をCatして みる - ELFのスペックを LinuxFoundation

    が出しているので それを参考にする と良い $ sudo cat /usr/include/elf.h | grep -B16 " Elf64_Ehdr;" typedef struct { unsigned char e_ident[EI_NIDENT];/* Magic number and other info */ Elf64_Half e_type; /* Object file type */ Elf64_Half e_machine; /* Architecture */ Elf64_Word e_version; /* Object file version */ Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; /* Processor-specific flags */ Elf64_Half e_ehsize; /* ELF header size in bytes */ Elf64_Half e_phentsize; /* Program header table entry size */ Elf64_Half e_phnum; /* Program header table entry count */ Elf64_Half e_shentsize; /* Section header table entry size */ Elf64_Half e_shnum; /* Section header table entry count */ Elf64_Half e_shstrndx; /* Section header string table index */ } Elf64_Ehdr;
  24. PerlでELFファイルをパースする(sectionとsymbol) - ELFではデータの文字列部分を symbolテーブルに分けているため、 symbolテーブルと今回のデータ の実態があるsectionテーブル を紐付けつつ読み下す必要がある - e.g. sectionテーブルにはindex

    1 みたいなのがあり、そのデータをsymbol テーブルから引いてみて初めて kprobe/sys_openという文字がわかる - ついでにリロケーションテーブルも パースしてる # section tableのセクション名を取得するために文字列テーブルセクションを取得 my $strtab_section_offset = $elf->{e_shoff} + $elf->{e_shstrndx} * $elf->{e_shentsize}; my $strtab_offset = unpack( 'Q', substr( $data, $strtab_section_offset + 24, 8 ) ); # セクションヘッダとシンボルテーブルをパースするための追加処理 $elf->{sections} = parse_sections( $data, $elf->{e_shoff}, $elf->{e_shnum}, $elf->{e_shentsize}, $strtab_offset ); $elf->{symbols} = parse_symbols( $data, $elf->{sections}, $elf->{e_shstrndx} ); $elf->{relocations} = parse_relocations( $data, $elf->{sections} ); return $elf; cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Elf/Parser.pm#L97
  25. 参考: Elf64_Shdr - sectionヘッダーの 定義が書かれた構造体 をCatしてみる - ELF Section Hdrに

    関するスペックもある $ sudo cat /usr/include/elf.h | grep -B16 " Elf64_Shdr;" typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign;/* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;
  26. bpf(2) syscallでmap/programを生成・ロードする - bpf(2) syscallを実行することでELFから取り出したeBPF Mapとプログラム を生成およびロードできる - eBPF Map生成はbpf(2)サブコマンドでBPF_MAP_CREATEを実行

    - 既存のeBPF Mapをロードするときはbpf(2)サブコマンドBPF_OBJ_GETを実行 - eBPF Programのロードはbpf(2)サブコマンドでBPF_PROG_LOADを実行 - これらの引数は union bpf_attr で表現される - 詳しくはLinux KernelのeBPF Syscallを参照
  27. ではPerlでSyscallを叩くのは? - h2ph とPerlの組み込み関数の syscall を利用する - syscallを実行するには、システムコール番号が必要 - h2phはPerlの開発環境に組み込まれたコマンド

    - Cで定義されたC Headerファイルから定数に変換してくれるのでLinuxに 定義されているSyscall番号を引っ張ってきて実行する pushd /usr/include/x86_64-linux-gnu/sys/ h2ph -d "$ORIG_PWD/lib/Sys/Ebpf/Syscall" -a -l syscall.h popd require 'syscall.ph'; my $s = "hi there\n"; syscall(SYS_write(), fileno(STDOUT), $s, length $s); h2phでSyscall番号を引っこ抜く図 syswrite関数をエミュレートする図 cf. https://perldoc.jp/func/syscall
  28. いやまて、PerlでCPointerどうするの? - BPF_PROG_LOADの共用体を みると、プログラム列の長さと プログラムのポインタを渡すこと で実現しようとしてる - ログのバッファとかもそう - Perlでポインタみたいなのは

    どう取り扱えばいいのか? union bpf_attr { struct { /* Used by BPF_PROG_LOAD */ __u32 prog_type; __u32 insn_cnt; __aligned_u64 insns; /* 'const struct bpf_insn *' */ __aligned_u64 license; /* 'const char *' */ __u32 log_level; __u32 log_size; /* size of user buffer */ __aligned_u64 log_buf; /* user supplied 'char *' buffer */ __u32 kern_version; }; } __attribute__((aligned(8))); cf. https://man7.org/linux/man-pages/man2/bpf.2.html より、改変及び抜粋
  29. - 右の図はBPF_PROG_LOADをPerlで 実行するコード - pack(“P”)でメモリアドレス化して バイナリ文字列に変換 - unpack(“Q”)でuint64として解釈 - 再度pack(“Q”)でバイナリに

    - 一度Unpackしてるのはポインタを 常にuint64であることを保証 させるため - これで無事PerlからSyscallを 叩くことができて嬉しい!🥳 - BPF_MAP_CREATEは工夫無しでやるだけな ので気になったらコードを読んでください PackしてUnpackして解決 my $attr = pack( "L L Q Q L L Q L L", $attrs->{prog_type}, $attrs->{insn_cnt}, unpack( "Q", pack( "P", $attrs->{insns} ) ), unpack( "Q", pack( "P", $attrs->{license} ) ), $attrs->{log_level}, $attrs->{log_size}, unpack( "Q", pack( "P", $attrs->{log_buf} ) ), $attrs->{kern_version}, $attrs->{prog_flags} ); my $fd = syscall( Sys::Ebpf::Syscall::SYS_bpf(), BPF_PROG_LOAD, $attr, length($attr) ); cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Loader.pm#L171
  30. bpf(2) BPF_PROG_LOADは失敗した模様... Log buffer content: 0: (b7) r1 = 0

    1: (63) *(u32 *)(r10 -4) = r1 last_idx 1 first_idx 0 regs=2 stack=0 before 0: (b7) r1 = 0 2: (b7) r6 = 1 3: (7b) *(u64 *)(r10 -16) = r6 4: (bf) r2 = r10 5: (07) r2 += -4 6: (18) r1 = 0x0 8: (85) call bpf_map_lookup_elem#1 R1 type=inv expected=map_ptr - syscallは叩くことができたが、 map_ptrがinvalidらしい... verifierで怒られている模様 - Mapも生成したし、正しくeBPF の ProgramをロードしたのにeBPF Verifierに怒られてる...🥺 - r1 = 0x0 … ????
  31. ここでeBPFバイトコードの呼び出し規約を確認する - R1~R5は引数レジスタ - つまりR1は第一引数に対応 - R1がなぜか空っぽ? R0 汎用レジスタ(戻り値を格納) R1~R5

    汎用レジスタ(引数レジスタ) R6~R9 汎用レジスタ R10 フレームポインタ(読み出し専用) cf. https://www.kernel.org/doc/html/v6.11/bpf/standardization/abi.htm l#bpf-abi-recommended-conventions-and-guidelines-v1-0
  32. bpf_map_lookup_elemの第一引数は? - 第一引数がeBPF Map - 第二引数がmap key - つまりeBPF Mapが無効なので

    どうにかして有効なeBPF Map にしてあげる必要がある - よく考えれば、確かにMapの インスタンスを知らないと Progは接続できない - そもそも、createしたeBPF Map との紐づけがされてない... struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; __u32 key = 1; valp = bpf_map_lookup_elem(&kprobe_map, &key);
  33. - 実はBPF_MAP_CREATE を叩くと eBPF MapのFDが手に入るのでこれで 一意に生成されたMapのIDが取れる - これを実行時に渡してあげることでeBPFMapとの紐づけができる - リロケーションというのは、プログラムロード時にシンボルや

    メモリアドレスが決定されるプロセスである - で、前述した.relkprobe/sys_openに返ってくる... - つまり r1=[eBPF MapのFD] になるようにFDを埋め込む必要がある リロケーションでeBPF MapのFDを埋め込む Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map リロケーションセクションの図
  34. - 実はBPF_MAP_CREATE を叩くと eBPF MapのFDが手に入るのでこれで 一意に生成されたMapのIDが取れる - これを実行時に渡してあげることでeBPFMapとの紐づけができる - リロケーションというのはプログラム実行時に決まる領域のこと

    - で、前述した.relkprobe/sys_openに返ってくる... - つまり r1=[eBPF MapのFD] になるようにFDを埋め込む必要がある リロケーションでeBPF MapのFDを埋め込む Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map リロケーションセクションの図 ここのオフセットの値を 利用して書き換える
  35. リロケーションとオフセットを照らし合わせる - offsetは0x30, 0x70 - 48byte目と112byte目 なので8で割ると5と 13行目が書き換えの 対象とわかる -

    0オリジンに注意 - r1 = 0 ll とは...? $ llvm-objdump -D kprobe_file_open_counter.o kprobe_file_open_counter.o: file format elf64-bpf Disassembly of section kprobe/sys_open: 0000000000000000 <kprobe_sysopen>: 0: b7 06 00 00 01 00 00 00 r6 = 1 1: 63 6a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r6 2: 7b 6a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r6 3: bf a2 00 00 00 00 00 00 r2 = r10 4: 07 02 00 00 fc ff ff ff r2 += -4 5: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 7: 85 00 00 00 01 00 00 00 call 1 8: 55 00 09 00 00 00 00 00 if r0 != 0 goto +9 <LBB0_2> 9: bf a2 00 00 00 00 00 00 r2 = r10 10: 07 02 00 00 fc ff ff ff r2 += -4 11: bf a3 00 00 00 00 00 00 r3 = r10 12: 07 03 00 00 f0 ff ff ff r3 += -16 13: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 15: b7 04 00 00 00 00 00 00 r4 = 0 16: 85 00 00 00 02 00 00 00 call 2
  36. “r1 = 0 ll”ってなに? - lddw命令: 64bitの即値をレジスタにロードしてくれる - BPFは64bitの命令長で、immが32bitしかない=>128bit使って実現 -

    1命令目: 下位32bit, 2命令目: 上位32bit - ここにfdを入れる必要がある msb 32 48 52 56 lsb +------------------------+----------------+----+----+--------+ |immediate |offset |src |dst |opcode | +------------------------+----------------+----+----+--------+ BPFの命令をアスキーアートで書いた図 cf. https://github.com/iovisor/bpf-docs/blob/master/eBPF.md#instruction-encoding
  37. Mapの部分をSys::Ebpfで埋め込んでみると - 仮にFDが3だと、Sys::Ebpf上でバイトコードの表現するとこんな感じである - srcレジスタにBPF_PSEUDO_MAP_FDを詰めているのが肝 my $High = Sys::Ebpf::Asm->new( code

    => 0x18, # opcode(lddw) dst_reg => 0x1, # destination register (r1) src_reg => 0x1, # source register(Pseudo map fd) off => 0, # offset imm => 0x3 # immediate value (map fd) ); my $Low = Sys::Ebpf::Asm->new( code => 0, dst_reg => 0, src_reg => 0, off => 0, imm => 0);
  38. 実際のリロケーションの流れに起こしてみる - 入れるべきデータを得る - eBPF MapをCreateして, eBPF MapのFDを取得する - 変更すべき位置を決定する

    - .relkprobe~のoffsetを使い修正すべき命令をkprobe/sys_openセクション内から特定する - 何を書き換えるかを決定する - .relkprobe~からシンボルインデックスを取得し、シンボル名を解決し、 どのようなシンボル名のeBPF MapのFDを入れるかを決定する - つまりどのような名前のeBPF Mapなのかを得て、入れるべきFDを決めるということ - 実際に書き換える - 修正すべき命令に、期待したMapのFDを適用して、正しいマップへの参照に書き換える - この際、先程のlddw命令の形式で書き換える必要がある
  39. 実際にリロケーションをPerlで行う - lddw命令になる部分16byte を取得し、Asmにパースする - 128bitなのでhigh, lowにわけて 取り扱い、MapFDを入れる - 修正結果をシリアライズして

    上書きすることでFDを追加 出来る # ここにデバッグログがある(書き換え前を出力) # 指定されたオフセット位置にある `lddw` 命令(16バイト)を取得 my $bpf_insn = substr( $self->{reader}->{raw_elf_data}, $r_offset, 16 ); my $bpf_insn_len = length($bpf_insn); my ( $high, $low ) = Sys::Ebpf::Asm::deserialize_128bit_instruction($bpf_insn); # 即値 (64ビット) にマップFDを設定 $high->set_imm($map_fd); $low->set_imm( $map_fd >> 32 ); # src_reg に PSEUDO_MAP_FD (1) を設定 $high->set_src_reg(1); # 修正後の命令をパックして、元の場所に書き戻す my $new_bpf_insn = Sys::Ebpf::Asm::serialize_128bit_instruction( $high, $low ); substr( $self->{reader}->{raw_elf_data},$r_offset, 16, $new_bpf_insn ); # ここにデバッグログがある(書き換え後を出力) cf.https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Loader.pm#L264
  40. 再度ロードしてみると...ロードが成功する! sudo perl kprobe_file_open_counter.pl .relkprobe/sys_open Before relocation (offset 104): 18010000000000000000000000000000

    After relocation (offset 104): 18110000030000000000000000000000 Before relocation (offset 168): 18010000000000000000000000000000 After relocation (offset 168): 18110000030000000000000000000000 BPF program loaded successfully with FD: 4 Map FD: 3 Prog FD: 4 - Map FDが3なので、リロケーション前と後のデータのログを見てもわかるはず - 無事プログラム側のFDも生成されたのでロード完了! 0から3に変わって いるところからFDの 追加がわかる 動作OK!
  41. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points MapFDをaddressに書き換え済み
  42. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 1 システムコール でMapを作る MapFDをaddressに書き換え済み
  43. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 2 Map生成が成功し、 FDを取得できる MapFDをaddressに書き換え済み
  44. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 3 取得したMapFDを リロケーションで 置き換えて 入れてLoad MapFDをaddressに書き換え済み
  45. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT MapFDをaddressに書き換え済み Get Prog FD perf_event_open(2) Hook Points 4 Mapの実アドレスに 変換された上で動作
  46. 全体の流れを整理してみると #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <arpa/inet.h> SEC("xdp_drop")

    int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) 5 ProgのFDをAttachして BPFプログラムを動作させる (今回は省略) Hook Points MapFDをaddressに書き換え済み
  47. eBPF Mapを通じた読み書き 先程まででロードはできたが、 実利用のためにはPerlアプリとのデータ のやり取りができる必要がある。 Mapの読み書き(Read/Write)のAPI としてlookup,update,deleteがあり、 便利に利活用するには最低でも それらを実装する必要がある union

    bpf_attr { struct { /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY commands */ __u32 map_fd; __aligned_u64 key; union { __aligned_u64 value; __aligned_u64 next_key; }; __u64 flags; }; } __attribute__((aligned(8))); cf. https://man7.org/linux/man-pages/man2/bpf.2.html より、改変及び抜粋
  48. PerlでeBPF Mapへの読み書きをする my $attr = pack( "L L Q Q

    Q", $map_fd, 0, unpack( "Q", pack( "P", $key ) ), defined($value) ? unpack( "Q", pack( "P", $value ) ) : 0, $flags, ); my $result = syscall(    Sys::Ebpf::Syscall::SYS_bpf(), $cmd, $attr, length($attr) ); - またbpf(2)でリクエストすれば実現󰢐 - 実際にsyscallを叩いてるところはこちら - eBPF Mapへのアクセスはだいたい同じ Attributeなので結構簡単にコールできる cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Map.pm#L115
  49. 利用する側としては簡単ではない😇 具体的には自分でKey情報をpackして、 valueをunpackするという素敵(?)な 実装になってしまうというのはあまり お手軽ではない... なんとかして シリアライズ・デシリアライズを きれいにする必要がある...🤔 PerlでeBPF Mapへの読み書きをする

    sub raw_lookup { my ( $self, $key, $flags ) = @_; $flags //= 0; my $value = "\0" x $self->{value_size}; my $res = syscall_bpf_map_elem(      BPF_MAP_LOOKUP_ELEM(), $self->{map_fd}, $key,  $value, $flags ); my $key = 1; my $value = $map_kprobe_map->raw_lookup(         pack("L", $key)); print(unpack("Q", $value)) cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Map.pm#L144 Map lookupに先程のsyscallを共通化した例 今回のサンプルコードでmap lookupできるようにした例
  50. key, valueのschemaを定義できるようにした mapの生成後に、mapを操作するクラスでschema名と型を持つことで対応した my ( $map_data, $prog_fd ) = $loader->load_bpf($kprobe_fn);

    my $map_kprobe_map = $map_data->{kprobe_map}; $map_kprobe_map->{key_schema} = [ [ 'kprobe_map_key', 'uint32' ] ]; $map_kprobe_map->{value_schema} = [ [ 'kprobe_map_value', 'uint64' ] ]; my $key = { kprobe_map_key => 1 }; my $value = $map_kprobe_map->lookup($key); struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; 実際のCTypeでは Uint32,Uint64で 定義されてる Perl上でも同様に K/Vの名前と型名 をmappingして あげることで解決
  51. 内部的には型名をパースして解釈 PerlにはCType相当のものがなさそうで チョット涙ぐましい実装になったが 文字列操作が簡単にかけるので代用のコー ドもサクッとかけて良かった sub _match_uint_or_uint_array { my ($type)

    = @_; return $type    =~ /^uint(\d+)(?:\[(\d+)\])?$/ ? ( $1, $2 ) : (); } sub _get_type_size { my ($type) = @_; if ( my ( $bit_size, $array_size ) = _match_uint_or_uint_array($type) ) { $array_size //= 1; if ( $bit_size =~ /^(8|16|32|64)$/ ) { return ( $bit_size / 8 ) * $array_size; } } die "Unsupported type: $type";
  52. ということで、eBPF Loaderの完成! my $kprobe_info= Sys::Ebpf::Link::Perf::Kprobe::attach_kprobe( $prog_fd, $kprobe_fn ); print "Map

    FD: " . $map_kprobe_map->{map_fd} . "\n"; print "Program FD: $prog_fd\n"; sleep(1); print "Counting file opens. Press Ctrl+C to stop.\n"; while (1) { my $key = { kprobe_map_key => 1 }; my $value = $map_kprobe_map->lookup($key); if ( defined $value ) { printf "Files opened: %d\n", $value->{kprobe_map_value}; } sleep(1); } cf. https://github.com/takehaya/Sys-Ebpf/blob/main/sample/kprobe_file_open_counter/kprobe_file_open_counter.pl Sampleとして用意した「なんかファイルが開かれたらカウンターが増える eBPFプログラム」をハンドルするPerlコードの全貌です!!
  53. 動作結果の様子 $ sudo perl kprobe_file_open_counter.pl Map FD: 3 Program FD:

    4 Counting file opens. Press Ctrl+C to stop. Files opened: 4 Files opened: 4 Files opened: 8 Files opened: 8 Files opened: 12 Files opened: 12 Files opened: 16 Files opened: 16 FDが取得できてる ので無事Load OK open syscallが叩かれた回数 (file openとかされた だけ増える)
  54. 動作の様子 $ sudo perl ./xdp_count_8080_port.pl (中略) ens3 にXDPプログラムをアタッチしました。 XDPプログラムをアタッチしました。 Ctrl+Cで停止します。

    パケット数 : 0 パケット数 : 0 パケット数 : 0 パケット数 : 1 パケット数 : 2 パケット数 : 3 パケット数 : 9 8080 listen前なので、 portに来てもackが返らず、handshake失敗で終わるの で一回の実行で1つずつしか増えない 8080 listen後なので、handshake終了後、データを転 送してくるので一気に増えてる
  55. - サービスのポータビリティ性を高めるためにBTF対応に関する実装と そこの知見を共有したい。struct opsとかCO-REのためにも対応したい:) - 多くのhook point対応できるコードをいれることでパケット処理や オブザーバビリティに対しての洞察が得られるので進めていきたい - 例えばTC,

    uprobe, tracepoint, kretprobeとかはまだ... - 他の細かい再配置とかはサボってるのでやりたい... - ガッと書いたのでナウいPerlでは無い気がする ロガーとかエラーとか整備したい... 今後の展望 cf. https://docs.kernel.org/bpf/btf.html#btf-kernel-api
  56. 参考資料 - https://github.com/torvalds/linux - https://docs.kernel.org/userspace-api/ebpf/syscall.html - https://man7.org/linux/man-pages/man2/bpf.2.html - https://speakerdeck.com/yutarohayakawa/ebpfhahe-gaxi-siinoka -

    https://mechpen.github.io/posts/2019-08-03-bpf-map/index.html - https://blog.yuuk.io/entry/2021/ebpf-tracing - https://atmarkit.itmedia.co.jp/ait/articles/1910/07/news008.html