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のプログラムを対象に調べた例を示します。

c++filtコマンドでデマングリングして関数の名前や引数を見やすくすることができます。 下記実行例を示します。bpftraceは名前修飾したシンボルもデマングリングしたシンボルも扱えます。トレースしたいシンボルが見つからない場合はデバッグシンボルをインストールします。ubuntuの場合はこちらを参照ください。

余談ですが、.coldというのは実行される頻度が低い関数につけられるGCCの属性だそうです。

std::string

トレースにおいて文字列の出力は環境によってメモリレイアウトが異なるので注意が必要です。C++の文字列であるstd::stringにC言語からアクセスするにはstd::stringのメモリレイアウトを知る必要があります。メモリレイアウトはSmall String Optimization (SSO) による短い文字列のメモリ割り当てがあるかないかで変わっています。

GCC4までは、std::stringは次のような構造になっています。

SSOがあるとGCC9では次のようなものになりますが、コンパイラや環境により異なる場合があります。GCCの場合はbasic_string.hのヘッダーを見て確認してみてください。

GCCではいずれにしても最初に文字列のバッファへのポインタがあり、bpftraceは、下記のようなC言語の構造体を定義すればbufferから直接文字列へアクセスできます。

仮想関数テーブル(vtable)とメンバ変数

クラスのメンバ変数にアクセスするためには仮想関数テーブル(vtable)の有無を確認する必要があります。仮想関数テーブルは仮想関数へのポインタをまとめたテーブルで、多態性(ポリモーフィズム) を実現する仕組みです。

仮想関数がある場合は仮想関数テーブル(vtable)があります。次のように仮想テーブルがあるDataAと仮想テーブルがないDataBのクラスがあるとします。

bpftraceでは次のようなC言語の構造体になります。仮想関数があるDataAは、はじめにvtableへのポインタがあって、その次にメンバ変数が並びます。仮想関数がないDataBは、はじめにメンバ変数が並びます。

次にbpftraceでgetDataをトレースして中身のdataを取り出すコードを示します。

メンバ関数の第一引数(arg0)はクラスへのポインタになっており、C++ではthisのポインタと同じものです。メンバ関数に引数があれば第二引数(arg1)以降のものを使います。

スマートポインタ

boostのshared_ptrの場合、データへのポインタと参照カウントからなっています。

参照

C++の参照はC言語のポインタと同じものになります。下記に一例を示します。DataA&の参照をstruct DataA*のポインタにします。

flareの解析

上記の知見をもとにflareへアクセスのあるWebサーバーをbpftraceを用いて出力するプログラムを作り、解析を実施しました。flareへアクセスしているWebサーバーのIPアドレスをリクエストの種類(get,set,delete,incr)ごとに出力します。

flareソースコードを読みリクエストの処理を確認します。リクエストのパースが完了し、クライアントにレスポンスを返す処理をする_run_server関数にuprobeを設置します。その中でクラスのメンバにアクセスし、 sockaddr_in構造体からウェブサーバーのアドレスを取得して表示します。

まとめ

  • bpftraceでC++のプログラムをトレースして引数の中身やメンバ変数にアクセスするために、C++言語のデータ構造をC言語のデータ構造に変換するやり方を紹介しました。
  • トレースの適用例としてグリーで使用しているflareへアクセスしているWebサーバーのIPアドレスをトレースするプログラムを示しました。

参考文献