エンジニアですよ!

頑張れ俺くん、巨匠と呼ばれるその日まで

systemtap で udp の daddr, dport が 0.0.0.0 や 0 に見えることがあるのはなんでなのか

(udp だけど)tcpdump すれば当然宛先の ip address や port が見えるが、systemtap で daddr, dport を printf してみると daddr が 0.0.0.0, dport が 0 に見えることがある。

名前解決のトレースをしていて、通常名前解決をしているときは宛先ポートが 53 になので、 probe udp.sendmsgif (dport == 53) としておけば名前解決に関係する udp の送信だけトレースできるのだけど、たとえば dig で名前を引いたときにはこれだと引っかからなかったことで気付いた。

実際にトレースしたいプログラムで名前解決が実行される場合は普通に dport == 53 で引っかかるので特に問題はなさそうだが、どういうケースで 0 になってしまうのか確認した。

結論から言うと、(正確にはわかっていないが)

  • systemtap は socket 構造体の daddr, dportを見ているので、
  • データを送信(受信)している socket が -- connect していれば connect したアドレスが daddr, dport として見える -- connect していなければ daddr, dport は 0

ということのようだった。

確認した環境は

ーーー

systemtapudp.sendmsg の probe の daddr, dport の定義を見てみる。

systemtap自体の中身を追うのはかなりしんどいはずだが、probeについて調べるなら簡単だった。tapset 以下に stp ファイルがあり、そこに定義されている。tapset/linux/udp.stpを見てみると、udp.sendmsg はこのように定義されている。

sourceware.org Git - systemtap.git/blob - tapset/linux/udp.stp

  39 probe udp.sendmsg = kernel.function("udp_sendmsg") {
  40         name = "udp.sendmsg"
  41         sock    = $sk
  42         size    = $len
  43         %( systemtap_v >= "2.3" %?
  44         family  = __ip_sock_family($sk)
  45         saddr   = format_ipaddr(__ip_sock_saddr($sk), __ip_sock_family($sk))
  46         daddr   = format_ipaddr(__ip_sock_daddr($sk), __ip_sock_family($sk))
  47         sport   = __udp_sock_sport($sk)
  48         dport   = __udp_sock_dport($sk)
  49            %)
  50 }

確認したい dport__udp_sock_dport($sk) の部分。これはもっと上に定義されていて、

sourceware.org Git - systemtap.git/blob - tapset/linux/udp.stp

  15 @__private30 function __udp_sock_sport (sock) { return __tcp_sock_sport (sock) }
  16 @__private30 function __udp_sock_dport (sock) { return __tcp_sock_dport (sock) }

tcpのものを見ている。 tcp.stpを見てみると

sourceware.org Git - systemtap.git/blob - tapset/linux/tcp.stp

  82 /* return the TCP destination port for a given sock */
  83 function __tcp_sock_dport:long (sock:long)
  84 {
  85   port = @choose_defined(@inet_sock_cast(sock)->sk->__sk_common->skc_dport, # kernel >= 3.8
  86           @choose_defined(@inet_sock_cast(sock)->inet_dport, # kernel >= 2.6.33
  87            @choose_defined(@inet_sock_cast(sock)->dport, # kernel >= 2.6.11
  88                            @inet_sock_cast(sock)->inet->dport)))
  89   return ntohs(port)
  90 }

@hogehogeはよくわからないが、要するに skc_dport を見てるんだなとわかる。ような気がする。

これは大体 inet_dport と同じようなものだろう。

udp の場合、 connect で呼ばれるのは おそらく ip4_datagram_connect のはずで、

https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/datagram.c#L23

その中でソケットに inet_daddrinet_dport を設定している。

https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/datagram.c#L76

int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct inet_sock *inet = inet_sk(sk);
    struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;
...
    inet->inet_daddr = fl4->daddr;
    inet->inet_dport = usin->sin_port;
    sk->sk_state = TCP_ESTABLISHED;

これは、 udpsend で宛先を指定しなかった場合に宛先として利用される箇所と一致している、はず…。下記は udp_sendmsg の該当箇所。 msgsend にわたす宛先などの情報で、それがない場合は else の方に入り、 inet->inet_daddrinet->inet_dport をローカル変数の daddrdport に代入している。

