Uprobes の概要と使い方

Uprobes とは

以前に Kprobes を調べたので(Kprobes の概要と使い方 - SIerだけど技術やりたいブログ)、そのユーザ空間版である Uprobes について調べた。まあ大体一緒。

Uprobes はユーザ空間のアプリケーションに処理を差し込むための仕組み。自身が開発したアプリケーション というよりも、yum でダウンロードするソフトウェアや、アプリケーションが動作するうえで必要なソフトウェア(たとえば libc とか)の性能解析やバグ解析に使うんだと思う。自身で開発するソフトウェアなら、言語に合ったデバッガやパフォーマンス解析ソフトウェアを使ったほうが楽だと思うので。Uprobes を直接利用することは少ないが、SystemTap や bpftrace などのトレーシングツールの内部で利用されているので、知らず知らず使っている人も多いはず。

細かくいうと任意のアドレスに処理を差し込むものを uprobe 、関数の実行後に処理を差し込むものを uretprobe という。

Uprobe いまいち使い道わかりませんが、Uprobes をマスターすると他人の Bash の入力を盗み見れます。

]# /usr/share/bpftrace/tools/bashreadline.bt
Attaching 2 probes...
Tracing bash commands... Hit Ctrl-C to end.
TIME      PID    COMMAND
06:35:47  16574  ls
06:35:55  16574  echo "secret something"

検証環境

CentOS 8 を利用する。

]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
]# uname -a
Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64 #1 SMP Fri Jan 3 23:55:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

使い方

uprobe_register

register_uprobe を利用すると uprobe が利用できる。カーネル関数のため、直接呼び出すにはカーネルモジュールを作成する必要がある。

利用方法は、Kprobes に類似しているが、Kprobes はフックを設定する箇所をシンボル名で指定できたのに対して、 Uprobes は Offset で指定する。なぜこうなっているのかというと、(おそらく)カーネル関数はシンボル情報をカーネル内で腹持ちしている(参考 linux /proc/kallsyms をコードから理解する - SIerだけど技術やりたいブログ のに対し、ユーザ空間のプログラムはシンボル情報を実行ファイルに含めるとは限らないため。たとえば strip コマンドでシンボルを削除した場合、実行ファイルにシンボル情報が含まれない。yum でダウンロードするパッケージはだいたいのシンボルが strip されていて、デバッグ用にシンボル情報を debuginfo で持つのが一般的になっている。

また、Kprobes のように uretprobe_register は用意されておらず、ret_handler に値がセットされていれば uretprobe が設定される。

#include <linux/module.h>
#include <linux/ptrace.h>
#include <linux/uprobes.h>
#include <linux/namei.h>
#include <linux/moduleparam.h>

MODULE_AUTHOR("john doe");
MODULE_LICENSE("GPL v2");

// カーネルモジュールの引数で設定する
// 参考 [Linux の挙動を変更する 4 つの方法](https://www.kimullaa.com/entry/2020/05/21/073836)
static char *filename;
module_param(filename, charp, S_IRUGO);

static long offset;
module_param(offset, long, S_IRUGO);

static int handler_pre(struct uprobe_consumer *self, struct pt_regs *regs){
        pr_info("handler: arg0 = %d arg1 =%d \n", (int)regs->di, (int)regs->si);
        return 0;
}

static int handler_ret(struct uprobe_consumer *self,
                                unsigned long func,
                                struct pt_regs *regs){
        pr_info("ret_handler ret = %d \n", (int)regs->ax);
        return 0;
}

static struct uprobe_consumer uc = {
        .handler = handler_pre,
        .ret_handler = handler_ret,
};


static struct inode *inode;

static int __init uprobe_init(void) {
        struct path path;
        int ret;

        ret = kern_path(filename, LOOKUP_FOLLOW, &path);
        if (ret < 0) {
                pr_err("kern_path failed, returned %d\n", ret);
                return ret;
        }

        inode = igrab(path.dentry->d_inode);
        path_put(&path);

        ret = uprobe_register(inode, offset, &uc);
        if (ret < 0) {
                pr_err("register_uprobe failed, returned %d\n", ret);
                return ret;
        }

        return 0;
}

static void __exit uprobe_exit(void) {
        uprobe_unregister(inode, offset, &uc);
}

module_init(uprobe_init);
module_exit(uprobe_exit);

このソースコードをビルドし、カーネルモジュール(ko ファイル)を作成する。

]# cat Makefile
obj-m := hello-uprobe-world.o
KDIR    := /lib/modules/$(shell uname -r)/build
VERBOSE = 0

