20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2023

Day 24

Elixir Moduleの理解・分解・再構築

Last updated at Posted at 2023-12-22

今回のおはなし

某錬金術師の人が言ってましたが,錬金術の基本は理解・分解・再構築だそうです.
今回はElixirモジュールの理解・分解・再構築をやってみたいと思います.

理解

ご存知の通り,Elixir言語はBEAMというバイトコードに変換され,Erlang VM上で動作します.少しこの流れを追ってみましょう.

とりあえず中身のペラペラのコードをコンパイルしてみましょう.

sample.ex
defmodule Sample do
  def add() do
    1+2
  end
end
$ elixirc sample.ex

Elixir.Sample.beamができましたね.中身は・・
$ hexedit Elixir.Sample.beam
image.png
まぁ,バイナリっすね.ちょこちょこシンボル名やらチェックサム持ってそうなのが見えますが,なるほど分らんです.

関数内の「1+2」を「1+3」に書き換えてリコンパイルして比較してみます.
$ cmp -l Elixir.Sample.beam Elixir.Sample.beam.bak
めっちゃ変わってますし,2から3に変わった個所が探せません.MD5部分がゴリっと変わってるのに加えて,もうひとひねり入ってそうです.
バイナリを一か所書き換えて,ほら動作変わった!とかやってみたかったんですが,ダメそうですね.

elixircのなかみ

作戦を変更して,コンパイル工程を見ていきましょう.
elixircコマンドのコードは
 https://github.com/elixir-lang/elixir/blob/main/bin/elixirc
です.シェルスクリプトだったんですねー.なんやかんやして,/bin/elixirを呼んでます.
elixirコマンドのコードは,
 https://github.com/elixir-lang/elixir/blob/main/bin/elixir
こちらとなります.
こっちは結構大きなシェルスクリプトですね.
よくある,オプション解析して,オプション詰め直して,次の何かを呼ぶ感じのよくある構造です.以下最後の方の抜粋です.

elixir
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"

...

if [ -n "$ELIXIR_CLI_DRY_RUN" ]; then
  echo "$@"
else
  exec "$@"
fi

ELIXIR_CLI_DRY_RUNを有効にするとコマンドを実行せずに表示だけしてくれそうです.

$ ELIXIR_CLI_DRY_RUN=1 elixirc sample.ex
# (以下改行を加えてます.本来は一行です)
erl -pa /home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/eex/ebin 
/home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/elixir/ebin
/home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/ex_unit/ebin
/home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/iex/ebin 
/home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/logger/ebin
/home/hosoai/.asdf/installs/elixir/1.14.3-otp-25/bin/../lib/mix/ebin
-elixir ansi_enabled true -noshell 
-s elixir start_cli -extra +elixirc sample.ex

この段階で,いきなりerl(Erlang)コマンドを叩いてらっしゃる.
-paから後ろに続いてるのはロードするライブラリ指定のようです.-s 以降が実行するモジュールと関数です.

erlangのelixirモジュールのstart_cli関数ですね.
該当するコードがあったので追っかけてみましょう.

elixir/lib/elixir/src/elixir.erl:L197

elixir.erl
  'Elixir.Kernel.CLI':main(init:get_plain_arguments()),

んん?Elixir.Kernel.CLIってまたElixirを呼んでるんすか?ややこい.
ちなみに,erlang内でElixir.XXってモジュールを呼んでる場合はElixirのコードを,Elixir内で:module.func()って呼んでるのはerlangのコードを呼び出しています.