https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/udp.c#L932

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
        size_t len)
{
    struct inet_sock *inet = inet_sk(sk);
...
...
    /*
     *  Get and verify the address.
     */
    if (msg->msg_name) {
        DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name);
        if (msg->msg_namelen < sizeof(*usin))
            return -EINVAL;
        if (usin->sin_family != AF_INET) {
            if (usin->sin_family != AF_UNSPEC)
                return -EAFNOSUPPORT;
        }

        daddr = usin->sin_addr.s_addr;
        dport = usin->sin_port;
        if (dport == 0)
            return -EINVAL;
    } else {
        if (sk->sk_state != TCP_ESTABLISHED)
            return -EDESTADDRREQ;
        daddr = inet->inet_daddr;
        dport = inet->inet_dport;
        /* Open fast path for connected socket.
           Route will not be used, if at least one option is set.
         */
        connected = 1;
    }

ということで、

  • systemtapudp.sendmsg の probe 内の daddr, dport は実際にパケットを送信した宛先が見えているのではない
  • 使える変数は probe にもともと定義されている
  • udp.sendmsg の場合、socket から宛先の情報を取得している。
  • udpの場合、socket を connect して send のときには宛先を指定しないこともできるし、 connect せず send 時に宛先を指定することもできる
  • connect した場合は socket に宛先情報が設定されているので systemtap の probe 内から参照できる。
  • connect していない場合は socket の宛先情報とは関係なく送信するので、 systemtap の probe 内からは宛先を正しく参照することができない。

ということのようだ。

ちなみに glibcgetaddrinfo では connect しているっぽくて、実際 systemtap でトレースしていても dport がちゃんと取れている。 dig ではどうかと思ったが dig はソースコード読むのが難しかったので諦めた

stackprof の wall mode で rails のプロファイルをとろうとしたら mysql の接続エラーがたくさん出てホストが mysql にブロックされてしまう話

起きたこととしては

  • stackprofrack-stackprofで使って rails アプリのプロファイルを取ろうとした
    • stackprof は wall mode で動いてる
  • すると、Host 'x.x.x.x' is blocked because of many connection errors が発生した
  • 起動後しばらくは正常に動いているが、しばらくするとエラーが連続して発生してしまうようで mysql サーバにブロックされアプリが動かなくなる。

原因は

  • stackprof は profile を取るために setitimer で定期的にシグナルを送る
  • wall モードの場合、 setitimerITIMER_REAL で動くので、実時間で定期的にシグナルを送る。
  • libmysql は mysql に接続後 poll でデータを待ち受ける。
  • poll で待っている間にシグナルを受け取ると、 pollEINTR で失敗して connection を切ろうとする。
  • ここで、サーバが既に Server Greeting を送っているのに切断されてしまうと、サーバとしては接続が不正に中断されたとしてエラーだと判断する。
    • (このあたりの詳細はよくわかってない。)
  • mysql には max_connect_errors という値があり、それを超える回数連続で接続が失敗するとそのホストをブロックする。
  • ってことで連続で失敗してブロックされてしまうってことみたいだった。

原因はわかったものの、世の中の人は stackprof で rails アプリケーションのプロファイリングをしているのにこういうことで困ってないということはなにかが間違っているような気もするし、mysqllocalhost だとブロックされることはないとか、普通は mysqlskip-name-resolve を設定していて max_connect_errors は関係なくなるので気付かないとかそういう話なのかもしれない。(会社でもオンプレのときは skip-name-resolve は設定していたような気がする)

それぞれもう少し詳しく

stackprof

今回はじめて中身見た。 setitimer もはじめて知った。プロファイラってこうやってやるんだな〜という。

setitimer して定期的にシグナルを受け取って処理することで時間を測る。

計測モードにはいくつか種類があるが、README を見ると

four sampling modes are supported:

:wall (using ITIMER_REAL and SIGALRM)

:cpu (using ITIMER_PROF and SIGPROF) [default mode]

ということで、問題の wall モードでは ITIMER_REAL が使われている。 ITIMER_PROF だと poll の間は時間が進まないのでシグナルが送られることがなく、問題が起きないのだろう。きっと。

mysql2

問題の Mysql2::Client.new が実際に何をやってるかというと、initialize の最後で connect している

