AbemaTV Advent Calendar の10日の記事です。 最近作っているツールの話をしようかと思ってましたがちょっと開発が間に合わなかったので、iOSチームの同期から教えてほしいと言われたMach APIについて書く。
あまりMach APIに関する資料は日本語・英語ともに多くないので、いざ使おうとするとドキュメントの情報が足りず苦労する。必要に応じてカーネルソースを読んだほうが早いことも多くあるため、この記事ではCPUレジスタ値の取り出しをベースに、カーネルソースを読む上で頭に入れておきたいMach APIのいくつかの概念についても解説する。
レジスタ値の取得 (Linuxの場合)
まずMacの話をする前にLinuxではどうしているかについて簡単に紹介しておく。
CPUレジスタにアクセスするとなると、Linuxのシステムでは ptrace()
というシステムコールが利用される。
こちらのシステムコールは、特定のプロセスにアタッチしてメモリやCPUレジスタの中身をのぞいたり、書き換えたりすることができるため、 strace のようなシステムコールトレーサーや GDB のようなデバッガで利用されている。
最近だとGopherCon 2017のトークでもシステムコールトレーサーの実装を通したptraceの解説があったり、はてブのホッテントリにも度々ptraceに関する記事が上がっていたので既にご存知の方も少なくないかもしれない。
GopherCon 2017: Liz Rice - A Go Programmer's Guide to Syscalls
日本語でもいくつか記事が見つかる。
ptraceについては英語・日本語ともに詳しい解説が既にありますが、これらのプログラムをMacで動かそうと思うと少し苦労する。
Darwinの提供している ptraceは、特定のプロセスにアタッチして処理を停止・再開を制御することはできますが、CPUレジスタの中身を覗いたり書き換えるための機能(PTRACE_GETREGS
や PTRACE_SETREGS
)は存在しない。
Mach APIとレジスタ値の取得
MacでもGDBとかを使えばレジスタの値は見れる。なのでもちろん PTRACE_GETREGS
や PTRACE_SETREGS
の代わりになる機能が存在するはず。
ネットで検索してみると、 thread_get_state
というAPIが検索でヒットした。
- http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_get_state.html
- Unixjunkie Blog: Darwin, ptrace(), and registers
これらは Mach カーネル と呼ばれるカーネル基盤が提供していて、記事の中ではMach APIと呼ばれています。 Mach Kernelは マイクロカーネル として設計された (MachのGeneral Designや実装に関する話は、 Mach Overview に詳しくまとまっている)。 Uninformed - vol 4 article 3 によると、macOSで使用されているXNU(Appleが開発したOSカーネルでDarwinの一部として公開されている)は、Mach KernelとともにBSDのコードを含んだ ハイブリッドカーネル と呼ばれるもの。しかしXNUのようにBSDやMachを一緒に利用するハイブリッドカーネルでは、セキュリティポリシーの扱いが面倒になるらしい。 そこでMachは少し特殊なしくみでその問題を解決している。そのしくみについて勉強するうえでいくつか頭に入れて置かなければならない用語がある。
- タスク(Tasks): リソース所有権の単位。いわゆるプロセスに近い。macOSのプロセスやPOSIXスレッド(pthreads)はMachのtaskと次の行で紹介するthreadの上で実装されているようだ。
- スレッド(Threads): プロセス内のPCU実行単位。
- メッセージ(Msgs): スレッド間の通信を提供するためにMachで使用されます。 メッセージは、データオブジェクトの集合で構成されています。 メッセージが作成されると、そのメッセージは、起動タスクが適切なポート権を持つポートに送信されます。 ポート権はタスク間でメッセージとして送信できます。 メッセージは宛先にキューイングされ、受信スレッドの自由度で処理されます。 Mac OS X では
mach_msg()
関数を使用してポートとの間でメッセージを送受信する - ポート(Ports): カーネル制御通信チャネル。スレッド間でのメッセージのやりとりに使用する。ポート権限(Port rights)と呼ばれる権限をもつスレッドだけがそのポートにメッセージを送信できる。
- ポートセット(Port Set): 名前の通りポートのコレクション。あるポートセットに所属するポートは全て同じメッセージキューを使用する。
Machのコンセプトとしては タスク(task) の起動や停止、タスクアドレス空間の操作等を行う際に、 ポート(port) に対して メッセージ(messages) を送信する。こうすることで、BSDのセキュリティ機能の影響を受けないようにしたらしい。
このことを頭に入れた上で、 thread_get_state
について調べていこう。
thread_get_state
の使い方を調べる
さてtaskやportといった概念を把握したところで、実際に thread_get_state
を使ってみる。
今回の目的は、冒頭に紹介したLinuxで動作するシステムコールトレーサー(wtrace.c) と同様にプログラムカウンタの値を取り出すことにある。
ちなみに最初に紹介した wtrace.c
はx86の32bit CPUを想定したプログラムであったためプログラムカウンタは EIPレジスタ となっていた。
手元のMacbook Proは64bit CPUを積んでいて、プログラムカウンタは RIPレジスタ となる。
kern_return_t thread_get_state (thread_act_t target_thread, thread_state_flavor_t flavor, thread_state_t old_state, mach_msg_type_number_t old_state_count);
http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_get_state.html
ドキュメントの解説によると、target_thread
引数で指定した特定のスレッドの実行状態(CPUレジスタなど)を取得することができるらしい。また第2引数の flavor
で取得したい情報を指定するようだ。この説明からflavor の値をどれにするかによって kern_return_t
型の変数のどこかから目的のレジスタを取り出すことができそうだ。
しかし困ったことにドキュメントにはそれ以上の、説明が見当たらないのでDarwinの処理を追ってみる。
Darwin(XNU)カーネルのソースコードは macOS 10.12.6 - Source から閲覧できる。 今回はGithubでもMirrorとして GitHub - apple/darwin-xnu: The Darwin Kernel (mirror) が公開されたのでそちらをcloneしてきた。
それではRIPレジスタの中身を取得するために、flavorとして何を指定すれば取れそうか調べてみる。
cloneしたらまずは thread_get_state
というキーワードを頼りにgrepをかけてみる
$ git clone git@github.com:apple/darwin-xnu.git $ cd darwin-xnu $ find . -name "*.c" | xargs grep -n "thread_get_state" ./osfmk/arm/status.c:79:machine_thread_get_state( ./osfmk/arm64/status.c:255:machine_thread_get_state( ./osfmk/chud/i386/chud_thread_i386.c:53:chudxnu_thread_get_state( ./osfmk/i386/pcb.c:1063:machine_thread_get_state( ./osfmk/kern/thread_act.c:456:thread_get_state( ...
引っかかった行を見ていくと次のコードが見つかった。
kern_return_t thread_get_state( ... result = machine_thread_get_state(thread, flavor, state, state_count);
machine_thread_get_state
に渡しているため、もう少しほってみる。
$ git grep -n "machine_thread_get_state" *.c osfmk/arm/status.c:79:machine_thread_get_state( osfmk/arm64/status.c:255:machine_thread_get_state( osfmk/i386/pcb.c:1063:machine_thread_get_state( ...
ARMではなさそうなので、 osfmk/i386/pcb.c
が怪しそうだ。
中を見ると説明にあったとおり switch(flavor)
とflavorの値に応じて何か処理が分岐している。
kern_return_t
machine_thread_get_state(
thread_t thr_act,
thread_flavor_t flavor,
thread_state_t tstate,
mach_msg_type_number_t *count)
{
switch (flavor) {
...
}
}
ここでRIPを取る方法を調べるためにファイル内検索を書けてみるといくつか見つかった。
更に読んでいると get_thread_state32
関数のなかで、EIPにアクセスしていることが見て取れる。
static void get_thread_state32(thread_t thread, x86_thread_state32_t *ts) { x86_saved_state32_t *saved_state; ... ts->eip = saved_state->eip; ... }
machine_thread_get_state
では次の行で get_thread_state32
を
関数名からここが怪しそうだ。
static void get_thread_state64(thread_t thread, x86_thread_state64_t *ts) { ... ts->rip = saved_state->isf.rip;
case x86_THREAD_STATE: { x86_thread_state_t *state; ... if (thread_is_64bit(thr_act)) { ... get_thread_state64(thr_act, &state->uts.ts64);
case x86_THREAD_STATE64: {
x86_thread_state64_t *state;
x86_saved_state64_t *saved_state;
...
state->rip = saved_state->isf.rip;
...
これらのコードから少なくとも x86_THREAD_STATE
もしくは x86_THREAD_STATE64
のどちらかをflavor引数で指定すればRIPレジスタの値がとれそうだ。
ソースコード
ソースコード全体はこちら。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <mach/mach.h> #include <assert.h> #include <mach/mach_types.h> int main(int argc, char *argv[], char *envp[]) { pid_t pid = fork(); if (pid == 0) { sleep(4); return KERN_SUCCESS; } kern_return_t err; mach_port_t task; err = task_for_pid(mach_task_self(), pid, &task); if (err != KERN_SUCCESS) { fprintf(stderr, "task_for_pid() failed\n"); exit(EXIT_FAILURE); } err = task_suspend(task); if (err != KERN_SUCCESS) { fprintf(stderr, "task_suspend() failed\n"); exit(EXIT_FAILURE); } thread_act_array_t threads = NULL; mach_msg_type_number_t threadCount; err = task_threads(task, &threads, &threadCount); if (err != KERN_SUCCESS) { fprintf(stderr, "task_threads() failed\n"); exit(EXIT_FAILURE); } assert(threadCount > 0); x86_thread_state_t state; mach_msg_type_number_t count = x86_THREAD_STATE_COUNT; err = thread_get_state(threads[0], x86_THREAD_STATE, (thread_state_t)&state, &count); if (err != KERN_SUCCESS) { fprintf(stderr, "thread_get_state() failed\n"); exit(EXIT_FAILURE); } printf("RIP = %llx\n", state.uts.ts64.__rip); printf("RAX = %llx\n", state.uts.ts64.__rax); printf("RCX = %llx\n", state.uts.ts64.__rcx); printf("RDX = %llx\n", state.uts.ts64.__rdx); printf("RBP = %llx\n", state.uts.ts64.__rbp); printf("RSI = %llx\n", state.uts.ts64.__rsi); printf("RDI = %llx\n", state.uts.ts64.__rdi); printf("R8 = %llx\n", state.uts.ts64.__r8); printf("R9 = %llx\n", state.uts.ts64.__r9); err = task_resume(task); if (err != KERN_SUCCESS) { fprintf(stderr, "task_resume() failed\n"); exit(EXIT_FAILURE); } mach_port_deallocate(mach_task_self(), task); exit(EXIT_SUCCESS); }
実行結果は次のとおり。
$ gcc print-rip.c -o print-rip -g -O0 -Wall $ sudo ./print-rip RIP = 7fffc6fe736f RAX = 0 RCX = c RDX = ffffffffffffffff RBP = 7fff59ae2a30 RSI = 7fffc706d070 RDI = 0 R8 = 1c R9 = a0
とれた 🎉
おわりに
オブジェクトファイルのバイナリフォーマットもELFではなくMach-Oというものだったり、標準CライブラリもlibSystem.d.dyldという動的ローダーの中にあったり、デバッグシンボルもdSYMの中に生成されたりstatic linkもできなかったり、Linuxの環境で勉強したものとは結構違っていて、はまったときにgdbデバッグするのも一苦労でした。 今回は記事も少し長くなったので、そのあたりの話はもう少し調べて整理してから記事にしようかと思います。