The Rust primer extension explains bounded slices and overlapping scan windows with a small local example before those ideas are applied here.
Section: Why patterns help.
Lesson 5.2 recovered a live address by adding the current module base to a known R V A, relative virtual address. That works for the exact build whose R V A you measured. After a game update, the gold-changing instruction may move within the module, so the old R V A can point at different code even when the base calculation is perfect.
A byte pattern searches the current module for a distinctive instruction sequence. It gives us a candidate location to check in each explicitly supported build; it does not by itself prove that the candidate is still the gold-changing instruction.

Some bytes encode registers or opcodes and stay stable. Others encode addresses or displacements and may change.
Section: What a signature actually describes.
The C P U, central processing unit, sees one continuous byte stream. The disassembler groups that stream into instructions, and each instruction may contain several kinds of information:
Suppose two supported builds both subtract a value and then load a nearby field. The instruction shape may stay recognizable while the relative call target between those instructions moves. A useful signature keeps the stable shape and places wildcards over the changing target bytes.
Written out, that comparison looks like this. The same two instructions, taken from two builds of one game:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The first three bytes are sub dword ptr [e d i+4], e a x: the operation, the registers, and the field offset, none of which the compiler had any reason to change. E8 is a call, and the four bytes after it hold a relative distance to the target, measured from the end of the call instruction. That distance shifts whenever either the calling code or the called function moves, even by a single byte, even though it still calls exactly the same function.
So the signature keeps the first four bytes and wildcards the last four:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Wildcards are not “bytes we forgot.” Each one here has a stated reason — “relative call displacement, changes whenever code moves” — and that written reason is the difference between a wildcard and a shrug. Too few wildcards make the signature fragile. Too many make it common and ambiguous.
Section: A signature can overfit one recording.
A pattern overfits when it memorizes accidental details from one executable instead of describing the instruction identity you meant to find. An exact 30-byte sequence may work perfectly on the build used to create it and fail after an unrelated compiler layout change. The opposite mistake is a pattern so loose that it matches ordinary code everywhere.
When supporting two exact builds, design the candidate signature with one build and test it against the other before calling it version-tolerant. Then reverse the test: bytes from each build should locate the same kind of instruction, not merely return one arbitrary address.
Do not weaken a pattern until it matches an unknown build and then declare success. If the course supports only one build, an exact fingerprint plus a build-specific signature is more honest than pretending the signature is portable. The goal is a pattern that finds the same instruction in every build you named, not the longest or shortest possible byte string.
In brief: A good signature keeps the stable bytes and wildcards the ones that legitimately change. It must find the same instruction in every build you support.
Section: When a moved field changes the instruction’s length.
A field offset is a displacement, and moving the field can change more than the displacement’s own bytes. mov e a x, [e c x+offset] has two encodings. When the offset fits in one signed byte ( minus 128 to 127, so at most hexadecimal 7 F here), the compiler uses the short form: 8B 41 and that one byte, three bytes in all. A larger offset needs the long form: 8B 81 and four displacement bytes, six in all. The second byte is the ModR/M byte, and its top two bits say which form follows: 41 is 01 000 001 in binary, one displacement byte, and 81 is 10 000 001, four.
So a field that moves from hexadecimal 1 3 8 to hexadecimal 1 4 0 turns 8B 81 38 01 00 00 into 8B 81 40 01 00 00. One byte differs, and 8B 81 ?? ?? ?? ?? covers both builds, with one stated reason for all four wildcards: the field moved. A field that moves from hexadecimal 7 C to hexadecimal 8 4 changes the shape instead: 8B 41 7C in one build, 8B 81 84 00 00 00 in the other, and every later byte sits three places further on.
Predict first
Build A reads a field with 8B 41 7C, which is mov e a x, [e c x+hexadecimal 7 C]. In build B a new 8-byte field before it moves it to hexadecimal 8 4.
Which pattern finds the instruction in both builds?
Answer: No single pattern: one per build, each checked by decoding. The builds encode different instruction shapes, three bytes against six. A pattern per build, each required to match once and to decode as mov e a x, [e c x+offset], states that difference instead of hiding it.

