エキスパートPythonプログラミング 改訂2版が発売されました
改訂2版と書いてあるように、この本には初版があり日本語の翻訳書は2010年頃に出版されていました。 自分がPythonを書き始めたのが2014年頃だったのですが、当時通っていた学校の図書館で見つけてこの本を借りたことがあります。 プログラミングの勉強を初めたばかりの自分は、ほとんど何も理解出来ないまま返却したのを今でも覚えています。
今回は縁あって初版の翻訳メンバーである稲田さん、渋川さん、清水川さん、森本さんの4名と一緒に翻訳をすすめることになりましたが、4年前には何も理解できなかった自分がこれだけのベテラン陣と一緒に改訂2版の翻訳に関われていると思うと少し感慨深いです。
さて、初版から大幅に加筆され520ページとボリュームもあるので最初から最後まで読める人はあまりいないと思います。 必要になったらそのときに読んでみようと思っている方も多いと思いますが、参考までに自分が特に気に入っている章を3つあげておきます。
- 3章 構文ベストプラクティス: クラスの世界
- 7章: 他言語によるPython拡張: C拡張、Cython、ctypes
- 13章 並行処理: マルチスレッド、マルチプロセス、非同期プログラミング
3章は初版から大きく書き換えられていて、7章と13章の内容に関しては、そのほとんどが改訂2版から新しく追加されています。 購入してくださった方は、ぜひチェックしてみてください。
Amazonでは2/26(月)から販売開始です。実は自分もそれを見て2/26(月)に発売かと思っていたのですが、書泉ブックタワーなど今日から発売している店舗があるようです。ぜひお近くの方は立ち寄ってみてください。
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_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
を使ってプログラムカウンタの値をとってみる。
ちなみにx86やx86_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);
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) {
...
}
}
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;
...
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;
case x86_THREAD_STATE: { x86_thread_state_t *state; ... if (thread_is_64bit(thr_act)) { ... get_thread_state64(thr_act, &state->uts.ts64);
これらのコードから少なくとも x86_THREAD_STATE
もしくは x86_THREAD_STATE64
のどちらかをflavor引数で指定すればRIPレジスタの値がとれそうだ。
ソースコード
flavorで指定する値はわかったので、早速実装してみる。
forkして生成した子プロセスのpidからthreadの一覧を取得し、 get_thread_state
に x86_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の方々が書いてくれるみたいなので楽しみ。
- 台湾で開催された「PyCon APAC 2015」参加レポート:レポート|gihyo.jp … 技術評論社
- 「PyCon APAC 2016 in Korea」参加レポート:レポート|gihyo.jp … 技術評論社
詳しいレポートは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
事前にスケジュールを見てLTが見当たらなかったので、無いのかなと思っていたんですが、当日行ってみたら普通に募集していた。カンファレンス2日目の朝、ぶっちゃけ寝坊したんですが、朝食あと回しにして会場に寄ったらLT枠に空きがあったので応募。
2日目のセッションは台湾のTzu-ping Chungさん(Macdown開発者。PyCon JP 2016でもasync/awaitの話をしてました)のトークだけ聞いて、その後はスタバでLTの資料作りしてました。
Tzu-ping Chungさんのトークは、自分にとって特に新しい話は少なかったかなとも思ったのですが、うまくまとまっていて話も面白かった。自分とそれほど歳も変わらないと思うんですが、自分よりもずっとコードを書いているんだろうなと、ふと思ってしまった。負けないように頑張ろう。。。
発表スライドはこちら
今回のカンファレンスでは特に動画撮影がなかったのでYoutubeとかにもあがらない。あとで見返すように撮っておけばよかった。
マレーシア / クアラルンプールについて
今回が初のマレーシアでした。
基本情報
- 公用語に英語が含まれていて、大体どこにいっても英語が通じるので楽だった。
- 教育を受ける時に、英語とマレーシア語を選ぶ感じらしい。マレーシア語を話せない人もそれなりにいるという話を聞いた。
- 通貨は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月。
移動手段
- 日本から来た他のメンバーと空港で合流したので、ホテルまではタクシーを使った。
- 1人あたり30MR(900円)くらいだった気がする
- ↑の写真にあるような、ミニバンみたいなのに乗った
- モノレールは、1.5MR(45円)くらいで乗れる。
- 市街地でも徒歩で移動するのは少し大変。
- 都市部はいろんな所で工事をしていて、当然のように歩道が途切れたりする
- 押しボタン式信号は大体反応しないので、車やバイクに注意しながら横断歩道を渡る。
- タクシーは特に中心部だとぼったくられるらしいので注意。移動はUber使ったりした。
おわりに
まずは思っていた以上に楽しかったのでよかった。 PyCon APACでもない限りマレーシアには来ないかなと思っていたけど、普通に楽しい国でした。 来年はシンガポールでAPAC開いてほしい。
あとは柔軟に出勤日を調整してくれた、会社の人たちにすごく感謝してます。 今の会社でそんなに楽しそうに仕事してるのがちょっと不思議でいまだによく分からないというのを、今回お酒飲ん出る時に言われたのですが、普通に楽しく働けてます。
就活終わってからもなんでこの会社にしたのみたいな声は各所から頂いていたんですが、詳しい話聞きたい方はご飯行きましょう
オマケ: 撮った写真
チャイナタウン
Younggun(PSF ボードメンバー)が、2日目の夜に飲んでたJPメンバーを案内してくれたホテルのバーの夜景
ツインタワー前のウォーターショー。良すぎて2日連続行った。