c-bata web

@c_bata_ のメモ。python多め

Mach APIとCPUレジスタ値の取得について

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_GETREGSPTRACE_SETREGS)は存在しない。

Mach APIレジスタ値の取得

MacでもGDBとかを使えばレジスタの値は見れる。なのでもちろん PTRACE_GETREGSPTRACE_SETREGS の代わりになる機能が存在するはず。

f:id:nwpct1:20171211022715p:plain

ネットで検索してみると、 thread_get_state というAPIが検索でヒットした。

これらは Mach カーネル と呼ばれるカーネル基盤が提供していて、記事の中ではMach APIと呼ばれています。 Mach Kernelは マイクロカーネル として設計された (MachのGeneral Designや実装に関する話は、 Mach Overview に詳しくまとまっている)。 Uninformed - vol 4 article 3 によると、macOSで使用されているXNU(Appleが開発したOSカーネルDarwinの一部として公開されている)は、Mach KernelとともにBSDのコードを含んだ ハイブリッドカーネル と呼ばれるもの。しかしXNUのようにBSDMachを一緒に利用するハイブリッドカーネルでは、セキュリティポリシーの扱いが面倒になるらしい。 そこで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 を使ってプログラムカウンタの値をとってみる。 ちなみにx86x86_64のプログラムカウンタは、 Instruction Pointer と呼ばれていてx86_64では RIP レジスタがそれに相当するので、ソースコードを調べる際には 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してきた。

thread_get_state をみつける

cloneしたらまずは thread_get_state の処理を探してみる。

$ 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);

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/kern/thread_act.c#L455-L503

machine_thread_get_state に渡しているため、もう少しほってみる。

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)  {
        ...
    }
}

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/i386/pcb.c#L1056-L1479

x86_THREAD_STATE64 からの取得