elixir/lib/elixir/lib/kernel/cli.ex:L24

  • process_commandsでオプションから関数に変換(関数実体はL391以降)
  • run -> exec_funと進んで別プロセスで実行
  • 今回追いたいCompileの関数はL463のprocess_command({:compile,,)
  • Kernel.ParallelCompiler.compile_to_pathへ続く

elixir/lib/elixir/lib/kernel/parallel_compiler.ex:L178

パラレルコンパイラとか嫌な予感しかしませんが・・

  • spawn_workers(files, )~で,しばらくたらい回して,L415のspawn_workers([file | queue])で一個ずつ処理(この辺はすでに並行動作してるはず)
  • L523: compile_fileに飛んで,:elixir_compiler.file()へ続く

はい,またerlangへ飛びます.そろそろツライヨー.

elixir/lib/elixir/src/elixir_compiler.erl:L28

  • L28: file -> L6: string -> L10: quotedと進みます.この時点でTokenizeは出来ており,Elixirの句ごとに分かれた構文木になっています.
  • L17: elixir_lexical:run()に構文木やら関数やら渡して呼んでいます.字句解析っぽいですね!
  • elixir_lexicalは,木を辿りながら,引数で渡した関数に句を渡して処理しているようです.こっちは一旦置いておきましょう.
  • 先ほど渡されていた L35: maybe_fast_compile()を追います.
  • fast_compile()とcompile()の2ルートありますが,L110: fast_compileへ行ってみましょう.早く終わらせたいですし.
  • L117: お,ここでマクロ展開してるんですね.
  • L125: elixir_module:compile()へと続きます・・.

elixir/lib/elixir/src/elixir_module.erl:L76

  • なんかこの中でもlexical:runを呼んだりしてますが,ネストってる何かをなんかしてるんでしょうか.
  • L95, L99からcompileへ飛びます.
  • L118: compile().binaryって文字がチラついてきましたね.ゴールは近いか?内部で諸々設定を引っ提げて,
  • L189: elixir_erl:compile()へ続きます.

elixir/lib/elixir/src/elixir_erl.erl:L114

  • L129: load_formへ
  • L446: load_form(), elixir_erl_compiler:forms()へ

elixir/lib/elixir/src/elixir_erl_compiler.erl:L62

  • L65: erl_to_core(),erlからcore(バイナリ)へ変換してそう!
  • L50: erl_to_core() -> L54: v3_core:module()

ここからElixirソースではなく,erlang/otpへ飛びます.

erlang/otp/blob/master/lib/compiler/src/v3_core.erl

  • なんかめっちゃコンパイルしてそうな雰囲気ですね(諦
  • 字句ごとに関数が呼ばれ,バイナリに詰めてる感があります.

ちなみに,opcodeの一覧はこちらにあります.
https://github.com/erlang/otp/blob/master/lib/compiler/src/genop.tab

おさらい

とまぁ,なんか狐につままれたような気もしますが,ざっとおさらいしておきます.

  • elixir ファイルからなんやかんやして,Elixir抽象構文木を作ります.
  • Elixir抽象構文木から,erlの抽象構文木を作ります(≠erlang構文木)
  • erlからバイナリを作ります.
    試してみましょう.
$ iex
# ファイル表示
iex> File.read!("sample.ex")
"defmodule Sample do\n  def add() do\n    1+3\n  end\nend\n"

# Elixir構文木
iex> File.read!("sample.ex") |> Code.string_to_quoted!
{:ok,
 {:defmodule, [line: 1],
  [
    {:__aliases__, [line: 1], [:Sample]},
    [
      do: {:def, [line: 2],
       [{:add, [line: 2], []}, [do: {:+, [line: 3], [1, 3]}]]}
    ]
  ]}}

# erl構文木
iex(5)> File.read!("sample.ex")|>Code.string_to_quoted!|>:elixir_erl.elixir_to_erl
{:tuple, 0,
 [
   {:atom, 0, :defmodule},
   {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 1}]}, {nil, 0}},
   {:cons, 0,
    {:tuple, 0,
     [
       {:atom, 0, :__aliases__},
       {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 1}]}, {nil, 0}},
       {:cons, 0, {:atom, 0, :Sample}, {nil, 0}}
     ]},
    {:cons, 0,
     {:cons, 0,
      {:tuple, 0,
       [
         {:atom, 0, :do},
         {:tuple, 0,
          [
            {:atom, 0, :def},
            {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 2}]},
             {nil, 0}},
            {:cons, 0,
             {:tuple, 0,
              [
                {:atom, 0, :add},
                {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 2}]},
                 {nil, 0}},
                {nil, 0}
              ]},
             {:cons, 0,
              {:cons, 0,
               {:tuple, 0,
                [
                  {:atom, 0, :do},
                  {:tuple, 0,
                   [{:atom, 0, :+}, {:cons, 0, {...}, ...}, {:cons, 0, ...}]}
                ]}, {nil, 0}}, {nil, 0}}}
          ]}
       ]}, {nil, 0}}, {nil, 0}}}
 ]}
