ftraceの仕組みの軽い調査
概要
Linux Kernel には version 4.0から Kernel Live Patchという仕組みが存在し、
この仕組みにより再起動をせずともKernelのupdateが可能になる。
そして、Kernel Live Patchはftraceと呼ばれる技術により実現されている。
ftraceはその名の通り、カーネルの関数のトレースを行う技術でありカーネル関数呼び出しに関する情報を取得することが可能である。
ここでは、Kernel Live Patchを理解するための1歩目として、ftraceがどのように動作するのかを仄かに理解することを目指す。
ftraceを支える技術
このftraceのトレーシングは、GCCのプロファイリング機能に支えられている。
GCCのプロファイリング機能は簡単に利用することができ、以下のようにコンパイル時に -pg
オプションを付けるだけで利用できる。
$ ~/Sources/mcount-test$ gcc -o profile_target_binary -pg main.c
-pg
をつけることによりバイナリにどのような変化が現れるかを以下で示す。
まずは、コンパイル対象のCソースファイルである。
#include<stdio.h>
int my_func_1(int i) {
return i + 1;
}
int my_func_2(int i) {
return i + 2;
}
int main(void) {
int j = 0;
for(int i=0; i < 100000; i++){
j = my_func_1(j);
}
for(int i=0; i < 1000000; i++){
j = my_func_2(j);
}
return 0;
}
このCソースファイルを-pg
付きでアセンブリへコンパイルしたものを profile_enabled.s
に、そうでなく普通にアセンブリにコンパイルしたものをdefault.s
にそれぞれ出力した。
この2つのファイルのdiffを取ったものが以下である。
*** default.s 2022-07-21 00:44:02.792136058 +0900
--- profile_enabled.s 2022-07-21 00:43:12.669131352 +0900
***************
*** 11,20 ****
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl $1, %eax
! popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
--- 11,22 ----
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
+ subq $8, %rsp
+ 1: call *mcount@GOTPCREL(%rip)
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl $1, %eax
! leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
***************
*** 31,40 ****
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl $2, %eax
! popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
--- 33,44 ----
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
+ subq $8, %rsp
+ 1: call *mcount@GOTPCREL(%rip)
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl $2, %eax
! leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
***************
*** 52,57 ****
--- 56,62 ----
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
+ 1: call *mcount@GOTPCREL(%rip)
movl $0, -12(%rbp)
movl $0, -8(%rbp)
jmp .L6
これを見ると-pg
付きはどうやら各関数(全行を載せれていないため申し訳ないが、この各関数とはmain, my_func_1, my_func_2のことである)が呼ばれた際にその関数のメインの処理を実行する前段階で*mcount@GOTPCREL(%rip)
が追加されてcallされているようである。
筆者は現時点で *
とGOTPCREL
の意味はわからないが、どうやらglibcのmcount関数が呼ばれる挙動になるらしい。
順番にざっくり見るなら以下の順番に読めば内部実装にそれっぽく行き着く。
- https://elixir.bootlin.com/glibc/glibc-2.25/source/sysdeps/x86_64/sysdep.h#L77
- https://elixir.bootlin.com/glibc/glibc-2.25/source/sysdeps/x86_64/_mcount.S#L27
- https://elixir.bootlin.com/glibc/glibc-2.25/source/sysdeps/x86_64/machine-gmon.h#L32
- https://elixir.bootlin.com/glibc/glibc-2.25/source/gmon/mcount.c#L59
ここでは、これ以上GCCのプロファイリング機能に深入しないが、この機能を使うことによってある関数のメイン処理以前にある処理(ここでは、mcount関数によりプロファイリング処理)を仕込むことが可能なことがわかる。
ftraceを追う
前述したようにftraceはGCCのプロファイリング機能を利用したものである。
というより、プロファイリング機能をカーネルの関数に適応させたものである。
ここで2つの問題が想像できる。
- カーネルはglibcを使えない
- カーネル関数全てにこれが適応されると重そう
1の問題は、ユーザ空間のプログラムならglibcによるmcount関数が呼び出されることによりプロファイリングが可能であるが、カーネルはglibcを利用できないため、これはできない。そのため、カーネルはmcount関数を実装する必要があり、これはおそらくここの実装であると思われる
2の問題は、sysctlを利用することによるON/OFFやデフォルトではnop命令をmcount関数で呼び出すことにより解決されているらしい。
以降は今後調査を行う(寝る
参考文献
調べる環境を作る
ftraceを追うための環境を作りたくなったので作った。
大体のやり方は
に沿うが、以下のような追加手順を行った- Kernelの準備
- Busyboxの準備
- ftraceのデモをこの環境用に改変する
Kernelの準備
5.4.207
を利用した。
config は 以下のgistのものを利用する。
ftraceのデモを動かすためにKernel Moduleもビルドする
make modules -j `nproc`
Busyboxの準備
busybox-1.35.0
を利用した。
make install
後に _install
ディレクトリにftraceのデモのリポジトリcloneし、makefileを改変しビルドしておく。
cd _install
git clone https://github.com/ilammy/ftrace-hook.git
# 以下のように変更する
KERNEL_PATH ?= <Linux Kernelのソースディレクトリ>
make
これをしておいて、root.imgにcpioを用いて固める。
投稿時点のupstreamのtweetはこれ
ftraceによる書き換えを実際に見てみる
動作環境ができたので、次にftraceの動きを動かしながら見てみる。
ftrace-hook は execveシステムコールとcloneシステムコールをhookする。
そのため、今回はexecveシステムコールに着目して観察する。
ソースのSystem.mapからexecveシステムコールのアドレスを取得する
おおよそ以下のような感じのものが見つかるはずだ。
ffffffff8113b160 T __x64_sys_execve
execveシステムコールのアドレスがわかったので、動作させているQEMUでこのアドレスを見てみる。
観察する時間としては、以下の3つで観察を行う。
- ftraceの初期化前
- ftraceの初期化後
- ftrace-hookをロード後
具体的に示すと、ftraceの初期化は以下で行われるため、この関数がよばれる前後が1と2に対応する。ftrace_hook.ko
をinsmodした後である。
観察結果
まずは、ftrace初期化前にexecveシステムコールのアドレスを表示したものである。
(gdb) x/gi 0xffffffff8113b160
0xffffffff8113b160 <__x64_sys_execve>: callq 0xffffffff81401430 <__fentry__>
この時点では、 __fentry__
を callqしている。
次に、ftrace初期化直後である
(gdb) x/gi 0xffffffff8113b160
0xffffffff8113b160 <__x64_sys_execve>: data16 data16 data16 xchg %ax,%ax
先ほど、callq __fentry__
だった部分が書き換えられていることが見える
最後にftrace-hookロード後である。
(gdb) x/gi 0xffffffff8113b160
0xffffffff8113b160 <__x64_sys_execve>: callq 0xffffffffc0009000
ロード後は 後ろの方のアドレスをcallqするように変わった。
観察の解釈
まずは、__fentry__
についてであるが、これは最初に述べた「GCCのプロファイリング機能」の mcount
とほぼ同等のものである。(何が違うのかはいまいちわかっていない)
次に、ftrace初期化直後の該当部分の命令列である data16 data16 data16 xchg %ax,%ax
であるが、これはおそらくNOP命令と同等である。
GDB上ではオペランドが ax
になっているのが気になるが...。
最後に、ftrace-hook ロード後に呼び出されようとしてるアドレスだが、これはftrace-hookが配置されている場所に近い。
/ftrace-hook # cat /proc/modules
ftrace_hook 16384 0 - Live 0xffffffffc0000000 (O)
次に、ftrace-hookが配置されている周辺について述べる。