all:
        $(MAKE) -C $(KDIR) M=$(PWD) KBUILD_VERBOSE=$(VERBOSE) CONFIG_DEBUG_INFO=y modules
clean:
        rm -f *.o *.ko *.mod.c Module.symvers modules.order

]# make
make -C /lib/modules/4.18.0-147.3.1.el8_1.x86_64/build M=/root/work/uprobe KBUILD_VERBOSE=0 CONFIG_DEBUG_INFO=y modules
make[1]: ディレクトリ '/usr/src/kernels/4.18.0-147.3.1.el8_1.x86_64' に入ります
  Building modules, stage 2.
  MODPOST 1 modules
make[1]: ディレクトリ '/usr/src/kernels/4.18.0-147.3.1.el8_1.x86_64' から出ます

トレーシング対象のソフトウェアを用意する。

main.c

#include <stdio.h>

int add(int a, int b) {
        return a + b;
}

int main(void) {
        add(1, 2);
}

作成したカーネルモジュール(ko ファイル)をインストールし dmesg でメッセージを確認すると、トレーシングできているのがわかる。

]# gcc -o main main.c
]# ./main

]# insmod hello-uprobe-world.ko filename=/root/work/uprobe/main offset=0x536
]# rmmod hello-uprobe-world

]# dmesg | tail
[281804.126225] handler: arg0 = 1 arg1 =2
[281804.126246] ret_handler ret = 3

なお、 offset は 関数アドレス(0000000000400536)から text 領域のアドレス(0000000000400450)を引き、オフセット(000450)を足すと求められる。

]# readelf -S main -W
There are 34 section headers, starting at offset 0x8098:

セクションヘッダ:
  [番] 名前              型              アドレス         Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  ...
  [11] .text             PROGBITS        0000000000400450 000450 000195 00  AX  0   0 16

]# readelf -s main -W
...
Symbol table '.symtab' contains 88 entries:
  番号:      値         サイズ タイプ  Bind   Vis      索引名
  ... 
  71: 0000000000400536    20 FUNC    GLOBAL DEFAULT   11 add

uprobe_events

uprobe_events は、カーネルモジュールを作成せずに Uprobe を利用できる仕組み。Ftrace と同じインタフェースで probe を動的に作成できる。

]# echo 'p:sample_uprobe /root/work/uprobe/main:0x536 %di %si' >  /sys/kernel/debug/tracing/uprobe_events
]# echo 'r:sample_uretprobe /root/work/uprobe/main:0x536 %ax' >>  /sys/kernel/debug/tracing/uprobe_events
]# echo 1 > /sys/kernel/debug/tracing/events/uprobes/sample_uprobe/enable
]# echo 1 > /sys/kernel/debug/tracing/events/uprobes/sample_uretprobe/enable

]# ./main
]# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            main-9258  [000] d... 283732.080847: sample_uprobe: (0x400536) arg1=0x1 arg2=0x2
            main-9258  [000] d... 283732.080867: sample_uretprobe: (0x40055d <- 0x400536) arg1=0x3

削除するには -:<id> を書き込む。

]# echo 0 > /sys/kernel/debug/tracing/events/uprobes/sample_uprobe/enable
]# echo 0 > /sys/kernel/debug/tracing/events/uprobes/sample_uretprobe/enable
]# echo "-:sample_uprobe" >> /sys/kernel/debug/tracing/uprobe_events
]# echo "-:sample_uretprobe" >> /sys/kernel/debug/tracing/uprobe_events

詳細は Documentation/trace/uprobetracer.rst が参考になる。

perf_event_open

perf_event_open システムコールで、BPF プログラムを uprobe のイベントにアタッチできる。BPF プログラムを直接書くのはつらいので bpftrace 経由で利用する。

]#  bpftrace -e 'uprobe:/root/work/uprobe/main:add {printf("%d %d\n", arg0, arg1); exit(); }'
Attaching 1 probe...
1 2

]#  bpftrace -e 'uretprobe:/root/work/uprobe/main:add {printf("%d\n", retval); exit(); }'
Attaching 1 probe...
3