# erl構文木からバイナリへ・・
iex(7)> File.read!("sample.ex")|>Code.string_to_quoted!|>:elixir_erl.elixir_to_erl|>:elixir_erl_compiler.erl_to_core([])
** (ErlangError) Erlang error: {:bad_generator, {:tuple, 0, [{:atom, 0, :defmodule}, {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 1}]}, {nil, 0}}, {:cons, 0, {:tuple, 0, [{:atom, 0, :__aliases__}, {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 1}]}, {nil, 0}}, {:cons, 0, {:atom, 0, :Sample}, {nil, 0}}]}, {:cons, 0, {:cons, 0, {:tuple, 0, [{:atom, 0, :do}, {:tuple, 0, [{:atom, 0, :def}, {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 2}]}, {nil, 0}}, {:cons, 0, {:tuple, 0, [{:atom, 0, :add}, {:cons, 0, {:tuple, 0, [{:atom, 0, :line}, {:integer, 0, 2}]}, {nil, 0}}, {nil, 0}]}, {:cons, 0, {:cons, 0, {:tuple, 0, [{:atom, 0, :do}, {:tuple, 0, [{:atom, 0, ...}, {:cons, ...}, {...}]}]}, {nil, 0}}, {nil, 0}}}]}]}, {nil, 0}}, {nil, 0}}}]}}
    (stdlib 4.3) erl_internal.erl:621: :erl_internal."-predefined_functions/1-lc$^0/1-0-"/1
    (stdlib 4.3) erl_internal.erl:621: :erl_internal.predefined_functions/1
    (stdlib 4.3) erl_internal.erl:618: :erl_internal.add_predefined_functions/1
    (compiler 8.2.4) v3_core.erl:185: :v3_core.module/2
    iex:7: (file)

流れを追ってバイナリを作るには,たぶん何かをロードしとかないとダメっぽいすね.だいぶ手順をサボったんで仕方ないか・・.残念.

かなり読み飛ばしてきましたが,Elixirのモジュール生成について,ある程度「理解した」と言っていいですよね?

分解・再構築

疲れたから以降はサラっと流しまーす.

実行時にモジュールのバイナリを得るには,

iex> {name, binary, path} = :code.get_object_code(Sample)
{Sample,
 <<70, 79, 82, 49, 0, 0, 5, 84, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 168,
   0, 0, 0, 17, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101,
   8, 95, 95, 105, 110, 102, 111, 95, 95, 10, ...>>,
 '/home/hosoai/elixir/advent/Elixir.Sample.beam'}

受け側のコードは以下になります.上記の逆転なんですが,load_binaryの引数順が異なるので注意です.

{name, binary, path} = data |> Base.decode64! |> :erlang.binary_to_term
:code.load_binary(name, path, binary)

別プロセスのiex間で受け渡し

ターミナルを二枚開いて,試してみましょう.二枚目のターミナルは別のディレクトリで立ち上げてください.

まずはモジュールデータを引っこ抜きます.先ほどと同じ手順ですが,個別に変数にばらす必要もないので,まとめて受け渡ししやすいBase64に成って貰います.

Terminal1
# beamにしておかないとiexで自動で読み込んでくれないので,コンパイルしておきます.
$ elixirc sample.ex
$ iex
iex> :code.get_object_code(Sample)|>:erlang.term_to_binary|>Base.encode64
"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"
Terminal2
$ iex

# 当然Sampleはロードされてないので見つかりません.
iex> Sample.add
** (UndefinedFunctionError) function Sample.add/0 is undefined (module Sample is not available)
    Sample.add()
    iex:4: (file)

