Running the Firmware
Ryan Torvik - April 21, 2023 - 3 minute read
We are going to take the firmware we wrote in the last blog and run it. We are creating a calculator device and the firmware is using it to generate a fibonacci number. We’ll start by compiling the firmware and running it in Hawthorne.
Compiling
The last time, we compiled it into x86 and looked at in BinaryNinja. This time we are going to compile into a MIPS32 bit big endian binary. This won’t be an issue since both of our emulators use Ghidra’s PCODE and slaspecs for instruction translation. For more information on our use of PCODE, see our other blog Emulating Ghidra’s PCODE.
Hawthorne currently supports these architectures. 6502, ARM4_be, ARM4_le, ARM4t_be, ARM4t_le, ARM5_be, ARM5_le, ARM5t_be, ARM5t_le, ARM6_be, ARM6_le, mips32be_34k, mips32be, mips32le, mips32R6be, mips32R6le, mips64be, x86-64, x86. The slaspecs include all the possible registers, which are very numerous. We’ve gone through each of these instruction sets and provided default registers to display.
We split the code into three files so that gcc doesn’t inline all the functions.
lib.h
#pragma once
#include <stdint.h>
void load_register(uint32_t value);
uint32_t read_result();
void select_add();
void select_sub();
void select_mul();
void select_div();
lib.c
#include "lib.h"
#define register_addr ((volatile uint32_t*)0x20000000)
#define function_addr ((volatile uint32_t*)0x20000004)
#define ADD_IDX 1
#define SUB_IDX 2
#define MUL_IDX 3
#define DIV_IDX 4
void load_register(uint32_t value) {
*register_addr = value;
}
uint32_t read_result() {
return *register_addr;
}
void select_add() {
*function_addr = ADD_IDX;
}
void select_sub() {
*function_addr = SUB_IDX;
}
void select_mul() {
*function_addr = MUL_IDX;
}
void select_div() {
*function_addr = DIV_IDX;
}
main.c
#include "lib.h"
#define COUNT 10
int main(void) {
int nums[10] = {1};
int last = 0, current = 1;
const int LEN = sizeof(nums)/sizeof(int);
for (int i = 0; i < LEN - 1; i++) {
load_register(nums[i]);
select_add();
nums[i+1] = read_result();
}
return nums[LEN - 1];
}
Then we run a couple commands to compile it, and then extract the binary portions we care about. You can put these in a Makefile if you are going to do this a bunch. We had to install gcc-mips-linux-gnu
in our Debian docker container.
mips-linux-gnu-gcc -I. -mfp32 -march=mips32r2 -O3 -fPIE -fno-plt main.c lib.c -o flash.elf
mips-linux-gnu-objcopy -j .text -O binary flash.elf flash.img
This will give us the text section of the elf which contains just the machine code that our C code compiled into. Since we are focusing on running snippets of code, we don’t support parsing all the sections of an ELF. Hawthorne runs with a virtual RAM device to cover the entire possible 32bit address space. It loads the imported bytes at address 0x0
.
As Hawthorne matures, we expect to support loading different binary file formats.
Run it in hawthorne
We can take this .img
file and run it in Hawthorne. Let’s do that.
Hawthorne loads it at address 0x0
.
This is really great for running the code, but other tools can do better at helping us visually analyze it. Let’s open the flash.elf
in cloud.binary.ninja. The cloud version doesn’t support snippets, but we can open the elf and navigate to the section that we’re looking at.
The first handful of instructions are all the variable creation and initialization, offset 0x674
in binja is where the loop starts. That corresponds to 0x54
in the snippet. So let’s go there by clicking the step button a bunch.
Now, we can step through the load_register function call. This loads the value from 0xffff_ffc8
(which is the variable nums[0]
) into 0x2000_0000
.
So, we’ve written 1 to the device register 0x2000_0000
. Hawthorne is just an instruction set simulator and does not support devices. It has a virtual RAM device for all addressable memory. Next, we’ll step through the select_add function and see it set 1
to 0x2000_0004
.
Finally, we’ll step through read_result which does a read on 0x2000_0000
. In a full-system emulator, this would send a read command to the device. Since we don’t have that ability in Hawthorne, this just moves the value from 0x2000_0000
into a register.
Conclusion
We can continue stepping through the code until we get to the end of the loop to see if our code actually correctly computes a fibonacci number using the device. We can even manually change the values in memory to mimic what the device would do if it were attached.
This very simple example could be done in a more flexible instruction set simulator than Hawthorne. But as we generate more and more complicated firmwares, it gets pretty ridiculous pretty fast.
To do what we’ve already done in hardware requires a lot of specialized knowledge and hardware. You need a board to run it on, and some way to read outputs from the SPI and UART devices.
What you need is a full-system emulator that lets you create your own devices in a language you’re comfortable with. Tulip Tree Technology happens to have an in house flexible full-system emulator, Emerson. We’ll talk about running our firmware in Emerson next time.