Section: Represent exact and wildcard bytes.
A written signature such as 29 47 04 E8 ?? ?? ?? ?? contains two different kinds of instruction: compare this byte exactly, or accept any byte at this position. Represent that choice in the type rather than reserving a magic value like zero. The matcher below receives copied code bytes, a starting offset, and that typed pattern; it returns false when the whole pattern would run past the buffer.
Code example. The code’s comments explain: 📏 Ask for the whole candidate window at once. Near the buffer end, an incomplete window is simply “not a match” instead of an indexing panic. 🔍 `Any` consumes exactly one byte too; it relaxes equality, not length.
The safe matcher makes wildcard meaning and range failure explicit:
The original matcher accepts an exact byte pattern and compares each byte by direct indexing. A candidate near the buffer end can panic, and the byte list has no separate way to express a wildcard.
Before: compare exact bytes through direct indexes
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Request the complete bounded window first. Then compare each byte with a PatternByte enum item: Exact requires equality, while Any still consumes one byte but accepts any value. An empty pattern is excluded by the caller’s pattern validation.
Improved code
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Show the diff
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Takeaway: validate a complete candidate range before comparing its contents.
The direct indexes can panic near the end of the buffer, while get turns an incomplete window into false. PatternByte prevents a magic byte such as hexadecimal 0 0 from secretly meaning “wildcard.” zip and all express the matching condition directly: each discovered byte must satisfy its corresponding pattern item.
saturating_add also keeps the end of the window from wrapping to a small number. Lesson 3.3’s checked slices and iterator chains explain get, zip, and all step by step. The function only borrows the copied bytes and the pattern, so it never copies the module again.
Section: Scan safely.
One safe comparison is not a scan yet. The next function tries every start position where the full pattern could fit, collecting offsets in the local buffer. If the buffer has three bytes and the pattern has two, both starts 0 and 1 must be considered; start 1 is easy to miss with an exclusive range.
Code example. The code’s comments explain: ⚠️ An empty pattern would otherwise appear to match between every byte, and subtracting a longer pattern would underflow. ✅ The inclusive end keeps the final legal starting position searchable.
For n input bytes and an m-byte pattern, this simple algorithm does at most roughly n times m comparisons. That is easy to reason about and fast enough for bounded course modules. More advanced search algorithms matter only after measurement shows this loop is the bottleneck.
The early return handles two important edge cases. An empty pattern would otherwise “match” at every offset, and subtracting a longer pattern length from the input length would underflow.
Section: Parse a friendly pattern.
The scanner’s type is precise, but writing PatternByte::Exact eight times for every trial is awkward. A small parser turns the familiar spaced hex form into that type. A mistyped token becomes an error before any memory is scanned:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
collect() is doing extra work here: the iterator produces Result<PatternByte> values and combines them into Result<Vec<PatternByte>>. The first invalid hex token stops the parse and returns its error. No half-parsed pattern escapes. This is the same illustrative two-build pattern we just derived, so the four ?? tokens still mean the call’s changing relative distance. The runnable Wesnoth lab below uses a different, exact three-byte signature from its one pinned executable; it does not claim that hypothetical call follows the real gold instruction.
Section: Scan the correct range.
Do not scan an entire process by default. Locate the intended module and executable section, copy that bounded region, and scan the local buffer.