# 先ほどの文字列を一旦変数に入れます.
iex> data="g2gDZAANRWxpeGlyLlNhbXBsZW0AAAZoRk9SMQAABmBCRUFNQXRVOAAAAKgAAAARDUVsaXhpci5TYW1wbGUIX19pbmZvX18KYXR0cmlidXRlcwdjb21waWxlCmRlcHJlY2F0ZWQLZXhwb3J0c19tZDUJZnVuY3Rpb25zBm1hY3JvcwNtZDUGbW9kdWxlBnN0cnVjdANuaWwGZXJsYW5nD2dldF9tb2R1bGVfaW5mbwNhZGQLbW9kdWxlX2luZm8ULWlubGluZWQtX19pbmZvX18vMS1Db2RlAAAArAAAABAAAAAAAAAAqQAAABIAAAAFARCZAAISIhABIDsDlRcIEjKFQoVSdWJlclWCdZKFokWyNQEwQMIDEwFAQBIDEwFQQEcAAxMBYEBHEAMTAXBAAgMTAYBAAxNAEgNOIAABkAYQDREBoJkQAhLyAAGwQEcgAxMBwJkAAhIKEAAB0EASA04QEAHgmQACEgoQEAHwQAMTQBIDTiAAAQgQmQACEgoREAEIET0NEANTdHJUAAAAAEltcFQAAAAcAAAAAgAAAA0AAAAOAAAAAgAAAA0AAAAOAAAAAUV4cFQAAAA0AAAABAAAABAAAAABAAAADwAAABAAAAAAAAAADQAAAA8AAAAAAAAACwAAAAIAAAABAAAAAkxpdFQAAADJAAAA23icJY09DgFRFIUfNiAKCQuwk9mAHRDFEBoNhebehxhM428oBIn/iSgQIRO9dRxkVLMFb0Zzk3vOd84RQkSEEFFZVDekhyuRTC6XEQX1xWXJd7xnQ0tlW+Yica3pyXRMaavAmbnb8eveAffc/QHcB22+cwaZntMFDcEGyAYdwRfIE6QFafvwbg2yQOs/DGJwG6QiW1ATNACNQHXQAzTxHPpMV+/bGcyucVS6VsxX82XIHXgJOQwm9kGDz4NbQYmtgj/C73hUAAAATG9jVAAAABAAAAABAAAAEQAAAAEAAAARQXR0cgAAACiDbAAAAAFoAmQAA3ZzbmwAAAABbhAAG0rbaoysNs97KvOwy75xPGpqQ0luZgAAAKqDbAAAAANoAmQAB3ZlcnNpb25rAAU4LjIuNGgCZAAHb3B0aW9uc2wAAAAEZAAZbm9fc3Bhd25fY29tcGlsZXJfcHJvY2Vzc2QACWZyb21fY29yZWQAD25vX2NvcmVfcHJlcGFyZWQADm5vX2F1dG9faW1wb3J0amgCZAAGc291cmNlawAkL2hvbWUvaG9zb2FpL2VsaXhpci9hZHZlbnQvc2FtcGxlLmV4agAARGJnaQAAAYeDUAAAAgp4nGVRS07DMBB1oSAB5RasqTgEJ+AAkVtPiCMnqRwngqUdhPhu+LNAsODTUiEWwAKEuMwAKiuuwCSlEhIbj96bN29Gz8G4YLMCWtmyJ2M/8fIFwaZByRWpPdAqoPbUL8wXDGOsIViD+wYIg5b+akh6boyWrcxASqjRTqKOVOAlHVPiGQG+jKWRSZwqMqgF9WBMsHEuBGdUqT2kiawrGQMfC391//gwjIg6H3RP3l+20e0Nbu/Q7aO9+bpwaHe/X3fQHqLbRNtHe4/uCYsHLI6x6Jfi3jXaY7TXQzFah24LLY100a6jPUB7hHYN7Rva0+9X+3l29fH8iM4NNu+JX6xSwKKH7hKLw2rFbeVQ6tFtVCZ9GgzLTAR0NLS5AUGo7lMg5elzzSCJgJ404bI5DLbJRQ6xaaY86iiYhxXKUKZeCwKeyyTTgk34XKUwiqEm2GSUiEwRMzu8an6pmiWsQXEjc/BGG6f+2E6mRmdtQ6HHUtHPZLEG3g54S0EY/gDTftuYAERvY3MAAABpg1AAAABweJxNzDkOgDAQA0BzBGj4B1SIF6EVG5RwbKQQjucTqCjceGSbklGyG/fh7ClhFHq1t/UbgDroO3Qb+YXdJYxcnOgQAWtMYpTJGNV0yBisi54RM4HST98DFYumnf/L+QGymRxkAAAARXhDawAAAGSDaAJkABFlbGl4aXJfY2hlY2tlcl92MXQAAAABZAAHZXhwb3J0c2wAAAABaAJoAmQAA2FkZGEAdAAAAAJkABFkZXByZWNhdGVkX3JlYXNvbmQAA25pbGQABGtpbmRkAANkZWZqTGluZQAAACEAAAAAAAAAAAAAAAUAAAABAAAAARIhAAlzYW1wbGUuZXgAAABUeXBlAAAAGgAAAAEAAAABH/8AAAAAAAAAAP//////////AABrAC0vaG9tZS9ob3NvYWkvZWxpeGlyL2FkdmVudC9FbGl4aXIuU2FtcGxlLmJlYW0="

iex> {name, binary, path} = data |> Base.decode64! |> :erlang.binary_to_term
iex> :code.load_binary(name, path, binary)
{:module, Sample}

# ちゃんとロードされたので,今度は実行できます.
iex> Sample.add
4

以上,モジュールの分解と再構築でした.如何だったでしょうか?

サンプルで示している文字列をお手元で再構築すれば,私の元から皆さんのiexへモジュールを転送できたことになりますし,試してみてください.
こっそり?モジュールを受け渡したい時のテクニックとして使えるかもしれませんね!ぜひいろいろ遊んでみてください.

言い訳

当初は,:code.get_object_codeして バイナリを送って,再構成したら面白いよねー,動的ロードはロマンだよねーみたいな軽いノリだったのですが.これって分解・再構築じゃん,錬金術ったら理解・分解・再構築じゃん!ちゃんと理解もしなきゃ!と,よく分からんスイッチが入ったため,よく分からない記事になりました.

今年もElixirのAdvent CalendarにあまりElixirが出てこない記事を書いてしまった事を深くお詫び申し上げます.来年も頑張ります.

20
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?