perf_event_open が利用されている様子も確認する。

]# strace -e 'bpf,perf_event_open' bpftrace -e 'uprobe:/root/work/uprobe/main:add {printf("%d %d\n", arg0, arg1); exit(); }'
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERF_EVENT_ARRAY, key_size=4, value_size=4, max_entries=1, map_flags=0, inner_map_fd=0, map_name="printf", map_ifindex=0}, 112) = 3
Attaching 1 probe...
perf_event_open({type=PERF_TYPE_SOFTWARE, size=0 /* PERF_ATTR_SIZE_??? */, config=PERF_COUNT_SW_BPF_OUTPUT, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 5
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x7ffe8be6666c, value=0x7ffe8be66670, flags=BPF_ANY}, 112) = 0
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=43, insns=0x7f9074a40000, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(4, 18, 0), prog_flags=0, prog_name="add", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 112) = 7
perf_event_open({type=0x7 /* PERF_TYPE_??? */, size=PERF_ATTR_SIZE_VER5, config=0, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 6

ユーザ空間のツール

上記のようにカーネルモジュールを作成したりシステムコールを直接呼び出すほかに、ユーザ空間のツールを使う方法がある。むしろこれが一般的な使い方。

SystemTap

SystemTap は、D 言語に似たスクリプトからカーネルモジュールを作成してトレーシングするツール。これは register_uprobe を利用する。

]# stap -e 'probe process("/root/work/uprobe/main").function("add") { printf("%d %d\n", $a, $b); exit(); }'
1 2

]# stap -e 'probe process("/root/work/uprobe/main").function("add").return { printf("%d\n", $return); exit(); }'
3

perf

uprobe を用いて動的にイベントを追加できる。内部で uprobe_events を利用する。

]# perf probe -x ./main 'add %di %si'
Added new event:
  probe_main:add       (on add in /root/work/uprobe/main with %di %si)

You can now use it in all perf tools, such as:

        perf record -e probe_main:add -aR sleep 1

]# perf probe -x ./main 'radd=add%return %ax'
Added new event:
  probe_main:radd__return (on add%return in /root/work/uprobe/main with %ax)

You can now use it in all perf tools, such as:

        perf record -e probe_main:radd__return -aR sleep 1

]# perf record -e probe_main:* -aR sleep 5
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.270 MB perf.data (2 samples) ]

]# perf script
            main 13357 [000] 339723.155099:          probe_main:add: (400536) arg1=0x1 arg2=0x2
            main 13357 [000] 339723.155123: probe_main:radd__return: (400536 <- 40055d) arg1=0x3

削除するときは -d オプションを利用する。

]# perf probe -d '*'
Removed event: probe_main:add

詳細は perf probe の man が参考になる。

bpftrace

先ほど述べたが BPF プログラムを uprobe イベントにフックできる。これは perf_event_open を利用する。

]#  bpftrace -e 'uprobe:/root/work/uprobe/main:add {printf("%d %d\n", arg0, arg1); exit(); }'
Attaching 1 probe...
1 2

]#  bpftrace -e 'uretprobe:/root/work/uprobe/main:add {printf("%d\n", retval); exit(); }'
Attaching 1 probe...
3

uprobe の内部動作

Breakpoint 命令を利用して uprobe を実現する点は kprobe と同じ。ただし一つの実行ファイルから複数のプロセスが生成される可能性があり、なおかつ、uprobe を設定しないプロセスが存在する可能性もあるため、 inode から address_space 経由で該当命令を含む仮想アドレスを辿り(kernel/events/uprobes.c#register_for_each_vma)、プロセスごとに対応する実ページを Breakpoint 命令に書き換えたページに置き換える(kernel/events/uprobes.c#install_breakpoint)。

また Uprobe 設定後に起動したプロセスにも Uprobe が設定されるように、テキスト領域の初回マップ時(mmap)に Uprobe の有無を確認して Breakpoint 命令を設定する。

また Kprobes では int3 に対する例外ハンドラ中で追加する処理を実行していたが、Uprobes は notify_die で スレッドに TIF_UPROBE を設定するだけ。あとは例外ハンドラの処理が終わってユーザモードに戻すときに exit_to_usermode_loop から uprobe_notify_resume が呼ばれて handle_swbp などが実行される。…たぶん。まあユーザ空間の処理を実行するために割込み中の大事な時間を使う理由はないと思うのでたぶん合ってると思うたぶん。

blacklist

uprobe は blacklist がない。ユーザ空間ごときに拒否権はないのだ…

現在のプローブ

Kprobes の /sys/kernel/debug/kprobes/list のように、全ての Uprobes の一覧を表示するファイルはない。uprobe_events を利用したものだけでよければ、 /sys/kernel/debug/tracing/uprobe_events ファイルに記録されている。

最後に

Uprobes は Kprobes のユーザ空間版!以上!