Dealing with Try/Catch Exceptions in Java bytecode? ("stack height inconsistent")
Asked Answered
D

3

6

I am trying to do some error handling in java bytecode. I first tried to implement some catch-like subroutines, where I would check for the error condition, and jump to the appropriate subroutine, a little like:

  iconst_1
  iconst_0
  dup
  ifeq calldiverr
  goto enddivtest
calldiverr:
  jsr divError
enddivtest:
  idiv

...More instructions...

divError:
  getstatic java/lang/System/out Ljava/io/PrintStream;
  ldc "Oh dear you divided by 0!"
  invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V

The problem with the above is that when I have multiple instructions that jump to this subroutine, I get an error message when running the bytecode, saying that the stack height is inconsistent.

Perhaps using exceptions is the best way to get around this?

From some googling I have found that you can create instances of Exception classes and initialise them with something like:

new java/lang/Exception
dup
ldc "exception message!"
invokespecial java/lang/Exception/<init>(Ljava/lang/String;)V

I have also found that you can throw them with athrow and this seems ok.

What is confusing me however is exactly how exceptions are caught. There seems to be a magical "Exception table" which glues the throwing and catching of exceptions together, but I do not know how to define one of these when writing bytecode from scratch (and assembling using Jasmin). Can somebody tell me the secret of creating an exception table? And possibly give me an example of exception handling that will assemble with jasmin?

Datestamp answered 12/12, 2011 at 22:36 Comment(0)
D
2

In the end I came up with a better solution than jsr - defining a method using .method in Jasmin. I just used invokestatic to call my error handler once I had detected the error.

For those looking for actual exception handling - I think that defining the exception table in Jasmin may be done using .catch, but I haven't looked into it as the method definition solved my problem.

Edit:

I did have to look at .catch in the end, and found it is really easy to use. It's documented here.

Datestamp answered 13/12, 2011 at 17:22 Comment(0)
N
0

First off its worth pointing out that class files from version 51.0 may not contain the jsr instruction. Repeat the code or use a method.

At every point in bytecode, the static type of every element in the frame must be known. Each frame is not a call stack.

Generally you don't want to play around with huge big stacks. Store temporaries in local variables to keep things simple.

If an exception is thrown then obviously the frame might have had the contents from any place the exception could have thrown. So the contents are discarded and replaced by the exception. You wouldn't be able to go back and resume to use the frame contents anyway.

Nilsanilsen answered 12/12, 2011 at 23:32 Comment(0)
J
0

The rules for verifying jsr are pretty complicated, and as Tom indicated the opcode is deprecated. So it's best avoided.

My memory on jsr is a bit fuzzy, but ...

(Updated) There is a rule in Java bytecode verification that wherever two control flows join together the stack depth must be identical along both branches. jsr subroutines are exempted from this rule to a point -- multiple exception points with different stack depths can "reach" the same jsr routine, but the net change in stack depth from jsr routine entry to subsequent ret must be zero (or actually minus 1, since the exception cause is always pushed on entry to the routine).

Further, while a jsr routine can "escape" and branch back into regular control flow, if it does so the jsr is not exempted from the stack depth rule for join points. This severely limits the situations where you can do this, since a jsr routine can potentially be entered with different stack depths.

(And I've no doubt still got some of that wrong, but it's the best I can do.)

(I don't quite understand how you're planning to "get around" your jsr problem with exceptions.)

(Also, Sun made bytecode writing much more complicated with 4 or 5 (can't remember which), making it pretty much impossible to hand-code bytecodes. They did this because they didn't know how to do verification fast enough to beat IBM's verifier otherwise, but that's another matter.)

Jocund answered 13/12, 2011 at 1:55 Comment(6)
Actually, it's perfectly legal to have a jsr which never rets. The restrictions on subroutines are 1) each subroutine can contain only a single ret (so you can't ret on both branches of an if without joining them for example). 2) subroutine calls must form a tree. Other than that, there are no restrictions beside those imposed by type checking.Politicize
The really complicated part is the precise definition of what is considered the body of the subroutine, since this depends on the details of the type checking algorithm. Fortunately, Hotspot is open source and you can determine this yourself as I have if you're interested. Here's the relevant file hg.openjdk.java.net/jdk7/jdk7/jdk/file/tip/src/share/native/…. Obviously, this is implementation dependent, so if you care about non-Hotspot vms, you should make sure your subroutines are well structured to avoid such ambiguities.Politicize
@Politicize -- Like I said, my memory of the rules for jsr is fuzzy -- haven't mucked with the verifier for 5-6 years now. IIRC, though, the rules for what's inside a subroutine are fairly straight-forward -- to be in subroutine a point must be reachable from the entry point, and a ret must be reachable from the point in question. Shouldn't be any ambiguity to that.Jocund
that's fine if your subroutines are isolated, but what happens when they jump into the rest of your code? The actual way things work is that the verifier maintains a stack of dominating jsr calls for each instruction. When control flow is merged, it takes a common subsequence, but the particular subsequence chosen has to do with the precise implementation details of the verifier. And no, you are not required to have a ret instruction.Politicize
The described way in the spec for following JSRs is not the simplest way -- requires unnecessary additional "hardware" in the verifier. It can all be done with simple extensions to the data flow -- making each JSR entry appear to be an "assignment". You're in the subroutine when the JSR dominates, according to data flow. (But like I said, it's been years since I wrote the thing, so I don't recall all the details.) But in any case with ANY join of two execution paths (other than via JSR) the stack depth must be equal on both paths. This is what the verifier is apparently complaining about.Jocund
Edit: On second thought, you're right. Even though the calculations are order dependent, the result is always the same. It's effectively just computing all dominators.Politicize

© 2022 - 2024 — McMap. All rights reserved.