Garbage Byte After Conditional Jump
An always-taken conditional jump is followed by a garbage opcode byte that a linear-sweep disassembler wrongly consumes, derailing the listing.
This is the textbook anti-disassembly primitive: set the flags to a known value, take a conditional jump that is therefore always taken, and place a single junk byte immediately after the jump in the fall-through gap that execution never visits. Because real control flow always branches away, the junk byte is dead — but a linear-sweep disassembler decodes straight through and reads the junk byte as the start of a (usually multi-byte) instruction.
The chosen junk byte is the first byte of a long instruction — an opcode like
0xE8 (call rel32, 5 bytes) or 0xE9 (jmp rel32, 5 bytes) — so the sweep
greedily swallows the next several legitimate bytes as a phantom operand. The
disassembler emerges from the phantom instruction mis-aligned, and every
instruction it decodes afterwards is wrong until it happens to resynchronise.
One byte of junk can corrupt many lines of the listing.
How it works
; Bytes: 33 C0 74 01 E8 8B 4D 08 ...
00: 33 C0 xor eax, eax ; ZF := 1 (always)
02: 74 01 jz short 0x05 ; ALWAYS taken because ZF is 1
04: E8 ; <- GARBAGE byte. 0xE8 = "call rel32" opcode
05: 8B 4D 08 mov ecx, [ebp+8] ; the REAL next instructionExecution: xor eax,eax forces ZF=1, so jz short 0x05 is always taken and
lands on offset 0x05, the real mov ecx, [ebp+8]. The byte at 0x04 is never
reached. But a linear sweep does not evaluate the predicate. After decoding the
jz it continues at 0x04, reads 0xE8 as call rel32, and consumes the four
real bytes 8B 4D 08 ... as the call's displacement:
; What the linear sweep WRONGLY produces:
02: 74 01 jz short 0x05
04: E8 8B 4D 08 ?? call <garbage> ; ate the real mov + one more byte
09: ... ; desynchronised from here onThe five real bytes starting at 0x05 are now buried inside a phantom call,
and the listing stays wrong until it realigns. Any flag-setting instruction that
fixes ZF works — xor reg,reg, sub reg,reg, cmp al,al, test reg,reg — and
the inverse pairing (jnz after a guaranteed ZF=0) is equally common.
Detection & analysis
Static analysis:
- Spot the pattern: a flag-deterministic instruction (
xor reg,reg,sub reg,reg,test/cmpof equal operands) feeding ajcc, with an opcode-shaped byte (E8,E9,0F,FF) sitting immediately after the jump. - If IDA/Ghidra shows a
call/jmpwith a nonsensical target right after a conditional jump, undefine the byte after thejccand force a re-decode at the branch target — the listing realigns and the real instructions reappear. - The fall-through byte having no incoming code xref while the branch target does is the structural tell.
Dynamic analysis:
- Single-step the conditional jump: it is taken on every run, and the junk byte is never the address of an executed instruction. A short trace confirms the byte is dead filler.
Detection rule hint:
Flag any jcc whose flags were set to a constant by the immediately preceding
instruction (always-taken predicate) and whose fall-through byte is a long-
instruction opcode (E8/E9/0F xx/FF) that is not the start of any
recursively-reachable instruction. This exact pairing — deterministic predicate
plus unreachable opcode-shaped byte — is the canonical garbage-byte signature and
almost never appears in compiler output.