Toolkit: pe-walker · pe-diff · rtti-dump · vtable-dump · pattern-scan · unwind-map
pe-diff compares two Portable Executable files and reports what changed in their structure: headers, sections, imported DLLs and functions, and exports. Byte-level diffs drown in noise from addresses that shift with every rebuild; pe-diff shows the changes the loader and the linker care about. It exits non-zero when anything differs, so it can gate a CI pipeline.
- Patch analysis: see what an update changed in a binary before reversing the code.
- Dependency drift: catch a release build that gains an unexpected DLL or API import.
- Reproducibility: confirm two builds are structurally identical.
- Supply-chain checks: fail a pipeline when an artifact's exports or imports change unexpectedly.
Real output comparing the two test fixtures in tests/fixtures. The v2 build adds one exported function, sample_ticks, which calls GetTickCount:
$ pe-diff -T sample64.dll sample64-v2.dll
pe-diff
A: sample64.dll
B: sample64-v2.dll
== Headers ==
entry_point: 0x0000177c -> 0x0000178c
== Sections ==
~ .rdata virt_size 0x183c -> 0x186c chars 0x40000040 -> 0x40000040
~ .text virt_size 0x1358 -> 0x1368 chars 0x60000020 -> 0x60000020
== Imports ==
+ KERNEL32.dll::GetTickCount
== Exports ==
+ sample_ticks
== Summary ==
1 header, 2 section, 1 import, 1 export change(s)
$ echo $?
1The v2 fixture differs from v1 by one function. pe-diff reports its direct effects (a new export and a new import) and its side effects (two sections grow and the entry point moves), each as one line.
Download a prebuilt binary for Windows x64, Linux x64 (statically linked) or macOS arm64 from the latest release, or build from source with CMake 3.15+ and any C++17 compiler:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build -C Release # optional: run the test suitepe-diff [--ignore-timestamp|-T] [--color auto|always|never] <old.exe|old.dll> <new.exe|new.dll>
| Flag | Effect |
|---|---|
--ignore-timestamp, -T |
Skip TimeDateStamp in the header comparison. Two legitimate builds almost always differ there. |
--color WHEN |
auto (default) colors added lines green, removed lines red and changed lines yellow, only on a terminal; always and never force it. Setting NO_COLOR turns colors off. |
| Exit status | Meaning |
|---|---|
0 |
The files are structurally identical. |
1 |
At least one difference was found. |
2 |
Input error: missing file, or a malformed or truncated PE. |
Use it as a gate in a CI pipeline:
pe-diff -T baseline/app.exe build/app.exe || echo "structure changed, review the diff above"| Area | Compared |
|---|---|
| Headers | machine, timestamp (unless -T), characteristics, subsystem, image base, size of image, entry point |
| Sections | added and removed sections, virtual size, characteristics |
| Imports | added and removed DLLs, and functions as DLL::name or DLL::#ordinal |
| Exports | added and removed names |
It does not diff section contents byte by byte, or compare resources, relocations, TLS callbacks, the debug directory or signatures. For byte-level changes inside a section, pair it with a hex or binary diff tool.
Both inputs are parsed with bounds-checked reads; a malformed file is reported with exit status 2 instead of crashing. When that parser was hardened, the old and new builds were run on 300 deliberately corrupted DLLs. The old build crashed on 92 of them and the new one on none. On 230 comparisons of real System32 and SysWOW64 binaries, the output stayed byte-for-byte identical.
ctest runs golden-file tests on the fixture DLLs: identical inputs, the v1 to v2 change with and without -T, colored output, a missing file and usage errors, checking both output and exit status. CI runs them on Windows (MSVC), Linux (GCC) and macOS (Clang), and cross-builds with MinGW-w64.
- pe-walker shows the full structure of a single binary.
MIT, see LICENSE.