ここでRIPを取る方法を調べるためにファイル内検索を書けてみるといくつか見つかった。まず1つは x86_THREAD_STATE64 を渡したときに RIPレジスタの値を取り出している。

     case x86_THREAD_STATE64: {
        x86_thread_state64_t    *state;
        x86_saved_state64_t *saved_state;
        ...
        state->rip = saved_state->isf.rip;
        ...

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/i386/pcb.c#L1533-L1572

x86_THREAD_STATE からの取得

更にgrepで引っかかったところを読んでいると get_thread_state64 関数のなかでも、EIPにアクセスしていることが見て取れる。 machine_thread_get_state では次の行で get_thread_state64 を呼び出していることから、この処理が怪しそう。

static void
get_thread_state64(thread_t thread, x86_thread_state64_t *ts)
{
    ...
    ts->rip = saved_state->isf.rip;

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/i386/pcb.c#L694-L724

     case x86_THREAD_STATE:
        {
        x86_thread_state_t  *state;
        ...
        if (thread_is_64bit(thr_act)) {
            ...
                get_thread_state64(thr_act, &state->uts.ts64);

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/i386/pcb.c#L1329-L1354

これらのコードから少なくとも x86_THREAD_STATE もしくは x86_THREAD_STATE64 のどちらかをflavor引数で指定すればRIPレジスタの値がとれそうだ。

ソースコード

flavorで指定する値はわかったので、早速実装してみる。 forkして生成した子プロセスのpidからthreadの一覧を取得し、 get_thread_statex86_THREAD_STATE を指定してレジスタ値を取得すればいい。 ソースコード全体はこちら。

#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 libraryも作れなかったり(static link binaryやstatic archive libraryというものは作れるみたいです)、Linuxの環境で勉強したものとは結構違っていて、はまったときにgdbデバッグするのも一苦労でした。 今回は記事も少し長くなったので、そのあたりの話はもう少し調べて整理してから記事にしようかと思います。

参考資料

PyCon APAC 2017 参加レポート

今年で3回目のPyCon APAC参加になりました。 前回まではgihyo.jpさんの方で連載していたのですが、今回はpyladiesの方々が書いてくれるみたいなので楽しみ。

詳しいレポートはgihyo.jpの方にあがると思うので、LTの話とマレーシア(クアラルンプール)について知ったことをメモ程度にまとめようと思います。

物価は安いし、大体どこに行っても英語も通じるし、治安もそれほど悪くなくていい街でした。

PyCon APAC/Malaysia

トークセッションのプロポーザル (Rejected)

Talk: Develop a Web Application Framework from the Ground Up.

While developing web application in python, most of pythonista uses WSGI Framework like Django, Flask. However, there are not many people who understand the implementation of these frameworks very much. In this talk, I will explain the specification of WSGI and the components of wsgi framework such as Router, Request object, Response object, and so on. The full source code that I will explain is here: https://github.com/c-bata/webframework-in-python/blob/master/src/app.py

トークセッションのプロポーザルを出してました。 内容は昨年のPyCon JPで話した、WSGIフレームワークの作り方。 残念ながらRejectされました。

基礎から学ぶWebアプリケーション・フレームワークの作り方 at PyConJP 2016

採択されたトークのほとんどはデータ系でしたが、PyCon KoreaのKwon-hanさんがいうには、Koreaでも同じ状況らしい。ここはPyCon JPもそうですね。

データ系の盛り上がりに乗っかって、Python x Webも少し盛り上げていけるといいな。

日本からは、関根さん、野中さん、赤池さんの3人のプロポーザルが採択されてました 👏。 赤池さんは初のPyConだったらしい、おつかれさまでした。

Lightning Talk

f:id:nwpct1:20170829211840j:plain

事前にスケジュールを見てLTが見当たらなかったので、無いのかなと思っていたんですが、当日行ってみたら普通に募集していた。カンファレンス2日目の朝、ぶっちゃけ寝坊したんですが、朝食あと回しにして会場に寄ったらLT枠に空きがあったので応募。

2日目のセッションは台湾のTzu-ping Chungさん(Macdown開発者。PyCon JP 2016でもasync/awaitの話をしてました)のトークだけ聞いて、その後はスタバでLTの資料作りしてました。

Tzu-ping Chungさんのトークは、自分にとって特に新しい話は少なかったかなとも思ったのですが、うまくまとまっていて話も面白かった。自分とそれほど歳も変わらないと思うんですが、自分よりもずっとコードを書いているんだろうなと、ふと思ってしまった。負けないように頑張ろう。。。

発表スライドはこちら

今回のカンファレンスでは特に動画撮影がなかったのでYoutubeとかにもあがらない。あとで見返すように撮っておけばよかった。

マレーシア / クアラルンプールについて

今回が初のマレーシアでした。

基本情報

f:id:nwpct1:20170825091014j:plain

  • 公用語に英語が含まれていて、大体どこにいっても英語が通じるので楽だった。
    • 教育を受ける時に、英語とマレーシア語を選ぶ感じらしい。マレーシア語を話せない人もそれなりにいるという話を聞いた。
  • 通貨はMR(マレーシア・リンギット)で、1MR=30円くらい。
    • 空港よりも市街地の方がレートがいいので、次行くときは食事、ホテルまでの移動費、SIM代ぐらいだけ空港でExchangeしておけばよさそう。
  • 物価は1/3くらい。写真は空港で食べたご飯も12MR(=360円)だった。
    • 都市部の企業でも初任給8万円とかだったりするらしい。
    • 酒税が高いらしく、お酒の値段は日本とあまり変わらない。
    • ムスリムの人がお酒飲まないからか、メニューにアルコールがない店とかもわりと多かった
  • インターネットは、7日間のSIMを20MR(=600円)で購入。
    • 4Gが使えて速度も十分に速い。安くてよかった。

天気・気候

  • 1年中、大体30度くらいで少し暑い。
  • 今の時期は雨季らしく、天気予報全部雨マークだったので残念がってたけど、スコールが2-3時間降るだけですぐ晴れた。傘も持っていったけど結局1回も使わず。
    • カンファレンスのランチの時に、1人できてた女性がいたので一緒にご飯食べて話していたんだけど、EAST AREAの方(クアラルンプールはWEST AREA)はこの時期大量の雨が降っているらしい。
    • マレーシアだとEAST AREAの離島(海がめっちゃきれいで人気の観光地)は、すごくおすすめだけどこの時期は雨のせいで近づけないという話を聞いた。
  • 1年中、気温があまり変化しないけど、どうやら旬はあるらしい。ちなみにドリアンの旬は6-8月。

移動手段

f:id:nwpct1:20170825085143j:plain

  • 日本から来た他のメンバーと空港で合流したので、ホテルまではタクシーを使った。
    • 1人あたり30MR(900円)くらいだった気がする
    • ↑の写真にあるような、ミニバンみたいなのに乗った
  • モノレールは、1.5MR(45円)くらいで乗れる。
  • 市街地でも徒歩で移動するのは少し大変。
    • 都市部はいろんな所で工事をしていて、当然のように歩道が途切れたりする
    • 押しボタン式信号は大体反応しないので、車やバイクに注意しながら横断歩道を渡る。
  • タクシーは特に中心部だとぼったくられるらしいので注意。移動はUber使ったりした。

おわりに

まずは思っていた以上に楽しかったのでよかった。 PyCon APACでもない限りマレーシアには来ないかなと思っていたけど、普通に楽しい国でした。 来年はシンガポールでAPAC開いてほしい。

あとは柔軟に出勤日を調整してくれた、会社の人たちにすごく感謝してます。 今の会社でそんなに楽しそうに仕事してるのがちょっと不思議でいまだによく分からないというのを、今回お酒飲ん出る時に言われたのですが、普通に楽しく働けてます。

就活終わってからもなんでこの会社にしたのみたいな声は各所から頂いていたんですが、詳しい話聞きたい方はご飯行きましょう

オマケ: 撮った写真

f:id:nwpct1:20170825122014j:plain

チャイナタウン

f:id:nwpct1:20170829010607j:plain

Younggun(PSF ボードメンバー)が、2日目の夜に飲んでたJPメンバーを案内してくれたホテルのバーの夜景

f:id:nwpct1:20170828000255j:plain

ツインタワー前のウォーターショー。良すぎて2日連続行った。

python-prompt-toolkitをgolangに移植。kubernetesクライアント作った

Github Trendingに載ってから、予想以上に伸びました。ありがとうございます。

python-prompt-toolkitをgolangでも使いたいなと前から思い移植していたのですが、今日go-promptという名前で公開しました。

kube-promptは実際にgo-promptを使って作成したインタラクティブなkubernetesクライアントです。

kube-prompt

python-prompt-toolkit の移植

GitHub - jonathanslenders/python-prompt-toolkit: Library for building powerful interactive command lines in Python

python-prompt-toolkitは簡単にいうとインタラクティブな補完ができるライブラリなのですが、これを使った便利なツールがたくさん登場していて、かなりのstarを集めています。

自分はpython3を当然のようにインストールしているので、Pythonのツールを使うことが面倒には感じないのですが、普段pythonを使っていない人はpythonのインストールから始めないといけないので結構面倒そうです。

python-prompt-toolkitをGolangに移植する人が現れないかなと思っていたのですが、中々登場しませんでした。 ふと思い立って実装を読んでみると、テストコードなしの実装だけでも2万行以上とボリュームがあります。この強力な補完のために裏では結構面倒なことをしていて、実装もそれなりに複雑になっているので、なるほどこれは大変と思い挑戦してみました。

自分はこういう端末制御ゴリゴリのソフトウェアを作ったことなく、コードを読む上で必要な前提知識が色々と欠けていたので、いい勉強になりました。この記事では実装に関して書きませんが、需要がありそうなら記事書くかどこかで話すかしたいと思ってます。

詳しい使い方はREADMEに書いたので興味のある方は使ってみてください。比較的かんたんに使えるようになっているかなと思います。 オプションも色々用意してます。

f:id:nwpct1:20170813221708p:plain

今後追加予定の機能は

  • Windowsサポート
    • mattnさんがPRくれました。ありがとうございます。windows環境手元にないのでまだ試せないのですが、どうしようか
  • vi modeのサポート
  • multi line inputに対応
  • emacs キーバインドが本来と違う挙動をする箇所が見つかったので修正
    • Ctrl+Wの挙動だったのですが、b4b4r04さんが直してPRくれました。ありがとうございます。
  • Fuzzy Searchingとかprompt作る上で便利なutility系の用意

あたりです。

kube-prompt

kubernetesクライアントも用意したので、kubernetes触ってる方はぜひ使ってみてください。 Github Releaseからバイナリ落とすか、Homebrewで入ります。