From 3749ccec47d0ff340ff2bcc1d5a06308d3ad42c0 Mon Sep 17 00:00:00 2001 From: Syed Rishad Date: Sat, 10 Oct 2026 22:45:35 -0400 Subject: [PATCH] feat(ghidra): recover signed AArch64 relative jump tables (#1592) * feat(ghidra): recover signed AArch64 relative jump tables Generalise the AArch64 relative-table matcher to signed byte/halfword entries, UXTW indexes, byte or LSL #2 offsets, B.CS/B.HS bounds and a CBZ route for zero entries. Cross-check it against Ghidra's own load table when one exists, and add a fixture modelled on the RenderScript blend kernel. Refs #487 * fix(ghidra): preserve typed labels for normalized switch selectors --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- bridge/ghidra/ReaGhidraBridge.java | 573 ++++++++++++------ docs/testing.md | 18 +- scripts/verify-real-ghidra-function.mjs | 107 ++++ scripts/verify-real-ghidra.mjs | 70 ++- tests/conformance/ghidra/README.md | 8 + .../ghidra/relative-switch-signed.S | 83 +++ 6 files changed, 649 insertions(+), 210 deletions(-) create mode 100644 tests/conformance/ghidra/relative-switch-signed.S diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index c29ed30d4..3f5692255 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -2004,9 +2004,20 @@ public final class ReaGhidraBridge extends HeadlessScript { ); JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables(); RecoveredAArch64RelativeTable recoveredAArch64 = - loadTables.length == 0 - ? recoverAArch64RelativeTable(jumpTable) - : null; + recoverAArch64RelativeTable(jumpTable); + if (recoveredAArch64 != null && loadTables.length > 0) { + // A decoded table only adds evidence when Ghidra's own load table + // describes the same bytes; any disagreement keeps Ghidra's view. + boolean agrees = false; + for (JumpTable.LoadTable loadTable : loadTables) { + if (loadTable.getAddress().equals(recoveredAArch64.tableAddress) && + loadTable.getSize() == recoveredAArch64.entrySize && + loadTable.getNum() == recoveredAArch64.mappings.size()) { + agrees = true; + } + } + if (!agrees) recoveredAArch64 = null; + } List
referencedDataAddresses = loadTables.length == 0 ? dispatchPathDataReferences(jumpTable) : List.of(); @@ -2040,7 +2051,7 @@ public final class ReaGhidraBridge extends HeadlessScript { source.add("evidence", evidence); dataSources.add(source); } - if (recoveredAArch64 != null) { + if (recoveredAArch64 != null && loadTables.length == 0) { JsonObject source = new JsonObject(); source.addProperty( "address", @@ -2055,9 +2066,16 @@ public final class ReaGhidraBridge extends HeadlessScript { inferenceEvidence( "jump-table", "ghidra-instruction-and-memory", - "Decoded " + recoveredAArch64.mappings.size() + + "Decoded " + recoveredAArch64.mappings.size() + " " + + (recoveredAArch64.signedEntries ? "signed" : "unsigned") + " entries of size " + recoveredAArch64.entrySize + " bytes from " + canonicalAddress(recoveredAArch64.tableAddress) + - "; the AArch64 LDRB-or-LDRH/ADR/ADD/BR chain uses a selector guarded by an unsigned upper-bound branch." + "; the AArch64 ADRP/ADD table, indexed load, ADR base, ADD" + + (recoveredAArch64.offsetShift == 0 ? "" : " LSL #" + recoveredAArch64.offsetShift) + + "/BR chain uses a selector bounded by CMP #" + recoveredAArch64.compareImmediate + + " and " + recoveredAArch64.guardMnemonic + " on one straight-line path" + + (recoveredAArch64.nullTarget == null + ? "." + : "; zero entries are routed by CBZ to " + canonicalAddress(recoveredAArch64.nullTarget) + ".") ) ); source.add("evidence", evidence); @@ -2096,6 +2114,33 @@ public final class ReaGhidraBridge extends HeadlessScript { result.add("data_sources", dataSources); Address[] targets = jumpTable.getCases(); TypedJumpTableCases typedCases = typedJumpTableCases(jumpTable, caseTokens); + // Table indices identify the register bounded by CMP. Typed labels may + // instead identify a selector before normalization (for example, SUB + // index, selector, #10). Do not merge those distinct value identities. + if (recoveredAArch64 != null && recoveredAArch64.priorSelectorDefinition != null) { + typedCases.limitations.add( + "REA decoded the relative table, but the compared selector has an earlier register definition at " + + canonicalAddress(recoveredAArch64.priorSelectorDefinition) + "; its table indices are not proven to use the typed switch selector's case values. Ghidra's typed labels are retained and additional case labels remain unknown." + ); + recoveredAArch64 = null; + } + if (recoveredAArch64 != null) { + for (Map.Entry typed : typedCases.cases.entrySet()) { + long value = typed.getKey(); + if (value < 0 || value >= recoveredAArch64.mappings.size()) { + typedCases.limitations.add("Typed case labels and decoded table indices do not use a verified common selector; Ghidra's typed labels are retained."); + recoveredAArch64 = null; + break; + } + RecoveredAArch64Mapping decoded = recoveredAArch64.mappings.get((int) value); + boolean divertedZero = decoded.nullEntry && typed.getValue().equals(recoveredAArch64.unreachableZeroTarget); + if (!typed.getValue().equals(decoded.targetAddress) && !divertedZero) { + typedCases.limitations.add("Typed case evidence and the decoded relative table disagree on a destination; Ghidra's typed labels are retained."); + recoveredAArch64 = null; + break; + } + } + } JsonArray mappings = new JsonArray(); JsonArray defaultTargets = new JsonArray(); for (Address target : typedCases.defaults) { @@ -2103,27 +2148,30 @@ public final class ReaGhidraBridge extends HeadlessScript { } Set
coveredTargets = new HashSet<>(typedCases.cases.values()); coveredTargets.addAll(typedCases.defaults); + if (recoveredAArch64 != null && recoveredAArch64.unreachableZeroTarget != null) { + Address unreachable = recoveredAArch64.unreachableZeroTarget; + coveredTargets.add(unreachable); + for (RecoveredAArch64Mapping recovered : recoveredAArch64.mappings) { + if (recovered.nullEntry && unreachable.equals(typedCases.cases.get((long) recovered.tableIndex))) { + typedCases.cases.remove((long) recovered.tableIndex); + } + } + typedCases.limitations.add( + "Ghidra recovered destination " + canonicalAddress(unreachable) + + " as the zero-offset decode of the relative table, but the CBZ before the dispatch diverts zero entries to " + + canonicalAddress(recoveredAArch64.nullTarget) + ", so the indirect branch never reaches it for an in-range case." + ); + } if (recoveredAArch64 != null) { for (int index = 0; index < recoveredAArch64.mappings.size(); index += 1) { monitor.checkCancelled(); RecoveredAArch64Mapping recovered = recoveredAArch64.mappings.get(index); if ( typedCases.unknownTargets.contains(recovered.targetAddress) || - typedCases.ambiguousValues.contains((long) recovered.caseValue) + typedCases.ambiguousValues.contains((long) recovered.tableIndex) ) continue; - Address typedTarget = typedCases.cases.get((long) recovered.caseValue); - if (typedTarget != null && !typedTarget.equals(recovered.targetAddress)) { - typedCases.cases.remove((long) recovered.caseValue); - typedCases.ambiguousValues.add((long) recovered.caseValue); - typedCases.unknownTargets.add(typedTarget); - typedCases.unknownTargets.add(recovered.targetAddress); - typedCases.limitations.add( - "Typed case evidence and the verified relative table disagree on a destination; both destinations remain unknown for that case." - ); - continue; - } JsonObject mapping = new JsonObject(); - mapping.addProperty("case_value", recovered.caseValue); + mapping.addProperty("case_value", recovered.tableIndex); String targetAddress = canonicalAddress(recovered.targetAddress); mapping.addProperty("target_address", targetAddress); mapping.addProperty("confidence", "high"); @@ -2132,14 +2180,18 @@ public final class ReaGhidraBridge extends HeadlessScript { inferenceEvidence( "jump-table", "ghidra-instruction-and-memory", - "Case value " + recovered.caseValue + (recoveredAArch64.entrySize == 1 ? " indexes byte " : " indexes halfword at byte offset ") + - (recovered.caseValue * recoveredAArch64.entrySize) + " at " + canonicalAddress(recoveredAArch64.tableAddress) + - "; the loaded unsigned entry selects a four-byte instruction offset from " + - canonicalAddress(recoveredAArch64.branchBase) + ", reaching " + targetAddress + "." + "Case value " + recovered.tableIndex + (recoveredAArch64.entrySize == 1 ? " indexes byte " : " indexes halfword at byte offset ") + + (recovered.tableIndex * recoveredAArch64.entrySize) + " at " + canonicalAddress(recoveredAArch64.tableAddress) + + (recovered.nullEntry + ? "; the entry is zero, so the CBZ before the dispatch branches to " + targetAddress + "." + : "; the loaded " + (recoveredAArch64.signedEntries ? "signed" : "unsigned") + + " entry " + recovered.encodedEntry + " selects a " + + (recoveredAArch64.offsetShift == 2 ? "four-byte instruction" : "byte") + + " offset from " + canonicalAddress(recoveredAArch64.branchBase) + ", reaching " + targetAddress + ".") ) ); mapping.add("evidence", evidence); - if (!typedCases.cases.containsKey((long) recovered.caseValue)) { + if (!typedCases.cases.containsKey((long) recovered.tableIndex)) { mappings.add(mapping); } coveredTargets.add(recovered.targetAddress); @@ -2213,7 +2265,7 @@ public final class ReaGhidraBridge extends HeadlessScript { ); } if (recoveredAArch64 == null && loadTables.length == 0 && currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) { - limitations.add("REA did not verify a supported byte/halfword relative dispatch chain with exact unsigned bounds and register definitions; unsupported encodings and ambiguous bounds remain unknown."); + limitations.add("REA did not verify a supported signed or unsigned byte/halfword relative dispatch chain with exact unsigned bounds and register definitions; unsupported encodings and ambiguous bounds remain unknown."); } if (targets.length == 0) { limitations.add( @@ -2343,12 +2395,21 @@ public final class ReaGhidraBridge extends HeadlessScript { } private static final class RecoveredAArch64Mapping { - private final int caseValue; + private final int tableIndex; private final Address targetAddress; + private final long encodedEntry; + private final boolean nullEntry; - private RecoveredAArch64Mapping(int caseValue, Address targetAddress) { - this.caseValue = caseValue; + private RecoveredAArch64Mapping( + int tableIndex, + Address targetAddress, + long encodedEntry, + boolean nullEntry + ) { + this.tableIndex = tableIndex; this.targetAddress = targetAddress; + this.encodedEntry = encodedEntry; + this.nullEntry = nullEntry; } } @@ -2356,24 +2417,70 @@ public final class ReaGhidraBridge extends HeadlessScript { private final Address tableAddress; private final Address branchBase; private final int entrySize; + private final Address priorSelectorDefinition; + private final boolean signedEntries; + private final int offsetShift; + private final String guardMnemonic; + private final long compareImmediate; + private final Address nullTarget; + private final Address unreachableZeroTarget; private final List mappings; private RecoveredAArch64RelativeTable( Address tableAddress, Address branchBase, int entrySize, + Address priorSelectorDefinition, + boolean signedEntries, + int offsetShift, + String guardMnemonic, + long compareImmediate, + Address nullTarget, + Address unreachableZeroTarget, List mappings ) { this.tableAddress = tableAddress; this.branchBase = branchBase; this.entrySize = entrySize; + this.priorSelectorDefinition = priorSelectorDefinition; + this.signedEntries = signedEntries; + this.offsetShift = offsetShift; + this.guardMnemonic = guardMnemonic; + this.compareImmediate = compareImmediate; + this.nullTarget = nullTarget; + this.unreachableZeroTarget = unreachableZeroTarget; this.mappings = mappings; } } + private static final Pattern AARCH64_DISPATCH_ADD = Pattern.compile( + "(?i)^([wx]\\d+),\\s*([wx]\\d+),\\s*([wx]\\d+)(?:,\\s*LSL\\s*#(?:0x)?([0-9a-f]+))?$" + ); + private static final Pattern AARCH64_TABLE_LOAD = Pattern.compile( + "(?i)^([wx]\\d+),\\s*\\[([wx]\\d+),\\s*([wx]\\d+)(?:,\\s*(LSL|UXTW)\\s*(?:#(?:0x)?([0-9a-f]+))?)?\\s*\\]$" + ); + private static final Pattern AARCH64_REGISTER_AND_ADDRESS = Pattern.compile( + "(?i)^([wx]\\d+),\\s*(0x[0-9a-f]+)$" + ); + private static final Pattern AARCH64_ADD_IMMEDIATE = Pattern.compile( + "(?i)^([wx]\\d+),\\s*([wx]\\d+),\\s*#?(0x[0-9a-f]+|[0-9]+)$" + ); + private static final Pattern AARCH64_COMPARE_IMMEDIATE = Pattern.compile( + "(?i)^([wx]\\d+),\\s*#?(0x[0-9a-f]+|[0-9]+)$" + ); + private static final Pattern AARCH64_MOVE_REGISTER = Pattern.compile( + "(?i)^([wx]\\d+),\\s*([wx]\\d+)$" + ); + private static final Set AARCH64_FLAG_PRESERVING = + Set.of("MOV", "ADRP", "ADR", "ADD"); + /** - * Recover only the directly verified AArch64 byte/halfword-indexed relative branch - * table form. Other instruction sequences keep their case labels unknown. + * Recover only directly verified AArch64 relative branch tables: + * ADRP/ADD table base, LDRB/LDRH/LDRSB/LDRSH entry load with an exact + * index scale, ADR branch base, optional CBZ on the loaded entry, and + * ADD (byte or LSL #2 scale)/BR, with a selector bounded by CMP #imm and an + * unsigned B.HI or B.CS/B.HS guard on one straight-line path. Any other + * instruction sequence keeps its case labels unknown. */ private RecoveredAArch64RelativeTable recoverAArch64RelativeTable( JumpTable jumpTable @@ -2395,167 +2502,265 @@ public final class ReaGhidraBridge extends HeadlessScript { if (instruction.getAddress().equals(jumpTable.getSwitchAddress())) break; } int branchIndex = instructions.size() - 1; - if (branchIndex < 1 || - !instructions.get(branchIndex).getMnemonicString().equalsIgnoreCase("BR")) { + if (branchIndex < 1 || !mnemonic(instructions.get(branchIndex)).equals("BR")) { + return null; + } + Set recoveredTargetSet = new HashSet<>(); + for (Address target : jumpTable.getCases()) recoveredTargetSet.add(canonicalAddress(target)); + if (recoveredTargetSet.isEmpty()) return null; + return matchAArch64RelativeDispatch( + instructions, + branchIndex, + recoveredTargetSet, + dispatchPathDataReferences(jumpTable) + ); + } + + private RecoveredAArch64RelativeTable matchAArch64RelativeDispatch( + List instructions, + int branchIndex, + Set recoveredTargetSet, + List
dataAddresses + ) throws Exception { + String branchRegister = normalizedRegister(operands(instructions.get(branchIndex))); + + // ADD Xd, Xn, Xm{, LSL #s}; BR Xd. LSL applies to Xm only. + Instruction dispatchAdd = instructions.get(branchIndex - 1); + if (!mnemonic(dispatchAdd).equals("ADD")) return null; + java.util.regex.Matcher addMatcher = AARCH64_DISPATCH_ADD.matcher(operands(dispatchAdd)); + if (!addMatcher.matches() || !addMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) return null; + if (!normalizedRegister(addMatcher.group(1)).equals(branchRegister)) return null; + int offsetShift = addMatcher.group(4) == null ? 0 : Integer.parseInt(addMatcher.group(4), 16); + if (offsetShift != 0 && offsetShift != 2) return null; + String firstSource = normalizedRegister(addMatcher.group(2)); + String secondSource = normalizedRegister(addMatcher.group(3)); + if (firstSource.equals(secondSource)) return null; + + // The entry register is the source whose latest definition is the + // table load; the other source must be defined by ADR. + int loadIndex = -1; + String entryRegister = null; + String baseRegister = null; + for (String candidate : offsetShift == 0 ? List.of(secondSource, firstSource) : List.of(secondSource)) { + int writer = latestWriter(instructions, branchIndex - 1, candidate); + if (writer >= 0 && Set.of("LDRB", "LDRH", "LDRSB", "LDRSH").contains(mnemonic(instructions.get(writer)))) { + loadIndex = writer; + entryRegister = candidate; + baseRegister = candidate.equals(firstSource) ? secondSource : firstSource; + break; + } + } + if (loadIndex < 0) return null; + + Instruction load = instructions.get(loadIndex); + String loadMnemonic = mnemonic(load); + int entrySize = loadMnemonic.endsWith("B") ? 1 : 2; + boolean signedEntries = loadMnemonic.startsWith("LDRS"); + java.util.regex.Matcher loadMatcher = AARCH64_TABLE_LOAD.matcher(operands(load)); + if (!loadMatcher.matches()) return null; + // A signed load into a W register sign-extends only to 32 bits; the + // 64-bit dispatch ADD would then read a zero-extended value. + if (signedEntries && !loadMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) return null; + String tableBaseRegister = normalizedRegister(loadMatcher.group(2)); + String indexRegister = normalizedRegister(loadMatcher.group(3)); + boolean wordIndex = loadMatcher.group(3).toLowerCase(Locale.ROOT).startsWith("w"); + String extend = loadMatcher.group(4) == null ? null : loadMatcher.group(4).toUpperCase(Locale.ROOT); + int indexShift = loadMatcher.group(5) == null ? 0 : Integer.parseInt(loadMatcher.group(5), 16); + if (wordIndex ? !"UXTW".equals(extend) : "UXTW".equals(extend)) return null; + if (indexShift != (entrySize == 1 ? 0 : 1)) return null; + + // Only ADR and one CBZ on the entry may sit between load and ADD. + Address nullTarget = null; + for (int index = loadIndex + 1; index < branchIndex - 1; index += 1) { + Instruction instruction = instructions.get(index); + String name = mnemonic(instruction); + if (name.equals("ADR")) continue; + if (name.equals("CBZ") && nullTarget == null) { + String[] parts = operands(instruction).split(","); + Address[] flows = instruction.getFlows(); + if (parts.length != 2 || flows.length != 1 || + !normalizedRegister(parts[0]).equals(entryRegister)) return null; + nullTarget = flows[0]; + continue; + } return null; } - Address[] recoveredTargets = jumpTable.getCases(); - Set recoveredTargetSet = new HashSet<>(); - for (Address target : recoveredTargets) recoveredTargetSet.add(canonicalAddress(target)); - List
dataAddresses = dispatchPathDataReferences(jumpTable); + int baseIndex = latestWriter(instructions, branchIndex - 1, baseRegister); + if (baseIndex < 0 || !mnemonic(instructions.get(baseIndex)).equals("ADR")) return null; + java.util.regex.Matcher baseMatcher = AARCH64_REGISTER_AND_ADDRESS.matcher(operands(instructions.get(baseIndex))); + if (!baseMatcher.matches()) return null; + Address branchBase = addressFromOperand(baseMatcher.group(2)); - for (int loadIndex = 0; loadIndex < branchIndex; loadIndex += 1) { - Instruction load = instructions.get(loadIndex); - int entrySize = load.getMnemonicString().equalsIgnoreCase("LDRB") ? 1 : load.getMnemonicString().equalsIgnoreCase("LDRH") ? 2 : 0; - if (entrySize == 0 || branchIndex != loadIndex + 2 || loadIndex < 3 || - !instructions.get(loadIndex - 1).getMnemonicString().equalsIgnoreCase("ADR") || - !instructions.get(loadIndex - 2).getMnemonicString().equalsIgnoreCase("ADD") || - !instructions.get(loadIndex - 3).getMnemonicString().equalsIgnoreCase("ADRP")) continue; - String loadOperands = operands(load); - java.util.regex.Matcher loadMatcher = java.util.regex.Pattern - .compile(entrySize == 1 ? "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+)\\]$" : "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+),\\s*LSL\\s*#(?:0x)?1\\]$") - .matcher(loadOperands); - if (!loadMatcher.matches()) continue; - String loadedRegister = normalizedRegister(loadOperands.substring(0, loadOperands.indexOf(','))); - String tableBaseRegister = normalizedRegister(loadMatcher.group(1)); - String indexRegister = normalizedRegister(loadMatcher.group(2)); + int tableAddIndex = latestWriter(instructions, loadIndex, tableBaseRegister); + if (tableAddIndex < 0 || !mnemonic(instructions.get(tableAddIndex)).equals("ADD")) return null; + java.util.regex.Matcher tableAddMatcher = AARCH64_ADD_IMMEDIATE.matcher(operands(instructions.get(tableAddIndex))); + if (!tableAddMatcher.matches() || + !normalizedRegister(tableAddMatcher.group(1)).equals(tableBaseRegister) || + !normalizedRegister(tableAddMatcher.group(2)).equals(tableBaseRegister)) return null; + int tablePageIndex = latestWriter(instructions, tableAddIndex, tableBaseRegister); + if (tablePageIndex < 0 || !mnemonic(instructions.get(tablePageIndex)).equals("ADRP")) return null; + java.util.regex.Matcher pageMatcher = AARCH64_REGISTER_AND_ADDRESS.matcher(operands(instructions.get(tablePageIndex))); + if (!pageMatcher.matches() || !normalizedRegister(pageMatcher.group(1)).equals(tableBaseRegister)) return null; + Address tableAddress = addressFromOperand(pageMatcher.group(2)).add(parseInteger(tableAddMatcher.group(3))); + if (!currentProgram.getMemory().contains(tableAddress) || + (!dataAddresses.isEmpty() && !dataAddresses.contains(tableAddress))) return null; - Address resolvedTableAddress = null; - for (int index = loadIndex - 3; index < loadIndex - 1; index += 1) { - Instruction instruction = instructions.get(index); - String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT); - String text = operands(instruction); - if (mnemonic.equals("ADRP") && - text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) { - String[] parts = text.split(","); - if (normalizedRegister(parts[0]).equals(tableBaseRegister)) { - resolvedTableAddress = addressFromOperand(parts[1]); - } - } - else if (mnemonic.equals("ADD") && resolvedTableAddress != null && - text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*#?(?:0x[0-9a-f]+|[0-9]+)$")) { - String[] parts = text.split(","); - if (normalizedRegister(parts[0]).equals(tableBaseRegister) && - normalizedRegister(parts[1]).equals(tableBaseRegister)) { - resolvedTableAddress = resolvedTableAddress.add(parseInteger(parts[2].trim().replaceFirst("^#", ""))); - } - } + // Unsigned guard: CMP Rs, #imm, then B.HI (imm + 1 entries) or + // B.CS/B.HS (imm entries), with only flag-preserving moves and + // address arithmetic between the compare and the load. + int guardIndex = -1; + for (int index = loadIndex - 1; index >= 0; index -= 1) { + String name = mnemonic(instructions.get(index)); + if (name.startsWith("B.")) { + guardIndex = index; + break; } - if (resolvedTableAddress == null || !currentProgram.getMemory().contains(resolvedTableAddress) || - (!dataAddresses.isEmpty() && !dataAddresses.contains(resolvedTableAddress))) continue; - Address tableAddress = resolvedTableAddress; - - Address branchBase = null; - boolean addressChain = false; - for (int index = loadIndex - 1; index < branchIndex; index += 1) { - Instruction instruction = instructions.get(index); - String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT); - String text = operands(instruction); - if (index < loadIndex && mnemonic.equals("ADR") && text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) { - int comma = text.indexOf(','); - String destination = normalizedRegister(text.substring(0, comma)); - if (!destination.equals(normalizedRegister(operands(instructions.get(branchIndex))))) continue; - branchBase = addressFromOperand(text.substring(comma + 1)); - } - if (index > loadIndex && mnemonic.equals("ADD") && branchBase != null && - text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*[wx]\\d+,\\s*LSL\\s*#(?:0x)?2$")) { - String[] parts = text.split(","); - String destination = normalizedRegister(parts[0]); - String base = normalizedRegister(parts[1]); - String offset = normalizedRegister(parts[2]); - if (destination.equals(base) && - base.equals(normalizedRegister(operands(instructions.get(branchIndex)))) && - offset.equals(loadedRegister)) { - addressChain = true; - } - } - } - if (!addressChain || branchBase == null) continue; - - int entryCount = -1; - Address defaultTarget = null; - for (int compareIndex = 0; compareIndex < loadIndex; compareIndex += 1) { - Instruction compare = instructions.get(compareIndex); - if (!compare.getMnemonicString().equalsIgnoreCase("CMP")) continue; - java.util.regex.Matcher compareMatcher = java.util.regex.Pattern - .compile("(?i)^([wx]\\d+),\\s*#?(0x[0-9a-f]+|[0-9]+)$") - .matcher(operands(compare)); - if (!compareMatcher.matches()) continue; - String comparedRegister = normalizedRegister(compareMatcher.group(1)); - long upperBound = parseInteger(compareMatcher.group(2)); - if (upperBound < 0 || upperBound >= 4096) continue; - for (int guardIndex = compareIndex + 1; guardIndex < loadIndex; guardIndex += 1) { - Instruction guard = instructions.get(guardIndex); - String mnemonic = guard.getMnemonicString().toUpperCase(Locale.ROOT); - if (!mnemonic.equals("B.HI")) continue; - Address[] flows = guard.getFlows(); - if (flows.length != 1 || !recoveredTargetSet.contains(canonicalAddress(flows[0]))) continue; - boolean supportedSequence = true; - for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex += 1) { - String sequenceMnemonic = instructions.get(sequenceIndex) - .getMnemonicString().toUpperCase(Locale.ROOT); - if (!Set.of("B.HI", "MOV", "ADRP", "ADD", "ADR").contains(sequenceMnemonic)) { - supportedSequence = false; - break; - } - } - Set selectorAliases = new HashSet<>(); - selectorAliases.add(comparedRegister); - Set boundedIndexes = new HashSet<>(); - if (compareMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) boundedIndexes.add(comparedRegister); - for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex++) { - Instruction instruction = instructions.get(sequenceIndex); - String[] parts = operands(instruction).split(","); - if (parts.length == 0) continue; - if (instruction.getMnemonicString().equalsIgnoreCase("MOV")) { - String destination = normalizedRegister(parts[0]); - String source = parts.length == 2 ? normalizedRegister(parts[1]) : ""; - boolean aliasesSelector = selectorAliases.contains(source); - boolean fullWidthBounded = boundedIndexes.contains(source) || - (parts[0].trim().toLowerCase(Locale.ROOT).startsWith("w") && aliasesSelector); - if (aliasesSelector) selectorAliases.add(destination); - else selectorAliases.remove(destination); - if (fullWidthBounded) boundedIndexes.add(destination); - else boundedIndexes.remove(destination); - } else if (Set.of("ADRP", "ADD", "ADR").contains(instruction.getMnemonicString().toUpperCase(Locale.ROOT))) { - selectorAliases.remove(normalizedRegister(parts[0])); - boundedIndexes.remove(normalizedRegister(parts[0])); - } - } - boolean selectorFeedsTable = boundedIndexes.contains(indexRegister); - if (compareIndex < loadIndex - 3 && selectorFeedsTable && supportedSequence) { - entryCount = (int) upperBound + 1; - defaultTarget = flows[0]; - break; - } - } - if (entryCount > 0) break; - } - if (entryCount <= 0 || entryCount > 4096 || defaultTarget == null) continue; - - List mappings = new ArrayList<>(); - Set mappedTargets = new HashSet<>(); - boolean valid = true; - for (int caseValue = 0; caseValue < entryCount; caseValue += 1) { - Address entry = tableAddress.add((long) caseValue * entrySize); - int encodedOffset = entrySize == 1 ? currentProgram.getMemory().getByte(entry) & 0xff : currentProgram.getMemory().getShort(entry) & 0xffff; - Address target = branchBase.add((long) encodedOffset * 4L); - String canonicalTarget = canonicalAddress(target); - if (!recoveredTargetSet.contains(canonicalTarget)) { - valid = false; - break; - } - mappedTargets.add(canonicalTarget); - mappings.add(new RecoveredAArch64Mapping(caseValue, target)); - } - if (!valid || mappedTargets.contains(canonicalAddress(defaultTarget))) continue; - Set accountedTargets = new HashSet<>(mappedTargets); - accountedTargets.add(canonicalAddress(defaultTarget)); - if (!accountedTargets.equals(recoveredTargetSet)) continue; - return new RecoveredAArch64RelativeTable(tableAddress, branchBase, entrySize, List.copyOf(mappings)); + if (!AARCH64_FLAG_PRESERVING.contains(name)) return null; } - return null; + if (guardIndex < 1) return null; + Instruction guard = instructions.get(guardIndex); + String guardMnemonic = mnemonic(guard); + if (!Set.of("B.HI", "B.CS", "B.HS").contains(guardMnemonic)) return null; + Address[] guardFlows = guard.getFlows(); + if (guardFlows.length != 1) return null; + Address defaultTarget = guardFlows[0]; + int compareIndex = -1; + for (int index = guardIndex - 1; index >= 0; index -= 1) { + String name = mnemonic(instructions.get(index)); + if (name.equals("CMP")) { + compareIndex = index; + break; + } + if (!AARCH64_FLAG_PRESERVING.contains(name)) return null; + } + if (compareIndex < 0) return null; + java.util.regex.Matcher compareMatcher = AARCH64_COMPARE_IMMEDIATE.matcher(operands(instructions.get(compareIndex))); + if (!compareMatcher.matches()) return null; + String comparedRegister = normalizedRegister(compareMatcher.group(1)); + boolean comparedFullWidth = compareMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x"); + long compareImmediate = parseInteger(compareMatcher.group(2)); + long entryCountLong = guardMnemonic.equals("B.HI") ? compareImmediate + 1 : compareImmediate; + if (entryCountLong <= 0 || entryCountLong > 4096) return null; + int entryCount = (int) entryCountLong; + + // The loaded index must carry the compared value: either the compared + // register itself, unchanged since CMP, or a MOV copy made after CMP. + // A W compare bounds only the low 32 bits, so an X index needs a + // zero-extending W copy; a W index is zero-extended by UXTW. + int indexWriter = latestWriter(instructions, loadIndex, indexRegister); + boolean bounded = false; + if (indexWriter < compareIndex) { + bounded = indexRegister.equals(comparedRegister) && (wordIndex || comparedFullWidth); + } + else if (mnemonic(instructions.get(indexWriter)).equals("MOV")) { + java.util.regex.Matcher moveMatcher = AARCH64_MOVE_REGISTER.matcher(operands(instructions.get(indexWriter))); + if (moveMatcher.matches() && + normalizedRegister(moveMatcher.group(2)).equals(comparedRegister) && + latestWriter(instructions, indexWriter, comparedRegister) < compareIndex) { + boolean wordCopy = moveMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("w"); + bounded = wordCopy || comparedFullWidth; + } + } + if (!bounded) return null; + + // Every fact above holds only on one straight-line fall-through path + // with no other entry into the window. + int windowStart = Math.min(Math.min(tablePageIndex, baseIndex), compareIndex); + for (int index = windowStart + 1; index <= branchIndex; index += 1) { + monitor.checkCancelled(); + Instruction previous = instructions.get(index - 1); + Instruction current = instructions.get(index); + Address fallThrough = previous.getFallThrough(); + if (fallThrough == null || !fallThrough.equals(current.getAddress())) return null; + ReferenceIterator incoming = currentProgram.getReferenceManager().getReferencesTo(current.getAddress()); + while (incoming.hasNext()) { + if (incoming.next().getReferenceType().isFlow()) return null; + } + } + + List mappings = new ArrayList<>(); + Set mappedTargets = new HashSet<>(); + for (int tableIndex = 0; tableIndex < entryCount; tableIndex += 1) { + monitor.checkCancelled(); + Address entry = tableAddress.add((long) tableIndex * entrySize); + if (!currentProgram.getMemory().contains(entry)) return null; + long encoded; + if (entrySize == 1) { + byte value = currentProgram.getMemory().getByte(entry); + encoded = signedEntries ? value : value & 0xffL; + } + else { + short value = currentProgram.getMemory().getShort(entry); + encoded = signedEntries ? value : value & 0xffffL; + } + if (encoded == 0 && nullTarget != null) { + mappings.add(new RecoveredAArch64Mapping(tableIndex, nullTarget, encoded, true)); + continue; + } + Address target; + try { + target = branchBase.addNoWrap(encoded << offsetShift); + } + catch (ghidra.program.model.address.AddressOverflowException overflow) { + return null; + } + String canonicalTarget = canonicalAddress(target); + if (!recoveredTargetSet.contains(canonicalTarget)) return null; + mappedTargets.add(canonicalTarget); + mappings.add(new RecoveredAArch64Mapping(tableIndex, target, encoded, false)); + } + if (mappedTargets.contains(canonicalAddress(defaultTarget))) return null; + // Ghidra's recovered destinations must be fully accounted for by the + // decoded entries, the guard default, and the CBZ null path. + Set accountedTargets = new HashSet<>(mappedTargets); + accountedTargets.add(canonicalAddress(defaultTarget)); + if (nullTarget != null) accountedTargets.add(canonicalAddress(nullTarget)); + // Ghidra does not model the CBZ, so it may decode a zero entry as a + // branch to the base itself. Only a zero entry encodes offset 0, and + // the CBZ diverts every zero entry, so the BR never reaches the base. + Address unreachableZeroTarget = null; + boolean divertedZeroEntry = mappings.stream().anyMatch(mapping -> mapping.nullEntry); + if (divertedZeroEntry && + recoveredTargetSet.contains(canonicalAddress(branchBase)) && + !mappedTargets.contains(canonicalAddress(branchBase)) && + !branchBase.equals(defaultTarget) && + !branchBase.equals(nullTarget)) { + unreachableZeroTarget = branchBase; + accountedTargets.add(canonicalAddress(branchBase)); + } + if (!accountedTargets.containsAll(recoveredTargetSet)) return null; + int selectorWriter = latestWriter(instructions, compareIndex, comparedRegister); + return new RecoveredAArch64RelativeTable( + tableAddress, + branchBase, + entrySize, + selectorWriter < 0 ? null : instructions.get(selectorWriter).getAddress(), + signedEntries, + offsetShift, + guardMnemonic, + compareImmediate, + nullTarget, + unreachableZeroTarget, + List.copyOf(mappings) + ); + } + + private static String mnemonic(Instruction instruction) { + return instruction.getMnemonicString().toUpperCase(Locale.ROOT); + } + + /** Index of the last instruction before {@code before} that writes {@code register}, or -1. */ + private static int latestWriter(List instructions, int before, String register) { + for (int index = before - 1; index >= 0; index -= 1) { + for (Object result : instructions.get(index).getResultObjects()) { + if (result instanceof Register written && + normalizedRegister(written.getBaseRegister().getName()).equals(register)) { + return index; + } + } + } + return -1; } private static String operands(Instruction instruction) { diff --git a/docs/testing.md b/docs/testing.md index 300bf473e..cbb4bf39c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -450,15 +450,15 @@ installation (`application.java.min` through `application.java.max`). Current 12.1 releases require JDK 21 or newer and set no maximum. The lanes below still prove behavior on the verified Ghidra 12.1.4 and JDK 21 build. -| Ghidra lane | Supported runner/target | Additional local tools | -| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 | -| `npm run verify:ghidra:seeds` | POSIX host; source-owned 32-bit x86 raw fixture; function order and BSS labels | Ghidra 12.1.4 and full JDK 21 | -| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 | -| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 | -| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 | -| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites | -| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | +| Ghidra lane | Supported runner/target | Additional local tools | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 | +| `npm run verify:ghidra:seeds` | POSIX host; source-owned 32-bit x86 raw fixture; function order and BSS labels | Ghidra 12.1.4 and full JDK 21 | +| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 | +| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 | +| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; signed/unsigned byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 | +| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites | +| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | The optional Swift lane compiles a source-owned fixture and checks modern and provider-demangled procedure names through real stdio MCP, with and without the diff --git a/scripts/verify-real-ghidra-function.mjs b/scripts/verify-real-ghidra-function.mjs index 16bb172b6..8acfeb102 100644 --- a/scripts/verify-real-ghidra-function.mjs +++ b/scripts/verify-real-ghidra-function.mjs @@ -674,6 +674,113 @@ export async function verifyRelativeSwitch(client, procedures, entrySize) { }; } +/** + * Oracle for relative-switch-signed.S: signed halfword byte offsets from a base + * inside the case bodies, a CBZ route for the zero entry, and a B.CS bound. + */ +const SIGNED_RELATIVE_SWITCH_CASES = [ + { value: 0, immediate: 200 }, + { value: 1, immediate: 201 }, + { value: 2, immediate: 202 }, + { value: 3, immediate: 250 }, + { value: 4, immediate: 204 }, +]; + +export async function verifyRelativeSignedSwitch(client, selectorBias = 0) { + const names = await inventoryCall(client, "list_names", { + document: null, + address: null, + }); + const procedure = requireProcedure(names, "rea_relative_switch_signed"); + const dossier = await functionCall(client, "analyze_function", { + procedure: procedure.address, + }); + const table = dossier.native_api?.jump_tables.find((item) => + item.data_sources.some( + (source) => + source.entry_size_bytes === 2 && + source.entry_count === SIGNED_RELATIVE_SWITCH_CASES.length, + ), + ); + if (table === undefined) + throw new Error( + `Exact signed halfword relative table unavailable: ${JSON.stringify(dossier.native_api)}`, + ); + const expectedValues = SIGNED_RELATIVE_SWITCH_CASES.map( + ({ value }) => value + selectorBias, + ); + if ( + table.mappings.some( + (mapping) => + mapping.case_value !== null && + !expectedValues.includes(mapping.case_value), + ) + ) + throw new Error( + `Signed relative switch emitted an unsupported case label: ${JSON.stringify(table)}`, + ); + if (table.mappings.length !== SIGNED_RELATIVE_SWITCH_CASES.length) + throw new Error( + `Signed relative switch has unexpected mappings: ${JSON.stringify(table)}`, + ); + if (selectorBias !== 0) { + if ( + !table.limitations.some((limitation) => + limitation.includes("table indices are not proven"), + ) || + !table.mappings.some((mapping) => mapping.case_value === null) || + table.mappings.some((mapping) => mapping.case_value === selectorBias + 3) + ) + throw new Error( + `Normalized selector must retain unknown case evidence: ${JSON.stringify(table)}`, + ); + } + for (const { value: tableIndex, immediate } of SIGNED_RELATIVE_SWITCH_CASES) { + const value = tableIndex + selectorBias; + // Without a proven relationship between the machine index and the typed + // switch selector, the CBZ-only case must stay unknown rather than being + // assigned the normalized index or Ghidra's unreachable base destination. + if (selectorBias !== 0 && tableIndex === 3) continue; + const mapping = table.mappings.find((item) => item.case_value === value); + if ( + mapping === undefined || + mapping.confidence !== "high" || + (selectorBias !== 0 && + !mapping.evidence.some( + (evidence) => evidence.source === "ghidra-clang-case-token", + )) + ) + throw new Error( + `Signed relative switch case ${value} was not verified: ${JSON.stringify(table)}`, + ); + const instruction = await functionCall( + client, + "inspect_native_instruction", + { address: mapping.target_address }, + ); + if ( + instruction.status !== "decoded" || + !instruction.operands.some((operand) => + operand.components.some( + (component) => + component.kind === "immediate" && + component.value === `0x${immediate.toString(16)}`, + ), + ) + ) + throw new Error( + `Signed relative switch target does not implement source case ${value}: ${JSON.stringify(instruction)}`, + ); + } + return { + entry_size: 2, + signed: true, + selector_bias: selectorBias, + dispatch_address: table.dispatch_address, + mappings: table.mappings.length, + }; +} + export async function verifyNativeValueTrace(client, procedures, target) { const entry = requireProcedure(procedures, "rea_ghidra_inventory_entry"); const names = await inventoryCall(client, "list_names", { diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs index 2ed9b0013..8eec5ef03 100644 --- a/scripts/verify-real-ghidra.mjs +++ b/scripts/verify-real-ghidra.mjs @@ -38,6 +38,7 @@ import { verifyJumpTableDenseSwitch, verifyNativeTypeLayout, verifyRelativeSwitch, + verifyRelativeSignedSwitch, verifyNativeValueTrace, } from "./verify-real-ghidra-function.mjs"; const exec = promisify(execFile); @@ -173,6 +174,36 @@ try { ]); } } + const signedRelativeSource = fileURLToPath( + new URL( + "../tests/conformance/ghidra/relative-switch-signed.S", + import.meta.url, + ), + ); + for (const selectorBias of [0, 2, 10]) { + for (const format of [ + "elf", + ...(expectedNativeTarget.architecture === "arm64" ? ["mach-o"] : []), + ]) { + const targetPath = join( + fixtureRoot, + `relative-signed-${selectorBias}-${format}.o`, + ); + await exec(clang, [ + ...(format === "elf" ? ["--target=aarch64-linux-gnu"] : []), + `-DREA_SELECTOR_BIAS=${selectorBias}`, + "-c", + signedRelativeSource, + "-o", + targetPath, + ]); + crossTargets.push([ + targetPath, + `relative-signed-${selectorBias}`, + { format, architecture: "arm64" }, + ]); + } + } await exec(clang, [ "--target=aarch64-linux-gnu", @@ -522,25 +553,30 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) { client, }) : null; - const probes = variant.startsWith("relative-") - ? await verifyRelativeSwitch( + const probes = variant.startsWith("relative-signed-") + ? await verifyRelativeSignedSwitch( client, - procedures, - Number(variant.slice(-1)), + Number(variant.split("-").at(-1)), ) - : variant === "type-layout" - ? await verifyNativeTypeLayout(client, names) - : variant === "jump-table" - ? await verifyJumpTableDenseSwitch(client, procedures) - : variant === "custom" || variant === "aarch64-jump-table" - ? null - : await verifyInventoryOperations({ - client, - variant, - procedures, - names, - strings, - }); + : variant.startsWith("relative-") + ? await verifyRelativeSwitch( + client, + procedures, + Number(variant.slice(-1)), + ) + : variant === "type-layout" + ? await verifyNativeTypeLayout(client, names) + : variant === "jump-table" + ? await verifyJumpTableDenseSwitch(client, procedures) + : variant === "custom" || variant === "aarch64-jump-table" + ? null + : await verifyInventoryOperations({ + client, + variant, + procedures, + names, + strings, + }); const nativeValues = variant === "debug" ? await verifyNativeValueTrace(client, procedures, parsedTarget.value) diff --git a/tests/conformance/ghidra/README.md b/tests/conformance/ghidra/README.md index 6247efa8f..ae9cc7b4c 100644 --- a/tests/conformance/ghidra/README.md +++ b/tests/conformance/ghidra/README.md @@ -39,3 +39,11 @@ bounded, and address-bearing; it never compares their text with Hopper. The callback fixture also proves that an unresolved targetless flow is not silently promoted to a direct callee. Every target runs in a separate owned project and must leave no process, socket, project, or runtime root after close. + +`relative-switch-signed.S` covers signed halfword offsets, the CBZ zero-entry +route, and selector biases of 0, 2 and 10 in the AArch64 jump-table lane. The +nonzero biases exercise overlapping and disjoint typed label/index ranges. REA +retains Ghidra's typed labels when the compared selector has an earlier register +definition; additional labels remain unknown until their relationship to the +typed switch selector can be proved. The oracle rejects extra numeric labels +and verifies each retained target against the source instruction immediate. diff --git a/tests/conformance/ghidra/relative-switch-signed.S b/tests/conformance/ghidra/relative-switch-signed.S new file mode 100644 index 000000000..eb0343ce7 --- /dev/null +++ b/tests/conformance/ghidra/relative-switch-signed.S @@ -0,0 +1,83 @@ +// Hand-written signed halfword relative dispatch, modelled on the AOSP +// RenderScript blend kernel shape reported in issue #487: +// adrp/add table; cmp; b.cs default; ldrsh; cbz null; adr base; add; br. +// Entries are signed byte offsets from Lsigned_base (no scaling), so cases +// before the base encode negative offsets. A zero entry means "no case body" +// and is routed through the CBZ before the branch. The source labels and the +// immediates below are the oracle; no REA or Ghidra result defines them. +// case 0 -> 200, 1 -> 201, 2 -> 202, 3 -> null path (250), 4 -> 204, +// selector >= 5 -> default (-1). +#ifndef REA_SELECTOR_BIAS +#define REA_SELECTOR_BIAS 0 +#endif +// Nonzero variants switch on the original selector (bias .. bias+4), +// while the guarded table index is selector - bias. +#if defined(__APPLE__) +.section __TEXT,__text,regular,pure_instructions +.globl _rea_relative_switch_signed +.p2align 2 +_rea_relative_switch_signed: +#else +.text +.globl rea_relative_switch_signed +.type rea_relative_switch_signed,%function +.p2align 2 +rea_relative_switch_signed: +#endif +#if defined(__APPLE__) + adrp x5, Lsigned_table@PAGE + add x5, x5, Lsigned_table@PAGEOFF +#else + adrp x5, Lsigned_table + add x5, x5, :lo12:Lsigned_table +#endif +#if REA_SELECTOR_BIAS != 0 + sub w8, w0, #REA_SELECTOR_BIAS + cmp w8, #5 + b.cs Lsigned_default + ldrsh x6, [x5, w8, uxtw #1] +#else + cmp w0, #5 + b.cs Lsigned_default + ldrsh x6, [x5, w0, uxtw #1] +#endif + cbz x6, Lsigned_null + adr x5, Lsigned_base + add x6, x5, x6 + br x6 +Lsigned_case0: + mov w0, #200 + ret +Lsigned_case1: + mov w0, #201 + // The base sits inside case 1 so that no real case has a zero offset. +Lsigned_base: + ret +Lsigned_case2: + mov w0, #202 + ret +Lsigned_case4: + mov w0, #204 + ret +Lsigned_null: + mov w0, #250 + ret +Lsigned_default: + mov w0, #-1 + ret +#if !defined(__APPLE__) +.size rea_relative_switch_signed, .-rea_relative_switch_signed +#endif + +#if defined(__APPLE__) +.section __TEXT,__const +#else +.section .rodata +#endif +.p2align 1 +Lsigned_table: + .short Lsigned_case0-Lsigned_base + .short Lsigned_case1-Lsigned_base + .short Lsigned_case2-Lsigned_base + .short 0 + .short Lsigned_case4-Lsigned_base