PostHeaderIcon Reverse Engineering Chip LPC2114FBD64 Code: Reading It After You Have It

Recovering the bytes is the part everybody quotes for. Reading them is the part that produces the deliverable — a description of what the firmware does, which registers it touches, what protocol it speaks, and where the values that matter are stored.

On this device the reading has a specific character, because three properties of the platform shape what the compiler produced: a wide flash interface with an accelerator in front of it, a core that mixes two instruction sets, and an addressing model that puts constants inline in the code stream.

The accelerator explains the code layout

The flash array is read 128 bits at a time, and a memory accelerator module sits between the array and the core. Its job is to prefetch sequential fetches so that the core, which wants one instruction at a time, does not stall.

The accelerator is configured at start-up through two registers: one that selects the mode and one that sets the number of clocks the array needs. The values written there are the first thing to look for in a recovered image, because they tell you the clock rate the firmware expects. A build configured for a slow clock with generous wait states indicates a product that never ran near the maximum, which in turn suggests a design constrained by something else — power, an older board revision, or a safety margin somebody insisted on.

The layout consequence is more subtle. Code runs best when it is contiguous, and firmware that cares about timing places its inner loops in SRAM rather than in flash. When a disassembly shows a block of instructions that also exists as data earlier in the file, the data copy is the load image and the SRAM block is the execution copy. Recognising that duplication prevents the common mistake of treating the file as if it were the running program.

Two instruction sets in one stream

The core executes ARM instructions, which are four bytes wide, and Thumb instructions, which are two. Compilers mix them freely: an interrupt-heavy product typically keeps handlers in Thumb to reduce code size and puts arithmetic routines in ARM to use the barrel shifter and conditional execution.

The mixing is signalled by the low bit of a branch target address. A branch to an even address stays in the current instruction set; a branch to an odd address switches. Disassemblers handle this correctly when the symbols are present and get it wrong constantly when they are not, producing output that looks like a plausible but nonsensical instruction sequence.

The practical rule when reading a stripped image: follow the branches, not the linear sweep. Start at the reset vector, follow what it calls, and let the interworking bits tell you which decoder to use at each destination. A linear sweep from the base of the array will desynchronise within a few hundred bytes and take hours to recover from.

Literal pools are the giveaway for constants

Because ARM instructions are fixed width, a constant cannot be embedded in the instruction that uses it. The compiler places constants in a pool after the function that needs them and loads them with a program-counter-relative access.

In a recovered image these pools look like data in the middle of code, and they are the fastest way to find what the firmware is doing with hardware. A pool containing peripheral base addresses identifies every peripheral the product uses. A pool containing odd-looking values such as 0x0000A5A5 or 0x00000055 identifies a register written with a key sequence — and on this family several peripherals require exactly that.

What the pool holds What it tells you
peripheral block addresses which peripherals the product actually initialises
small magic values written twice a protected register that needs a key sequence
float or fixed-point constants control loop coefficients, filter tuning
table base addresses lookup tables for linearisation or calibration
the same constant in many functions a configuration value compiled in, not read from memory

That last row is worth attention. A constant repeated across unrelated functions is a design decision made at build time — a gain, a baud rate, a timeout. It is frequently the answer to a question the customer cannot answer from documentation, because the documentation describes the product and the constant describes the build.

Reading order that works

Start with the vector table and the reset path: what is initialised, in what order, and which clock configuration is set before anything else. Then the interrupt dispatcher, which reveals how many sources are handled and how work is organised. Then the functions the dispatcher calls, which are the product’s real behaviour. Then the data regions, which hold the configuration and the calibration.

Reading in this order produces a document that can be checked against the board. Reading in address order produces a pile of listings that nobody can verify.

Where the value lies

Customers usually ask for the source. What they need is almost never the source; it is the knowledge required to maintain, replicate, or repair the product. That knowledge is: the clock tree, the pin assignment, the protocol on each port, the calibration values and where they live, and the error handling.

Every one of those is recoverable from a dump read in the order above, and none of them requires the original variable names.

Neighbouring cases

The same reading discipline applies up and down the family. The differences are in what the device offers rather than in how the code looks — LPC2134FBD64 firmware work shows how the interrupt controller’s vector slots make the same analysis faster, and LPC2103FBD48 heximal delivery shows what has to be true before the image will boot at all. For a Cortex-M part where the same constants question arises with different instruction encodings, STM32F103CB recovery is a useful comparison.

A note on stripping

Compilers leave fingerprints: prologue patterns, switch tables, library calls that appear as recognisable stubs. Those fingerprints are how an unknown image gets attributed to a toolchain and, sometimes, to a vendor’s SDK. That attribution is worth recording in the report, because it tells the customer whether the code they are looking at was written in-house or licensed, which is often the first question their legal team asks.

Comments are closed.