2026-04-28   4 min   reverse engineering, windows, tooling

The copy constructor is the struct layout

Rule 4 in an earlier post was “the copy constructor is the struct layout”. This is the long version: how the method works, how it became a command, and what went wrong on the way there.

The observation

A compiler-generated copy constructor walks the struct member by member. Scalars are copied inline: mov reg, [src+off] then mov [dst+off], reg. Strings, vectors and sub-objects are copied by calling their copy constructors, with src+off and dst+off as arguments. Follow the calls and you have every field in order, with its offset and its width.

Nobody writes this function. The compiler emits it from the class definition, which makes it the most faithful description of the layout that exists in the binary.

Finding it

You find the constructor from its end. The last field copied is at size - 4 (or size - 8), so the tail looks like:

mov eax, [rcx+0x3414]
mov [rdx+0x3414], eax
ret

Search the image for the two displacements, take the function boundaries from .pdata, and you have the constructor. The sizeof is the last copied offset plus its width. Two independent checks: a histogram of imul r64, r64, imm sites, where the stride is by far the most common immediate, and reversing a magic-number division, d ≈ 2^(64+shift) / M.

Diffing two builds

When a patch inserts a field, everything after it shifts. Instead of searching each field again, read the constructor in both builds as a sequence of tokens and align the two sequences with a standard diff algorithm. The insert blocks name the insertion points exactly:

old build          new build          delta
+0x000 .. +0x2F8   +0x000 .. +0x2F8   0
                   +0x2F8 .. +0x300   +8    inserted
+0x2F8 .. +0x5A0   +0x300 .. +0x5A8   +8
                   +0x5A8 .. +0x5B0   +16   inserted

Three inserts in a 13 KB struct, found in seconds. The more valuable part is running the same diff over the other structs and watching them come out clean. Knowing what did not move saves more time than the find.

Two mistakes that produced fake fields

Both of these are in the tool now, and both cost a run each.

The mnemonic is not enough as a token. A struct with three thousand fields uses maybe ten different mnemonics, so four in a row will match by accident somewhere, and a false re-entry shifts a band boundary. The token has to be mnemonic + distance to the previous field. The distance is invariant under shifting, so it aligns correctly across builds and it almost never matches by chance. And the confirmation length, how many consecutive matches count as a real alignment, has to be long. At 8 the tool invented two 4-byte bands that did not exist. At 24 it stopped.

Pick the constructor across both images together. Several functions end on the same last dword, because a wrapper object that contains the struct copies the same tail. Picking “the largest candidate” in each image independently paired the wrapper’s constructor in one build with the real constructor in the other, and the diff was nonsense. Choose the pair whose sequences align best, not the two individually largest.

Traps in the input

.pdata in a live dump is often partially paged out. In one dump, 3860 of 32183 entries were valid. The fix is to lay the on-disk executable flat at its RVAs and compare .text against the dump; when the two are byte-identical, it is the same build and the disk’s .pdata can be used. And sub-call addresses have to be normalised before diffing, otherwise every call line differs and the alignment degrades to noise.

The command

All of this is one invocation now:

fieldmap <old.exe> <new.exe> --size <old size>

It works on files, no driver and no running process, and it prints the bands and the inserts. The manual method above is the explanation of why it works, not the way to do it any more. But building it taught me the two token rules, and I would have gotten them wrong again without writing them down.