Tracing Infrastructure and an Instruction Ring Buffer

Engineering

Posted by Bruce Lee on 2024-08-18

About Me

Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.

Main Topics

  • Engineering Projects: Exploring implementation details and how technical systems work.
  • C/C++: Notes on language features and programming techniques.
  • The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.

For more, visit the categories page.

Contact

If you have questions or would like to discuss something, please get in touch through the About page.

Thank you for reading and for your support. I hope these notes help you on your own technical journey!


From Single-Stepping to an Instruction Ring Buffer

Two global variables in cpu_exec and execute are easy to overlook: g_nr_guest_inst and g_print_step. The first counts executed guest instructions. The second controls whether a short execution request prints each step.

The continue command, cmd_c, requests an effectively unbounded instruction count. A small si request is treated as something the user wants to inspect instruction by instruction. If n < MAX_INST_TO_PRINT, g_print_step becomes true. The threshold can be changed.

A larger si count does not mean the requested count is ignored; it means per-step console printing is disabled while that number of instructions is executed.

The Loop in execute

The loop limit comes from the instruction count passed by the caller. Each iteration invokes exec_once, which performs the instruction and, when CONFIG_ITRACE is enabled, constructs the trace information.

Two Output Paths and Panic Diagnostics

One output path writes to nemu-log.txt. init_log establishes log_fp, and the log_write macro in utils.h wraps writes through that file pointer. _Log adds another formatting layer.

trace_and_difftest, called after each instruction in the execution loop, uses the trace-output facilities.

trace_and_difftest

When the instruction-trace condition is satisfied, log_write records the instruction information in the log. A separate check of g_print_step decides whether the information should also appear on the console for a short step request.

_Log supplies standardized context such as file, line, and function names, using variadic-macro machinery. Log wraps _Log again. Other messages, including statistics and the final handling in cpu_exec, use this higher-level logging interface.

Where the Instruction Description Comes From

To find the actual trace text, follow execution deeper than execute, into exec_once. The Decode structure contains a 128-byte logbuf when CONFIG_ITRACE is enabled.

Within the conditional tracing code, a pointer p begins at that buffer. The code appends the instruction address from s->pc, its binary bytes from isa.inst.val, and its disassembled representation. All of this ends up in logbuf.

The panic Macro

panic wraps Assert and is used for conditions such as unimplemented operations and out-of-bounds memory accesses.

This path prints as much useful information as it can and then invokes the assertion mechanism. It does not pass through the ordinary end-of-execution cleanup in cpu_exec.

Other Error and Exit Paths

The execution loop checks NEMU’s state after each instruction. The state can be changed deeper in the call chain:

1
exec_once -> isa_exec_once -> decode_exec

When decode_exec matches ebreak, NEMUTRAP calls set_nemu_state with NEMU_END. The halt code then distinguishes success from failure according to the guest program’s convention.

If the invalid-instruction pattern is reached, invalid_inst reports the current PC and explains that the instruction is unsupported or its decoding may be wrong. It then sets the state to NEMU_ABORT.

Putting the Failure Cases Together

  • An instruction can have a recognized encoding but incorrect semantics. A guest test’s check may detect the resulting wrong value and halt the program.
  • An unrecognized encoding reaches invalid_inst, prints diagnostics, and aborts execution.
  • An invalid memory access can print an out-of-bounds diagnostic and enter the assertion path directly.

These cases use different combinations of output channels and state transitions. Understanding the differences tells us where to attach extra diagnostics.

Designing iringbuf

A ring buffer keeps the most recent instruction descriptions. Based on the paths above, useful places to dump it include the final state handling in cpu_exec and the out-of-bounds memory handler. That covers failures that return through the execution loop as well as failures that abort directly.

The existing trace text should be reused. Filling the buffer in trace_and_difftest avoids building a second, nearly identical instruction-formatting path. The feature can be conditionally compiled alongside the instruction-trace configuration so it remains optional.

What a Failed check Tells Us

A failed guest-side check can be frustrating to debug. The helper in trap.h has itself been compiled into guest instructions. By the time the program reaches the failing check and exits with a bad trap, the last few traced instructions may mostly show the check-and-halt path rather than the original faulty operation.

The check establishes that a tested condition was false. It often suggests that an instruction was recognized but behaved incorrectly, yet the check alone does not identify the root cause. A longer trace or a more targeted inspection is then needed.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !