Running in Emerson

Ryan Torvik - May 5, 2023 - 3 minute read

Tulip Tree Technology

So far we’ve built a simple piece of firmware, compiled it, and observed it running in our instruction set simulator Hawthorne. But, in order to fully debug our firmware we need to run it in a full-system emulator.

Emerson vs Hawthorne

This might seem like a face off between prolific 19th century New England authors, but it is a comparison between our different emulation offerings.

Hawthorne, as has been discussed earlier, is an instruction set simulator. It only translates instructions and executes them. It does not try to mimic any of the behaviors of the underlying operating system or hardware.

Emerson is a full-system emulator. In addition to translating and executing instructions, it mimics the underlying hardware a piece of code runs on. If the processor has an MMU, it models that behavior. It has peripheral devices so it can accurately model the interactions between the code and those devices. It handles CPU exceptions, alerting the target operating system when devices want the CPU to do something. With Emerson, a piece of software can run exactly as it would on actual hardware.

Each use case has its place. If you are debugging an application, you can probably get away with a userland debugger like gdb. But, if you are trying to see how an entire firmware behaves, you have to get a full-system emulator to support your efforts.

How Emerson Works

Emerson is written entirely in Rust. We host the application inside a docker container. We have an HTML/JavaScript GUI that interacts with the running server through a REST API. Since it is a simple REST API, we also have support to control the server with Python. This gives us a lot of flexibility with where the actual emulation is happening. Put the server anywhere and we can still communicate with it. We can also automate execution with Python and scale our efforts easily.

The default peripheral devices are written entirely in Rust. But, we have recently provided support to load memory mapped I/O devices written in Python. We will write our calculator device in Python exactly as a user of our product would if the device did not come with their Emerson installation. While this is not as performant, it gives the user the flexibility to create their own devices. Passing this code to the Tulip Tree Tech team would allow us to code up the device in Rust and increase the execution speed of your system.

Run our firmware in Emerson

Emerson needs a project to run the target firmware. We create a directory that will hold our calculator project, calling it tenkey. We’ll also copy the flash.img that we created in the previous blogs into this project.

mkdir tenkey
cp ~/flash.img tenkey/flash.img

Next, Emerson needs a yaml file to describe the underlying hardware. We are going to use a mips32be generic processor and set up a read write memory region to mimic RAM. We also set up a read only memory region that mimics the firmware flash image. Finally, We set the pc register to start executing the instructions in the ROM and set the stack pointer to 0x1000.

name: tenkey
processor:
  architecture: generic.mips32
  initial_registers:
    pc: 0x1fc00000
    sp: 0x1000
  details:
    keystone: mips32be
  devices:
    - name: ram
      start_address: 0x00000000
      size:          0x02000000 # 32MB of ram
      kind: generic.sparse_ram
      endian: big
    - name: flash
      start_address: 0x1fc00000
      size:          0x00400000
      kind: generic.rom
      endian: big
      details:
        backing_file: flash.img

Our Emerson docker image listens on a specific port that needs to be mapped, it also is looking for projects in a specific directory. So, we’ll map our tenkey directory into the projects directory and map the REST API port to something on the host.

Now that the server is running, we can connect to it and run our project.

We can do the same thing we did with Hawthorne previously. By stepping through the code and seeing what happens.

We don’t have anything mapped in where our device should be, so we actually get a fault from the system.

Next time, we’ll start building out our device!

Conclusion

Full-system emulation gives us a significant advantage over other debuggers. Being able to model the actual hardware lets us observe an entire firmware image executing as it actually does on hardware. This means, we can dynamically identify vulnerabilities in operating systems and device drivers. We can also find bugs in applications that require accurate interactions with the operating system and the underlying hardware.

Previous Post Next Post

Tulip Tree Technology
Learn Deep | Dream Big

© 2024 Tulip Tree Technology