Dalvik exception handler
Hex-Rays' Dalvik decompiler uses the exception tables stored in DEX files to rebuild Java exception constructs in the decompilation: try, catch, multi-catch (catch ( A | B e )), finally and synchronized. This page explains how these constructs are encoded in Dalvik bytecode and describes cases where the decompiler cannot rebuild the original source construct and shows a lower-level equivalent instead.
The documentation describes:
How Java exception constructs are encoded in Dalvik bytecode. It is recommended that users read this first.
How
finallyblocks are recognized.Known limitations, with examples:
try-with-resources is shown as a plaintry/catch-all.finallywith multiple exits is shown as atry/catch-all.finallyblocks that the compiler optimized differently in each copy (eg. due constant value propagation) are shown as atry/catch-all.
BACKGROUND ON DALVIK EXCEPTIONS
TRY RANGES AND HANDLERS
A DEX method may have an exception table. The table is a list of try ranges (address ranges of bytecode), and each range has a list of handlers. A handler is either:
a typed handler, e.g.
catch(java.io.IOException) @35C: taken when the thrown object is an instance of that class;a catch-all handler, e.g.
catch-all @29C: taken for anyThrowable.
This is all the table contains. Dalvik bytecode has no finally, synchronized or try-with-resources. The Java compiler (javac) and the DEX compiler (d8/dx) turn these constructs into ordinary code plus catch-all handlers. The decompiler has to recognize the patterns they produce to show the source constructs again.
A catch-all handler that the decompiler cannot turn into finally or synchronized is shown as:
catch ( Throwable v3 )
{
...
}HELPER CALLS
Internally, the decompiler marks the try ranges and handlers from the exception table with helper calls. Later these calls are turned into try, catch, finally and synchronized constructs and removed. The main ones are:
__eh_try()
beginning of a try block
__eh_catch()
beginning of the list of handlers for the try block
__eh_catch_type(Type)
a catch handler for exception type Type, e.g. IOException
__eh_catch_ellipsis()
a catch-all handler (finally, synchronized or catch ( Throwable ))
Normally these calls never appear in the output. In very rare cases, when the decompiler cannot build a structured construct from them (for example, because of unusual control flow in the try range or the handler), some of them may remain in the decompilation. The code is still correct: the helper calls just mark where the exception regions and handlers are.
HOW FINALLY IS COMPILED
Consider:
The compiler copies the finally block into every place where it runs:
At the end of each normal exit from the
tryblock (falling through,return,break,continue), the copy runs before control leaves the block.In a catch-all handler that covers the
tryblock (and anycatchblocks). This copy runs thefinallycode and rethrows the exception.
The bytecode looks like this:
The decompiler rebuilds finally by comparing the catch-all handler body with the code on every normal exit. If each exit has an identical copy of the handler body (without the final throw), it removes the copies and shows a finally block.
HOW SYNCHRONIZED IS COMPILED
synchronized ( obj ) { ... } is compiled to monitor-enter obj, the body, and monitor-exit obj on every exit. A catch-all handler also runs monitor-exit obj and rethrows. The decompiler recognizes this pattern and shows a synchronized block. Nested blocks and blocks that lock the same object again are supported.
KNOWN LIMITATIONS
In all the cases below the decompiled code is semantically correct: it shows what the bytecode does. It is only less close to the original source than it could be. Look for this pattern:
It usually means the source had a finally block (or a try-with-resources) that was not rebuilt.
1. TRY-WITH-RESOURCES IS SHOWN AS A PLAIN TRY/CATCH-ALL
try-with-resources has no bytecode representation either. javac expands it into a try block, a catch-all handler that closes the resource, a nested try around close(), and a call to Throwable.addSuppressed() if close() also throws. When the app targets old Android API levels, d8 also replaces the addSuppressed() call with a call to a synthetic helper class (backporting), which calls addSuppressed through reflection.
The decompiler does not rebuild try ( ... ). The expanded code is shown as it is.
Java source:
Decompilation:
The synthetic helper that d8 generated:
How to read the output:
v2 = new FileOutputStream(p0)just before thetryis the resource declaration.The
trybody is the originaltry-with-resources body.catch ( Throwable v3 ) { try { v2.close(); } catch ( Throwable v4 ) { ...addSuppressed... } throw v3; }is the automatic close when an exception is thrown.v2.close()after thetryis the automatic close on the normal path.
When the try-with-resources is inside another try, or has its own catch clauses, the expanded code is nested inside the outer try/catch:
which came from:
2. FINALLY WITH MULTIPLE EXITS IS SHOWN AS A TRY/CATCH-ALL
If the try block has several exits (several return statements, throw, break, ...), the compiler creates one copy of the finally block for each exit. Each copy is then optimized with what the compiler knows at that exit. Often the copies are also merged with the code after them (for example, the return). The copies are no longer identical to the catch-all handler, so the decompiler cannot remove them. The catch-all handler is shown as a catch ( Throwable ... ) and the finally code shows up in every exit path.
Java source:
Decompilation:
Note:
There are four exits: the
throw, the tworeturnstatements, and the fall-through aftercounter *= 2. Each has its own copy ofreturn finFunc(counter).The
returninfinallyoverrides thereturn counter * 3andreturn counter * 4statements. The compiler removed the dead multiplications, which is why they do not appear in the output.In each copy
counterwas replaced by the value known at that point:finFunc(1)forcounter == 1,finFunc(0)in the handler.The compiler only kept a
tryrange around the instruction that can throw (throw new Exception()), so thetryblock is small and only contains that path.
MISCELLANEOUS
A catch-all handler is shown as
catch ( Throwable vN ). A handler that does not use the exception object is shown without a variable name, e.g.catch ( Exception e ).A handler for several types with the same body is shown as a multi-catch:
catch ( NullPointerException | ArithmeticException v6 ).As with other compilers, DEX
tryranges only cover the instructions that can throw.tryblocks in the decompilation can therefore start later or end earlier than in the source. Statements that cannot throw can be shown just before or after thetryblock instead of inside it.
Last updated
Was this helpful?
