C Part II · Module 25 · Debugging DPI-C and Simulator Integration
How DPI Failures Actually Present Themselves
Before you know anything about a DPI failure you know one thing: what it looked like. Six symptoms, each with a measured exit code that names a family of causes before any log is read — 1 for a link error, 3 for a function nothing registered, 134 for an abort inside the simulator's own source, 139 for a crash that printed nothing at all including the lines that had already run. And then the two that exit 0, where the run is clean and the data is wrong or quietly incomplete, which is where this track has spent six modules.
Advanced22 min readCPart IIVerificationDPI-C
Part II · Module 25 · Chapter 25.1 · How DPI Failures Actually Present Themselves
1. Starting From the Symptom
Every previous module in this track started from a mechanism and derived what could go wrong. Debugging runs the other way: something has already failed, you know what it looked like, and you need to get from that to a cause.
So this chapter is a symptom vocabulary — what you actually see, and what each thing narrows the cause to. It treats none of them; each of the next nine chapters owns one row.
The module's organising claim, from its own blurb, is that every failure here is one the earlier modules make explainable rather than mysterious. The index is what connects the two.
2. The Cheapest Discriminator Is the Exit Code
Before reading a log, read the exit status. Measured across this track, five distinct values name five different families:
compile 0, link 1 -> symbols and translation units
3 -> a function nothing registered, or no library
134 -> an abort inside the simulator's own source
139 -> a memory fault in your C
0 -> everything the toolchain can check is fineThat costs nothing and it is almost never the first thing anyone looks at. A run that exits 3 and a run that exits 139 have essentially nothing in common, and the log may look similarly unhelpful for both.
3. The Five Non-Zero Symptoms
It will not build. Measured:
compile exit: 0
link exit: 1
Undefined symbols for architecture arm64:
"_crc8_step", referenced from:
_main in user.oThe compile succeeded, so this is not a syntax or type problem — it is about symbols and translation units, and nothing about the simulator is involved yet. Chapter 25.6, which also covers the version of this message that means your C was compiled as C++.
It will not start. Two shapes. Measured, a function nothing registered:
tb_add.v:4: Error: System task/function $c_add() is not defined by any module.
exit 3and a library that could not be found:
dlopen FAILED: dlopen(./nosuch.dylib, 0x0002): tried: './nosuch.dylib' (no such file), …The first names your testbench line, which is excellent. The second lists every path it tried, which is the single most useful thing in a loader message. Chapters 25.2 and 25.7.
It aborts part-way. Measured:
Assertion failed: (expr), function vpi_get_value, file vpi_priv.cc, line 1027.
exit 134An assertion in the simulator's own source file. The instinct is to conclude the simulator has a bug; the cause is almost always the call. Chapter 25.2.
It dies silently. Measured:
exit: 139
output lines: 0Zero lines — and three $display statements had already executed. Chapter 25.8, and §5 is why the output vanished.
It runs clean and the data is wrong. Exit 0, plausible numbers, and nothing in the toolchain to narrow it. Chapters 25.3, 25.4 and 25.10 — and most of Modules 20 to 24.
4. The Sixth: Information That Is Simply Gone
The last symptom is worth separating from "wrong data" because it looks like nothing at all.
Measured, a 32-bit value of 0x000000F0 with bit 5 set to Z, read two ways in the same call:
[C] 4-state: aval=0x000000D0 bval=0x00000020 -> HAS X or Z ; read as int: 0x000000D0Read as a four-state value, the Z is visible in bval. Read as an integer — which is what ordinary code does — the value is 0xD0.
The original was 0xF0. Bit 5 did not change to zero; it stopped existing, and the integer access reported the result without comment. A model receiving 0xD0 has no way to know it was ever anything else. Chapter 25.9.
5. Why the Output Vanished
The crash row deserves its own paragraph because the missing output misleads people more than the crash does.
Measured, the same program two ways:
vvp … exit: 139, output lines: 0
stdbuf -o0 vvp … exit: 139, and all three lines survive:
[SV] initialising the table with 0 entries
[SV] init returned 0
[SV] about to read entry 3Nothing changed about the program or the fault. What changed is buffering: standard output is block-buffered when it is not a terminal, so lines accumulate in memory and are written when the buffer fills or the program exits cleanly. A process killed by a signal does neither.
So the rule, which saves more time than any other single thing in this module:
The last line you see is the last line that was flushed, not the last line that ran.
Chapter 25.8 turns that into a method.
6. The Misconception: The Last Line Shows Where It Got To
"The log stops after the third transaction, so it failed on the fourth."
Measured false. Three $display statements executed and zero lines appeared. Had the crash happened on transaction four hundred, the log would have looked identical — empty.
Two consequences worth holding onto.
An empty or short log is evidence about buffering, not about progress. The only way to read progress from a log is to make the output survive, which §5 measured and Chapter 25.8 makes into a procedure.
And the converse: a log that ends mid-line is informative. If output is unbuffered and the last line is incomplete, the process really did die at that point — which is a much stronger signal than a log that simply stops.
The accurate position:
Establish that your output survives before you reason from it. One environment variable or one flush call turns a log from a misleading artefact into the cheapest localisation tool available — and it is the same change that makes every other method in this module work better.
7. What You Should Be Able to Say Now
- Debugging starts from the symptom, not the mechanism — this chapter is the index and treats nothing.
- The exit code is the cheapest discriminator and is almost never read first.
- Measured, five families: compile 0 / link 1 · 3 · 134 · 139 · 0.
- Will not build — undefined symbols, compile succeeded, so it is symbols and translation units (25.6).
- Will not start — exit 3, "not defined by any module", naming your line; or a loader listing every path tried (25.2, 25.7).
- Aborts part-way — exit 134, an assertion in the simulator's source, usually caused by the call (25.2).
- Dies silently — exit 139 and 0 lines of output (25.8).
- Runs clean, data wrong — exit 0, and nothing in the toolchain narrows it (25.3, 25.4, 25.10).
- Runs clean, information missing — measured,
0xF0reads back as0xD0once one bit isZ; the bit stopped existing (25.9). - Output vanishes because of buffering — measured, the same crash shows 3 lines under
stdbuf -o0. - The last line you see is the last line flushed, not the last line that ran.
- Record the exit code with every run — it is free and it names a family.
8. Exercises
- Run a failing DPI build and record the compile and link exit codes separately. Which one failed?
- Produce each of the five non-zero symptoms deliberately. Write down the exit code and the first line of output for each.
- Crash a program that has already printed several lines, with and without unbuffered output. How many lines survive in each case?
- From a log that stops after three transactions, list everything you can and cannot conclude.
- Read a four-state value as an integer and compare it with its
aval/bvalform. Construct a case where the integer form is a plausible value that was never there. - For the last three DPI failures you saw, place each in the figure. Which row was hardest, and did its exit code tell you so?
- Your regression log records "pass" or "fail". What would recording the exit code add, and what would it have told you about a recent failure?
- Why are the non-zero symptoms described as the good cases? Answer in terms of what the toolchain has already done for you.
9. Summary
- Start from the symptom; this chapter indexes, the next nine treat.
- Read the exit code first — measured: link 1 · 3 · 134 · 139 · 0.
- Will not build → symbols and translation units (25.6).
- Will not start → exit 3 naming your line, or a loader naming every path tried (25.2, 25.7).
- Aborts part-way → exit 134, an assertion in the simulator's file, caused by the call (25.2).
- Dies silently → exit 139, 0 lines (25.8).
- Clean exit, wrong data → nothing narrows it (25.3, 25.4, 25.10, and Modules 20–24).
- Clean exit, missing information → measured
0xF0→0xD0; the bit stopped existing (25.9). - Buffering, not progress, explains a short log — measured, 3 lines recovered under
stdbuf -o0. - Record the exit code with every run.
Two of the six symptoms were a function the simulator could not find, or could find and could not call. Both come from the same place: a declaration on one side and a definition on the other, with nothing in the toolchain able to compare them.
Where this fits
Part of the C Programming curriculum.
