"""Miscellaneous inspection tools """ from tempfile import NamedTemporaryFile, TemporaryDirectory import os import warnings from numba.core.errors import NumbaWarning def disassemble_elf_to_cfg(elf, mangled_symbol): """ Gets the CFG of the disassembly of an ELF object, elf, at mangled name, mangled_symbol, and renders it appropriately depending on the execution environment (terminal/notebook). """ try: import r2pipe except ImportError: raise RuntimeError("r2pipe package needed for disasm CFG") def get_rendering(cmd=None): from numba.pycc.platform import Toolchain # import local, circular ref if cmd is None: raise ValueError("No command given") with TemporaryDirectory() as tmpdir: # Write ELF as a temporary file in the temporary dir, do not delete! with NamedTemporaryFile(delete=False, dir=tmpdir) as f: f.write(elf) f.flush() # force write, radare2 needs a binary blob on disk # Now try and link the ELF, this helps radare2 _a lot_ linked = False try: raw_dso_name = f'{os.path.basename(f.name)}.so' linked_dso = os.path.join(tmpdir, raw_dso_name) tc = Toolchain() tc.link_shared(linked_dso, (f.name,)) obj_to_analyse = linked_dso linked = True except Exception as e: # link failed, mention it to user, radare2 will still be able to # analyse the object, but things like dwarf won't appear in the # asm as comments. msg = ('Linking the ELF object with the distutils toolchain ' f'failed with: {e}. Disassembly will still work but ' 'might be less accurate and will not use DWARF ' 'information.') warnings.warn(NumbaWarning(msg)) obj_to_analyse = f.name # catch if r2pipe can actually talk to radare2 try: flags = ['-2', # close stderr to hide warnings '-e io.cache=true', # fix relocations in disassembly '-e scr.color=1', # 16 bit ANSI colour terminal '-e asm.dwarf=true', # DWARF decode '-e scr.utf8=true', # UTF8 output looks better ] r = r2pipe.open(obj_to_analyse, flags=flags) r.cmd('aaaaaa') # analyse as much as possible # If the elf is linked then it's necessary to seek as the # DSO ctor/dtor is at the default position if linked: # r2 only matches up to 61 chars?! found this by experiment! mangled_symbol_61char = mangled_symbol[:61] # switch off demangle, the seek is on a mangled symbol r.cmd('e bin.demangle=false') # seek to the mangled symbol address r.cmd(f's `is~ {mangled_symbol_61char}[1]`') # switch demangling back on for output purposes r.cmd('e bin.demangle=true') data = r.cmd('%s' % cmd) # print graph r.quit() except Exception as e: if "radare2 in PATH" in str(e): msg = ("This feature requires 'radare2' to be " "installed and available on the system see: " "https://github.com/radareorg/radare2. " "Cannot find 'radare2' in $PATH.") raise RuntimeError(msg) else: raise e return data class DisasmCFG(object): def _repr_svg_(self): try: import graphviz except ImportError: raise RuntimeError("graphviz package needed for disasm CFG") jupyter_rendering = get_rendering(cmd='agfd') # this just makes it read slightly better in jupyter notebooks jupyter_rendering.replace('fontname="Courier",', 'fontname="Courier",fontsize=6,') src = graphviz.Source(jupyter_rendering) return src.pipe('svg').decode('UTF-8') def __repr__(self): return get_rendering(cmd='agf') return DisasmCFG()