Engineering Debugging Notes

Project engineering

Posted by Bruce Lee on 2024-05-31

About This Note

This is a first English test post for the site. It mirrors the Chinese post debug.md and gives the English site one real article to render, list, and switch from.

The original note records several debugging lessons from implementing and validating instruction behavior in a small system project.


Debugging Instruction Implementation

Load and store instructions call the Mr and Mw macros. Those macros wrap vaddr_read and vaddr_write, which then go through the physical address helpers, guest_to_host, the physical memory helpers, and finally host_read in host.h.

One important detail is that these read helpers return uint32_t. When reading one or two bytes, the value is zero-extended by default. Four-byte reads are fine in a 32-bit architecture, but smaller signed values need explicit handling.

The first possible fix was to add a new family of memory access helpers that return sign-extended values. That would only require a few call-site changes, but it would duplicate a lot of existing access code. The smaller fix was to keep the existing read path and do the required trimming and sign extension inside the instruction action block.

Another issue appeared in the instruction action block: src1, src2, and imm are unsigned by default, while some instruction semantics are signed. This mismatch became visible when debugging the max program.

For U-type instructions, the immediate value has already been shifted left by 12 bits in the immU macro. The action block should not shift it again.

A Practical Debugging Loop

  1. Read the program source and understand the expected behavior. When the behavior is unclear, simplify the program so it produces the instruction sequence you want to observe.
  2. Inspect the generated assembly and mark suspicious instructions. Compare it with a known working program.
  3. Use sdb to locate the failing instruction, then compare disassembly, register values, and expected instruction semantics.
  4. If no issue is found, return to the source-level behavior and inspect again. If an issue is found, classify it and check the corresponding implementation.
  5. If the problem is still hidden, intentionally change the behavior to trigger a clearer failure, then return to instruction-level debugging.

Why Unimplemented Instructions Fail This Way

The instruction matching flow tries each instruction pattern. If none of them match, execution falls through to the final invalid instruction pattern.

That invalid pattern calls the INV macro with the current pc. INV calls invalid_inst, which reads the current instruction, prints the error report and register state, then updates the emulator state to NEMU_ABORT.

Understanding this path helps separate two different cases: an instruction that is implemented incorrectly, and an instruction that is not matched at all.

Branch Delay Slots

For architectures such as MIPS, a compiler may place a safe instruction in the branch delay slot. The candidate instruction must not conflict with the branch operands or change the branch decision.

This is a reminder that instruction behavior is not only about the instruction itself. Scheduling, reordering, and pipeline assumptions can also affect what a correct implementation needs to support.

Instruction Name Cross-Reference

Architecture manuals often include pseudo-instructions and explain how they lower into real instructions. Using that mapping is a useful way to debug differences between source-level assembly and the actual instruction stream.


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 !