For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 finally blocks are recognized.

  • Known limitations, with examples:

    • try-with-resources is shown as a plain try/catch-all.

    • finally with multiple exits is shown as a try/catch-all.

    • finally blocks that the compiler optimized differently in each copy (eg. due constant value propagation) are shown as a try/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 any Throwable.

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:

Function name
Description

__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:

  1. At the end of each normal exit from the try block (falling through, return, break, continue), the copy runs before control leaves the block.

  2. In a catch-all handler that covers the try block (and any catch blocks). This copy runs the finally code 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 the try is the resource declaration.

  • The try body is the original try-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 the try is 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 two return statements, and the fall-through after counter *= 2. Each has its own copy of return finFunc(counter).

  • The return in finally overrides the return counter * 3 and return counter * 4 statements. The compiler removed the dead multiplications, which is why they do not appear in the output.

  • In each copy counter was replaced by the value known at that point: finFunc(1) for counter == 1, finFunc(0) in the handler.

  • The compiler only kept a try range around the instruction that can throw (throw new Exception()), so the try block 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 try ranges only cover the instructions that can throw. try blocks 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 the try block instead of inside it.

Last updated

Was this helpful?