I am doing some sort of dalvik bytecode instrumentation using dexlib2. However, there are a couple of remaining issues. The register type merging that seems to happen after goto instructions and catch blocks, more precisely at the corresponding label, somehow derives an unexpected register type, which in turn breaks the instrumented code.
The instructions that get inserted look as follows:
move(-wide,-object,/16,/from16) vNew, v0
const-string v0, "some string"
invoke-static, {v0}, LPathToSomeClass;->SomeMethod(Ljava/lang/String;)V
move(..) v0, vNew
So, v0 is used to hold some parameter for the static function call, while vNew is a new (local) register to store and restore the original content of v0. The register type of v0 is derived in advance in order to derive the right move instruction, i.e. move-wide, move or move-object. However, when this code is included within some try-block, the instrumentation breaks. The output of baksmali (baksmali d -b "" --register-info ALL,FULLMERGE --offsets ) reveals that the type of v0 after the const-string instruction (which is Reference,L/java/lang/String) is considered as input for the merging procedure happening for instance at the corresponding catch-block label. Assuming that the type before the inserted code was Reference,[I (int array) the resulting type is now Reference,L/java/lang/Object (which produces a verification error), although the final move instruction restores the original register type.
Now to my questions:
1) When is this merging actually happening?
2) Why is the merging procedure considering the type of v0 after the const-string instruction? Is it considering every instruction modifying the type of any register?
3) Is this problem only related to try-catch blocks?
4) What are the restrictions for try-catch blocks in this matter?
5) Is there any solution to this problem apart from constructing an own method for each code to inject without parameters? So is it possible to use an additional register to solve this problem?
6) Can I detect with dexlib2 try-catch blocks and determine the set of instructions they include?
7) Are there any notes/literature discussing this problem, e.g. the merging procedure, and related technicalities, e.g. further limitations/restrictions for the instrumentation?
I highly appreciate any help in this matter. Thanks in advance!
When merging registers at the start of a catch block, there is an incoming edge from every instruction in the try block that can throw. Only certain instructions can throw - as determined by the CAN_THROW opcode flag.
In your particular example, the invoke-static instruction after the const-string instruction can throw, and so there's an edge from just before that instruction to the start of the catch block.
If you take a step back, execution can jump from any instruction in the try block that can throw to the start of the catch block. And so the code in the catch block must be prepared for the registers to be in a state that is consistent with the register contents just before any of those instructions that can throw.
So, for example, if there is one possible "jump" from the try block to the catch block where the register contains an primitive int type, and another possible jump where it contains an object, that register is considered "conflicted", because the the register may contain either type at that point in the code, and the two types are not compatible with each other. E.g. a primitive int can never be passed to something expecting a reference type and vice versa. And there's no mechanism in the bytecode for static register type checking.
One possible solution might be to split the try block at the point where you insert your instrumentation, so that the instrumentation itself is not covered by a try block, but both "sides" of the original code is. And keep in mind that in the bytecode, the same catch block can be used by multiple try blocks, so you can split the original try block into two, and have both reference the original catch block.
Otherwise, you'll just have to figure out some way to manage the registers appropriately to avoid this problem.
As for 6), see MethodImplementation.getTryBlocks(), which will give you a list of try blocks in that method. Each try block specifies where it starts, how many instructions it covers, and all of the catch blocks associated with it (different catch blocks for different exceptions).