For a P E, portable executable, image, confirm:
DOS and P E signatures;
section table bounds;
executable characteristic;
virtual address plus size stays in the module.
Libraries such as goblin can parse P E files, but live in-memory images still need careful address and section validation.
This separation matters. ReadProcessMemory works at the Windows boundary and can fail because a page changed. Once the bytes are in a local Vec<u8>, the matching loop uses only checked slices: no remote pointers, no changing protections, and no partial Windows reads during comparisons.
Section: Require one match.
The scan returns a list because one pattern can occur more than once. Before turning an offset into a candidate instruction address, make the three possible counts explicit. Returning the first entry would disguise an ambiguous signature as a successful find.
Code example. The code’s comments explain: 🎯 “First result wins” hides ambiguity. All three match counts are handled explicitly so a weak pattern cannot choose code by accident.
Picking the first of five matches is not version tolerance.
Alternative: Think of searching a library by a book’s first line: one hit is a lead, many hits mean your quote is too common, and none means wrong library.
The three outcomes mean different things:
zero matches means the build, range, or pattern assumption is wrong;
one match means the signature has located one candidate, which still needs semantic verification;
several matches mean the signature does not contain enough identity.
Do not “fix” ambiguity by silently lengthening the scan to another unrelated section. Extend the pattern into the instructions around the intended one until only it matches.
Section: Verify surrounding meaning.
After a match:
disassemble the surrounding bytes;
confirm whole expected instructions;
verify the relative target or field offset;
check the supported module fingerprint;
reject an unknown build.

Section: Test edge cases.
A two-byte pattern at the end of a three-byte buffer exercises the range boundary that ordinary middle-of-buffer examples miss. The expected offset is 1, so this test catches an accidental exclusive upper bound:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Follow that boundary test as two candidate windows. All byte values below are hexadecimal; offset 1 must be included in the scan:
Also test empty patterns, all wildcards, a pattern longer than input, no matches, overlapping matches, and multiple matches.
A useful test asks the program to do something and checks an observable result. It does not merely repeat the code it is supposedly checking:
Code example. The code’s comments explain: ❌ This only proves that a two-item array has two items.
By contrast, the boundary test above can catch a real off-by-one bug. That is the standard to use when A I, artificial intelligence, or autocomplete suggests tests too: ask what incorrect implementation would make the test fail. If the answer is “none,” the test is decoration.
A pattern only finds where bytes match. It does not replace target-version checks or instruction decoding.
Narration: A matcher that stops one position early passes every middle-of-buffer test and still misses a match at the very end. Off-by-one slips hide at the edges.
Section: Read the complete program as a pipeline.
The executable at the end of the lesson has five phases:
Process::open_by_name finds the exact target and owns its Windows process handle.
process.module returns the current base and mapped size of wesnoth.exe.
process.regions asks Windows how that module is divided and keeps readable executable regions.
Each accepted region is copied before .windows(3) compares local slices.
The program prints every match and refuses success unless the match count is exactly one.
Notice what it does not do: it does not scan every allocation, patch the result, or assume matching bytes prove the instruction’s meaning. Those are separate decisions with separate checks.
Section: Run it against Wesnoth.
The course binary opens the live 32-bit wesnoth.exe, finds its current main-module base, visits its readable executable regions, and searches for the exact gold-subtraction instruction bytes 29 42 04. Those are measured bytes from the pinned Wesnoth build, not the illustrative 29 47 04 E8 ... sequence above. It prints every match and refuses to call the result usable unless there is exactly one.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The complete executable is pattern_scanner.rs. Shared ToolHelp module discovery, memory-region validation, and bounded reads are in process.rs.
Additional detail: Complete lab source: pattern_scanner.rs.
Code example. The code’s comments explain: 🔒 Code signatures belong in executable image pages, not heaps or unrelated readable allocations that happen to share three bytes. 📏 Clip Windows' page-sized region to the exact module interval. 🔍 Copy once, then scan safe local slices while the game keeps running.
A three-byte match identifies a candidate only; patching requires a separately verified address and expected bytes.
Section: When results are wrong.
When a scan result is wrong
Error messages should preserve this distinction. “Scanner failed” is not actionable; “pattern matched six locations in executable regions” tells the learner exactly which assumption to revisit.