Releasing simpleimg-loadaddr
Recovering hidden load addresses from stripped PowerPC simpleImages and the story behind simpleimg-loadaddr

When a router boots a kernel from the wrong address, it doesn't print a useful error — it just hangs. Earlier this year we ran into exactly that class of bug while validating an OpenWRT 24.10 image for the Enterasys WS-AP3715i. The image looked fine, the boot loader accepted it, and the platform went nowhere.

The root cause turned out to be a one-line mismatch in arch/powerpc/boot/wrapper, and chasing it down led us to build a small tool we're open-sourcing today: simpleimg-loadaddr.

This post is the announcement, but more importantly it's a walk through why the bug existed in the first place, what the simpleImage format actually contains, and how a few hundred lines of C can recover information the build chain deliberately throws away.

The bug, in one sentence

OpenWRT commit history contains a change that updated the uImage header's declared load address for the WS-AP3715i, but forgot to update the linker script's load address for the simpleImage target — so the binary was linked for address A, the bootloader was told to load it at address B, and the platform's firmware build produced something that was guaranteed not to boot.

The fix and the original report are tracked in:

openwrt/openwrt#23121

Our contribution isn't the fix — it's the diagnostic that lets a maintainer, integrator, or auditor ask:

What load address does this image actually expect?

given nothing but the flat binary.

Why "just read the ELF" isn't an option

The simpleImage is a stripped flat binary. The build flow inside arch/powerpc/boot/wrapper is roughly:

  1. Compile and link the bootloader stub into an ELF executable with the real load address baked into the program header.
  2. objcopy -O binary to flatten it.
  3. Wrap the result in a uImage (or dd it directly into the firmware image).

Step 2 destroys every piece of metadata the linker recorded. Step 3 wraps the result in a uImage header — but the uImage header's load address field is just a hint the bootloader may or may not trust, and as the bug above demonstrates, it can drift out of sync with reality.

The intermediate ELF, the build log, and the original wrapper invocation are all gone by the time the simpleImage ships.

For most architectures, this doesn't matter. The PowerPC simpleImage is neither metadata-preserving nor position-independent.

If you don't know where the linker intended to place it, you cannot run it correctly.

What information actually survives

Stripping an ELF to a flat binary is lossy, but not uniformly so.

The PowerPC crt0.S contains a small but critical piece of arithmetic that depends on the load address:

p_start:    .long   _start
p_base:                 /* only used here */
mflr    r30
...

_start is defined in zImage.lds.S and equals the link-time load address.

Because p_start is a 4-byte constant placed at a fixed offset from the beginning of .text, the literal value of the load address survives as a raw 32-bit big-endian word inside the binary.

The trick is locating it.

The beginning of p_base contains a recognizable instruction sequence:

bcl     20, 31, 0          ; 42 9f 00 05
mflr    r30                ; 7d 48 02 a6

The exact byte sequence:

42 9f 00 05 7d 48 02 a6

appears on essentially every mpc85xx target.

Once found:

p_start_offset = p_base_offset - 0x18

(for 32-bit PowerPC, with alignment adjustment for 64-bit)

and the 4-byte big-endian value at that location is the load address.

In other words, the image is stripped, but the information we need is still hidden in plain sight.

The tool

simpleimg-loadaddr packages the observation above into a self-contained C program requiring only libc.

Given a flat binary simpleImage it:

  1. Loads the file into memory.
  2. Searches for the bcl+mflr signature.
  3. Computes p_start_offset.
  4. Reads the big-endian 32-bit value.
  5. Reports whether the image is position-independent.

Example:

$ ./simpleimg-loadaddr simpleImage.tl-wdr4900-v1

# SimpleImage Analysis Results:

Position Independent: NO
The image must be loaded at a specific address.
Load address: 0x1500000

The reported value matches the known load address of the TP-Link TL-WDR4900 v1.

The implementation remains intentionally small (~370 lines) and avoids libbfd and libopcodes because the format has already discarded the metadata those libraries depend on.

Why we let an AI write it

We want to highlight the AI-assisted development process because it provided its own lesson.

The first several generated implementations were technically correct programs that solved the wrong problem:

  • Read the file with libbfd and parse it as an object file.
  • Read the uImage header and return the header load address.
  • Accept the original ELF and print its program-header address.

All of these are reasonable interpretations of an incomplete specification.

The breakthrough came after adding two explicit requirements:

The ELF form is not available in the expected use case; the program must work on the flat binary only, and may not assume any symbolic information.

After that clarification, the model produced the pattern-matching approach in a single pass.

The lesson is simple:

AI output quality tracks prompt precision more closely than prompt length.

When the tool will break

We want to be honest about the limitations.

  • The tool depends on the bcl 20,31,.+0; mflr r30 sequence remaining the opening of p_base.
  • The fixed offset between p_base and p_start is empirically derived from upstream PowerPC kernels.
  • A truly position-independent image will report a load address of 0x0.

Future changes to crt0.S or PowerPC boot logic may require new detection anchors.

Try it

Repository:

github.com/hardenedvault/simpleimg-loadaddr

Build:

cc simpleimg-loadaddr.c -o simpleimg-loadaddr

Reference data:

If you maintain a downstream kernel or firmware build system producing PowerPC simpleImages, we'd love feedback on:

  • Boards where the tool reports an incorrect address.
  • Cases where the tool is correct but the uImage header disagrees.
  • Additional 64-bit PowerPC images for testing.

Pull requests, issues, and reproductions are welcome.

The OpenWRT ecosystem is better off when the build chain and the diagnostic tools agree on what the binary actually wants.