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 のユーザ空間版!以上!