Stack Trace
Posted: 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
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...
...results in this output:
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.
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),
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.
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
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.