2024.10.10
将来は卵1パックの価格が2倍に? 多くの日本人が知らない世界の新潮流、「動物福祉」とは
バイナリを書き換えてシステムコールをフックする(全1記事)
リンクをコピー
記事をブックマーク
yasukata氏(以下、yasukata):yasukataといいます。発表を始めます。
今回は、「Zpoline」という、バイナリを書き換えることでシステムコールをフックする仕組みを紹介します。ここではx84-64のCPUで動作するLinuxを想定しています。(スライドを示して)ソースコードはこちらにURLがあるので、よろしければ見てみてください。あとでスライドも公開するので、そちらも併せてご覧ください。
まず、なぜシステムコールをフックしたくなったのかですが、個人的にカーネルに実装されている機能を、アプリケーションを変えないでユーザー空間で置き換えたいと思ったからです。
ネットワークスタックをユーザー空間に持っていきたいと思った時、システムコールをフックして、適宜ユーザー空間実装を実行すればよさそうだと考えて、作り始めました。その時に「もしかすると、この用途に合ったシステムコールをフックする仕組みはないのでは?」と気づきました。
システムコールをフックするには具体的に4つの要件があります。1つ目はフックの適用後にアプリケーションの性能劣化が小さいこと。2つ目は、フック適用の確度が高い、つまりフックし損ねないこと。3つ目は、ユーザー空間プログラムの再コンパイルが不要であること。4つ目は、カーネルを変える必要がなく、カーネルモジュールもいらないことです。
調べてみた結果、代表的なところを3つ上げると、ptraceのような既存のカーネル機能や、LD_PRELOADを使ったライブラリ関数の置き換え、既存のバイナリ書き換え手法などがありました。これらを一見したところ、既存の仕組みでは、「性能」と「フックをし損ねないこと」の両立が難しそうでした。
今回のモチベーションは、それらを両立する仕組みを作りたいということです。ここで紹介するZpolineという仕組みは、バイナリ書き換えでシステムコールをフックします。バイナリ書き換えの特性上、性能の劣化は抑えやすいのですが、一方で、既存の仕組みでも起こるように、フックをし損ねてしまうことがあります。
なので、どうすればバイナリ書き換えでフックし損ねないようにできるかが今回のチャレンジで、なぜそれが起こるのかと、何が難しいのかをお話しします。
x86-64のCPUでシステムコールを発行しようとすると、基本的にsyscallもしくはsysenterというCPU命令が使われます。これらはそれぞれ2byteですが、具体的にやりたいのは、syscall、sysenter命令を置き換えて、任意のフック関数のアドレスへジャンプすることです。
この時難しいのは、任意のアドレスを指定するのに2byteでは小さくて、2byteを超えてしまうと、ほかの命令を壊してしまうということです。
このため、既存のバイナリ書き換えの仕組みでは、確実な置き換えの保証が難しいという問題があります。
今回のZpolineの考え方ですが、2byteでジャンプ先のアドレスを指定するのは難しそうなので、代わりにシステムコールの呼出規約を利用した書き換えを行って、かつ、適切にトランポリンコードを用意する方向性でやっていきます。
では、どのようにバイナリを書き換えるのかですが、Zpolineでは、syscall、sysenter命令を「callq *%rax」へ置き換えます。読みにくいので「コールアールエーエックス」と呼びます。
ポイントは、callq *%raxはオペコードが「0xff 0xd0」の2byteなので、syscall、sysenterをそのまま置き換えられることです。「置き換えた後はどうなるの?」と思われるかもしれませんが、callq *%raxを実行すると、raxレジスタに入っている値を宛先アドレスと解釈してジャンプします。
さらに「それどうなるの?」と思われることについては、ここでシステムコールの呼出規約が利いてきます。x86-64のCPUで動いているLinux上では、ユーザー空間プログラムは利用したいシステムコールの番号をraxレジスタに入れた後に、syscall、sysenter命令を実行することが決められています。
システムコール番号は、カーネルが中でシステムコールを識別するために定義している番号で、例えばreadシステムコールだったら0、writeシステムコールだったら1、と決まっています。システムコールは合計4〜500個あるので、システムコール番号は400から500ぐらいまでとなっています。
syscall、sysenter命令が実行される時には、raxレジスタにシステムコール番号が入っています。syscall、sysenterをcallq *%raxで置き換えると、アドレス0から400、500程度までジャンプするところがポイントです。
なのでここでは、callq *%raxでジャンプしてくるアドレス0から500程度までを含む領域に、トランポリンコードを用意していきます。(スライドを示して)Linuxでは、こちらのコマンドにあるように、procfsから設定するとmmapでアドレス0にメモリを確保できるようになります。
ちなみに、Zpolineの名前は、アドレス0に置かれるトランポリンコードというところから来ています。
(スライドを示して)具体的にトランポリンコードの中身はこのようになっています。まず、システムコール番号の数だけ先頭をnopで埋めます。その直後に任意のフックへのジャンプのコードを置きます。
これによって、callq *%raxを実行すると、最初に置いたnopのどれかに着地して、その後は、下に続いているnopを辿ってフックへジャンプする処理まで行きます。そして、任意のフックに飛んでくれます。これで任意のフックへジャンプする処理が書けました。
今回は、初期化する部分をLD_PRELOADで最初にロードされることを想定した共有ライブラリとして実装して、この例のように実行します。すると、トランポリンコードの用意とバイナリの書き換えを、a.out内のmain関数が開始する前に実行します。
バイナリ書き換え自体は、メモリにロードされたプログラムに対して行うので、プログラムファイル自体の変更は必要ありません。
フックのオーバーヘッドがどのようなものかを知るために、実際にシステムコールのフックを適用した後に、getpidという軽いシステムコールを1回実行するために必要なCPUサイクルを計測しました。
2パターン試しました。フック関数の中でgetpidシステムコールを実際に発行して結果を返すパターンと、メモリ上にキャッシュしたpidの値を返すパターンです。テーブルには、pidキャッシュなし、あり、という区別で書いてあります。
今回は、ユーザー空間でシステムコールを置き換えて自分で実装したかったので、pidキャッシュありのほうが、今回のケースでのオーバーヘッドがより見えるようになっていると思っています。
この環境で計った感じでは、Zpolineでシステムコールをフックした場合、ptraceで実装する場合に比べて、getpidが100倍以上高速にエミュレートできる結果になりました。
まとめです。今回はZpolineという、バイナリを書き換えることでシステムコールをフックする仕組みを紹介しました。ソースコードは(スライドを示して)こちらのURLにあるので、よろしければ試してみてください。
それから、今回は時間の関係で省いたのですが、具体的なフック関数のプログラミングの方法については、新しく記事を用意したので、よろしければこちらもご覧ください。以上です。ご清聴ありがとうございました。
司会者:ありがとうございます。けっこう質問が出ますね。「純粋なフックなしに比べれば、逆にどれくらいのオーバーヘッドなんだろう?」「ほかより速いのはわかったけど、オーバーヘッドはどれくらい?」という質問があります。
yasukata:(スライドを示して)pidキャッシュありのこの数字は、メモリ上にキャッシュしたpidの値を返しているだけなので、これはかなり純粋なオーバーヘッドに近いと思っています。
関連タグ:
2024.11.13
週3日働いて年収2,000万稼ぐ元印刷屋のおじさん 好きなことだけして楽に稼ぐ3つのパターン
2024.11.11
自分の「本質的な才能」が見つかる一番簡単な質問 他者から「すごい」と思われても意外と気づかないのが才能
2024.11.11
気づいたら借金、倒産して身ぐるみを剥がされる経営者 起業に「立派な動機」を求められる恐ろしさ
2024.11.11
「退職代行」を使われた管理職の本音と葛藤 メディアで話題、利用者が右肩上がり…企業が置かれている現状とは
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
2024.11.12
自分の人生にプラスに働く「イライラ」は才能 自分の強みや才能につながる“良いイライラ”を見分けるポイント
2024.11.12
マネーゲームの「手駒」にされる起業家たち 経営学者が指摘するエコシステムの落とし穴
2024.11.12
先週まで元気だったのに、突然辞める「びっくり退職」 退職代行サービスの影響も?上司と部下の“すれ違い”が起きる原因
2024.11.08
仕事の成果につながる「自分の才能」を言語化するヒント 今は「自分らしさ」を求められるけど、誰からも教わっていない…
2024.11.13
“退職者が出た時の会社の対応”を従業員は見ている 離職防止策の前に見つめ直したい、部下との向き合い方