この connect メソッドは native extension の方で定義されている。実際には rb_mysql_connect

実際に接続しているのはここで、nogvl_connect というメソッド。

こいつが libmysql の mysql_real_connect を呼んで接続する。

クライアント側が接続失敗に気付かないのは、この rb_mysql_connect の中で、エラーが EINTR であればリトライしているから。↓この部分。

mysql2/client.c at 3b9a26708fa86aba23763626331eb317ed457cc1 · brianmario/mysql2 · GitHub

libmysql

バージョンは 5.7.24。(そういえば、地味に cmake のときに boost が見つからないっていう問題があり解決できなかったなぁ。メッセージに従って -DDDOWNLOAD_BOOST=1 したんだけど)

いろいろ見たけど、 poll しているのは vio/viosocket.c の vio_io_wait の中 786 行目から。print 入れまくってここでエラーが出ているのにたどり着いた。

  switch ((ret= poll(&pfd, 1, timeout)))
  {
  case -1:
    /* On error, -1 is returned. */
    break;
  case 0:
    /*
      Set errno to indicate a timeout error.
      (This is not compiled in on WIN32.)
    */
    errno= SOCKET_ETIMEDOUT;
    break;
  default:
    /* Ensure that the requested I/O event has completed. */
    DBUG_ASSERT(pfd.revents & revents);
    break;
  }

poll

Man page of POLL のエラーのところを見ると

EINTR 要求されたイベントのどれかが起こる前にシグナルが発生した。 signal(7) 参照。

エラーの原因はこいつです。多分

RSTが起きるのはなぜか

tcpdump の結果はこういう感じで、ハンドシェイク直後にクライアントが FIN を送っていて、サーバは Greeting を送っており、その後クライアントは RST を返す。これはなんでなのかよくわからなかった。

f:id:totem_3:20190107212554p:plain

どう解決すべきなのか?

本当はクライアント側のもう少し早いところで EINTR のリトライをすればいいのでは?という気がする。けどまぁものによるだろうから仕方ないんだろうなぁ。

libmysql の mysql 8 のソースコードを見ると、この部分には ppoll が使われるようになっていて、シグナルがマスクできるようになっている。

しかし、それを設定できるのはサーバ側だけで、クライアント側からは設定できない。残念。ここを設定できるようにすればいいよね。

実際に、 5.7 のソースコードppoll に切り替えて SIGALRM をマスクするようにしたところ問題は発生しなくなった。プロファイルの結果には微妙に影響があるだろうけど、エラーが出るよりは全然良い。

ただ、さすがに libmysql にパッチを当てるのは厳しいのでクライアント側で対応するのは難しい。

とするとサーバ側でなんとかするしかない。

で、↓を見た。 max_connect_errors 回連続した接続失敗でホストをブロックするのをやめよう。ほんとにエラーでなくするだけだけど………

MySQL 5.6のインストール後にチューニングすべき項目 | Yakst

max_connect_errors というのは skip-name-resolve を有効にしていると無効になるようだ。skip-name-resolve は名前解決を利用しない限り設定することが推奨される項目であり、この度エラーが出ていた環境では設定していなかったが経験上本番環境でも設定されていた。

つまり, max_connect_errors 回を超える接続失敗によって接続を拒否するなんてしなくてもいいんだ。そもそも mysql を外部に晒すなんてことはないわけで、攻撃を受けるとしたら内部から。内部に侵入されているのにこれで検知できるような攻撃されるってことはないだろう…多分。ってことでセキュリティ的にはあまり懸念はないはず。

あとはこういう微妙なケースで実は接続エラーが発生しているということに気付かないという可能性はあるが、めったにあることではないので、 SHOW GLOBAL STATUSAborted_connects でも監視しておけばいいでしょう。

時系列

ここからは調べた履歴。行き当たりばったりで調べて時間を費やしたので自戒

続きを読む

mac で dev_appserver.py を再起動しないと変更が反映されなかった

OSは mac, runtime は python27 のお話。 Google Cloud SDK のバージョンは 228.0.0。 components のバージョンは最後に書いた

結論から

起動してるGAEアプリケーションが . で始まるディレクトリ以下にあるとダメ

