|
| 1 | +// Tests for Function::Analyze's switch-aware block walker. |
| 2 | +// |
| 3 | +// Hand-crafted synthetic PPC bytecode; self-contained; no dependency |
| 4 | +// on any particular test corpus. |
| 5 | +// |
| 6 | +// Three scenarios: |
| 7 | +// (1) happy path: switchMap populated, walker pushes switch labels |
| 8 | +// (2) null-map safety: switchMap == nullptr, walker uses legacy path |
| 9 | +// (3) wrong-map miss: switchMap populated but no entry for this bctr; |
| 10 | +// walker uses legacy path |
| 11 | +// |
| 12 | +// See tests/README.md for the build command and expected output. |
| 13 | + |
| 14 | +#include "function.h" |
| 15 | +#include <array> |
| 16 | +#include <cassert> |
| 17 | +#include <cstdint> |
| 18 | +#include <cstdio> |
| 19 | +#include <cstring> |
| 20 | +#include <vector> |
| 21 | + |
| 22 | +namespace { |
| 23 | + |
| 24 | +// Little helper to emit a big-endian 32-bit insn word into a byte vector. |
| 25 | +// Xenon is big-endian PPC; our synthetic blobs must match for the walker |
| 26 | +// to decode them correctly. |
| 27 | +void emitBE(std::vector<uint8_t>& out, uint32_t insn) |
| 28 | +{ |
| 29 | + out.push_back((insn >> 24) & 0xFF); |
| 30 | + out.push_back((insn >> 16) & 0xFF); |
| 31 | + out.push_back((insn >> 8) & 0xFF); |
| 32 | + out.push_back((insn >> 0) & 0xFF); |
| 33 | +} |
| 34 | + |
| 35 | +// Encode a handful of PPC instructions we need for the synthetic |
| 36 | +// fixtures. These are standard encodings; values are cross-checked |
| 37 | +// against capstone decoding as a sanity step. |
| 38 | +constexpr uint32_t PPC_BLR = 0x4E800020; // blr |
| 39 | +constexpr uint32_t PPC_BCTR = 0x4E800420; // bctr |
| 40 | +constexpr uint32_t PPC_NOP = 0x60000000; // ori r0, r0, 0 == nop |
| 41 | + |
| 42 | +// cmplwi cr6, rA, imm16 — primary opcode 10, field cr=6, rA, imm. |
| 43 | +// Encoding: 001010 (6) | 110 (cr=6) | 0 | rA(5) | imm(16) |
| 44 | +// = 0x2B | (rA<<16) | imm |
| 45 | +uint32_t cmplwi_cr6(uint32_t rA, uint32_t imm) |
| 46 | +{ |
| 47 | + return 0x2B000000u | (rA << 16) | (imm & 0xFFFF); |
| 48 | +} |
| 49 | + |
| 50 | +// bgt cr6, BD — primary opcode 16, BO=12 (branch if true), BI=25 (cr6 gt). |
| 51 | +// BD is a signed 14-bit displacement in bytes (we pass the displacement). |
| 52 | +uint32_t bgt_cr6(int32_t displacement) |
| 53 | +{ |
| 54 | + // Encoding: 010000 | 01100 | 11001 | BD(14) | 00 |
| 55 | + return 0x41990000u | (uint32_t(displacement) & 0xFFFC); |
| 56 | +} |
| 57 | + |
| 58 | +// Build a minimal "switch function" synthetic fixture: |
| 59 | +// |
| 60 | +// 0x00: cmplwi cr6, r3, 3 ; 4 cases |
| 61 | +// 0x04: bgt cr6, +0x30 ; default @ 0x34 |
| 62 | +// 0x08: (would be LIS/ADDI/RLWINM/LWZX/MTCTR — stubbed as nops here |
| 63 | +// because our walker treats mtctr/bctr by opcode; the prior |
| 64 | +// instructions only matter to XenonAnalyse's SCAN side, not |
| 65 | +// to Function::Analyze's block walker) |
| 66 | +// 0x08: nop |
| 67 | +// 0x0C: nop |
| 68 | +// 0x10: nop |
| 69 | +// 0x14: nop |
| 70 | +// 0x18: mtctr r0 (synthesized as nop — Function::Analyze doesn't |
| 71 | +// check mtctr specifically) |
| 72 | +// 0x18: bctr ; <-- switch-dispatch site |
| 73 | +// 0x1C: label[0] target @ 0x1C: blr |
| 74 | +// 0x20: label[1] target @ 0x20: blr |
| 75 | +// 0x24: label[2] target @ 0x24: blr |
| 76 | +// 0x28: label[3] target @ 0x28: blr |
| 77 | +// 0x2C: (padding) |
| 78 | +// 0x34: default target: blr |
| 79 | +// |
| 80 | +// The walker starts at offset 0, walks to the bctr at 0x18, consults |
| 81 | +// switchMap, and (if populated) pushes all 5 successor blocks (4 labels |
| 82 | +// + default). The walker then reaches each block, processes its `blr`, |
| 83 | +// and terminates. fn.size should cover through 0x34 (the default block's |
| 84 | +// blr). |
| 85 | +// |
| 86 | +// Base address: we use 0x10000 as a synthetic "guest VA" for these |
| 87 | +// tests. Anything > 0 works; 0x10000 is arbitrary. |
| 88 | + |
| 89 | +struct SyntheticSwitch |
| 90 | +{ |
| 91 | + std::vector<uint8_t> bytes; |
| 92 | + uint32_t baseVa; |
| 93 | + uint32_t bctrVa; |
| 94 | + std::vector<uint32_t> labelVas; |
| 95 | + uint32_t defaultVa; |
| 96 | +}; |
| 97 | + |
| 98 | +SyntheticSwitch buildFourCaseSwitch() |
| 99 | +{ |
| 100 | + SyntheticSwitch s; |
| 101 | + s.baseVa = 0x10000; |
| 102 | + s.bctrVa = s.baseVa + 0x18; |
| 103 | + s.defaultVa = s.baseVa + 0x34; |
| 104 | + s.labelVas = { s.baseVa + 0x1C, s.baseVa + 0x20, s.baseVa + 0x24, s.baseVa + 0x28 }; |
| 105 | + |
| 106 | + // Pre-switch head |
| 107 | + emitBE(s.bytes, cmplwi_cr6(3, 3)); // cmplwi cr6, r3, 3 |
| 108 | + emitBE(s.bytes, bgt_cr6(0x30)); // bgt cr6, +0x30 (→ 0x34 default) |
| 109 | + for (int i = 0; i < 4; ++i) emitBE(s.bytes, PPC_NOP); // filler |
| 110 | + emitBE(s.bytes, PPC_BCTR); // 0x18: the bctr |
| 111 | + |
| 112 | + // Label blocks — each a single blr |
| 113 | + for (int i = 0; i < 4; ++i) emitBE(s.bytes, PPC_BLR); |
| 114 | + |
| 115 | + // Padding between the 4 labels (at 0x2C) and default (at 0x34) |
| 116 | + emitBE(s.bytes, PPC_NOP); |
| 117 | + emitBE(s.bytes, PPC_NOP); |
| 118 | + |
| 119 | + // Default block |
| 120 | + emitBE(s.bytes, PPC_BLR); // 0x34: default blr |
| 121 | + |
| 122 | + return s; |
| 123 | +} |
| 124 | + |
| 125 | +// Helper to populate a switchMap entry from a SyntheticSwitch. |
| 126 | +AnalyzerSwitchTableMap buildMap(const SyntheticSwitch& s) |
| 127 | +{ |
| 128 | + AnalyzerSwitchTableMap m; |
| 129 | + AnalyzerSwitchTable entry; |
| 130 | + entry.defaultLabel = s.defaultVa; |
| 131 | + entry.labels = s.labelVas; |
| 132 | + m.emplace(s.bctrVa, std::move(entry)); |
| 133 | + return m; |
| 134 | +} |
| 135 | + |
| 136 | +int testHappyPath() |
| 137 | +{ |
| 138 | + auto s = buildFourCaseSwitch(); |
| 139 | + auto m = buildMap(s); |
| 140 | + Function fn = Function::Analyze(s.bytes.data(), s.bytes.size(), s.baseVa, &m); |
| 141 | + |
| 142 | + // Expected: walker reaches every label + default, all their blocks |
| 143 | + // get size = 4 (single-blr). fn.size should extend to cover the |
| 144 | + // default block (0x34 + 4 = 0x38). |
| 145 | + if (fn.base != s.baseVa) { |
| 146 | + fprintf(stderr, "[FAIL happy-path] fn.base = 0x%zX, expected 0x%X\n", |
| 147 | + fn.base, s.baseVa); |
| 148 | + return 1; |
| 149 | + } |
| 150 | + if (fn.size < 0x38) { |
| 151 | + fprintf(stderr, "[FAIL happy-path] fn.size = 0x%zX, expected >= 0x38 " |
| 152 | + "(switch labels + default)\n", fn.size); |
| 153 | + return 1; |
| 154 | + } |
| 155 | + fprintf(stderr, "[ok happy-path] fn.base=0x%zX, fn.size=0x%zX, blocks=%zu\n", |
| 156 | + fn.base, fn.size, fn.blocks.size()); |
| 157 | + return 0; |
| 158 | +} |
| 159 | + |
| 160 | +int testNullMapSafety() |
| 161 | +{ |
| 162 | + auto s = buildFourCaseSwitch(); |
| 163 | + Function fn = Function::Analyze(s.bytes.data(), s.bytes.size(), s.baseVa, |
| 164 | + /* switchMap = */ nullptr); |
| 165 | + |
| 166 | + // Expected: walker terminates at bctr without pushing successors, |
| 167 | + // discontinuity pass erases any blocks past the bctr, fn.size covers |
| 168 | + // only the pre-switch head (up to and including the bctr at 0x18, |
| 169 | + // so fn.size >= 0x1C, ≤ 0x20 or so). |
| 170 | + if (fn.size >= 0x38) { |
| 171 | + fprintf(stderr, "[FAIL null-map ] fn.size = 0x%zX, expected < 0x38 " |
| 172 | + "(switchMap=nullptr should use legacy walker)\n", fn.size); |
| 173 | + return 1; |
| 174 | + } |
| 175 | + fprintf(stderr, "[ok null-map ] fn.base=0x%zX, fn.size=0x%zX " |
| 176 | + "(legacy walker truncation preserved)\n", fn.base, fn.size); |
| 177 | + return 0; |
| 178 | +} |
| 179 | + |
| 180 | +int testWrongMapMiss() |
| 181 | +{ |
| 182 | + auto s = buildFourCaseSwitch(); |
| 183 | + // Build a map, but with an entry for a DIFFERENT bctr address. |
| 184 | + AnalyzerSwitchTableMap m; |
| 185 | + AnalyzerSwitchTable bogus; |
| 186 | + bogus.defaultLabel = 0x99999; |
| 187 | + bogus.labels = { 0xABCDEF }; |
| 188 | + m.emplace(/* bctr VA */ 0xDEADBEEF, std::move(bogus)); // not our bctr |
| 189 | + |
| 190 | + Function fn = Function::Analyze(s.bytes.data(), s.bytes.size(), s.baseVa, &m); |
| 191 | + |
| 192 | + // Expected: walker looks up our bctr (0x10018), finds nothing in the |
| 193 | + // map, falls through to legacy behavior. Same result as null-map. |
| 194 | + if (fn.size >= 0x38) { |
| 195 | + fprintf(stderr, "[FAIL wrong-map] fn.size = 0x%zX, expected < 0x38 " |
| 196 | + "(unrelated switchMap entry should fall through to legacy)\n", fn.size); |
| 197 | + return 1; |
| 198 | + } |
| 199 | + fprintf(stderr, "[ok wrong-map] fn.base=0x%zX, fn.size=0x%zX " |
| 200 | + "(unrelated map entry correctly ignored)\n", fn.base, fn.size); |
| 201 | + return 0; |
| 202 | +} |
| 203 | + |
| 204 | +} // anonymous |
| 205 | + |
| 206 | +int main() |
| 207 | +{ |
| 208 | + int failures = 0; |
| 209 | + failures += testHappyPath(); |
| 210 | + failures += testNullMapSafety(); |
| 211 | + failures += testWrongMapMiss(); |
| 212 | + |
| 213 | + if (failures) { |
| 214 | + fprintf(stderr, "\n%d test(s) FAILED\n", failures); |
| 215 | + return 1; |
| 216 | + } |
| 217 | + fprintf(stderr, "\nAll 3 tests PASSED\n"); |
| 218 | + return 0; |
| 219 | +} |
0 commit comments