Bytecode instrumentation

Kostromin Igor edited this page Oct 1, 2015 · 2 revisions

Method entry point instrumenting:

Method after instrumenting starts with next code:

  entry_point:
    if (Coro.getSafe() == null || Coro.getState() == null)   // If nothing to restore - go to original entry point
      goto original_entry_point
    if (!isStatic && Coro.isUnpatchableCall())               // We have to pop extra ref (saved "this") if current method is not static
      Coro.popRef()                                          //  and if this method has been called from unpatchable context
    Coro.setUnpatchableCall(false)                           // Reset Coro.unpatchableCall flag to false always (when restoring state)
    switch (state) {
      case 1: goto restore_point_1                           // Going to code that restoring state before call first restore point
      case 2: goto restore_point_2                           // Same for second restore point
      ...
    }
  original_entry_point:                                      // Original method code starts here

Restore point can be instrumented using one of two strategies: usual strategy and special strategy for methods that cannot be instrumented.

Call instrumenting (usual strategy):

After instumenting one call foo() instruction will be wrapped like this:

┌ restore_point:                  // We will be here if we need to restore state before calling foo()
│   popLocals                     // Restore locals from Coro storage
│   popStack                      // Restore operands stack (excluding args of method will be called) from Coro storageif (!callingMethodIsStatic)   // Restore instance of method will be called
│     popInstance
│   pushDefaultArgs               // Push 0 and null values for all arguments of foo()
└ no_active_coro:                 // We will be here if a) no need to restore state b) we have restored state just now
 ║  call foo()                    // Make call of original method foo()
┌ after_call:if (!isYielding)              // If there was no yielding happened inside foo(), we go to no_save_context
│     goto no_save_context
│   pushStack                     // Save the operands stack into Coro storage
│   pushLocals                    // Save the locals into Coro storageif (!isStatic)                // If *this* method is not static, saving reference to `this` (it will be used when
│     pushThis                    //   previous frame will restore the state of this call)
│   pushState                     // Save index of restore point foo() as `state` to switch by them when restore.return default(return_type)   // Return 0 or null, depends of what return type this method has actually
└ no_save_context:                // Continuing the method execution

Important note on exceptions handling. The generated code (code inside [restore_point; no_active_coro) and inside [after_call; no_save_context)) must not be in any try-catch block to avoid stack imbalance. Assume that inside pushStack() or pushLocals() can be an exception (actually there are no exceptions are expected, but bytecode verifier checks it formally). It will cause that exception handler will MAY be called with unexpected operands stack state, and verifier will generate error on this. Therefore, we split try-catch blocks to avoid generated code occur inside any original try-catch.

│-block - code to be excluded from any try-catch block

║-block - code allowed to present inside try-catch blocks

Call instrumenting (unpatchable strategy)

If we deal with call that cannot be instrumented (JDK call or obfuscated library), we can wrap the call like this:

┌  restore_point:                 // We will be here if we need to restore state before calling foo()
│    popLocals                    // Restore locals from Coro storage
│    popStack                     // Restore operands stack (excluding args of method will be called) from Coro
│    popArgs
│  no_active_coro:                // We will be here if a) no need to restore state b) we've restored state just now
│    saveArgsToTempStorage
│  before_call:
│║   call foo()                   // Make call of original method. If exception occurs, going to `exception` label
│  after_call:if (!isYielding)             // If there was no yielding happened inside foo(), we go to no_save_context
│      goto no_save_context
│    saveArgs                     //
│    pushStack                    // Save the operands stack into Coro storage
│    pushLocals                   // Save the locals into Coro storageif (!isStatic)               // If *this* method is not static, saving reference to `this` (it will be used when
│      pushThis                   //   previous frame will restore the state of this call)
│    pushState                    // Save index of restore point foo() as `state` to switch by them when restore.return default(return_type)  // Return 0 or null, depends of what return type this method has actually
│  no_save_context:
│     cleanTempStorage            // Remove the args stored in temp storage
│     goto continue
│  exception:
│     cleanTempStorage            // Remove the args stored in temp storage
│     athrow
└  no_exception:                  // Continuing the method execution

Here we deal with exceptions more simple. We just tell our bytecode tool to exclude all this code from any try-catch blocks, but we manually add one handler on call foo() and target it to exception label. If any exception occurs, we should clean the temp storage from stored args and then rethrow exception.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.