僕の場合は ghq を使って $HOME/.ghq/hoge/fuga 以下にアプリを clone して作業してたのでダメだった。

昔はいろいろあったようだが、現在の dev_appserver はファイルの変更を検知して変更を反映してくれるようになっている。

macの場合は inotify が使えないので、ファイルの mtime をポーリングして変更を検出している。ポーリング対象から除外する条件みたいなのが用意されており、恐らく意図としては .git とかを除外するために、 . から始まるディレクトリは除外するようになっている。

しかし、実際には working directory 以下の . 始まりのディレクトリを無視するのではなく、絶対パスで途中に . 始まりのディレクトリがあるとまるっとwatch対象から除外されてしまう。(inotify watcherでも同じ気はするが確認はしてない)

ということで ghq のルートディレクトリを $HOME/.ghq から $HOME/ghq にすることで問題は解決した。

おしまい

補足。 Google Cloud SDK の version

❯ gcloud --version
Google Cloud SDK 228.0.0
alpha 2018.11.09
app-engine-python 1.9.80
app-engine-python-extras 1.9.74
beta 2018.11.09
bq 2.0.39
cloud-datastore-emulator 2.0.2
core 2018.12.07
gsutil 4.34

facebookの自動テストツールの論文を読んだメモ

Facebook がバグの自動修正ツールを発表したという話があり、 code.fb.com

その紹介記事を見て、 Sapienz のほうは論文が発表されているという話だったので読んでみた。軽く。

バグの自動修正ツールのほうはSapFixというやつで、バグを修正するためにはまずバグを見つけなければいけないが、そのバグを検知するために使われているのがSapienz。

で、SapienzというのはAndroidアプリ向けの自動テストツール。

