Reverse engineering is often associated with breaking security mechanisms. However, it is mostly an extension of normal debugging; it starts where the documentation ends. When the datasheet is incomplete, a chip has long been discontinued, or you inherit firmware without the source code, there is only one way forward: understand how the system actually works.

The goal is always similar: gain enough knowledge to fix something, reproduce its behavior, or write your own driver.

Below is an overview of tools that work well in day-to-day embedded development. We will focus mostly on Ghidra, which is a natural starting point for many reverse-engineering tasks today.

When Is Reverse Engineering Useful?

A cheap sensor may come with little or no meaningful documentation, yet you still need to get it running on your own microcontroller. An old device may have been working reliably for years, but now needs a modification and the source code is nowhere to be found.

You may need to connect to an undocumented bus and figure out the protocol. Or perhaps a component has disappeared from the market, and you are looking for a replacement — but first you need to understand exactly what the original component was intended to do.

In all of these cases, the problem is essentially the same: the available documentation is not enough, so you have to extract the missing information yourself.

First-Line Tools

Before diving into firmware, it is often worth checking what is actually happening on the pins. A logic analyzer, paired with the open-source PulseView/sigrok stack, can decode I2C, SPI, or UART traffic into readable transactions and bytes. When the problem is more analog — such as voltage levels, glitches, ringing, or power-supply issues — an oscilloscope is the right tool. For USB and network traffic, Wireshark is an excellent choice.

Extracting Firmware

If the device exposes a debug interface, you can often dump its flash using OpenOCD and a common debug probe such as an ST-Link or J-Link. External flash memory can be read with a programmer using flashrom.

For larger images — such as router firmware or complete Linux-based systems — binwalk is a good place to start. It can identify filesystems and compressed sections, while entropy analysis can quickly point to areas that may be compressed or encrypted.

Ghidra; WizzDev

From Raw Bytes to Readable Code using Ghidra

Ghidra is a free, open-source reverse-engineering suite developed by the NSA. One of its biggest strengths is the combination of a disassembler and a decompiler that turns assembly into much more readable C-like pseudocode.

Ghidra supports most architectures commonly encountered in embedded systems, including ARM, MIPS, AVR, RISC-V, 8051, and many others.

With common executable formats such as ELF, Ghidra can usually identify the format and architecture automatically. With a raw memory dump, however, you need to provide more information. This is where bare-metal analysis becomes particularly interesting.

Decrypting embedded code

First, you need to select the processor and its variant — for example, an ARM Cortex-M in little-endian mode. Then set the base address corresponding to the image’s actual location in memory and define the memory map for flash, RAM, and peripheral regions.

Without the correct address configuration, jumps and pointers will lead to meaningless locations, and the analysis quickly becomes difficult to follow.

Once the automatic analysis is complete, you will spend most of your time in two views: the assembly listing and the decompiler, which shows the same code as C-like pseudocode.

The actual work is about gradually assigning meaning to what initially looks like a random collection of instructions. You rename automatically generated functions such as FUN_08001234 to meaningful names like sensor_init. You follow cross-references to see where a function is called and how its results are used. You also define data structures.

The latter can make a huge difference. Once you describe the register layout of a peripheral, the decompiler can replace something like *(uint *)(x + 0x20) with a much more readable expression such as cfg->baudrate. The code starts to look like a program rather than compiler output.

CodeBrowser2: WizzDev

Additional features useful for reverse engineering

SVD-Loader allows you to import an SVD file provided by the microcontroller manufacturer. Instead of dealing with raw addresses, you can use register names like GPIOA->ODR or USART1->DR. This can save hours of manual datasheet digging.

The Python scripting API and headless mode are useful when you want to automate repetitive tasks or analyze multiple firmware images in a batch, without manually clicking through the Ghidra interface.

For Cortex-M firmware, a good starting point is the vector table at the beginning of the image. The second word contains the address of Reset_Handler, the actual entry point executed after reset. From there, you can start following the functions responsible for hardware initialization.

CodeBrowser: WizzDev

An Unknown I2C Sensor

All of this comes together nicely in a simple scenario, but what if there are greater challenges?

Imagine you have an undocumented I2C sensor and a reference board where the sensor works correctly. You start by connecting a logic analyzer to SDA and SCL and observing what the reference board sends to the sensor during startup.

You record the initialization sequence: register 0x20 receives the value 0x0F, followed by a read from 0x28.

If you also have a firmware dump, you can load it into Ghidra and search for these values. Cross-references lead you to the function responsible for configuring the sensor, and analyzing the surrounding code helps explain why those particular operations are being performed.

Finally, you write your own driver, connect the logic analyzer to your implementation, and compare the transactions with those observed on the reference board.

Every discrepancy — incorrect timing, a wrong register value, or a missing acknowledgment — becomes immediately visible.

This is where combining firmware analysis with hardware analysis becomes particularly powerful: each source of information can confirm or challenge the conclusions drawn from the other.

Conclusion

Reverse engineering in embedded development is not secret knowledge. It is simply debugging performed in the place where the documentation ends.

The tools are mature, and many of them are freely available. A cheap logic analyzer, Sigrok, OpenOCD, binwalk, and Ghidra are enough to take you all the way from a signal on a pin, through a firmware image, to code that you can actually understand.

The rest comes down to patience, forming good hypotheses, and — perhaps most importantly — using meaningful names.

Dev tip: document your findings directly in the Ghidra project. Add comments, describe structures, and replace automatically generated names with meaningful ones as you go. It will save you — and the next person who has to revisit the code — a huge amount of time months or years down the road.

Key Takeaways

  • Reverse engineering serves as a practical extension of the standard debugging process.

  • Reverse engineering is especially useful when technical documentation is incomplete or unavailable.

  • Hardware analysis tools like logic analyzers and oscilloscopes should be utilized first to establish a baseline of physical signal behavior before analyzing firmware.

  • Ghidra facilitates the transition from raw assembly to readable C-like pseudocode across diverse architectures like ARM, MIPS, and RISC-V.

  • Effective bare-metal analysis in Ghidra requires the manual definition of memory maps and base addresses to ensure jumps and pointers remain valid.

  • Utilizing SVD-Loader to import microcontroller register layouts saves significant time.