Dynamic Analysis with QEMU Tracing

TL;DR


1. Build a trace target

Create:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/trace_target.c
#include "types.h"
#include "uart.h"

volatile u32 g_state = 0u;

static void step(u32 x) {
  g_state ^= (x + 0x1234u);
}

int main(void) {
  for (u32 i = 0u; i < 5u; i++) {
    step(i);
  }
  uart_puts("state=");
  uart_puthex32(g_state);
  uart_putc('\n');
  return 0;
}

Build it:

1
2
3
riscv64-unknown-elf-gcc -O0 -g -ffreestanding -nostdlib \
  -march=rv32im -mabi=ilp32 -T src/link.ld \
  src/start.s src/uart.c src/trace_target.c -o build/trace_target.elf

2. Trace instructions with QEMU

1
2
3
qemu-system-riscv32 -M virt -nographic -bios none \
  -kernel build/trace_target.elf \
  -d in_asm,exec -D build/trace.log

Now inspect build/trace.log to see instruction flow.


3. Correlate trace lines with disassembly

1
riscv64-unknown-elf-objdump -d -M numeric,no-aliases build/trace_target.elf | less

Use the PC values in the trace log to jump to the matching disassembly lines.


Exercises

  1. Add another function and verify it appears in the trace.
  2. Build with -O2 and compare the trace length.
  3. Add a conditional branch and see how it changes the trace.

Summary