この論文を書いた一人のMark Harmanという方(とMcMinnさんという方)が2001年にSearch-based software engineering (SBSE) という手法を提案しており(参考: http://a-lifelong-tester.cocolog-nifty.com/publications/STM07_Notes_on_new_software_testing_techniques.pdf)、 曰くSBSEとは、

ソフトウェア工学における各種の課題をヒューリスティックサーチの技法を使って解決を図る

手法であり、

このサーチ技法をテストの分野に適用するのがSearch-based testingと呼ばれる技法

である。そのSearch-based testingの手法をAndroidのテストに応用して成果が出たよんということのようだ.

上で参考にあげた資料に書いてあるようにサーチ技法として用いられるアルゴリズムにはいくつかの種類があるが、Sapienzで用いられているのは Evolutionary Algorigthm (進化的アルゴリズム) である。 また、Androidのテストにおいては、多目的最適化を使ったsearch-based testingの初めてのアプローチだぜ、と言っているし、論文のタイトルからしてMulti-objective Automated Testing for Android Applicationsであるように、多目的最適化問題として扱っている。

進化的アルゴリズムでは、解きたい問題の解を個体として表現する(多分)。

個体の性質は染色体によって決まり、染色体は複数の遺伝子から成る。

そして個体同士で交叉して子を作ったり突然変異したり一部の個体は淘汰されたりして進化をして世代を重ねていくことで、よりいい感じの解を探す。

詳しくや正確にはもちろん他の資料を参考に。

で、Sapienzで具体的にどんな風に問題が表現されているか。

Sapienzで最適化したい問題というのは、

  • より多くの範囲をテストし(coverage)
  • より多くの不具合を(fault revelation)
  • より短い手順で(shorter test sequence)

発見するようなテストを生成したいということ。

この3つを同時に最適化するので、Multi-objectiveな最適化という話。

論文中でも言及されているが、多目的最適化においては一般にパレート最適な解を見つけることが目的になる。 この場合上記の3つの要素についてパレート最適な解を探す。

Sapienzでは、進化的アルゴリズムにおける個体などの表現は下記のようになっている。

  • 個体: テストスイート
  • 染色体:テストケース
  • 遺伝子:なんらかのイベント

で、遺伝子には2種類ある。Atomic genesとMotif genes.

Atomic genesのほうはその名の通りAtomicであってそれ以上分割することのできない単位のイベント。 たとえばなんかのキーを押すとかどこかをタップするとか音量を調節するとかそういうの。

Motif genesのほうはAtomic genesの組み合わせ。

なんで組み合わせを使うのかというと、UIというのは複雑なものなので、状態やその時のコンテクストの知識なしに単純にAtomic genesだけ組み合わせているとちゃんとした動作を組み立てるのが難しいからだそう。まぁそれはそうっぽい感じがする。しかしこの論文の時点では、Motif genesは汎用的なもの1種類しか使っていないようだ。Motif geneをたくさん使うということはそれだけ人間の手がかかるということで、自動化を妨げることになるからということ。

個体や遺伝子についてはそれで、個体を進化させるためのアルゴリズムの話

下記のいずれかを確率的に個体に適用する。

  • 交叉(crossover)
  • 変異(mutation)
  • 繁殖(reproduction)

個体間では一様交叉を行う。個体内では変異するが、変異は少し複雑。

個体はテストケースを複数含むテストスイートなので、まずテストケースの順序をランダムにシャッフルする。このシャッフルは交叉の際の多様性を増すことを目的にしている。シャッフルしたあとに2つの隣合うテストケース同士で, qの確率で1点交叉を行う。 更に、テストケース内のイベントをqの確率でシャッフルする。Atomic eventsにはパラメータがあるので、それを変異させることもできるが、操作を単純にするためにイベントの並び順を変えることにしたようだ。 繁殖は単にランダムに選ばれた個体をそのまま使うらしい。

淘汰(selection)にはNSGA-Ⅱというアルゴリズムを使う。雰囲気しか理解できてない。

個体の評価は、さっきも書いたが下記の組で行われる。

  • カバレッジ
  • テストシーケンスの短さ
  • 発見したクラッシュの多さ

これらが最も望ましいケースと個体との距離で個体がランク付けされ、ランクが良いものが選ばれる。同じランクではより混雑距離が大きいものが選ばれる。混雑距離というのはランク内において隣接する個体への距離の和のこと。より混雑していないもののほうがが多様性を確保することができるため良い。

ここまで読むとなんとなく進化的アルゴリズムを適用してある程度良いものができそうな気がしてくる。むしろ、実際多様なアプリケーションに対してどのようにテストのイベントを生成するのかとかが気になってくるが、そのへんについては特に細かくは触れられてない。多分他にもテストを生成するツールは既にあるのでそこはこの論文にとってはあんまり重要ではないだろう。

ちなみにこのSapienzはDEAPという, python製の進化的アルゴリズムのためのフレームワークを使って構築されていたが、このDEAPはすごく簡単に使えて便利。自分の場合使う予定は特にないけど。NSGA-Ⅱを用いた多目的最適化とかもできる様子。

参考にした資料など

基本的に検索してすぐ出てきたもの

setxkbmapがわからない

xmodmapがもうダメダメなので、 setxkbmap を使うことにした。

しかし、man を読んでも自分で設定する方法がよくわからん…

一応安定のArchLinux wikiで設定方法は載ってるんだが…

Xorg でのキーボード設定 - ArchWiki

XkbOptionsに色々書き足すか、設定ファイルを書き換えるかする方法。

後者はまぁもちろんそれは動くだろう。 /usr/share/X11/xkb以下にxkbで使われるcomponentが用意してある。それを書き換えればまぁ動く。結局わからなかったので最終的にこれで解決した。

前者は、ubuntuだと/etc/default/keyboardのXKBOPTIONSを書き換えてdpkg-reconfiugreすることになる。

これはまぁ動くんだろうけど、問題はoption(というかsymbol)をどうやって自分で定義し、システムに読み込ませるか。

rulesファイルにoptionに関する記述はあり、caps lockを無効にしてctrlとして使うオプションであるctrl:nocapsの定義とかを見て$HOME以下に書いてみたりしたんだけどうーんうまくいかない。

unix.stackexchange.com

-Iでinclude pathを指定する方法でうまくいかないって言ってる人はいるみたい。

ちなみに、 /etc/gdm3/XSession で $HOME/.Xkbdmap が使われるようになっているので、このファイルで -model ctrl:nocaps を指定してみたが効かず。

これは多分、起動後にコマンド叩くのを自動化する系の解決法の場合によく見る sleep 入れないとだめっていう問題のような気がする。わからんけど。

結局何もわからなかった

まぁ こういう方法→Ubuntu 16.04 で XKB を使ってキーマップをカスタマイズする を使えば自分でいくらでも設定をかけるはずだが、なんかなぁ。自分で定義したオプションをさくっと追加してオプションを追加するだけの方法が知りたかった

Brainf*ckのインタープリタとJITコンパイラ

Adventures in JIT compilation: Part 1 - an interpreter - Eli Bendersky's website

Adventures in JIT compilation: Part 2 - an x64 JIT - Eli Bendersky's website

これらの記事を読み、part2まで真似して書いてみた。

brainf*ck jit · GitHub

記事ではC++で書かれているが、自分はC++は全くわからないのでRustで書いた。

まずシンプルなインタープリタを書いて、遅いよねってなって、まずはインタープリタとして徐々に最適化していく。

で、結構速くなったよね、というところで JIT コンパイルしてみる。

そうすると頑張って最適化したインタープリタよりも速いねぇとなり、さらに JIT しつつ最適化もするともっと速くなるよね。となりpart2は終わり。

このあとLLVMをbackendにしてやってみるとかが続くが、まだやってない。

この記事はJIT初心者の練習の題材としてとても良くて、JITといえばなんとなく実行時にコンパイルしてるんだろうな、という程度にしか理解がなく、JITコンパイラを書いてみるとかいうと途端に意味不明になるようなレベルの人ならこれを書いてみるとなるほど、となるところがあるかもしれない。

個人的にはどういうところが良いと感じたかというと、

  • パースしたコードをインタープリタとしてそのまま実行するのではなくて
  • パースした結果を機械語コンパイル
  • 確保しておいた実行可能なメモリ領域に書き込んで
  • そのメモリ領域を実行する

というところを実際にコードを書いて体験できたので理解が進んだ。

ここで書いたのはbrainf*ckであり個々の命令が単純なので全部を変換したが、実際にはまずはインタープリタでブロックの呼び出し回数をトレースしつつ実行し、頻繁に呼び出されるものだけをコンパイルして実行する、とかになるんだろう。

LuaJITとかがまさにそうで、実行回数を数えておいて必要に応じてJITの処理をする。

参考: - LuaJIT 解析 - Non-public jit.* API

むしろ他にどういったものがあるのかとかも興味あるが、まずは雰囲気がわかってきたのでLuaJITとか、最近だとJavaで書かれたGraalとかがあるんでそのへん調べてみたい。

余談だけど機械語を手書きするときは Online x86 and x64 Intel Instruction Assembler がとても便利。 as とか nasm でやってもいいのかもしれないけど、まとまって書くわけでもないときはこれが楽。

Rustで実行可能なメモリを確保

最近JITコンパイラを書いていて、実行可能なメモリ領域に命令列を直接書き込んで実行、ということがしたかった。

cならmmapなどで、PROT_EXECフラグを立ててメモリを確保するか、mprotectでPROT_EXECフラグを立ててやればいいという認識。

まず思いついたのは単純にhttps://crates.io/crates/libcを使う方法で、それは普通に思ったとおりにできて、最終的にそれ以外の方法はわからなかった。

こんなイメージ

Rust Playground

Rust 側で操作ができないと不便なので、 raw pointer として u8 で変数を宣言しておく。

で、別途 mmap でメモリ領域を確保して、 https://doc.rust-lang.org/std/mem/fn.transmute.html で u8 の pointer に coerce する。こうすると u8 を書き込める。

memmove や memcpy 相当の操作として copy_to(nonoverlapping) や copy_from(nonoverlapping) がある。 (cf. pointer - Rust )

https://doc.rust-lang.org/beta/std/primitive.slice.html#method.as_ptrでポインタを取り出して、確保した領域に u8 の配列に書き込んだ命令列をコピーする。

そしてまた transmute を使って命令列を書き込んだメモリ領域を関数ポインタに変換して、関数として呼び出す。 transmute, https://doc.rust-lang.org/nomicon/transmutes.htmlであり、危険で強力。

まぁlibc使うので当然だが、なんとも想定通りな感じ。

で、ちょうど Rust で JIT についての記事であり、メモリを確保して実行するもっとおしゃれでいい感じな方法を書いた記事がこちらにあったので、詳しくはこちら。

Building a simple JIT in Rust