Loading your own modules from your IDAPython scripts with idaapi.require()

TL;DR

If you were using import to import your own “currently-in-development” modules from your IDAPython scripts, you may want to use idaapi.require(), starting with IDA 6.5.

Rationale

When using IDAPython scripts, users were sometimes facing the following issue

Specifically:

  • User loads script
  • Script imports user’s module mymodule
  • Script ends
  • User modifies code of mymodule (Note: the module is modified, not the script)
  • User reloads script
  • Modifications to mymodule aren’t taken into consideration.

While that’s perfectly understandable (the python runtime doesn’t have to reload mymodule if it has been compiled & loaded already), this is somewhat of an annoyance for users that were importing modules that were often modified.

IDA <= 6.4: Ensuring a user-specified module gets reloaded, by destroying it.

Up until IDA 6.4, the IDAPython plugin would do some magic after you have run your user script.
(click “expand all” to reveal the diff)

The sequence becomes:

  • User loads script
  • Script imports user’s module mymodule
  • Script ends
  • [module mymodule is deleted]
  • User modifies code of mymodule
  • User reloads script
  • Modifications to mymodule are taken into consideration, since module was deleted.

Unfortunately we have to stop doing this because:

  • That prevents us from using python-based hooks to be used after the script is finished (see below).
  • That goes against the rest of the python philosophy (i.e., modifications to objects are not reverted), and is therefore unexpected.

Issues with hooks.

Imagine you have the following script, dbghooks.py:

from idaapi import *
import mydbghelpers
class MyHooks(DBG_Hooks):
  def __init__(self):
    ...
  def dbg_bpt(self, tid, ea):
    mydbghelpers.do_something()
    return 0
  def dbg_step_into(self):
    ...
hooks = MyHooks()
hooks.hook()
  • User loads script
  • Scripts imports mydbghelpers
  • Script creates instance of MyHooks, and hooks it into IDA’s debugger APIs
  • Script ends
  • [module mydbghelpers is deleted]
  • User runs debugger, and a breakpoint is hit. Two things can happen:
    • The hook fails executing
    • IDA crashes (that can happen if the form from mydbghelpers import * was used)

IDA > 6.4: Introducing idaapi.require()

Everywhere else in python, when you modify a runtime object, those changes will remain visible.

We decided it would be better to not go against that standard behaviour anymore, and provide a helper to achieve the same results as what was achieved before with the deletion of user modules.

You can now import & re-import of a module with: idaapi.require(name)

Here is its definition:

def require(modulename):
    if modulename in sys.modules.keys():
        reload(sys.modules[modulename])
    else:
        import importlib
        import inspect
        m = importlib.import_module(modulename)
        frame_obj, filename, line_number, function_name, lines, index = inspect.stack()[1]
        importer_module = inspect.getmodule(frame_obj)
        if importer_module is None: # No importer module; called from command line
            importer_module = sys.modules['__main__']
        setattr(importer_module, modulename, m)
        sys.modules[modulename] = m

EDIT (September 16th, 2016): After one of our users has reported an issue with this, we have updated the definition to:

def require(modulename, package=None):
    import inspect
    frame_obj, filename, line_number, function_name, lines, index = inspect.stack()[1]
    importer_module = inspect.getmodule(frame_obj)
    if importer_module is None: # No importer module; called from command line
        importer_module = sys.modules['__main__']
    if modulename in sys.modules.keys():
        reload(sys.modules[modulename])
        m = sys.modules[modulename]
    else:
        import importlib
        m = importlib.import_module(modulename, package)
        sys.modules[modulename] = m
    setattr(importer_module, modulename, m)

The problem with the previous definition, is that if module A loads module C, and then module A loads module B which, itself, also loads module C, then module A will have the module C properly bound to its globals(), but module B won’t (since the module was already present in sys.modules, we were only reloading it, not setting the attribute in module B)

Example

The example debugger hooks script above becomes:

from idaapi import *
idaapi.require("mydbghelpers")
class MyHooks(DBG_Hooks):
  def __init__(self):
    ...
  def dbg_bpt(self, tid, ea):
    mydbghelpers.do_something()
    return 0
  def dbg_step_into(self):
    ...
hooks = MyHooks()
hooks.hook()

I.e., only the second line changes.