How to use dexlib2 to instrument certain methods, especially allocating registers to add new instructions?

536 Views Asked by At

I'm using dexlib2 to programmatically instrument some methods in a dex file, for example, if I find some instructions like this:

invoke-virtual {v8, v9, v10}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;

I'd like to insert an instruction before it, and therefore at runtime I can know the exact arguments of Class.getMethod().

However, now I run into some questions about how to allocate registers to be used in my inserted monitoring instruction?

I know of two ways, but either way has its problems:

  1. I can use DexRewriter to increase the registerCount of this method (e.g from .register 6 to .register 9), so that I can have extra (3) registers to be used. But first this is restricted by 16 registers; second when I increase the registerCount, the parameters will be passed into the last ones, and therefore I have to rewrite all instructions in this method that use parameters, which is tiring.

  2. Or I can reuse registers. This way I have to analysis the liveness of every registers, while dexlib2 seems does not have existing API to construct CFG and def-use chain, which means I have to write it myself. Besides, I doubt whether by this way I can get enough available registers.

So am I understanding this problem right? are there any existing tools/algorithms to do this? Or any advice that I can do it in a better way?

Thanks.

2

There are 2 best solutions below

1
On

A few points:

  • You're not limited to 16 registers in the method. Most instructions can only address the first 16 registers, but there are mov instructions that can can use to swap values out with higher registers

  • If you can get away with not having to allocate any new registers, your life will be much easier. One approach is to create a new static method with your instrumented logic, and then add a call to that static method with the appropriate values from the target method.

  • One approach I've seen used is to increase the register count, and then add a series of move instructions at the beginning of the method to move all the parameter registers back down to the same registers they were in before you incremented the register count. This makes it so that you don't have to rewrite all the existing instructions, and guarantees that the new registers at the end of the range are unused. The main annoyance with this approach is when the new registers are v16 or higher, you'll have to do some swaps before and after the point at where they're used to get the value back down into a low register, and then restore whatever was in that register afterward.

1
On

You may code like this: if (it.opcode == Opcode.INVOKE_VIRTUAL || it.opcode == Opcode.INVOKE_STATIC) { logger.warn("${it.opcode.name} ${(it as DexBackedInstruction35c).reference}") } Format of Opcode.INVOKE_VIRTUAL is Format35c, so the type of instruction is DexBackedInstruction35c.