Skip to content

Fix Thumb2 conditional branch encoding with absolute targets - #607

Closed
xintenseapple wants to merge 1 commit into
keystone-engine:masterfrom
trail-of-forks:fix/thumb2-cond-branch-abs-target
Closed

Fix Thumb2 conditional branch encoding with absolute targets#607
xintenseapple wants to merge 1 commit into
keystone-engine:masterfrom
trail-of-forks:fix/thumb2-cond-branch-abs-target

Conversation

@xintenseapple

Copy link
Copy Markdown

Fixes #451.

Problem

Thumb2 conditional branches to an absolute address are encoded from the raw target address rather than a PC-relative offset, so they branch to the wrong location.

$ kstool thumb "bne 0x15f0" 0x1248
bne 0x15f0 = [ 41 f0 f8 82 ]     # decodes to 0x283c, not 0x15f0

Every wide (T3) conditional branch is affected, including branches to a nearby instruction. Backward branches additionally fail to set the sign bit.

Root cause

This is not in the t2Bcc bit-shifting in ARMGenMCCodeEmitter.inc, which is where the issue report pointed. Patching that code changes nothing, because the value reaching it is already wrong.

ARMMCCodeEmitter::getBranchTargetOpValue() dispatched Thumb2 to the generic ::getBranchTargetOpValue() helper, which returns immediate operands unchanged. The T3 encoder then extracted S/J1/J2/imm6/imm11 from what it assumed was a PC-relative offset, but was in fact the absolute target address.

The unconditional t2B path already does this subtraction:

Val = (MO.getImm() - MI.getAddress() - 4) >> 1;

The conditional path simply omitted it. That is why b, bl, blx, and the narrow T1 conditional form are all correct, and only the wide conditional form is broken.

Fix

Mirror the t2B structure: expression operands still take the fixup_t2_condbranch path (unchanged — that path was already correct), while immediates are converted to a PC-relative byte offset. The value is left as a byte offset because the existing encoder masks shift out the low bit themselves.

Verification

$ kstool thumb "bne 0x15f0" 0x1248
bne 0x15f0 = [ 40 f0 d2 81 ]     # decodes to 0x15f0

This matches the encoding expected in the issue report.

  • Round-tripped 14 offsets through a T3 decoder — forward, backward, and the ±1MB range limits — as both bne and forced-wide bne.w. All decode back to the requested target.
  • Short offsets still correctly select the narrow T1 encoding.
  • ARM mode, b/bl/blx/cbz, and all condition codes are unaffected.
  • ARM regression tests in suite/regress show no change versus master. arm_sym_resolver.py and arm_sym_resolver_thumb.py fail both with and without this change (pre-existing, unrelated to branch encoding).

Note: suite/regress/regress.py halts partway through on a pre-existing Python 2 syntax error in x86_issue293.py (except Exception, e:), so the tests above were run individually.

🤖 Generated with Claude Code

Thumb2 conditional branches to an absolute address were encoded from the
raw target address instead of a PC-relative offset, producing branches to
the wrong location.

ARMMCCodeEmitter::getBranchTargetOpValue() dispatched Thumb2 to the generic
helper, which returns immediate operands unchanged. The T3 encoder then
extracted S/J1/J2/imm6/imm11 from what it assumed was a PC-relative offset
but was actually the absolute address. The unconditional t2B path already
subtracts the instruction address; the conditional path did not.

Assembling "bne 0x15f0" at 0x1248 emitted 41 f0 f8 82, which decodes to
0x283c. It now emits 40 f0 d2 81, which decodes to 0x15f0. Every wide
conditional branch was affected, including backward branches, which failed
to set the sign bit. Symbolic targets are unaffected: they still take the
fixup_t2_condbranch path, which was already correct.

Fixes keystone-engine#451

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xintenseapple

Copy link
Copy Markdown
Author

Closing as a duplicate of #601, which was opened first and fixes the same root cause with a smaller diff.

I verified both patches produce byte-for-byte identical output across ARM and Thumb b/bl/blx/bcc/cbz/adr. My version moved the correction into the Thumb2 caller out of concern that the shared helper had 16 callers, but on checking each one, they all guard with if (MO.isExpr()) and never reach the immediate path — so #601's one-line change is safe and preferable.

Detailed verification posted at #601. My analysis of the root cause remains in #451 for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keystone can not encode thumb2 bcc instructions correctlly

1 participant