This concept is about a trap so specific and so well-hidden that it is worth a whole lesson on its own: the class file format uses two different conventions for what a branch offset means, both of them inside the same attribute, and nothing in the bytes says which is which.
Offsets in instructions are relative to the address of the branch instruction itself. Offsets in the exception table are absolute positions in the code array. Both are two-byte or four-byte unsigned or signed numbers. Both appear in the same Code attribute. A reader that applies the wrong convention does not error — it computes a destination that is a valid offset, just not the intended one.
And the error is not a constant. It is the position of the branch, so every branch in a method is wrong by a different amount. A disassembler built on the wrong convention produces output that looks structurally perfect: real offsets, real instruction boundaries, plausible destinations. It is wrong everywhere and visibly so only if you know where to look.
Both conventions, stated side by side, because the whole point is that they are easy to confuse:
Convention A is the anomaly and it is worth asking why. The answer is position independence, and it is a good reason. An absolute offset would mean that moving a method's code within the array, or the method within the class, required rewriting every branch. A relative offset survives both, because the branch and its target move together. The same reasoning is why an x86 or ARM jmp is relative and why a PE import thunk is a relative jmp.
But note the cost, because it is not free: the relative offset cannot be read without knowing where the instruction is, which means a reader that scans for opcodes must track a position even for instructions it does not care about, and a disassembler cannot print a target without also printing the instruction's own address. The absolute tables have no such coupling. Convention A buys relocatability with a dependency on position; conventions B and C buy simplicity with a dependency on stability. A format that never moves code could use absolute everywhere, and this one cannot.
Before the two conventions can be demonstrated they have to be read past a padding rule, because the two switch opcodes are the only instructions in the format with variable-length operands that are not length-prefixed:
tableswitch, 48 bytes of code, opcode at offset 1:
00: 1b iload_1
01: aa tableswitch
02: 00 00 2 bytes of PADDING
04: 00 00 00 2d default = 45
08: 00 00 00 00 low = 0
0c: 00 00 00 04 high = 4
10: 00 00 00 23 target[0] = 35 <- RELATIVE, not absolute
14: 00 00 00 25 target[1] = 37
18: 00 00 00 27 target[2] = 39
1c: 00 00 00 29 target[3] = 41
20: 00 00 00 2b target[4] = 43
24: 04 ac iconst_1 ; ireturn
...
The padding exists to 4-byte-align the operands, and the alignment is measured from the start of the code array, not from the instruction. The opcode is at offset 1, the operands would start at 2, and the next 4-byte boundary is 4 — so exactly two bytes of padding. Every one of those numbers is checkable against the file, and the arithmetic above is what this course's decoder reads.
Now apply convention A. The stored targets are 35, 37, 39, 41, 43 and the default is 45. The instruction is at offset 1, so the real destinations are 36, 38, 40, 42, 44 and 46. And here is the confirmation, from javap -c on the same file:
$ javap -c Shapes.class
1: tableswitch // 0 to 4
0: 36
1: 38
2: 40
3: 42
4: 44
default: 46
-- end of switch
36: iconst_1
38: iconst_2
40: iconst_3
42: iconst_4
44: iconst_5
46: iconst_0
Six numbers, and every one is the stored value plus one. The stored values are not destinations; they are displacements, and the only thing that turns them into addresses is knowing where the instruction was.
The lookupswitch in the same class behaves identically, with a different layout — a match/target pair list instead of a dense range, and a count rather than a high value:
00: 1b iload_1
01: ab lookupswitch
02: 00 00 2 bytes of padding, same rule
04: 00 00 00 2c default = 44
08: 00 00 00 03 npairs = 3
0c: 00 00 00 01 00 00 00 23 match 1 -> 35 -> real 36
14: 00 00 00 64 00 00 00 26 match 100 -> 38 -> real 39
1c: 00 00 27 10 00 00 00 29 match 10000 -> 41 -> real 42
Here is the contrast, and it needs no arithmetic at all — the numbers javap prints are the numbers in the file:
$ javap -c Shapes.class (trycatch, same class)
Exception table:
from to target type
0 5 10 Class java/lang/ArithmeticException
0 5 18 Class java/lang/RuntimeException
0 5 27 any
10 13 27 any
18 22 27 any
27 29 27 any
Offset 10 is where the ArithmeticException handler begins, and the file says 10. No adjustment, and that is the point. The same two-byte field, in the same attribute, twenty bytes further on, means something different. A reader that applied convention A here would add each handler's own position — and handler 1's position is 10, so it would report 20 and be wrong by ten, while handler 3 at position 27 would be reported as 54 and be wrong by twenty-seven. Three handlers, three different errors, none of them the same as the other.
Two further details in that table, both easy to get wrong. to is exclusive: the first row protects [0, 5), which is bipush 100; iload_1; idiv; istore_2 — the division and its result, and not the ifle that follows. And rows are tried in order, so the three rows covering [0, 5) form a chain: arithmetic first, then runtime, then any. That is the compiled form of a catch list followed by a finally, and the any rows at offsets 10, 18 and 27 are the finally block being entered from each exit path.
Code attribute, and making it relative would buy nothing while making every handler lookup an addition. The tables are index-like and the branches are code-like, and each gets the convention its own kind of thing deserves. The trap exists only because the format put both inside one attribute — a different design would have put the exception table in its own top-level block, where its offsets would be obviously a different kind of number, as a COFF relocation table and a DWARF line table both do.One more instruction-encoding detail, and the reason it exists is arithmetic rather than design. A local variable index is one byte, which caps it at 255 — and 256 does not fit. So there is a prefix opcode that widens the index that follows:
c4 36 01 00 wide istore 256 4 bytes
36 01 istore 1 2 bytes
36 ff istore 255 2 bytes, the maximum
without the prefix
0xc4 widens exactly ten opcodes: iload, lload, fload, dload,
aload, istore, lstore, fstore, dstore, astore -- plus ret and
iinc. Nothing else may carry it.
verified on a generated class with 300 live locals:
max_locals = 303
96 real wide prefixes
the first is c4 36 01 00 = wide istore 256, at code offset 1139
the preceding instruction is an unprefixed istore 255
The prefix appears at exactly 256 and not before, because that is the first index a single byte cannot hold. This is the same class of decision as the ELF header's choice of field widths, and the same trap it creates: a reader that assumes a single instruction width will desynchronise at exactly one point, and the bytes there are a plausible opcode followed by a plausible index. The mitigation is the same too — decode the instruction stream and check the position against code_length, which is the assertion from the previous concept, because here it is the only thing that will notice.
One number that confirms both conventions at once, which is the most efficient verification this course has found. The StackMapTable of the tableswitch method is eight bytes:
00 06 24 01 01 01 01 01
| | +-- five more frames, each one byte, frame_type 0x01
| +----- frame_type 0x24 = 36, a same_frame at offset 36
+-------- 6 frames
a StackMapTable frame's offset is ABSOLUTE (convention C), and
frame N's offset is:
first frame: offset = frame_type
later frames: offset = previous_offset + frame_type + 1
which gives:
frame 0: 36
frame 1: 36 + 1 + 1 = 38
frame 2: 38 + 1 + 1 = 40
frame 3: 42
frame 4: 44
frame 5: 46
So the six frames are at offsets 36, 38, 40, 42, 44, 46. And the switch's six destinations, after adding the opcode's address of 1, are 36, 38, 40, 42, 44, 46.
The same six numbers, from two different mechanisms. One is a table of absolute frame positions, read directly. The other is a list of relative displacements that only become positions once the instruction's own address is added. They agree to the byte, and that agreement is the proof: a relative branch that was misread as absolute would put the frames at 35, 37, 39, 41, 43, 45, which are all odd numbers pointing into the middle of instructions, and the frames would not line up with anything.
That is what a cross-check like this is for. Neither number is self-evidently right on its own — one is a table of integers and the other is a table of displacements, and a reader that has the arithmetic subtly wrong will produce a self-consistent wrong answer in both. Two independent derivations landing on the same six values is the only evidence that both are right, and it is much stronger than either table looking plausible.
What makes this particular cross-check available is a coincidence of the sample: the tableswitch method's only branches are its six destinations, so every frame has a branch and every branch has a frame. When you design a sample, designing it so two independent structures must agree is worth more than adding a tenth feature — and it is why the next concept can be written at all, since the frame offsets are the subject.
$ javap -c -p out/Shapes.class
$ python3 courses/jvm/assets/samples/class_decode.py out/Shapes.classhandler_pc. Handler 1 is at 10 and reports 20; handler 3 is at 27 and reports 54. Three handlers, three different errors, and the any rows all point at 27 so they all get the same wrong answer — which makes the failure look consistent when it is not.to exclusivity by hand. For the [0, 5) row, name the four instructions at offsets 0 to 4 and the one at offset 5. Confirm the division is inside the protected range and the comparison is not, which is what the compiler's try block actually covers.max_locals is 303, find the first 0xc4, and check the index it widens is exactly 256. Then look at the instruction immediately before it and confirm its index is 255 with no prefix, so the boundary is exactly where the arithmetic says it is.StackMapTable's six offsets, decode the switch's six destinations with the relative rule applied, and confirm they match. Doing this once by hand is the single most convincing check in the course, and it takes about ten minutes.Without looking back: in a 48-byte method, a tableswitch's opcode is at offset 1 and its five stored targets are 35, 37, 39, 41, 43 with a stored default of 45. What are the real destinations, how many bytes of padding precede the operands, and what would a reader report if it treated the stored values as absolute?
You are writing a bytecode-to-source mapper. It reports correct line numbers for most of a large codebase. For methods containing a tableswitch or a try/catch, it reports line numbers that are off, and the offsets it prints are always smaller than the ones javap prints. Everything else about those methods is correct: the instruction stream decodes cleanly, the method boundaries are right, the line table is read correctly. The error is always negative. What is the defect, what does the consistent sign of the error tell you, and what is the general habit for handling a field whose meaning depends on where it appears?
The general habit: an offset's meaning is a property of the table it came from, not of the field. Keep stored values stored, and resolve to addresses in one place — then the convention becomes a parameter you can pass rather than a habit you can forget.
Relative branch offsets are not a JVM peculiarity but the universal choice for anything that relocates, and the collection is full of the comparison. An ELF R_X86_64_PC32 relocation is the same idea at link time: the addend is a displacement, not an address, precisely so the linker can move code. A PE import thunk is a five-byte jmp through a relative pointer for the same reason. A COFF IMAGE_REL_I386_REL32 again. Every format that patches addresses into code uses a displacement rather than an absolute value, and every one of them is choosing relocatability over simplicity in exactly the same trade.
What the JVM adds is the table convention alongside the code one, and that is rarer. An ELF relocation's offset field is absolute — it says where in the section to patch — and so is a DWARF line-table entry's address, and so is a PE base relocation's target rva. So the JVM is not unusual in having two conventions; what is unusual is putting both inside one attribute, where nothing marks the difference. Every other format keeps them in separate structures — code in a section, tables in a section — so a reader knows which convention applies from where it is reading.
The switch padding is the same alignment discipline as WebAssembly's block-type lookahead in one sense and the exact opposite in another. Both are variable-width operands that have to be located before they can be read. WebAssembly's problem is a one-byte peek, which is cheap; the JVM's is a 4-byte alignment rule, which is why every disassembler for the JVM has had this bug at least once. Compare a PE section's alignment, where the same arithmetic appears at file-layout scale — the rule that a loader rounds an offset up to a power of two is the rule the switch is applying to its own operands.
The wide prefix connects to something this course has already established twice. It is the same shape as the ELF header choosing a 2-byte e_machine and a 4-byte e_entry to match the ranges each field needs, and the same shape as a format running out of address bits and growing a second header. The prefix exists because the format chose one byte for compactness and then had a value that did not fit, and the fix was to add an escape rather than widen the field — paying a byte on the rare instruction rather than a byte on every one. That is the correct trade and it is the same reasoning behind a variable-width integer in a different format.
Next: the verifier's data — where the six frame offsets computed above turn out to be the subject, and where a type checker writes its conclusions into the file the verifier is about to check.