Skip to content

Commit fce79a7

Browse files
committed
XenonAnalyse: add synthetic PPC fixtures for switch-aware walker
Three test scenarios hand-crafted in-source from big-endian PPC instruction words, no proprietary bytecode. Each exercises a different path through Function::Analyze's new switchMap handling: 1. Happy path: switchMap populated with an entry for the synthetic function's bctr. Walker pushes all 4 case labels + default as successor blocks; fn.size covers through the default block. 2. Null-map safety: switchMap = nullptr. Walker uses legacy pre- patch behavior; discontinuity pass erases label blocks; fn.size covers only the pre-bctr head. Guards against the default-null behavior regressing. 3. Wrong-map miss: switchMap populated but with an entry for an unrelated bctr VA. switchMap->find(our_bctr) misses; walker falls through to legacy. Guards against the switchMap handling spuriously firing on wrong entries. Assertions use fprintf + return-1 rather than a test framework, to keep the test self-contained. If the repository later adopts a standard test runner (Catch2 / doctest / gtest), porting is straightforward. See tests/README.md for the manual build command until CMake integration is decided.
1 parent 861e04d commit fce79a7

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

XenonAnalyse/tests/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# XenonAnalyse tests
2+
3+
Synthetic hand-crafted PPC bytecode fixtures for `Function::Analyze`'s
4+
switch-aware block walker.
5+
6+
## Fixtures
7+
8+
`switch_aware_walker_test.cpp` exercises three scenarios against a
9+
synthetic 4-case absolute-form switch function built in-source from
10+
hand-emitted big-endian instruction words:
11+
12+
1. **Happy path**`switchMap` populated. Walker pushes all 4 labels
13+
+ default as successor blocks, `fn.size` extends to cover through
14+
the default block.
15+
2. **Null-map safety**`switchMap = nullptr`. Walker uses legacy
16+
pre-patch behavior; discontinuity pass erases the label blocks;
17+
`fn.size` covers only the pre-bctr head.
18+
3. **Wrong-map miss**`switchMap` populated, but with an entry for
19+
an unrelated bctr VA. `switchMap->find(our_bctr)` returns `end()`;
20+
walker falls through to legacy behavior.
21+
22+
All three assert on `fn.size` landing in the expected range. The
23+
tests use `assert`-style `fprintf(stderr, "[FAIL ...]" ...) + return 1`
24+
rather than a framework, to keep the test self-contained (no new
25+
dependencies).
26+
27+
## Running
28+
29+
Compile against a built `LibXenonAnalyse` + `XenonUtils` + `disasm` +
30+
`fmt`. Example (from a VS 2022 Developer Command Prompt with LLVM in
31+
PATH, after building the rest of the tree):
32+
33+
```
34+
cd XenonAnalyse/tests
35+
clang-cl /std:c++20 /EHsc /nologo ^
36+
/I"..\" /I"..\..\XenonUtils" ^
37+
switch_aware_walker_test.cpp ^
38+
"..\..\build\XenonAnalyse\LibXenonAnalyse.lib" ^
39+
"..\..\build\XenonUtils\XenonUtils.lib" ^
40+
"..\..\build\thirdparty\disasm\disasm.lib" ^
41+
"..\..\build\thirdparty\fmt\fmt.lib" ^
42+
/link /OUT:walker_test.exe
43+
walker_test.exe
44+
```
45+
46+
Expected:
47+
48+
```
49+
[ok happy-path] fn.base=0x10000, fn.size=0x38, blocks=7
50+
[ok null-map ] fn.base=0x10000, fn.size=0x1C (pre-patch truncation preserved)
51+
[ok wrong-map] fn.base=0x10000, fn.size=0x1C (unrelated map entry correctly ignored)
52+
53+
All 3 tests PASSED
54+
```
55+
56+
## CMake integration
57+
58+
Not included in this commit. If the repository adopts a test-runner
59+
pattern, a minimal CMake shape would be:
60+
61+
```cmake
62+
add_executable(xenonanalyse_switch_walker_test switch_aware_walker_test.cpp)
63+
target_link_libraries(xenonanalyse_switch_walker_test PRIVATE LibXenonAnalyse)
64+
add_test(NAME switch_walker COMMAND xenonanalyse_switch_walker_test)
65+
```
66+
67+
Or as an opt-in build behind `-DXENONANALYSE_TESTS=ON`. Whatever
68+
convention the repository prefers.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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

Comments
 (0)