BPFによるトレーシングが簡単にできる「bpftrace」の使い方:Berkeley Packet Filter(BPF)入門(9)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、「bpftrace」によるトレーシングについて。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。前回はBPFによるトレーシングについて、おおまかな仕組みや概要を説明しました。今回は、BPFによる代表的なトレーシングツール「bpftrace」について、具体的にどのようなことができるのかを紹介します。また、bpftraceの注意点や分かりにくい点についてもまとめます。
bpftraceとは
bpftraceはBPFを利用したトレーシングツールの一つです。代表的なBPFのライブラリである「BCC(BPF Compiler Collection)」と同じく、現在は「iovisor」が管理しています。BCCと比較すると、bpftraceはトレース処理に特化し、より簡単に利用できるようになっているといえるでしょう。
bpftraceには下記のような特徴があります。
- トレーシング処理を専用の言語で記述
- BPFプログラム特有の細かい制約や注意事項を気にせずにトレースプログラムが書ける
- 主要トレーシング機能のサポート
- カーネルトレーシング
- tracepoints
- k(ret)probe
- k(ret)func
- perf event
- ユーザープログラムトレーシング
- u(ret)probe
- USDT(Userland Statically Defined Tracing)
- カーネルトレーシング
- カーネルの最新機能の対応
- BTF(BPF Type Format)によるカーネルデータ構造の取得
- BPF Trampolineによるトレーシング
BPFを用いて何かトレーシングしたい場合、一番お薦めのツールです。
bpftraceの入手
bpftraceをインストールする主要な方法を紹介します。前提として、一般にLinux 4.9以上のBPFが有効化されたカーネルが必要です。Ubuntu 18.04+、Fedora 31+、REHL(Red Hat Enterprise Linux) 8、REHL 7(BPF機能がバックポートされている)などで利用できます。もしbpftraceを試すために新規のOS環境を構築する場合は、BTFのサポートがあるFedora 32がお薦めです。
パッケージマネジャーからの入手
多くのパッケージマネジャーでbpftraceを取り扱っています。
sudo snap install bpftrace --devmode sudo snap connect bpftrace:system-trace
Ubuntu 20.04ではaptからもインストールできますが、snap版の方がパッケージの更新頻度が高いです。
sudo dnf install -y bpftrace
パッケージマネジャーに存在するbpftraceは、場合によってはバージョンが古いことがあります。もしバグと思われる挙動に遭遇した場合は、以下の方法で最新版を試してみてください。bpftraceは現在活発に開発されているので、バグが修正されている可能性があります。
GitHubのembeded buildを取得
GitHub Actionsのembedded buildから、最新のビルドを入手することが可能です。
Dockerイメージの利用
quay.ioにbpftraceのDockerイメージがあります。このDockerイメージには最新版のbpftraceが含まれています。このDockerイメージは、以下のようにカーネルソースやdebugfsを適切にコンテナにマウントした上でprivilegeなコンテナとして利用します。
$ docker run -ti -v /usr/src:/usr/src:ro \ -v /lib/modules/:/lib/modules:ro \ -v /sys/kernel/debug/:/sys/kernel/debug:rw \ --net=host --pid=host --privileged \ quay.io/iovisor/bpftrace:latest
コンテナ内の/usr/bin/bpftraceにbpftraceがあります。また/usr/local/bin/以下にbpftraceのスクリプトがインストールされています。
自分でビルドする
bpftraceの最新機能、特にBTFに関する機能を利用する場合は、自分でBCC、libbpfとbpftraceをビルドする必要があります。詳細については「INSTALL.md」を参照してください。
bpftrace入門
bpftraceの実行例を見てみましょう。
まず「bpftrace --info」でbpftraceがサポートしている機能および、カーネルがサポートしているBPFの機能の有無を確認できます。なお、このコマンドが実行できない場合、bpftraceのバージョンが古いです。また、bpftraceの実行にはroot権限(CAP_SYS_ADMIN)が必要です。
$ bpftrace --info System OS: Linux 5.7.0+ #2 SMP Mon Jun 1 12:50:01 JST 2020 Arch: x86_64 Build version: v0.10.0-163-gdc96 LLVM: 10 foreach_sym: yes unsafe uprobe: no bfd: yes bpf_attach_kfunc: yes Kernel helpers probe_read: yes probe_read_str: yes probe_read_user: yes probe_read_user_str: yes probe_read_kernel: yes probe_read_kernel_str: yes get_current_cgroup_id: yes send_signal: yes override_return: yes [...]
それではbpftraceを使ってみましょう。
$ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s\n"、str(args->filename)); }' Attaching 1 probe... /proc/15720/cmdline [...]
これは「openat(2)」システムコールが実行された際に、引数のファイル名を表示するbpftraceのスクリプトです。「tracepoint:syscalls:sys_enter_openat」がトレース対象のイベント名(「プローブ」といいます)、その後の{ }で囲まれた部分が、そのイベントが発生した際に実行するアクション(=BPFプログラム)です。
BCCでBPFプログラムを作成する場合は【1】BPFプログラム本体と、【2】トレーシング結果を表示するためのプログラムを別々に作成する必要がありました。bpftraceの場合は、この部分を統一的に記述できるようになっています。内部的にはbpftraceはスクリプト言語をLLVM IRを経由してBPFプログラムにコンパイルした後、それをカーネルにロードします。その後bpftraceは、BPFプログラムの出力を監視し、必要に応じて結果を表示します。
bpftraceがトレースできるプローブは「bpftrace -l」で表示できます。
$ bpftrace -l software:bpf-output: software:context-switches: software:cpu-clock: [...]
ただし、このままだと数が多過ぎるので、以下のように限定して検索するといいでしょう。
$ bpftrace -l 'kprobe:vfs_*' // vfs_からはじまるkprobeでトレース可能な関数を表示 $ bpftrace -l 'tracepoint:syscalls:*' // syscallのtracepointのみを表示
bpftraceは以下のプローブに対応しています。各種プローブの詳細は前回の記事を参照してください。
プローブタイプ | 概要 | BPFプログラムタイプ |
---|---|---|
tracepoints | tracepiont | `BPF_PROG_TYPE_TRACEPOINT` |
kprobe | カーネル関数トレーシング | `BPF_PROG_TYPE_KPROBE` |
kretprobe | 同上(returnのトレーシング) | `BPF_PROG_TYPE_KPROBE` |
uprobe | ユーザープログラムトレーシング | `BPF_PROG_TYPE_KPROBE` |
uretrobe | 同上(returnのトレーシング) | `BPF_PROG_TYPE_KPROBE` |
usdt | USDT | `BPF_PROG_TYPE_KPROBE` |
software | perf software event | `BPF_PROG_TYPE_PERF_EVENT` |
hardware | perf hardware event | `BPF_PROG_TYPE_PERF_EVENT` |
profile | 一定時間間隔でイベントが発生 | `BPF_PROG_TYPE_PERF_EVENT` |
interval | 一定時間間隔でイベントが発生 | `BPF_PROG_TYPE_PERF_EVENT` |
kfunc | BPF trampolineによるトレース | `BPF_PROG_TYPE_TRACING` |
kretfunc | 同上 (returnのトレーシング) | `BPF_PROG_TYPE_TRACING` |
BEGIN | プログラム開始時に実行される | |
END | プログラム終了時に実行される |
また、「-v」オプションを一緒に付けると、プローブの詳細な情報が得られます。tracepiontの場合はアクセス可能な引数の情報が得られます。
下記のbpftraceプログラムでは、「args->filename」でファイル名にアクセスしていました。
$ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s\n"、str(args->filename)); }' Attaching 1 probe... /proc/15720/cmdline [...]
この「filename」は下記で表示されているtracepointのデータに対応します。
$ bpftrace -lv 'tracepoint:syscalls:sys_enter_openat' tracepoint:syscalls:sys_enter_openat int __syscall_nr; int dfd; const char * filename; int flags; umode_t mode;
アクションでできること
先に紹介したbpftraceプログラムでは「printf()」を利用して、ファイル名を出力していました。代表的なアクションの中身としては、「printf()」による出力の他に、BPFマップを利用したイベントの計数があります。例えば、以下のプログラムは、bpftrtace実行から[Ctrl]+[C]キーで実行を終了するまでの「openat(2)」システムコールの発生回数を計数します。
$ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @ = count(); }' ^C @: 929
ここで、「@」がBPFマップを意味します。BPFマップはグローバルなデータ構造として利用できます。「count()」はマップに対する特別な関数で、そのプローブが呼ばれた関数をマップに保持します。結果として今回の場合は「openat(2)」が呼ばれた回数が記録されます。bpftraceはプログラム終了時に自動でマップの中身を表示します。
以下のようにすると、「oepnat(2)」の引数のファイル名が"/etc/passwd"であったもののみ計数します。
$ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { if (str(args->filename) == "/etc/passwd") { @ = count(); }}' @: 15
ファイル名別で集計することも可能です。
$ $bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[str(args->filename)] = count(); }' Attaching 1 probe... ^C @[/proc/16288/cmdline]: 1 [...]
このように、bpftraceではある条件にマッチしたイベントの集計や抽出を簡単に行えます。この際ユーザーはBPFについてはほとんど意識する必要はありません。一方でトレーシングプログラムはBPFプログラムにコンパイルされ、verifierで安全性が検証されたのちにカーネル内で動作します。結果として、低オーバーヘッドでトレーシング処理が実現できます。
kprobe/kretprobe
kprobeを利用すると、カーネル内の関数(「/proc/kallsyms」で表示されるほとんどの関数)をトレースすることができます。以下は「vfs_open()」関数が呼ばれた際に、その引数からファイル名を表示します。
$ bpftrace -e 'kprobe:vfs_open { $filename = str(((struct path *)arg0)->dentry->d_name.name); printf("%s\n"、$filename); }' --include linux/fs.h Attaching 1 probe... cmdline [...]
kprobeプローブの中では、「arg0」「arg1」……で関数の引数にアクセスできます。このとき、明示的に引数を自分でキャスト(今回の場合は「(struct path*)」)する必要があります。また、「struct path」の定義を取得するために「linux/fs.h」をインクルードしています。後述のkfuncを利用すると、この記述が不要になります。
kretprobeはカーネル関数がreturnする際にイベントが発生します。なお、kretprobeはその制約により、関数の引数に直接アクセスすることはできません。kretprobeから関数の引数にアクセスしたい場合は、事前にkprobeのアクションで適当なBPFマップに引数を保存する必要があります。
uprobe/uretprobe
uprobe/uretprobeを利用して、ユーザープログラムをトレースすることができます。例えば、以下の簡単なプログラムを考えます。
#include <stdio.h> int f(int a、int b){ return a + b; } int main(){ printf("%d\n"、f(1,2)); }
このプログラムをコンパイルした後、「-l」オプションでトレース可能な関数が取得できます。
$ bpftrace -lv "uprobe:./a.out" BTF: using data from /sys/kernel/btf/vmlinux uprobe:./a.out:__do_global_dtors_aux uprobe:./a.out:__libc_csu_fini uprobe:./a.out:__libc_csu_init uprobe:./a.out:_fini uprobe:./a.out:_init uprobe:./a.out:_start uprobe:./a.out:deregister_tm_clones uprobe:./a.out:f uprobe:./a.out:frame_dummy uprobe:./a.out:main uprobe:./a.out:register_tm_clones
以下のようにして関数「f」が呼ばれた際に、そのときの引数を出力できます。
$ bpftrace -e 'uprobe:./a.out:f { printf("%d、%d\n"、arg0、arg1); }' Attaching 1 probe... 1、2
uprobeは共有ライブラリの関数に対しても利用可能です。例えば下記は、「libc」の「nanosleep」関数を呼んだ回数をプロセス名ごとに集計します。
$ bpftrace -e 'u:/lib/x86_64-linux-gnu/libc.so.6:*nanosleep* { @[comm] = count(); }' Attaching 7 probes... ^C @[sleep]: 3
なお、uprobeではカーネルの機能の制約により浮動小数点レジスタにアクセスすることはできません。
USDT(Userland Statically Defined Tracing)
USDTはユーザープログラム内で明示的にプローブを定義する方法で、もともとは「DTrace」や「SystemTap」で利用されてきたものです。USDTを定義する方法は幾つかありますが、以下に「sys/sdt.h」を利用する例を示します。
#include <stdio.h> #include <sys/sdt.h> int f(int a、int b){ DTRACE_PROBE2(test、probe、a、b); return a + b; } int main(){ printf("%d\n"、f(1,2)); }
ここで「DTRACE_PROBE2(...)」がUSDTの定義です。なお、「DTRACE」という名前が付いていますが、これはもともとDTraceで使われていたデータ形式を利用しているからであって、bpftraceを実行する際に何かDTraceの機能を利用しているわけではありません。
このプログラムをコンパイルした後、「-l」オプションで以下のように定義したUSDTが確認できます。
$ bpftrace -lv "usdt:./a.out" BTF: using data from /sys/kernel/btf/vmlinux usdt:./a.out:test:probe
USDTの情報はELFの特定セクションに格納されています。これは「readelf」コマンドによって確認できます。
$ readelf -n ./a.out [...] Displaying notes found in: .note.stapsdt Owner Data size Description stapsdt 0x0000003b NT_STAPSDT (SystemTap probe descriptors) Provider: test Name: probe Location: 0x0000000000000654, Base: 0x0000000000000718, Semaphore: 0x0000000000000000 Arguments: -4@-4(%rbp) -4@-8(%rbp)
bpftraceからUSDTは以下のように利用します。「arg0」「arg1」……でUSDTに与えた引数にアクセスできます。
$ bpftrace -e 'usdt:./a.out:test:probe { printf("%d、%d\n"、arg0、arg1); }' Attaching 1 probe... 1、2
software/hardware
software/hardwareプローブを利用すると、指定したperf eventがある一定回数発生した際にアクションを実行することができます。
$ bpftrace -e 'software:faults:100 { @[comm] = count(); }' Attaching 1 probe... ^C @[git]: 1 @[tr]: 1 @[getent]: 1
profile
profileプローブを利用すると、CPUごとに一定間隔でイベントが発生します。これは主に「kstack」「ustack」といったスタックトレースを取得する機能と合わせてプロファイリングに利用されます。以下のプログラムは、99hzでカーネルスタックを取得し、そのスタックトレースが出現した回数を記録します。
$ bpftrace -e 'profile:hz:99 { @[kstack] = count(); }' Attaching 1 probe... ^C @[ native_safe_halt+23 __sched_text_end+38 arch_cpu_idle+21 default_idle_call+35 do_idle+462 cpu_startup_entry+29 rest_init+174 arch_call_rest_init+14 start_kernel+1435 x86_64_start_reservations+36 x86_64_start_kernel+116 secondary_startup_64+164 ]: 98 [...]
interval
intervalは一定間隔でイベントが発生します。profileと似ていますが、イベントが発生するのは1つのCPUのみです。主に以下のように他のプローブと組み合わせることを目的に利用されます。
// 1秒後にbpftraceの実行を終了 i:1:s { printf("count = %d\n," @); // 1秒後にbpftraceの実行を終了 i:1:s { exit(); }
kfunc/kretfunc
kfunc/kretfuncはLinux 5.5から導入された、BTFを利用したトレース方法です。使い方はkprobeとほぼ同様ですが、BTFを利用することにより、kprobeと違ってカーネル内関数の引数の型をBPFプログラムが自動で認識してくれます。上記のkprobeの処理をkfuncで記述すると、下記のようになります。kprobeと違い、明示的に引数をキャストする必要がありません。
$ bpftrace -e 'kfunc:vfs_open { $filename = str(args->path->dentry->d_name.name); printf("%s\n"、$filename); }' Attaching 1 probe... cmdline
BTFについては詳しくは次回説明します。
bpftraceをより学ぶために
より詳しくbpftraceを学ぶには、まず公式のチュートリアルを参照することをお勧めします。拙訳の日本語版もあるので、参考にしてください。
また、公式リポジトリの「tools」にbpftraceのスクリプトが多数含まれています。実用的なトレーシングをする場合はまずここのツールを基に、必要に応じて改良を加えていくのがいいと思います。より詳しい言語の機能に関してはレファレンスガイドを参照してください。
パフォーマンス測定に関する詳しい説明や「どんなときにどのイベントをトレースすべきか」に関しては、下記の文献が参考になります。特に、「BPF Performance Tools」にはbpftraceを用いたトレースツールの例が多数含まれています。
- Brendan Gregg、BPF Performance Tools、Addision-Wesley Professional、2019.
- Brendan Gregg、Systems Performance: Enterprise and the Cloud、Prentice Hall、2013.
- 翻訳版(西脇 靖紘(監修)、長尾 高弘(翻訳)、詳解システムパフォーマンス、オライリージャパン刊、2017)
bpftraceの注意点・分かりにくい点
最後に、bpftraceの分かりにくい点やはまりやすい点をまとめます。トラブルシュートの参考にしてください。ここで参照しているbpftraceはバージョン0.10です。なおbpftraceは、まだバージョン1.0になっておらず、ここに書いてあることが今後変わる可能性が十分あります。
文法
bpftraceはC言語ライクな文法ですが、あくまで独自の文法を持ち、構文解析も自前のものを利用しています。これは構文解析にClangを利用するBCCと異なります。従って、BCCのようにC言語の文法が使えると思ってプログラムを作成すると往々にしてエラーになります。
なお、bpftraceは構造体の解析処理にClangを利用しています。しかし、あくまでメインの構文解析はflex/bisonで処理しています。
マップ関数の処理
bpftraceのマップ関数(count()、hist()、lhist()、min()、max()、sum()、avg()、stats())は、対象となるマップの値を更新します。例えば下記のsum()関数は、
@ = sum($value);
内部的には、次のような命令列に変換されます。
$sum = @; // bpf_map_lookup_elem() $sum += $value; @ = $sum; // bpf_map_update_elem()
非同期関数
bpftraceの関数のうち、printf()、print()、time()、clear()、zero()、system()、join()、cat()、exit()は非同期関数です。これらの処理はBPFプログラム内だけでは完結しません。bpftraceの非同期関数は下記のように動作します。
- BPFプログラム内で非同期関数(例えばprintf())を実行(「bpf_perf_event_output()」関数を利用して、perf bufferにその関数が実行された旨を示すメッセージを出力)
- ユーザースペースで動作するbpftraceのランタイムがperf bufferのデータを読み取り
- 読み取ったデータに応じて非同期関数の処理を実行
従って、非同期関数の実行にはラグがあります。多くの場合これは問題にはなりませんが、意図しない動作を引き起こす可能性があります。例えば、以下のプログラムを考えます。
{ @ = 1; zero(@); printf("%d\n"、@); }
「zero()」はマップのエントリを「0」にする関数ですが、このプログラムは「1」を出力します。なぜなら「zero(@)」は非同期関数であり、printf()を実行した時点(printf()も非同期関数ですが、BPFプログラムはprintする引数をperf bufferに出力します)ではzero()の処理は完了していないためです。この例の場合ではzero()ではなく直接0を代入する(@ = 0。この場合bpf_map_update_elem())で問題は解決できます。
ただし、ここでは一要素しか扱っていませんが、クリア処理をマップの要素ごとに書く必要があります。
なお、delete()は非同期関数ではありません(bpf_map_delete_elem()を実行します)。
文字列の取り扱い
「printf(%s)」の引数や、マップのキーや値として文字列を利用したい場合があります。bpftraceで文字列を構成する方法は下記の3つです。
- 文字列リテラル(例えば、"abc")
- str()関数を利用する
- char []の構造体のフィールド値
ここで注意すべき点は、ポインタはstr()関数を利用して明示的に文字列に変換する必要がある一方で、char []の構造体のフィールドに関してはアクセス時に自動で文字列に変換されるということです。
具体例を挙げると、「do_sys_open()」関数の第2引数は「const char __user *filename」で、これはポインタなので、ファイル名の文字列を得るにはstr()を利用します。一方で、struct task_structのcommはchar[16]なので、単純に「curtask->comm」とアクセスするだけで文字列が得られます(curtaskはカーネルのcurrent変数(現在のタスクのtask struct)を保持するビルトイン変数です)。str()を利用する必要はありません(なお、task structのcommフィールドには専用のビルトイン変数commで直接アクセスできます)。
文字列はbpftraceのスタック上に保存されます。内部的には文字列は固定長で扱われており、その長さはデフォルトで64です。この値はBPFTRACE_STRLENで変更できます。BPFプログラムのスタックは512バイトしかないため、多くの文字列を扱う場合はスタックオーバーフローでエラーになることがあります。そのような場合はこのBPFTRACE_STRLENの値の変更を試してください。文字列を固定長で扱う理由は、BPFマップのキーや値のサイズが固定であるためです。
なお文字列に関してはインデックスでアクセスできない、長い文字列を短くできないなどの問題があり、処理方法の変更が議論されています。
ファイルのインクルード
「#include」を利用してCのヘッダをインクルードすることができます。これは構造体の解析に利用します。インクルードにより定義された構造体は後述のキャストで利用できます。なお、ヘッダをインクルードしたからといって、そのヘッダ内で定義されているインライン関数が使用できるようにはなりません。
enum値
enumが定義されているファイルをインクルードすると、プログラムの中でそのenum値を利用することができます。
キャスト
bpftraceプログラムでは、整数値を構造体へのポインタへキャスト可能です。
$a = (struct file*)$value;
また、整数値のキャストも可能です。
$a = (uint16)$value;
整数型として利用できるのはint8、int16、int32、int64、uint8、uint16、uint32、uint64です。
それ以外の型へのキャストはサポートされていません。
ポインタ演算
bpftraceにはC言語的なポインタ演算の概念はありません。ポインタ値はただの整数値として扱われます。ポインタ演算をする場合は、以下のようにポインタサイズ分だけオフセットを乗算する必要があります。
$a = $ptr+$index*8;
profileとintervalの違い
profileとintervalは共にperfのsoftware eventであるCPUを利用しますが、profileが全てCPUに対してイベントを有効にする一方で、intervalはCPU0に対してのみイベントを有効化します。
BEGIN/ENDイベント発生のタイミング
bpftraceでは以下のような流れで処理が進みます。
0.bpftraceプログラムのコンパイル
1.BEGINプローブの実行
2.BPFプログラムのアタッチ
3.処理の開始。[Ctrl]+[C]キーや「exit()」で終了
4.ENDプローブの実行
5.perf buffer処理(残った非同期関数の実行)
6.BPFプログラムのデタッチ
7.マップの値の出力
外部からのマップの操作
現在実行中のbpftraceプログラムが利用しているマップを操作したいことがあります。bpftoolを利用するとコマンドラインからマップ内容の確認や更新ができます。
なお、bpftraceプログラムから、bpftrace実行以前から存在するマップを参照する方法は、現在は存在しません。bpftraceが利用するマップはbpftraceプログラムコンパイル時に作成され、bpftrace終了時に削除されます。
その他
前述の通り、bpftraceは現在バージョン1.0に向けて活発に開発中です。もしバグのような挙動に遭遇した場合、最新版を試してみるとそれが修正されている可能性があります。最新版でもバグが存在している場合は、ぜひGitHubのissueに報告してみてください。
次回は、BTFを用いたトレーシング手法について
今回はBPFを用いた代表的なトレーシングツールであるbpftraceの使い方と、その仕組みについて説明しました。次回は、BPFによるトレーシングを支援するために導入された新機能BTFと、その応用例を紹介します。
筆者紹介
味曽野 雅史(みその まさのり)
東京大学 大学院 情報理工学系研究科 博士課程
オペレーティングシステムや仮想化技術の研究に従事。
- メール:misono(at)os.ecc.u-tokyo.ac.jp
- ブログ:http://mmi.hatenablog.com/
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 2017年版Linuxカーネル開発レポート公開――支援している企業トップ10とは?
The Linux Foundationは2017年版Linuxカーネル開発レポートを公開した。Linuxカーネル4.8から4.13までの開発に焦点を当て、カーネル開発に携わった開発者や変更数などについて言及した。 - Linuxカーネルのソースコードを読んで、システムコールを探る
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。 - SystemTapで真犯人を捕まえろ!