├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib └── tasks │ └── decompile.ex ├── mix.exs ├── src └── decompile_diffable_asm.erl └── test └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | decompile-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decompile 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | mix archive.install github michalmuskala/decompile 8 | 9 | ## Usage 10 | 11 | mix decompile ElixirModule --to expanded 12 | mix decompile ElixirModule --to erlang 13 | mix decompile ElixirModule --to asm 14 | mix decompile ElixirModule --to core 15 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :decompile, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:decompile, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/tasks/decompile.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Decompile do 2 | use Mix.Task 3 | 4 | def run(args) do 5 | {opts, modules} = OptionParser.parse!(args, strict: [to: :string]) 6 | 7 | Mix.Task.run("loadpaths") 8 | 9 | modules 10 | |> Enum.map(&get_beam!/1) 11 | |> Enum.each(&decompile(&1, opts)) 12 | end 13 | 14 | defp get_beam!(module_or_path) do 15 | with :non_existing <- :code.which(module(module_or_path)), 16 | :non_existing <- :code.which(String.to_atom(module_or_path)), 17 | :non_existing <- get_beam_file(module_or_path), 18 | :non_existing <- :code.where_is_file(basename(module_or_path)) do 19 | Mix.raise("Could not find .beam file for #{module_or_path}") 20 | end 21 | end 22 | 23 | defp module(string) do 24 | Module.concat(String.split(string, ".")) 25 | end 26 | 27 | defp basename(path) do 28 | String.to_charlist(Path.basename(path)) 29 | end 30 | 31 | defp get_beam_file(path) do 32 | list = String.to_charlist(path) 33 | 34 | if File.exists?(path) and not match?({:error, _, _}, :beam_lib.info(list)) do 35 | list 36 | else 37 | :non_existing 38 | end 39 | end 40 | 41 | defp decompile(path, opts) do 42 | format = get_format(opts) 43 | 44 | case :beam_lib.chunks(path, [:debug_info]) do 45 | {:ok, {module, [debug_info: {:debug_info_v1, backend, data}]}} -> 46 | from_debug_info(format, module, backend, data) 47 | 48 | {:error, :beam_lib, {:unknown_chunk, _, _}} -> 49 | abstract_code_decompile(path, format) 50 | 51 | {:error, :beam_lib, {:missing_chunk, _, _}} -> 52 | abstract_code_decompile(path, format) 53 | 54 | _ -> 55 | Mix.raise("Invalid .beam file at #{path}") 56 | end 57 | end 58 | 59 | defp get_format(to: format), do: map_format(format) 60 | defp get_format(_), do: Mix.raise("--to option is required") 61 | 62 | defp map_format("ex"), do: :expanded 63 | defp map_format("erl"), do: :erlang 64 | defp map_format("asm"), do: :to_asm 65 | defp map_format("diffasm"), do: :diff_asm 66 | defp map_format("disasm"), do: :to_dis 67 | defp map_format("kernel"), do: :to_kernel 68 | defp map_format("core"), do: :to_core 69 | defp map_format(other), do: String.to_atom(other) 70 | 71 | defp abstract_code_decompile(_path, :expanded) do 72 | Mix.raise("OTP 20 is required for decompiling to the expanded format") 73 | end 74 | 75 | defp abstract_code_decompile(path, format) do 76 | case :beam_lib.chunks(path, [:abstract_code]) do 77 | {:ok, {module, erlang_forms}} -> 78 | from_abstract_code(format, module, erlang_forms) 79 | 80 | _ -> 81 | Mix.raise("Missing debug info and abstract code for .beam file at #{path}") 82 | end 83 | end 84 | 85 | defp from_debug_info(:expanded, module, backend, data) do 86 | case backend.debug_info(:elixir_v1, module, data, []) do 87 | {:ok, elixir_info} -> 88 | format_elixir_info(module, elixir_info) 89 | 90 | {:error, error} -> 91 | Mix.raise( 92 | "Failed to extract Elixir debug info for module #{inspect(module)}: #{inspect(error)}" 93 | ) 94 | end 95 | end 96 | 97 | defp from_debug_info(format, module, backend, data) do 98 | case backend.debug_info(:erlang_v1, module, data, []) do 99 | {:ok, erlang_forms} when format == :erlang -> 100 | format_erlang_forms(module, erlang_forms) 101 | 102 | {:ok, erlang_forms} -> 103 | from_erlang_forms(format, module, erlang_forms) 104 | 105 | {:error, error} -> 106 | Mix.raise( 107 | "Failed to extract Erlang debug info for module #{inspect(module)}: #{inspect(error)}" 108 | ) 109 | end 110 | end 111 | 112 | defp from_abstract_code(:erlang, module, forms) do 113 | format_erlang_forms(module, forms) 114 | end 115 | 116 | defp from_abstract_code(other, module, forms) do 117 | from_erlang_forms(other, module, forms) 118 | # case :compile.noenv_forms(forms, [:to_core]) do 119 | # {:ok, ^module, core} -> 120 | # from_core(other, module, core) 121 | 122 | # {:ok, ^module, core, _warnings} -> 123 | # from_core(other, module, core) 124 | # end 125 | end 126 | 127 | defp format_elixir_info(module, elixir_info) do 128 | data = 129 | [ 130 | "defmodule ", inspect(module), " do\n", 131 | Enum.map(elixir_info.definitions, &format_definition/1), 132 | "end\n" 133 | ] 134 | |> IO.iodata_to_binary() 135 | |> Code.format_string!() 136 | 137 | File.write("#{module}.ex", data) 138 | end 139 | 140 | defp format_definition({{name, _arity}, kind, _meta, heads}) do 141 | Enum.map(heads, fn {_meta, args, _what?, body} -> 142 | [ 143 | " #{kind} #{name}(#{Enum.map_join(args, ", ", &Macro.to_string/1)}) do\n", 144 | Macro.to_string(body), 145 | " end\n" 146 | ] 147 | end) 148 | end 149 | 150 | defp format_erlang_forms(module, erlang_forms) do 151 | File.open("#{module}.erl", [:write], fn file -> 152 | Enum.each(erlang_forms, &IO.puts(file, :erl_pp.form(&1))) 153 | end) 154 | end 155 | 156 | defp from_erlang_forms(:diff_asm, module, forms) do 157 | case :compile.noenv_forms(forms, [:S]) do 158 | {:ok, ^module, res} -> 159 | {:ok, formatted} = :decompile_diffable_asm.format(res) 160 | File.open("#{module}.S", [:write], fn file -> 161 | :decompile_diffable_asm.beam_listing(file, formatted) 162 | end) 163 | {:error, error} -> 164 | Mix.raise( 165 | "Failed to compile to diffasm for module #{inspect(module)}: #{inspect(error)}" 166 | ) 167 | end 168 | end 169 | 170 | defp from_erlang_forms(format, module, forms) do 171 | case :compile.noenv_forms(forms, [format]) do 172 | {:ok, ^module, res} -> 173 | File.open("#{module}.#{ext(format)}", [:write], fn file -> 174 | :beam_listing.module(file, res) 175 | end) 176 | {:error, error} -> 177 | Mix.raise( 178 | "Failed to compile to #{inspect format} for module #{inspect(module)}: #{inspect(error)}" 179 | ) 180 | end 181 | end 182 | 183 | defp ext(:to_core), do: "core" 184 | defp ext(:to_kernel), do: "kernel" 185 | defp ext(:to_asm), do: "S" 186 | defp ext(other), do: other 187 | end 188 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Decompile.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :decompile, 7 | version: "0.1.0", 8 | elixir: "~> 1.6-dev", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/decompile_diffable_asm.erl: -------------------------------------------------------------------------------- 1 | -module(decompile_diffable_asm). 2 | 3 | -export([format/1, beam_listing/2]). 4 | 5 | format(Asm0) -> 6 | try 7 | {ok,Asm1} = beam_a:module(Asm0, []), 8 | Asm2 = renumber_asm(Asm1), 9 | {ok,Asm} = beam_z:module(Asm2, []), 10 | {ok, Asm} 11 | catch 12 | Class:Reason -> 13 | {error, Class, Reason} 14 | end. 15 | 16 | renumber_asm({Mod,Exp,Attr,Fs0,NumLabels}) -> 17 | EntryLabels = maps:from_list(entry_labels(Fs0)), 18 | Fs = [fix_func(F, EntryLabels) || F <- Fs0], 19 | {Mod,Exp,Attr,Fs,NumLabels}. 20 | 21 | entry_labels(Fs) -> 22 | [{Entry,{Name,Arity}} || {function,Name,Arity,Entry,_} <- Fs]. 23 | 24 | fix_func({function,Name,Arity,Entry0,Is0}, LabelMap0) -> 25 | Entry = maps:get(Entry0, LabelMap0), 26 | LabelMap = label_map(Is0, 1, LabelMap0), 27 | Is = replace(Is0, [], LabelMap), 28 | {function,Name,Arity,Entry,Is}. 29 | 30 | label_map([{label,Old}|Is], New, Map) -> 31 | case maps:is_key(Old, Map) of 32 | false -> 33 | label_map(Is, New+1, Map#{Old=>New}); 34 | true -> 35 | label_map(Is, New, Map) 36 | end; 37 | label_map([_|Is], New, Map) -> 38 | label_map(Is, New, Map); 39 | label_map([], _New, Map) -> 40 | Map. 41 | 42 | 43 | replace([{label,Lbl}|Is], Acc, D) -> 44 | replace(Is, [{label,label(Lbl, D)}|Acc], D); 45 | %% Drop line informations. They create noise in the diffs 46 | replace([{line,_}|Is], Acc, D) -> 47 | replace(Is, Acc, D); 48 | replace([{test,Test,{f,Lbl},Ops}|Is], Acc, D) -> 49 | replace(Is, [{test,Test,{f,label(Lbl, D)},Ops}|Acc], D); 50 | replace([{test,Test,{f,Lbl},Live,Ops,Dst}|Is], Acc, D) -> 51 | replace(Is, [{test,Test,{f,label(Lbl, D)},Live,Ops,Dst}|Acc], D); 52 | replace([{select,I,R,{f,Fail0},Vls0}|Is], Acc, D) -> 53 | Vls = lists:map(fun ({f,L}) -> {f,label(L, D)}; 54 | (Other) -> Other 55 | end, Vls0), 56 | Fail = label(Fail0, D), 57 | replace(Is, [{select,I,R,{f,Fail},Vls}|Acc], D); 58 | replace([{'try',R,{f,Lbl}}|Is], Acc, D) -> 59 | replace(Is, [{'try',R,{f,label(Lbl, D)}}|Acc], D); 60 | replace([{'catch',R,{f,Lbl}}|Is], Acc, D) -> 61 | replace(Is, [{'catch',R,{f,label(Lbl, D)}}|Acc], D); 62 | replace([{jump,{f,Lbl}}|Is], Acc, D) -> 63 | replace(Is, [{jump,{f,label(Lbl, D)}}|Acc], D); 64 | replace([{loop_rec,{f,Lbl},R}|Is], Acc, D) -> 65 | replace(Is, [{loop_rec,{f,label(Lbl, D)},R}|Acc], D); 66 | replace([{loop_rec_end,{f,Lbl}}|Is], Acc, D) -> 67 | replace(Is, [{loop_rec_end,{f,label(Lbl, D)}}|Acc], D); 68 | replace([{wait,{f,Lbl}}|Is], Acc, D) -> 69 | replace(Is, [{wait,{f,label(Lbl, D)}}|Acc], D); 70 | replace([{wait_timeout,{f,Lbl},To}|Is], Acc, D) -> 71 | replace(Is, [{wait_timeout,{f,label(Lbl, D)},To}|Acc], D); 72 | replace([{bif,Name,{f,Lbl},As,R}|Is], Acc, D) when Lbl =/= 0 -> 73 | replace(Is, [{bif,Name,{f,label(Lbl, D)},As,R}|Acc], D); 74 | replace([{gc_bif,Name,{f,Lbl},Live,As,R}|Is], Acc, D) when Lbl =/= 0 -> 75 | replace(Is, [{gc_bif,Name,{f,label(Lbl, D)},Live,As,R}|Acc], D); 76 | replace([{call,Ar,{f,Lbl}}|Is], Acc, D) -> 77 | replace(Is, [{call,Ar,{f,label(Lbl,D)}}|Acc], D); 78 | replace([{make_fun2,{f,Lbl},U1,U2,U3}|Is], Acc, D) -> 79 | replace(Is, [{make_fun2,{f,label(Lbl, D)},U1,U2,U3}|Acc], D); 80 | replace([{bs_init,{f,Lbl},Info,Live,Ss,Dst}|Is], Acc, D) when Lbl =/= 0 -> 81 | replace(Is, [{bs_init,{f,label(Lbl, D)},Info,Live,Ss,Dst}|Acc], D); 82 | replace([{bs_put,{f,Lbl},Info,Ss}|Is], Acc, D) when Lbl =/= 0 -> 83 | replace(Is, [{bs_put,{f,label(Lbl, D)},Info,Ss}|Acc], D); 84 | replace([{put_map=I,{f,Lbl},Op,Src,Dst,Live,List}|Is], Acc, D) 85 | when Lbl =/= 0 -> 86 | replace(Is, [{I,{f,label(Lbl, D)},Op,Src,Dst,Live,List}|Acc], D); 87 | replace([{get_map_elements=I,{f,Lbl},Src,List}|Is], Acc, D) when Lbl =/= 0 -> 88 | replace(Is, [{I,{f,label(Lbl, D)},Src,List}|Acc], D); 89 | replace([{recv_mark=I,{f,Lbl}}|Is], Acc, D) -> 90 | replace(Is, [{I,{f,label(Lbl, D)}}|Acc], D); 91 | replace([{recv_set=I,{f,Lbl}}|Is], Acc, D) -> 92 | replace(Is, [{I,{f,label(Lbl, D)}}|Acc], D); 93 | replace([I|Is], Acc, D) -> 94 | replace(Is, [I|Acc], D); 95 | replace([], Acc, _) -> 96 | lists:reverse(Acc). 97 | 98 | label(Old, D) when is_integer(Old) -> 99 | maps:get(Old, D). 100 | 101 | %%% 102 | %%% Run tasks in parallel. 103 | %%% 104 | 105 | p_run(Test, List) -> 106 | N = erlang:system_info(schedulers) * 2, 107 | p_run_loop(Test, List, N, [], 0). 108 | 109 | p_run_loop(_, [], _, [], Errors) -> 110 | io:put_chars("\r \n"), 111 | case Errors of 112 | 0 -> 113 | ok; 114 | N -> 115 | io:format("~p errors\n", [N]), 116 | halt(1) 117 | end; 118 | p_run_loop(Test, [H|T], N, Refs, Errors) when length(Refs) < N -> 119 | {_,Ref} = erlang:spawn_monitor(fun() -> exit(Test(H)) end), 120 | p_run_loop(Test, T, N, [Ref|Refs], Errors); 121 | p_run_loop(Test, List, N, Refs0, Errors0) -> 122 | io:format("\r~p ", [length(List)+length(Refs0)]), 123 | receive 124 | {'DOWN',Ref,process,_,Res} -> 125 | Errors = case Res of 126 | ok -> Errors0; 127 | error -> Errors0 + 1 128 | end, 129 | Refs = Refs0 -- [Ref], 130 | p_run_loop(Test, List, N, Refs, Errors) 131 | end. 132 | 133 | %%% 134 | %%% Borrowed from beam_listing and tweaked. 135 | %%% 136 | 137 | beam_listing(Stream, {Mod,Exp,Attr,Code,NumLabels}) -> 138 | Head = ["%% -*- encoding:latin-1 -*-\n", 139 | io_lib:format("{module, ~p}. %% version = ~w\n", 140 | [Mod, beam_opcodes:format_number()]), 141 | io_lib:format("\n{exports, ~p}.\n", [Exp]), 142 | io_lib:format("\n{attributes, ~p}.\n", [Attr]), 143 | io_lib:format("\n{labels, ~p}.\n", [NumLabels])], 144 | ok = file:write(Stream, Head), 145 | lists:foreach( 146 | fun ({function,Name,Arity,Entry,Asm}) -> 147 | S = [io_lib:format("\n\n{function, ~w, ~w, ~w}.\n", 148 | [Name,Arity,Entry])|format_asm(Asm)], 149 | ok = file:write(Stream, S) 150 | end, Code). 151 | 152 | format_asm([{label,_}=I|Is]) -> 153 | [io_lib:format(" ~p", [I]),".\n"|format_asm(Is)]; 154 | format_asm([I|Is]) -> 155 | [io_lib:format(" ~p", [I]),".\n"|format_asm(Is)]; 156 | format_asm([]) -> []. 157 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------