Go言語でモンキーパッチ

モンキーパッチを、RubyやPythonのような動的言語のみを扱うものと思っている人は多いようですが、それは違います。コンピューターは単独では何もできないマシンで、私たちが指示を与えることで動くものなのです。それでは、Go言語の関数がどのように機能して、実行時にどのように変更することができるのかを見ていきましょう。この記事では、インテルのアセンブリの構文が多く出てきますので、アセンブリ言語を読めない方はリファレンスを使いながら読み進めてください。

もしモンキーパッチがどのように機能しているかには興味がなく、単に使えるようになればいいと思っている方はライブラリがあるのでこちらをご覧ください。

注意:サンプルはgo build -gcflags=-lで作成し、インライン展開を無効にしています。この記事の内容は、アーキテクチャが64bitsで、Mac OSXやLinuxバリアントなどのUNIX系OSをご使用の方を対象としています。

逆アセンブルをすると、以下のコードが何を生成するか見てみましょう。


コンパイルしてHopperを使って見てみると、上記のコードは以下のアセンブリコードを生成します。


この記事では、様々な命令の左側に表示されたアドレスを参照します。

このコードはmain.mainというプロシージャから始まっていて、0x2010から0x2026までの行の命令でスタックをセットしています。これに関してはこちらでより詳しく説明をしていますので、今回の記事ではこのコードの説明は省きます。

0x202aの行は、0x2000の行にある関数main.aの呼び出しです。これは単に0x1をスタックに移動させ、戻しているだけです。そして、0x202fから0x2037の行は、その値をruntime.printintに渡しています。

何てシンプルなのでしょう! では続いて、関数値をどうやってGo言語で実装するかを見ていきましょう。

Go言語の関数値の動作

以下のコードで考えてみましょう。


行11ではaをfに代入していますが、これはf()を実行するとaを呼び出すようになるということです。次にGoパッケージunsafeを使って、fに保存されている値を直接読み出します。C言語の経験を持つ人なら、fを単純にaへの関数ポインタと考えて、このコードは0x2000(上のアセンブリコードでmain.aの位置)と出力するだろうと思うかもしれません。このコードを私のマシンで実行すると0x102c38と出力されますが、これはコードから遠く離れたアドレスです。逆アセンブルしてみると、上のコードの行11で次のような動作が起こっていることが分かります。

ここでは、main.a.fという名前のものを参照しています。その位置のコードを見ると、次のようになっています。

なるほど。main.a.fは0x102c38にあって値0x2000を持ち、この値はmain.aの位置です。fは関数へのポインタではく、関数へのポインタへのポインタだと思われます。コードを変更して補正しましょう。

これで、期待したとおり0x2000と出力されます。なぜこのように実装されるのかについてのヒントはこちらにあります。Goの関数値には、クロージャと結合インスタンスメソッドがどのように実装されるかに関する、追加情報を含むことができます。
関数値の呼び出しがどのように動作するのかを見ましょう。コードを変更して、fを代入してから呼び出すようにします。

逆アセンブルすると、次のようになります。

main.a.fがrdxにロードされ、rdxが指しているものがrdxにロードされてから呼び出されます。関数値のアドレスは常にrdxにロードされます。呼び出されるコードは、追加情報が必要な場合に、このアドレスを使って情報をロードすることができます。この追加情報は、結合インスタンスメソッドのインスタンスと無名関数のクロージャへのポインタです。さらに詳細を知りたい方は、逆アセンブラを用意して、深く調べてみることをお勧めします。
では、ここで得られた新しい知識を使って、Goでモンキーパッチを実装しましょう。

実行時に関数を置き換える

これから行うのは以下のコードで2を出力することです。


さて、replaceをどのように実装しましょうか。関数a本体が実行されるのではなく、関数aがbのコードにジャンプするようにコードを変更する必要があります。基本的にはbの関数値をrdxにロードしてからrdxが指す位置にジャンプするように置き換えなければなりません。

アセンブル後に各行で生成される、対応するマシンコードを隣に配置しました(アセンブリは、オンラインのアセンブラを使用してこのように簡単に試すことができます)。このコードを生成する関数を書くことが今のところ正攻法です。次のようになります。

これでaの関数本体をbへのジャンプに置き換えるために必要なことを全て準備できました。以下のコードは、関数本体の位置にマシンコードを直接コピーしようとしています。

しかし、このコードは実行しても動かず、セグメンテーション違反が発生します。これは、ロードされたバイナリは、デフォルトでは上書きできないためです。この保護を解除するにはmprotectシステム呼び出しを使用します。このコードの最終バージョンが正確に実行されると、関数aが関数bに置き換えられ、‘2’が出力されます。

優れたライブラリにコードをラップする

上記のコードを使いやすいライブラリに置きました。このコードは32ビットに対応し、リバースパッチとインスタンスメソッドのパッチをサポートしています。また、いくつかの例をREADMEに置きました。

まとめ

「為せば成る」というものです。モンキーパッチのように、実行時にプログラムが自ら変更を行うような便利な方法を取り入れることも、可能なのです。

私が楽しみながら書いたこの投稿が、皆さんのお役に立てば幸いです。