Systemtapを支えるuprobeを直接いじってみる


この記事はLinux Advent Calendar 2016の10日目の記事です。

概要

ftraceやeBPF/bcc、Systemtap等から使うことができるuprobesだが
kprobesのようにuprobes単体で実行できるサンプル(カーネルモジュール)を見つけることができない。
そこで、クリスマスも近いので今回は誰でも気軽に使えるuprobesのサンプルを紹介する(気軽にカーネルモジュール作るべきとは言ってない)。
なお詳細な実装の説明は割愛するが、該当命令を0xccに書き換えて飛ばすアレである。

環境

ディストリビューション : Ubuntu 16.04
調査および検証に利用したカーネル : 4.4.0-45-generic

uprobesのAPI

uprobesはカーネル空間からユーザプロセスのプローブを行うLinuxカーネルの機構である。
通常uprobesはftraceのようにユーザ空間から設定可能なインターフェースから使われたり
Systemtapのように独自スクリプトからuprobesを使うカーネルモジュールを生成し、その中で使われる。

今回はクリスマスが近いのでuprobesを直接触るためのカーネルモジュールを書く。

まずはuprobes機構を利用するためのカーネル側APIを見てみよう。

/*
 * uprobe_register - register a probe
 * @inode: the file in which the probe has to be placed.
 * @offset: offset from the start of the file.
 * @uc: information on howto handle the probe..
 *
 * Apart from the access refcount, uprobe_register() takes a creation
 * refcount (thro alloc_uprobe) if and only if this @uprobe is getting
 * inserted into the rbtree (i.e first consumer for a @inode:@offset
 * tuple).  Creation refcount stops uprobe_unregister from freeing the
 * @uprobe even before the register operation is complete. Creation
 * refcount is released when the last @uc for the @uprobe
 * unregisters.
 *
 * Return errno if it cannot successully install probes
 * else return 0 (success)
 */
int uprobe_register(struct inode *inode, loff_t offset, struct uprobe_consumer *uc)

設定が必要な情報は、ざっくり、プローブ対象ファイルのinode、プローブ対象ファイルのプローブ対象オフセット、プローブした際に実行するハンドラであることがわかる。

次にプローブ時の関数を設定するuprobe_consumer構造体を見てみよう。

struct uprobe_consumer {
        int (*handler)(struct uprobe_consumer *self, struct pt_regs *regs);
        int (*ret_handler)(struct uprobe_consumer *self,
                                unsigned long func,
                                struct pt_regs *regs);
        bool (*filter)(struct uprobe_consumer *self,
                                enum uprobe_filter_ctx ctx,
                                struct mm_struct *mm);

        struct uprobe_consumer *next;
};

uprobe_consumer構造体の定義から、プローブ時にハンドラ経由でpt_regs構造体が渡ってくることがわかる。

サンプル

今回は下記の適当なCのサンプルアプリを実行し、debuggee_func関数呼出しをプローブしてみる。

#include <stdio.h>

int debuggee_func(int a, int b)
{
        int result;
        result = a + b;
        return result;
}

void main()
{
        int result;
        result = debuggee_func(1, 2);
        printf("result: %d", result);
}

次にuprobesを利用するサンプルカーネルモジュールは下記に置いた。
ファイルパスからinodeへの変換などの処理は下記サンプルコードを確認してほしい。

今回のuprobesサンプルカーネルモジュールにおける下記DEBUGGEE_FILEはデバッグ対象のファイル、DEBUGGEE_FILE_OFFSETはオフセットを表す。
オフセットは対象バイナリに対してreadelf等を用いて対象関数のアドレスとテキストセグメント開始アドレスから求める。
私の環境では、debuggee_func関数は、0x526(0x400526 - 0x400000)となっていたので、この値を埋め込んでmakeした。

#define DEBUGGEE_FILE "/home/kentaost/debuggee_app"
#define DEBUGGEE_FILE_OFFSET (0x526)

このサンプルカーネルモジュールをinsmod後に、サンプルアプリを実行しdmesgを見てみると下記のようにプローブした形跡が見える。

…
[xxxx.xxxxxx] handler is executed
[xxxx.xxxxxx] ret_handler is executed

uprobe_register関数からもわかる通り、ユーザプロセス単位でプローブを設定する仕組みではないため
たとえば、同じプログラムを複数立ち上げてもプローブされることがわかる。

余談だが、頑張れば下記のSystemtapと同様に、ユーザ空間スタックトレースを仕込むことができる。
(頑張らなくて良いようにSystemtapはtapsetを用意してくれているので、通常はSystemtapとtapsetを活用しよう)

まとめ

今回は割愛したがuprobes内部実装も合わせて理解すれば、Systemtap等でユーザプロセスをプローブする際(uprobes利用時)の挙動を理解することができる。
直接利用する用途もなくはないが、クリスマス以外では素直にftraceやSystemtapを使うべきである。