ebpf-goによるLinuxカーネルトレース入門Zennyyamamot🐝ebpf-goによるLinuxカーネルトレース入門2023/12/30に公開GoLinuxkerneleBPFtechebpf-goによるLinuxカーネルトレース入門ebpf-goを使用したLinuxカーネルトレース(fentry)について紹介します。ebpf-goは、eBPFのGo向けライブラリです。このライブラリは、libbpfに依存せず(cgoを使用せず)にeBPFプログラムとデータのやり取りが可能であるため、ポータビリティが高くGo言語に適したeBPFライブラリとなります。fentryは、カーネル関数のエントリポイントにプログラムをアタッチするためのBPFプログラムタイプです。簡潔に言うと、カーネル関数にフック処理を行うことができます。Go本記事ではebpf-goのexamples内のtcprttを基に、ebpf-goの開発方法についても説明します。Format)を有効にする必要があります。bccのインストール手順、カーネル設定の詳細は下記を参照してください。(aptでインストール)注意事項として、環境構築でビルドが失敗する可能性があります。23.10とbccの組み合わせではビルドに失敗しました。22.10と最新のbccの組み合わせでもビルドに失敗しました。v0.29.1)の組み合わせです。tcprttの全体構成およびビルド方法tcprttのコード構成は以下の通りです。ビッグエンディアン向けのeBPF操作やデータのやり取りを行うコード。ビッグエンディアン向けのeBPFバイトコード。リトルエンディアン向けのeBPF操作やデータのやり取りを行うコード。リトルエンディアン向けのeBPFバイトコード。アプリケーションのメイン部分。このコードからbpf_bpfe[bl].o、bpf_bpfe[bl].goが生成されます。update.shebpf-goの特徴として、バイナリ内にバイトコードを取り込みシングルバイナリを実現しています。アプリケーションのメイン部分が実行されたとき、bpf(2)をコールしカーネルにバイトコードをロードする仕組みになっています。[]byteコード生成にはgo:generate構文を利用しています。-I../headers図で表すと以下の通りです。tcprttを単体でビルドするためには、以下のパッチを適用してください。ebpf-goプロジェクト全体でビルドする場合は不要です。Makefileを利用すると、開発がより効率的になります。eBPFコード(tcprtt.c)が修正された場合のみ、コードの生成を行います。./${TARGET}eBPFコード(tcprtt.c)下記のコードは、TCP接続が閉じられるときに実行されます。具体的には、tcp_closeカーネル関数のfentry(関数エントリ)ポイントにアタッチされます。TCP接続が閉じられるたびに実行され、接続に関する情報をリングバッファに送信します。このリングバッファの詳細な説明は省略します。カーネル空間とユーザ空間でデータをやり取りする手段として、マップやリングバッファなどが存在する、という認識で問題ありません。5.5でサポートが開始され、それ以前はkprobeのBPFタイプを使用してカーネル関数にアタッチしていました。fexitなどを使用する場合は、カーネルのメモリに直接アクセスできます。kprobeではカーネルメモリへのアクセス、例えばtcprtt.cの場合ではskへのアクセスを行う際には、bpf_probe_read*()を使用してアクセスする必要があります。0;}下記にLinuxカーネルヘッダの部分的な定義があります。この定義は、tcprtt.cで使用しているフィールドのみを記載しています。Format)から構造体のフィールドのアクセス先(オフセット)を適切に計算し、アクセスが可能になります。sock.hの定義を比較すると、フィールドが欠けていることが確認できると思います。略アプリケーションコード以下のコードは典型的なパターンで構成されており、入門レベルでは深く理解する必要はないと思います。5.11以降では何も行いません。5.111以前はメモリロック制限の解除に使用していました。互換性のために存在します。loadBpfObjects()バイトコードをカーネルに送信し、マップやリングバッファにアクセスできるようにします。link.AttachTracing()tcp_close()にアタッチします。ringbuf.NewReader()リングバッファのリーダーを作成します。注意コードを修正し、新たなカーネル関数にフックする場合は、link.AttachTracingに登録する必要があります。複数のカーネル関数を登録する場合は、link.AttachTracingをそれぞれ実行する必要があります。Wait<-stopper}これはeBPFからのリングバッファのデータを受け取る処理です。カーネルからデータが送られてくるのを待ち、データが到着したら読み込む無限ループとなっています。非常にシンプルなコードです。カーネルのトレースを採取する場合、多くの場合このパターンになると思われます。%-6d",intToIP(event.Saddr),event.Sport,intToIP(event.Daddr),event.Dport,event.Srtt,)}}より理解を深めるためには、ebpf-goのexamplesのUprobe、Tracepoint、XDPなどを参照すると良いでしょう。Format無効化版でのカーネルトレース採取方法)研究用途や個人利用ではBTFを有効化することが可能ですが、商用ではビルドツールやカーネルバージョンが指定されるため、現状ではBTFを有効化できない開発者も多いと考えられます。BTFが有効化できず、vmlinux.hが作成できない場合、構造体の定義を手動で作成する必要があります。この構造体定義が非常に至難です。私が調査した限りでは、sock.hのプリプロセッサ適用版のヘッダを簡単に作成することはできません。CONFIG_IKHEADERS=yに設定してカーネルヘッダを作成しても、この定義ではビルドができませんでした。eBPFの思想からは外れますが、解決策の一つとしてカーネルを修正しました。最小限のsock構造体を定義し、eBPFプログラムから容易にフックできるようにすることで、BTF無効化版でもカーネルトレースが可能となります。下記はtcp_closeの例です。timeout);release_sock(sk);sock_put(sk);}EXPORT_SYMBOL(tcp_close);参考ネットワークログの採取を目的として作成したサンプルプロジェクトです。これはtcprttの発展形です。Monitoryyamamotバッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。Proよくある質問LinksX(Twitter)GitHubメディアキットLegal利用規約プライバシーポリシ。