bpftraceでC++のアプリケーションをトレース
動機と概要
eBPFプログラムはLinuxカーネルのようなC言語で書かれた関数のトレースを得意としています。しかし、C++アプリケーション特有のトレース技法があまり公開されていないため、今回記事を書きました。以下ではeBPFを使ったトレーシングを提供するツールであるbpftraceを使います。
サーバーの移設を検討するためにはどのウェブサーバーからどの程度のアクセスがあるのか調べる必要があります。グリーではC++で書かれたflareというmemcached互換のサーバーが動いており、flareへのアクセス解析にbpftraceを利用しました。ウェブサーバーは一回の接続で何度もflareにリクエストを送るため、接続数ベースの計測では実際の利用頻度が分かりません。今回bpftraceを用いてどのサーバーからどんな種類のアクセスを何回リクエストしているか調査しました。
本記事ではトレースにおけるC++特有の問題を解決した後、flareの解析に使ったbpftraceのコードを紹介します。
C++特有の問題
トレースにおけるC++特有の問題を5つほど紹介します。
- 名前修飾(name mangling)
- std::string
- 仮想関数テーブル(vtable)とメンバ変数
- スマートポインタ
- 参照
bpftraceからC++の変数にアクセスするにはC言語のルールでアクセスする必要があります。しかし、C++からC言語への変換がトレースを困難にしています。具体的にはC++のクラスをC言語の構造体に、参照をポインタに翻訳する必要があります。
名前修飾(name mangling)
C++の関数名は名前修飾(マングリング)されており、名前修飾されたシンボルを使ってbpftraceに渡してトレースします。
名前修飾はint DataA::getData()
といったC++におけるクラスDataA
の関数getData
に対して、_ZN5DataA7getDataEv
のようなシンボルを割り当てる処理になります。この逆の処理はデマングリングといいます。
bpftrace -l
コマンドでトレースできる関数名を取得できます。そこでは名前修飾された関数名が出てきます。C言語からC++のシンボルを参照するなら名前修飾された関数名を使います。下記に/usr/bin/flared
のプログラムを対象に調べた例を示します。
1 2 3 4 5 6 7 8 9 10 11 |
$ sudo bpftrace -l 'uprobe:/usr/bin/flared:*' |grep flare|head uprobe:/usr/bin/flared:_ZNK5boost10shared_ptrIN4gree5flare6threadEEptEv.isra.0.part.0 uprobe:/usr/bin/flared:_ZNK5boost10shared_ptrIN4gree5flare19cluster_replicationEEptEv.isra.0.part.0 uprobe:/usr/bin/flared:_ZN4gree5flare15sa_usr1_handlerEi.cold uprobe:/usr/bin/flared:_ZN4gree5flare15sa_term_handlerEi.cold uprobe:/usr/bin/flared:_ZN4gree5flare6flared8shutdownEv.cold uprobe:/usr/bin/flared:_ZN4gree5flare6flared16on_storage_errorEv.cold uprobe:/usr/bin/flared:_ZN4gree5flare6flared19_set_resource_limitEv.cold uprobe:/usr/bin/flared:_ZN4gree5flare6flared19_set_signal_handlerEv.cold uprobe:/usr/bin/flared:main.cold uprobe:/usr/bin/flared:_ZN4gree5flare6flared3runEv.cold |
c++filt
コマンドでデマングリングして関数の名前や引数を見やすくすることができます。 下記実行例を示します。bpftraceは名前修飾したシンボルもデマングリングしたシンボルも扱えます。トレースしたいシンボルが見つからない場合はデバッグシンボルをインストールします。ubuntuの場合はこちらを参照ください。
余談ですが、.cold
というのは実行される頻度が低い関数につけられるGCCの属性だそうです。
1 2 3 4 5 6 7 8 9 10 11 |
$ sudo bpftrace -l 'uprobe:/usr/bin/flared:*' |grep flare|head | c++filt uprobe:/usr/bin/flared:boost::shared_ptr<gree::flare::thread>::operator->() const [clone .isra.0] [clone .part.0] uprobe:/usr/bin/flared:boost::shared_ptr<gree::flare::cluster_replication>::operator->() const [clone .isra.0] [clone .part.0] uprobe:/usr/bin/flared:gree::flare::sa_usr1_handler(int) [clone .cold] uprobe:/usr/bin/flared:gree::flare::sa_term_handler(int) [clone .cold] uprobe:/usr/bin/flared:gree::flare::flared::shutdown() [clone .cold] uprobe:/usr/bin/flared:gree::flare::flared::on_storage_error() [clone .cold] uprobe:/usr/bin/flared:gree::flare::flared::_set_resource_limit() [clone .cold] uprobe:/usr/bin/flared:gree::flare::flared::_set_signal_handler() [clone .cold] uprobe:/usr/bin/flared:main.cold uprobe:/usr/bin/flared:gree::flare::flared::run() [clone .cold] |
std::string
トレースにおいて文字列の出力は環境によってメモリレイアウトが異なるので注意が必要です。C++の文字列であるstd::stringにC言語からアクセスするにはstd::string
のメモリレイアウトを知る必要があります。メモリレイアウトはSmall String Optimization (SSO) による短い文字列のメモリ割り当てがあるかないかで変わっています。
GCC4までは、std::string
は次のような構造になっています。
1 2 3 4 5 |
struct string{ char* buffer; // 文字列のバッファ size_t size; // 文字列の長さ size_t capacity; // bufferのキャパシティ } |
SSOがあるとGCC9では次のようなものになりますが、コンパイラや環境により異なる場合があります。GCCの場合はbasic_string.h
のヘッダーを見て確認してみてください。
1 2 3 4 5 |
struct string_with_sso{ char* buffer; // 文字列のバッファ size_t size; // 文字列の長さ char sso_buffer[16]; } |
GCCではいずれにしても最初に文字列のバッファへのポインタがあり、bpftraceは、下記のようなC言語の構造体を定義すればbuffer
から直接文字列へアクセスできます。
1 2 3 4 5 |
struct string{ char* buffer; size_t size; // 文字列の長さ char dummy[16]; // 環境に合わせて穴埋め。 } |
仮想関数テーブル(vtable)とメンバ変数
クラスのメンバ変数にアクセスするためには仮想関数テーブル(vtable)の有無を確認する必要があります。仮想関数テーブルは仮想関数へのポインタをまとめたテーブルで、多態性(ポリモーフィズム) を実現する仕組みです。
仮想関数がある場合は仮想関数テーブル(vtable)があります。次のように仮想テーブルがあるDataA
と仮想テーブルがないDataB
のクラスがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 仮想関数テーブル(vtable)のあるclass DataA class DataA { public: virtual int get(); int getData(); int data; }; // 仮想関数テーブル(vtable)のないclass DataB class DataB { int getData(); int data; }; |
bpftraceでは次のようなC言語の構造体になります。仮想関数があるDataA
は、はじめにvtableへのポインタがあって、その次にメンバ変数が並びます。仮想関数がないDataB
は、はじめにメンバ変数が並びます。
1 2 3 4 5 6 7 8 9 |
// 仮想関数テーブル(vtable)のあるclass DataA struct DataA{ void* vtable; // ここがvtable int data; }; // 仮想関数テーブル(vtable)のないclass DataB struct DataB{ int data; }; |
次にbpftraceでgetData
をトレースして中身のdata
を取り出すコードを示します。
メンバ関数の第一引数(arg0
)はクラスへのポインタになっており、C++ではthis
のポインタと同じものです。メンバ関数に引数があれば第二引数(arg1
)以降のものを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct DataA{ void* vtable; int data; } struct DataB{ int data; } // DataAのメンバ関数getDataへのプローブ設定 // _ZN5DataA7getDataEvはDataAのメンバ関数getDataを名前修飾したもの。 uprove:プログラムへのパス:_ZN5DataA7getDataEv{ // arg0はc++のthisポインタと同じでクラスを指しています。 printf("DataA->data: %d\n",((struct DataA*)arg0)->data); } // DataBのメンバ関数getDataへのプローブ設定 // _ZN5DataB7getDataEvはDataBのメンバ関数getDataを名前修飾したもの。 uprove:プログラムへのパス:_ZN5DataB7getDataEv{ printf("DataB->data: %d\n",((struct DataB*)arg0)->data); } |
スマートポインタ
boostのshared_ptrの場合、データへのポインタと参照カウントからなっています。
1 2 3 4 5 |
// C言語でのスマートポインタの構造体 struct shared_pr{ void* ptr; int count; } |
参照
C++の参照はC言語のポインタと同じものになります。下記に一例を示します。DataA&
の参照をstruct DataA*
のポインタにします。
1 2 3 4 5 |
// C++での関数の宣言 int getData(DataA& dat){} // C言語での関数の宣言 int getData(struct DataA* dat){ } |
flareの解析
上記の知見をもとにflareへアクセスのあるWebサーバーをbpftraceを用いて出力するプログラムを作り、解析を実施しました。flareへアクセスしているWebサーバーのIPアドレスをリクエストの種類(get,set,delete,incr)ごとに出力します。
flareソースコードを読みリクエストの処理を確認します。リクエストのパースが完了し、クライアントにレスポンスを返す処理をする_run_server
関数にuprobeを設置します。その中でクラスのメンバにアクセスし、 sockaddr_in
構造体からウェブサーバーのアドレスを取得して表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// bpftraceのスクリプトのサンプル // /usr/include/netinet/in.hから持ってきています。 struct sockaddr_in { u_char sin_len; u_char sin_family; u_short sin_port; u_int32_t sin_addr; char sin_zero[8]; } // connectionクラスは_addr_inetというメンバーにIPアドレスを持っている。 struct connection { int* vtable; u_short _addr_family; struct sockaddr_in _addr_inet; } struct smart_ptr { struct connection *ptr; // スマートポインタの参照カウントは参照しないので省略。 } // トレースする関数のクラスの構造体 // ウェブサーバーとの接続情報をconnectionで管理している。 struct op{ int* vtable; struct smart_ptr connection; // 後続のメンバ関数は使わないので省略。 } // トレースする関数のリスト uprobe:/usr/bin/flared:_ZN4gree5flare6op_get11_run_serverEv, uprobe:/usr/bin/flared:_ZN4gree5flare6op_set11_run_serverEv, uprobe:/usr/bin/flared:_ZN4gree5flare7op_incr11_run_serverEv, uprobe:/usr/bin/flared:_ZN4gree5flare9op_delete11_run_serverEv { printf("%d %s %s\n", nsecs, probe, ntop( ((struct op*)arg0)->connection.ptr->_addr_inet.sin_addr )); } |
1 2 3 4 5 |
# bpftraceの実行例 # bpftraceのスクリプトを準備 $ cat > flare-trace.bt # bpftraceを実行 $ sudo bpftrace ./flare-trace.bt |
まとめ
- bpftraceでC++のプログラムをトレースして引数の中身やメンバ変数にアクセスするために、C++言語のデータ構造をC言語のデータ構造に変換するやり方を紹介しました。
- トレースの適用例としてグリーで使用しているflareへアクセスしているWebサーバーのIPアドレスをトレースするプログラムを示しました。
参考文献
- https://github.com/bpftrace/bpftrace/blob/master/docs/reference_guide.md
- https://qiita.com/4_mio_11/items/aa71f18b24ab55e4cb3d
- https://rrmprogramming.com/article/small-string-optimization-sso-in-c/
- https://joellaity.com/2020/01/31/string.html
- https://wiki.ubuntu.com/Debug%20Symbol%20Packages
- https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html