├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── decompilerl.ex └── decompilerl │ └── cli.ex ├── mix.exs ├── mix.lock └── test ├── decompilerl_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | /tmp 7 | /decompilerl 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 3 | Version 2, December 2004 4 | 5 | Copyright (C) 2016 Adam Rutkowski 6 | 7 | Everyone is permitted to copy and distribute verbatim or modified 8 | copies of this license document, and changing it is allowed as long 9 | as the name is changed. 10 | 11 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 12 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decompilerl 2 | 3 | Decompile Elixir/BEAM to Erlang abstract code. 4 | 5 | ## Why? 6 | 7 | When I started working with Elixir, my biggest gripe (coming from the Erlang 8 | world) was that I had no clue what it really compiled to. 9 | 10 | This tool aims to provide some answers. 11 | 12 | ### Sample question: what is a `struct`? 13 | 14 | ```elixir 15 | defmodule TheStruct do 16 | defstruct foo: 1 17 | 18 | alias __MODULE__ 19 | 20 | def new do 21 | %TheStruct{foo: 2} 22 | end 23 | end 24 | ``` 25 | 26 | ```elixir 27 | Decompilerl.decompile(TheStruct) 28 | ``` 29 | 30 | ```erlang 31 | %% lots of irrelevant stuff... 32 | 33 | '__struct__'() -> 34 | #{'__struct__' => 'Elixir.TheStruct', foo => 1}. 35 | 36 | '__struct__'(_@1) -> 37 | {_@6, _@7} = 'Elixir.Enum':reduce(_@1, 38 | {'__struct__'(), []}, 39 | fun ({_@2, _@3}, {_@4, _@5}) -> 40 | {maps:update(_@2, _@3, _@4), 41 | lists:delete(_@2, _@5)} 42 | end), 43 | case _@7 of 44 | [] -> _@6; 45 | _ -> 46 | erlang:error('Elixir.ArgumentError':exception(<<"the following keys must also be given " 47 | "when building ", 48 | "struct ", 49 | ('Elixir.Kernel':inspect('Elixir.TheStruct'))/binary, 50 | ": ", 51 | ('Elixir.Kernel':inspect(_@7))/binary>>)) 52 | end. 53 | 54 | new() -> 55 | #{'__struct__' => 'Elixir.TheStruct', foo => 2, 56 | '__struct__' => 'Elixir.TheStruct'}. 57 | ``` 58 | 59 | There we go! `struct` is an Erlang map with a special key `__struct__ => ?MODULE` 60 | that allows pattern matching further down the line. Noice! 61 | 62 | ## Command-line interface 63 | 64 | You can build `Decompilerl` as a standalone executable (escript). 65 | 66 | ``` 67 | $ mix escript.build 68 | Compiled lib/cli.ex 69 | Compiled lib/decompilerl.ex 70 | Generated escript decompilerl with MIX_ENV=dev 71 | 72 | $ ./decompilerl 73 | 74 | Decompilerl 75 | 76 | usage: decompierl [-o | --output=] 77 | ``` 78 | 79 | ## Usage 80 | 81 | By default, `Decompilerl.decompile` spits the Erlang abstract code to stdout. 82 | When provided with a second (optional) argument, it'll dump it to a file. 83 | 84 | ``` 85 | $ iex -S mix 86 | 87 | iex(1)> Decompilerl.decompile(Decompilerl, "/tmp/foo.erl") 88 | ``` 89 | -------------------------------------------------------------------------------- /lib/decompilerl.ex: -------------------------------------------------------------------------------- 1 | defmodule Decompilerl do 2 | def decompile(module_or_path_or_tuple, opts \\ []) do 3 | device = Keyword.get(opts, :device, :stdio) 4 | skip_info? = Keyword.get(opts, :skip_info, false) 5 | 6 | module_or_path_or_tuple 7 | |> get_beam() 8 | |> Enum.map(&do_decompile(&1, skip_info?)) 9 | |> write_to(device) 10 | end 11 | 12 | defp get_beam(module) when is_atom(module) do 13 | {^module, bytecode, _file} = :code.get_object_code(module) 14 | [bytecode] 15 | end 16 | 17 | defp get_beam(path) when is_binary(path) do 18 | case Path.extname(path) do 19 | ".beam" -> 20 | [String.to_charlist(path)] 21 | 22 | ".ex" -> 23 | code = File.read!(path) 24 | 25 | for {_module, beam} <- Code.compile_string(code) do 26 | beam 27 | end 28 | end 29 | end 30 | 31 | defp get_beam({:module, _module, beam, _result}) do 32 | [beam] 33 | end 34 | 35 | defp do_decompile(bytecode_or_path, skip_info?) do 36 | {:ok, {_, [abstract_code: {_, ac}]}} = :beam_lib.chunks(bytecode_or_path, [:abstract_code]) 37 | ac = if skip_info?, do: skip_info(ac), else: ac 38 | :erl_prettypr.format(:erl_syntax.form_list(ac)) 39 | end 40 | 41 | defp skip_info(ac) do 42 | ac 43 | |> Enum.reduce([], fn item, acc -> 44 | case item do 45 | {:attribute, _, :export, exports} -> 46 | exports = exports -- [__info__: 1] 47 | item = put_elem(item, 3, exports) 48 | [item | acc] 49 | 50 | {:attribute, _, :spec, {{:__info__, 1}, _}} -> 51 | acc 52 | 53 | {:function, _, :__info__, 1, _} -> 54 | acc 55 | 56 | _ -> 57 | [item | acc] 58 | end 59 | end) 60 | |> Enum.reverse() 61 | end 62 | 63 | defp write_to(code, device) when is_atom(device) or is_pid(device) do 64 | IO.puts(device, code) 65 | end 66 | 67 | defp write_to(code, filename) when is_binary(filename) do 68 | {:ok, result} = 69 | File.open(filename, [:write], fn file -> 70 | IO.binwrite(file, code) 71 | end) 72 | 73 | result 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/decompilerl/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Decompilerl.CLI do 2 | @switches [ 3 | help: :boolean, 4 | output: :string, 5 | skip_info: :boolean 6 | ] 7 | 8 | @aliases [ 9 | h: :help, 10 | o: :output 11 | ] 12 | 13 | def main(args) do 14 | {opts, argv} = OptionParser.parse!(args, switches: @switches, aliases: @aliases) 15 | 16 | case argv do 17 | [file] -> 18 | Decompilerl.decompile(file, opts) 19 | 20 | _ -> 21 | IO.puts(""" 22 | Decompilerl 23 | 24 | usage: decompilerl [-o | --output= | --skip-info] 25 | """) 26 | 27 | System.halt(1) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Decompilerl.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :decompilerl, 7 | version: "0.0.1", 8 | elixir: "~> 1.2", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | description: "Decompile Elixir modules to Erlang abstract code", 12 | package: package(), 13 | escript: escript(), 14 | deps: deps() 15 | ] 16 | end 17 | 18 | defp escript do 19 | [main_module: Decompilerl.CLI, embed_elixir: true] 20 | end 21 | 22 | def application do 23 | [applications: [:syntax_tools]] 24 | end 25 | 26 | defp deps do 27 | [{:ex_doc, "~> 0.10", only: :dev}] 28 | end 29 | 30 | defp package do 31 | [ 32 | maintainers: ["Adam Rutkowski"], 33 | licenses: ["WTFPL"], 34 | links: %{"GitHub" => "https://github.com/aerosol/decompilerl"} 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], [], "hexpm", "e2d24ab54061603b8dd65bc3d361897ded61f33a4b57044564320a3ac00e8e72"}, 3 | "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "6bf36498c4c67fdbe6d4ad73a112098cbcc09b147b859219b023fc2636729bf6"}, 4 | } 5 | -------------------------------------------------------------------------------- /test/decompilerl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecompilerlTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureIO 4 | 5 | @outfile "tmp/decompilerl_test.erl" 6 | 7 | test "output to stdout" do 8 | :ok = Decompilerl.decompile(Decompilerl.CLI) 9 | end 10 | 11 | test "output to file" do 12 | File.rm(@outfile) 13 | File.mkdir_p!("tmp") 14 | :ok = Decompilerl.decompile(Decompilerl, device: @outfile) 15 | assert File.exists?(@outfile) 16 | end 17 | 18 | test "beam file" do 19 | out = 20 | capture_io(fn -> 21 | path = "_build/test/lib/decompilerl/ebin/Elixir.Decompilerl.beam" 22 | :ok = Decompilerl.decompile(path) 23 | end) 24 | 25 | assert out =~ "-module('Elixir.Decompilerl')." 26 | end 27 | 28 | test "elixir file" do 29 | out = 30 | capture_io(fn -> 31 | :ok = Decompilerl.decompile("lib/decompilerl.ex") 32 | end) 33 | 34 | assert out =~ "-module('Elixir.Decompilerl')." 35 | end 36 | 37 | test "skip __info__" do 38 | out = 39 | capture_io(fn -> 40 | path = "_build/test/lib/decompilerl/ebin/Elixir.Decompilerl.CLI.beam" 41 | :ok = Decompilerl.decompile(path, skip_info: true) 42 | end) 43 | 44 | assert out =~ "-module('Elixir.Decompilerl.CLI')." 45 | refute out =~ "__info__" 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------