Skip to content

Emulator: loop instruction using globalset are not setting contextreg registers correctly #9337

Description

@niooss-ledger

Describe the bug
When the emulator (ghidra.pcode.emu.PcodeEmulator) encounters a loop instruction in PIC, XTensa or ARCompact (with Pull Request #3006), it fails to keep the value of context registers set through globalset statements in Sleigh. Instead, it warns "Context applied somewhere other than the counter." and forgets the value. This makes emulation of loop instructions broken.

To Reproduce
Steps to reproduce the behavior:

  1. Apply the ARCompact support from Pull Request Add support for ARCompact instruction set #3006
  2. Build the pcodetest binaries
  3. Run the pcodetest test suite in Eclipse
  4. See multiple emitted warnings "Context applied somewhere other than the counter." and failed tests, such as test_BIOPS4

To ease reproducing this bug, I created a repository https://github.com/niooss-ledger/ghidra-arcompact-test where:

This output contains:

ram:00038b3c (mov.f lp_count,r2             ) r0=0x7e7e7e7f r1=0x40404040 r2=0x00000006 r3=0x00000006 r4=0x00000000
...
ram:00038b50 (lpne 0x00038b5c               ) r0=0x3e3e3e3f r1=0xbfbfbfc1 r2=0x00000006 r3=0x000000c0 r4=0x00000000
ram:00038b54 (add1.f r0,r1,r0               ) r0=0x3e3e3e3f r1=0xbfbfbfc1 r2=0x00000006 r3=0x000000c0 r4=0x00000000
ram:00038b58 (sub.cc r0,r0,r1               ) r0=0x3c3c3c3f r1=0xbfbfbfc1 r2=0x00000006 r3=0x000000c0 r4=0x00000000
ram:00038b5c (lsr r1,r0,r2                  ) r0=0x3c3c3c3f r1=0xbfbfbfc1 r2=0x00000006 r3=0x000000c0 r4=0x00000000
...
Emulation produced unexpected output: 255 != 254

Instruction lpne did not execute a loop correctly, which made __udivmodsi4 return an incorrect result. The reason why it did not execute was that it is implemented using globalset to set the value of a context register:

define context contextreg
  phase = (0,0)
  loopEnd = (1,1) noflow
;

...

  :lp^l_cc_not_value op4_lp_loop_end is l_sub_opcode6=0x28 & l_flag=0 & l_op_format=3 & l_m=1 & l_u6 & l_cc_not_value & op4_lp_loop_end
    [ loopEnd = 1; globalset(op4_lp_loop_end, loopEnd); ]
  {
...

Expected behavior
Using setglobal to a context register on an address different from the current instruction is expected to work.

Environment (please complete the following information):

  • OS: Debian 13 (Linux) on x86_64
  • Java Version: openjdk 25.0.3
  • Ghidra Version: branch master and version 12.1.2
  • Ghidra Origin: locally built

Additional context
I was updating Pull Request #3006 to a more recent version of Ghidra and ran the pcodetests again, which failed because of this issue. While I only tested on ARCompact, I am quite confident the same issue exists with XTensa loop:

# LOOP - Loop (BRI8), pg. 392.
:loop as, LoopOffset8 is LoopOffset8 & ar = 8 & as & at = 0b0111 & op0 = 6
[ loopMode=1; loopEnd = 1; globalset(LoopOffset8, loopEnd); ] {
LCOUNT = as - 1;
LBEG = inst_next;
LEND = &LoopOffset8;
}

and with PIC do:
@if defined(dsPIC33E) || defined(dsPIC33C)
:do k15_t,n16_t is OP_23_20=0x0 & OP_19_16=0x8 & OP_15=0x0 & k15_t ;
OP_23_20=0x0 & OP_19_16=0x0 & n16_t & WordInstNext
[ blockEnd=1; globalset(n16_t,blockEnd); ]
{
# stack 4 levels deep but we don't enforce this
DL:2 = zext(CORCON_DL);
*[register]:2 (&:2 DCOUNT + DL*2) = k15_t + 1;
*[register]:3 (&:2 DOEND + DL*2) = &n16_t;
*[register]:3 (&:2 DOSTART + DL*2) = WordInstNext;
CORCON_DL = CORCON_DL + 1;
}
@endif

The warning is generated from:

if (address.getOffset() != counter &&
!Objects.equals(pins.getAddress(), address)) {
Msg.warn(this, "Context applied somewhere other than the counter.");
return;
}

By the way, I asked Claude Code (model Opus 4.8) to fix this issue and it generated the following patch: future_context.patch

Fix suggested by Claude Code
diff --git a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java
index 88cb52b59dc9..53d7f0705830 100644
--- a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java
+++ b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DefaultPcodeThread.java
@@ -253,6 +253,16 @@ public class DefaultPcodeThread<T> implements PcodeThread<T> {
 	private Address counter;
 	private RegisterValue context;
 
+	/**
+	 * Processor-context values committed (via Sleigh {@code globalset}) to an address other than
+	 * the instruction that produced them. These are recorded here and applied once the program
+	 * counter actually reaches the target address. This is what allows Sleigh constructs that set
+	 * context at a future address &mdash; notably zero-overhead hardware loops (e.g.,
+	 * ARCompact/Xtensa {@code LP}, dsPIC {@code DO}) which mark their loop-end address &mdash; to
+	 * function under emulation, rather than being silently dropped.
+	 */
+	private final Map<Address, RegisterValue> futureContexts = new HashMap<>();
+
 	protected Instruction instruction;
 	protected PcodeFrame frame;
 
@@ -519,6 +529,26 @@ public class DefaultPcodeThread<T> implements PcodeThread<T> {
 	}
 
 	public static RegisterValue getContextAfterCommits(Instruction instruction, long counter) {
+		return getContextAfterCommits(instruction, counter, null);
+	}
+
+	/**
+	 * Compute the processor context resulting from the given instruction's context commits.
+	 *
+	 * <p>
+	 * Commits targeting the (next) counter or the instruction's own address are folded into the
+	 * returned value, which becomes the context used to decode the next instruction. Commits
+	 * targeting any other address are recorded in {@code futureContexts} (when non-null), keyed by
+	 * the target address, so they can be applied later, when the program counter actually reaches
+	 * that address. Passing {@code null} discards such commits (the historical behavior).
+	 *
+	 * @param instruction the instruction whose commits to apply
+	 * @param counter the offset of the (next) program counter
+	 * @param futureContexts a map collecting commits to other addresses, or null to discard them
+	 * @return the context to apply at the counter
+	 */
+	public static RegisterValue getContextAfterCommits(Instruction instruction, long counter,
+			Map<Address, RegisterValue> futureContexts) {
 		PseudoInstruction pins = (PseudoInstruction) instruction;
 		Language language = instruction.getPrototype().getLanguage();
 		try {
@@ -531,12 +561,15 @@ public class DefaultPcodeThread<T> implements PcodeThread<T> {
 					if (!value.getRegister().isProcessorContext()) {
 						return;
 					}
-					if (address.getOffset() != counter &&
-						!Objects.equals(pins.getAddress(), address)) {
-						Msg.warn(this, "Context applied somewhere other than the counter.");
-						return;
+					if (address.getOffset() == counter ||
+						Objects.equals(pins.getAddress(), address)) {
+						ctxVal = ctxVal.assign(value.getRegister(), value);
+					}
+					else if (futureContexts != null) {
+						RegisterValue existing = futureContexts.get(address);
+						futureContexts.put(address,
+							existing == null ? value : existing.combineValues(value));
 					}
-					ctxVal = ctxVal.assign(value.getRegister(), value);
 				}
 			};
 			parserCtx.applyCommits(procCtx);
@@ -548,7 +581,7 @@ public class DefaultPcodeThread<T> implements PcodeThread<T> {
 	}
 
 	protected RegisterValue getContextAfterCommits() {
-		return getContextAfterCommits(instruction, counter.getOffset());
+		return getContextAfterCommits(instruction, counter.getOffset(), futureContexts);
 	}
 
 	/**
@@ -568,6 +601,13 @@ public class DefaultPcodeThread<T> implements PcodeThread<T> {
 					.combineValues(defaultContext.getDefaultValue(contextreg, counter))
 					.combineValues(defaultContext.getFlowValue(context))
 					.combineValues(getContextAfterCommits());
+			// Apply any context previously committed (via globalset) to this address. This is kept
+			// after being applied, so an address reached repeatedly (e.g., the end of a
+			// zero-overhead hardware loop) sees the committed context on every pass.
+			RegisterValue future = futureContexts.get(counter);
+			if (future != null) {
+				ctx = ctx.combineValues(future);
+			}
 			writeContext(ctx);
 		}
 		postExecuteInstruction();

This patch builds fine and makes the whole pcodetests suite pass for ARCompact (and makes function __udivmodsi4 returns the right values). While the changes look simple, adding a map of future contextreg values probably incurs some side-effects in the Emulator API, which is why I am opening an issue to discuss first, before eventually opening a PR with changes which would be tested more thoroughly.

What do you think?

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions