Stack Trace

General discussions about using the Astrobe IDE to program the FPGA RISC5 cpu used in Project Oberon 2013
Post Reply
gray
Posts: 109
Joined: Tue Feb 12, 2019 2:59 am
Location: Mauritius

Stack Trace

Post by gray » Thu Apr 20, 2023 5:16 am

I have always found a "stack trace", ie. the display of the chain of procedure calls that lead to an error, very useful to find the causes of run-time problems. This chain is unrolled by walking the stack backwards from the error point -- or from any state of calls -- frame by frame using the frame pointer, which is also stored on the stack.

However, the RISC5 processor, as implemented in the FPGA, and the compiler do not use a frame pointer. There exists a software solution based on heuristics to identify the single frames on the stack [1]. It requires a compiler change.

For some FPGA fun, I have implemented a solution there, which I call Calltrace. It works with the Astrobe compiler.

The device in the hardware simply
  • monitors the CPU's instruction register (IR) for the two instructions that indicate the entry and exit, respectively, into and out of a procedure, and
  • registers the corresponding value of the link register (LNK),
maintaining a "shadow stack" of link register values and thus procedure calls.

Two software modules serve to read out that stack and display the corresponding procedure calls [3]. Since we don't have the names of the procedures available at run-time (apart from the parameterless exported procedures, the commands), we can only display the module names, plus the code line number of the BL instructions in their disassembly listing. Some legwork needs to be done to figure out the calls, but since the source code is quoted as well in the assembly code, it's pretty straight forward and useful.

This simple test program...

Code: Select all

MODULE TestCall1;
  IMPORT Calltrace, CalltraceView, TestCall2, Texts;
  
  VAR W: Texts.Writer;

  PROCEDURE c2;
  BEGIN
    Texts.WriteString(W, "c2 ");
    TestCall2.Do2
  END c2;

  PROCEDURE c1;
  BEGIN
    Texts.WriteString(W, "c1 ");
    c2
  END c1;

  PROCEDURE c0;
  BEGIN
    Texts.WriteString(W, "c0 ");
    c1
  END c0;

  PROCEDURE do;
  BEGIN;
    CalltraceView.ShowTrace(0);
    c0
  END do;

  PROCEDURE Run*;
  BEGIN
    do
  END Run;
END TestCall1.

MODULE TestCall2;

  IMPORT TestCall3, CalltraceView;

  PROCEDURE Do2*;
  BEGIN
    CalltraceView.ShowTrace(1);
    TestCall3.Do3
  END Do2;
END TestCall2.

MODULE TestCall3;

  IMPORT CalltraceView;

  PROCEDURE c0;
  BEGIN
    CalltraceView.ShowTrace(3);
    ASSERT(FALSE)
  END c0;

  PROCEDURE Do3*;
  BEGIN
    c0
  END Do3;
END TestCall3.
...results in this output:

Code: Select all

Calltrace id: 0
  module                    addr   m-addr    line
  TestCall1             0000F2E0 000000B4      45
  TestCall1             0000F2FC 000000D0      52
  Oberon                0000B8EC 00000478     286
  Oberon                0000BCE8 00000874     541
  Oberon                0000BE2C 000009B8     622
max depth: 7
c0 c1 c2
Calltrace id: 1
  module                    addr   m-addr    line
  TestCall2             0000EFDC 0000000C       3
  TestCall1             0000F254 00000028      10
  TestCall1             0000F28C 00000060      24
  TestCall1             0000F2C4 00000098      38
  TestCall1             0000F2E4 000000B8      46
  TestCall1             0000F2FC 000000D0      52
  Oberon                0000B8EC 00000478     286
  Oberon                0000BCE8 00000874     541
  Oberon                0000BE2C 000009B8     622
max depth: 11

Calltrace id: 3
  module                    addr   m-addr    line
  TestCall3             0000EE3C 0000000C       3
  TestCall3             0000EE58 00000028      10
  TestCall2             0000EFE0 00000010       4
  TestCall1             0000F254 00000028      10
  TestCall1             0000F28C 00000060      24
  TestCall1             0000F2C4 00000098      38
  TestCall1             0000F2E4 000000B8      46
  TestCall1             0000F2FC 000000D0      52
  Oberon                0000B8EC 00000478     286
  Oberon                0000BCE8 00000874     541
  Oberon                0000BE2C 000009B8     622
max depth: 13

  pos  131  TRAP   7 in TestCall3 at 0000EE44

Calltrace id: -1
  module                    addr   m-addr    line
  TestCall3             0000EE40 00000010       4
  TestCall3             0000EE58 00000028      10
  TestCall2             0000EFE0 00000010       4
  TestCall1             0000F254 00000028      10
  TestCall1             0000F28C 00000060      24
  TestCall1             0000F2C4 00000098      38
  TestCall1             0000F2E4 000000B8      46
  TestCall1             0000F2FC 000000D0      52
  Oberon                0000B8EC 00000478     286
  Oberon                0000BCE8 00000874     541
  Oberon                0000BE2C 000009B8     622
max depth: 13
Note the trap handler's output indicating the assertion violation in the "top" procedure of the test program.

I have installed the utility procedure CalltraceView.ShowTrace in System.Trap, but it can be used at any point in the code, as in the demo program.

You can find the Verilog and Oberon modules in this GitHub repository [2]. The changes to the software are minimal: the call to the aforementioned procedure to read out the stack in the trap handler in module System, and two calls to reset the stack before calling Oberon.Loop in module Oberon. Since the calltrace stack needs to survive a system reset, this cannot be done directly in the hardware.

For the FPGA, I have added the calltrace device to the top level definition, using the two unused IO addresses. Since the IR and the LNK register need to be available, I have "pulled" them out as external signals from the RISC5 CPU. There's also a Vivado project file to build the hardware configuration for the Arty A7.

The changed files are found in the "lib-ext" directories. The changes are marked, and of course you can use git functionality to inspect the files. Let me know if anything goes wrong with building the software or configuring the FPGA. Happy to help.

PS: a fun extension could be to also register the stack pointer values together with the link register values, so the stack could be inspected as well. :)

[1] https://github.com/schierlm/Oberon2013M ... pBacktrace
[2] https://github.com/ygrayne/oberon-epo
[3] I tend to separate low level driver modules and any related text output modules, eg. to easily allow different outputs. The two modules could be unified into one.

Post Reply