Linux & Bash · Part II · Module 27 · Invoking Simulators and EDA Tools from the Shell
Launching a Viewer on the Right Database
Opening a waveform is one command. Knowing that the file you opened is the right one, and that it is complete, is not — and a truncated database ends on a valid record with a valid header, indistinguishable from a good one. This chapter measures what can and cannot be checked, and finds that the answer lives outside the file.
Intermediate21 min readLinuxPart IIWaveformsDebuggingSignals
Part II · Module 27 · Chapter 27.7 · Launching a Viewer on the Right Database
The command is trivial:
# FORMS, from each tool's documented interface -- see the note below
verdi -ssf runs/axi_burst_s7/dump.fsdb &
gtkwave runs/axi_burst_s7/dump.vcd &Everything difficult is upstream of it. Which of twenty run directories holds the failure you are investigating; whether its database is complete; whether the format matches the viewer; and whether the run that wrote it actually finished. Chapter 27.6 produced the database — this chapter is about trusting it.
1. Two Tools, Two Formats
The two simulators on this machine write two different things, and the difference is visible in the first four bytes:
$ for f in wv/*; do
> printf '%-14s %9d bytes first 4: %s\n' "$(basename $f)" "$(wc -c < $f)" \
> "$(head -c 4 $f | od -An -c | tr -s ' ')"
> done
icarus.vcd 481561 bytes first 4: $ d a t
nvc.fst 416 bytes first 4: 376 \0 \0 \0icarus.vcd is a VCD: plain ASCII, starting with the literal $date keyword. nvc.fst is an FST: binary, with a magic byte. The practical consequences are three:
A viewer reads some formats and not others, so the format is part of the invocation's correctness, not a detail. Passing an FST to a tool expecting a VCD is a failure whose message is usually about parsing rather than about formats.
Only one of the two can be inspected with text tools. Everything in the rest of this chapter that greps a database applies to VCD. For FST, the checks available to a shell are the magic byte and the size — and when I say in section 4 that a check is weak, FST is where it is weakest.
Size is not comparable across formats. The 416-byte FST and the 481 KB VCD are not describing runs of wildly different lengths; they are different encodings, and FST compresses. A disk-usage report that mixes them and ranks by size is ranking by format.
2. Finding the Database That Belongs to the Failure
With Chapter 27.3's arrangement — one directory per run — this is a listing rather than a search:
$ runs runs
RUN VERDICT DUMP BYTES WHEN
icarus_bogus_s1 FAIL dump.vcd 1291 2026-10-09 02:45:34
icarus_fairness_s1 PASS dump.vcd 1677 2026-10-09 02:45:34
icarus_smoke_s1 PASS dump.vcd 1470 2026-10-09 02:45:34
nvc_bogus_s1 FAIL dump.fst 537 2026-10-09 02:45:35
nvc_fairness_s1 PASS dump.fst 554 2026-10-09 02:45:35
nvc_smoke_s1 PASS dump.fst 549 2026-10-09 02:45:35The failing run is named, its directory is named, and the database inside it is the right one by construction. The viewer command then writes itself:
# FORM
gtkwave runs/icarus_bogus_s1/dump.vcd &Without that arrangement the question is genuinely hard, and Chapter 27.3 measured why: a VCD's only self-describing field is a wall-clock $date, so a directory of same-named dumps cannot be sorted back into runs. There is no recovery procedure. The listing above is only possible because the directories exist.
3. Which Process Did You Actually Kill?
Before trusting a database, it is worth knowing how runs end badly, because the most common cause of a truncated dump is a run that was stopped. And stopping one is less straightforward than it looks.
# A. the subshell form
( cd wv && vvp ../build.vvp +TESTNAME=random +CYCLES=400000 +SEED=5 \
+DUMP +DUMP_FILE=sub.vcd > sub.log 2>&1 ) &
pid=$!
sleep 1; kill -9 "$pid"; wait "$pid"immediately after the kill:
wait status 137 ; vvp still running? 1
sub.log verdict lines: 0 ; sub.vcd 27762688 bytes
after waiting for the orphan to finish:
vvp still running? 0
sub.log verdict lines: 1 ; sub.vcd 43563113 byteswait reported 137 — killed — and the tool was still running. $! was the PID of the subshell, not of vvp. Killing the subshell orphaned the simulator, which carried on writing: 27.8 MB at the moment of the kill, 43.6 MB by the time it finished, at which point it wrote a verdict into the log. The wrapper believed it had stopped a job that in fact ran to completion and consumed another sixteen megabytes of disk after being declared dead.
# B. exec, so $! IS the tool
( cd wv && exec vvp ../build.vvp +TESTNAME=random +CYCLES=400000 +SEED=5 \
+DUMP +DUMP_FILE=tool.vcd > tool.log 2>&1 ) &
pid=$!
sleep 1; kill -9 "$pid"; wait "$pid" wait status 137 ; vvp still running? 0
tool.log verdict lines: 0 ; tool.vcd 26165248 bytesWith exec, the subshell replaces itself with the simulator, so $! is the simulator's PID and the kill lands on it. No survivor, no verdict in the log, and a dump that stops wherever the write buffer did.
4. A Truncated Database Looks Almost Exactly Like a Good One
Now the question this chapter exists for. Here is a complete dump and the truncated one from case B, side by side:
$ for f in wv/good.vcd wv/tool.vcd; do
> printf '%-12s %9d B ends-with-newline=%s last line: %s\n' \
> "$(basename $f)" "$(wc -c < $f)" \
> "$( [ "$(tail -c 1 "$f" | od -An -c | tr -d ' ')" = '\n' ] && echo yes || echo NO )" \
> "$(tail -c 120 "$f" | tail -1)"
> done
good.vcd 481545 B ends-with-newline=yes last line: 1"
tool.vcd 26165248 B ends-with-newline=NO last line: b11000100101001101101010This time the truncation is visible: the file does not end with a newline, and the last line is a b-prefixed vector with no identifier after it, which is not a legal VCD record. A tail check catches this.
It does not always. An earlier kill of the same command, differing only in where the write buffer happened to stop, produced this:
tool.vcd 12689408 B ends-with-newline=yes last line: #15#15 is a timestamp — a perfectly legal record — and the file ended with a newline. The same check that caught the first case passes this one.
The deterministic discriminator is outside the file:
$ grep -cE 'TEST (PASSED|FAILED)' wv/good.log
1
$ grep -cE 'TEST (PASSED|FAILED)' wv/tool.log
0The log beside it. A run that finished wrote a verdict; a run that was killed did not. That is the whole test, it is the same answer every time, and it works because it asks about the run rather than about the file.
5. A Check That Discriminates
#!/bin/bash
# wavecheck DUMP — is this waveform database complete and usable?
#
# Header checks do NOT discriminate: a killed run leaves a dump with the same
# $date, $enddefinitions and $var counts as a complete one, and it may be far
# larger. The tail check below is worth having and is NOT sufficient, because
# truncation sometimes lands on a legal record boundary. The load-bearing check
# is the run's OWN log, beside it in the run directory.
set -u
dump=${1:?usage: wavecheck DUMP}
dir=$(dirname "$dump")
rc=0
say() { printf '%-6s %s\n' "$1" "$2"; }
[ -s "$dump" ] || { say FAIL "$dump is missing or empty"; exit 1; }
case "$dump" in
(*.fst)
m=$(head -c 1 "$dump" | od -An -tu1 | tr -d ' ')
[ "$m" = 254 ] && say ok "FST magic present, $(wc -c < "$dump") bytes" \
|| { say FAIL "not an FST (first byte $m)"; rc=1; }
say note "no FST reader exists on this machine — completeness unverified"
exit $rc ;;
(*.vcd) : ;;
(*) say warn "unknown extension; only the size was checked"; exit 0 ;;
esac
grep -q 'enddefinitions' "$dump" || { say FAIL "no \$enddefinitions — header never completed"; rc=1; }
# 1. corroborating, non-deterministic: is the final record well formed?
last=$(tail -c 256 "$dump" | tail -1)
case "$last" in
([bBrR]*' '?*) say ok "final record complete: ${last:0:30}" ;;
([bBrR]*) say FAIL "final record severed — a vector with no identifier: ${last:0:30}"; rc=1 ;;
('#'[0-9]*) say ok "final record is a timestamp: $last" ;;
([01xXzZ]?*) say ok "final record complete: $last" ;;
(*) say FAIL "final record unrecognised: ${last:0:30}"; rc=1 ;;
esac
# 2. load-bearing: the log that belongs to THIS dump, in THIS directory
log=""
for cand in "$dir/run.log" "${dump%.vcd}.log"; do
[ -r "$cand" ] && { log=$cand; break; }
done
if [ -z "$log" ]; then
say FAIL "no log beside the dump — completeness cannot be confirmed"
rc=1
elif grep -qE 'TEST (PASSED|FAILED)' "$log"; then
say ok "paired log $(basename "$log") carries a verdict"
else
say FAIL "paired log $(basename "$log") has no verdict — the run never finished"
rc=1
fi
# 3. corroborating: did the dump reach the time the log says the run reached?
fin=$(grep -oE '\$finish called at [0-9]+' "${log:-/dev/null}" | tail -1 | grep -oE '[0-9]+$' || true)
lastt=$(grep -o '^#[0-9]*' "$dump" | tail -1 | tr -d '#' || true)
if [ -n "${fin:-}" ] && [ -n "${lastt:-}" ]; then
if [ "$lastt" -lt "$fin" ]; then
say FAIL "dump stops at $lastt but the run reached $fin"; rc=1
else
say ok "dump reaches the run's finish time ($fin)"
fi
fi
exit $rcFive cases, including a copy made the wrong way:
$ wavecheck wv/good.vcd
ok final record complete: 1"
ok paired log good.log carries a verdict
ok dump reaches the run's finish time (62705)
$ echo $?
0
$ wavecheck wv/tool.vcd
FAIL final record severed — a vector with no identifier: b11000100101001101101010
FAIL paired log tool.log has no verdict — the run never finished
$ echo $?
1
$ wavecheck wv/nvc.fst
ok FST magic present, 416 bytes
note no FST reader exists on this machine — completeness unverified
$ echo $?
0
$ wavecheck wv/nope.vcd
FAIL wv/nope.vcd is missing or empty
$ echo $?
1
$ cp wv/good.vcd /tmp/lonely/ && wavecheck /tmp/lonely/good.vcd
ok final record complete: 1"
FAIL no log beside the dump — completeness cannot be confirmed
$ echo $?
1Three design decisions in that script are worth stating, because each corrects an earlier draft.
The two corroborating checks are separated from the load-bearing one by a comment, because their roles genuinely differ. The tail check and the time comparison can each catch a real problem and neither can be relied on to notice one. Writing that down in the script is how the next person avoids re-deriving it.
A missing log is a failure, not a warning. An earlier version warned and exited 0, which means an unpaired dump passed. If completeness cannot be confirmed, the honest answer is that the check did not pass — and the last case above is exactly the situation that produces it.
FST exits early with an explicit note. The script says what it did not check rather than implying the file is sound. Reporting ok for a binary format with no reader available would be the same false green in a different costume.
6. Opening It, Including From Somewhere Else
With the right database identified and checked, the invocation is the easy part — and the one piece of shell that matters is detaching it properly:
# FORM. & alone is not enough: the viewer dies with your shell.
gtkwave runs/icarus_bogus_s1/dump.vcd &
# detached, surviving a disconnect, with its noise out of your terminal
nohup gtkwave runs/icarus_bogus_s1/dump.vcd > /dev/null 2>&1 &
disownnohup makes the process ignore the hangup signal that arrives when your session ends, the redirections keep the viewer's diagnostics out of your terminal, and disown removes it from the shell's job table so exiting does not warn about it. This is Chapter 7.5's machinery applied to the one process you genuinely want to outlive you.
For a remote session there are two approaches and they are not equivalent:
Run the viewer remotely and forward the display. The waveform stays where it is and the window is drawn locally. This is the usual arrangement on a compute server, and its weakness is that a large waveform means a lot of interaction latency.
Copy the database locally and run the viewer there. This is often faster to use and slower to start, and it introduces a correctness hazard worth naming: the copy is a second artifact with no log beside it. Everything in section 4 stops working, because the discriminator was the paired log. If you copy a database, copy the log with it:
# the lone file: the only check that discriminates is now impossible
$ cp wv/good.vcd /tmp/lonely/ && wavecheck /tmp/lonely/good.vcd
ok final record complete: 1"
FAIL no log beside the dump — completeness cannot be confirmed
# the whole run directory: everything still works
$ cp wv/good.log /tmp/lonely/run.log && wavecheck /tmp/lonely/good.vcd
ok final record complete: 1"
ok paired log run.log carries a verdict
ok dump reaches the run's finish time (62705)So the remote copy is one flag away from being useless to check:
# copy the run, not the file -- so the dump keeps the log that validates it
rsync -a server:/proj/runs/icarus_bogus_s1/ ./icarus_bogus_s1/
wavecheck ./icarus_bogus_s1/dump.vcdThat is the difference between a database you can check and one you can only hope about.
Practice
- Identify the format of two databases from their first four bytes, and say which of them a shell can inspect.
- Explain why ranking databases by size is misleading when the set mixes VCD and FST.
- List your run directories with verdicts and pick out the database belonging to a specific failure without searching.
- Start a simulation with
( … ) &, kill$!, then check withpgrepwhether the tool is still running — and check the dump's size again a minute later. - Do it again with
execand confirm the tool actually dies. - Compare the two runs' logs for a verdict line, and explain which run finished.
- Compare a truncated dump with a complete one on size, final newline and last record. Then kill a second run and compare again — note whether the same property separates them both times.
- Confirm the truncated dump is larger than the complete one and say what that does to a size-based check.
- Write
wavecheckand prove it fails on the truncated dump and passes on the complete one. - Make the paired-log check into a warning that exits 0, then run it on an unpaired dump and decide whether you would want that result.
- Copy a dump without its log to another directory and run
wavecheckon it. Then copy the whole run directory and run it again.
Check Your Understanding
- What are the first four bytes of a VCD, and of an FST, and why does it matter to a shell script?
- Why can a shell check an FST much less thoroughly than a VCD?
- With one directory per run, why is finding the right database a listing rather than a search?
- In the subshell form, whose PID is
$!, and what happened to the simulator when it was killed? - What did
waitreport in that case, and why was it misleading? - What two things does
execfix in a wrapper? - Why is a tail-of-file check on a truncated dump non-deterministic?
- What is the deterministic check, and why does it work where file-level checks cannot?
- What were the two independent defects in the first version of
wavecheck? - Why is copying a waveform to your laptop a correctness hazard, and what is the fix?
7. Remember
- The format is part of the invocation's correctness, and only the ASCII one can be inspected from a shell.
- One directory per run turns "which database" into a listing, and without it there is no recovery — a VCD's only self-description is a timestamp.
( … ) &withkill $!kills the subshell and orphans the tool.waitreported 137 while the simulator kept writing, from 27.8 MB to 43.6 MB, and then recorded a verdict.execmakes$!the tool's PID, which is what makes every signal your flow sends land on the right process.- A file-level truncation check is non-deterministic: two kills of the same command left a severed vector one time and a legal
#15timestamp the next. It corroborates; it cannot decide. - Size and header are no help at all — the truncated dump was fifty-four times larger, with identical
$date,$enddefinitionsand$varcounts. - The deterministic discriminator is the run's own log beside it, which is a statement about the run rather than the file.
- A missing paired log is a failure, not a warning. If completeness cannot be confirmed, the check did not pass.
- Say what you did not check rather than reporting ok — an unverifiable binary format gets a note, not a pass.
- Copy the run directory, never the lone database, or you discard the only thing that can validate it.
8. What Is Next
Everything so far has been one tool at a time. Chapter 27.8 puts two genuinely different simulators behind one interface — one with three stages and one with two, one reading plusargs and one reading elaboration generics — and measures what that difference costs.
Where this fits
Part of the Linux & Bash curriculum.
