Search Apps Documentation Source Content File Folder Download Copy

Debugging Gno Programs

In this article, we introduce the new Gno debugger feature and show how it can be used to better understand Gno programs and help to fix bugs.

Motivation for a Gno debugger

Debugging is twice as hard as writing code.

-- Brian Kerninghan, "The Elements of Programming Style"

On average, you spend about eight to ten times debugging as you do writing code.

-- Anonymous

Having a good debugger is important. But the Gno language is almost Go, and gno.land itself is entirely written in Go. Could I just use the existing Go tools, i.e. the delve debugger, to take control and debug my Gno programs?

You cannot debug your Gno program this way because doing so would entail debugging the Gno virtual machine rather than your own program. The relevant state information would be opaque and would need to be reversed and reconstructed from internal Gno virtual machine data structures.

The Gno debugger addresses this issue by displaying the state of the Gno program memory symbolically. It allows for control of program execution at the source code level, regardless of the virtual machine implementation.

Setting up

The Gno debugger is fully integrated in the gno binary, which is the tool required to build, test, and run Gno programs locally.

There is no need to install a specific tool. You just have to install the gno tool itself with:

1git clone https://github.com/gnolang/gno
2cd gno
3go install ./gnovm/cmd/gno

We are now ready to play with Gno programs. Let's consider a simple classic example as a target program, the computation of Fibonacci numbers:

 1// fib.gno
 2package main
 3
 4// fib returns the nth number in the Fibonacci sequence.
 5func fib(n int) int {
 6        if n < 2 {
 7                return n
 8        }
 9        return fib(n-2) + fib(n-1)
10}
11
12func main() {
13        println(fib(4))
14}
15

To execute this program, we run the command gno run ./fib.gno. To activate the debugger, we just pass the -debug flag: gno run -debug ./fib.gno. Use gno run -help to get more options if needed.

Quick tour of the debugger

When you start a program in debug mode, you are greeted by a prompt allowing you to interact with it via the terminal:

1$ gno run -debug ./fib.gno
2Welcome to the GnoVM debugger. type 'help' for list of commands.
3dbg>

Entering help gives you the list of available commands and their short usage:

 1dbg> help
 2The following commands are available:
 3
 4break|b [locspec]         Set a breakpoint.
 5breakpoints|bp            Print out info for active breakpoints.
 6clear [id]                Delete breakpoint (all if no id).
 7continue|c                Run until breakpoint or program termination.
 8detach                    Close debugger and resume program.
 9down [n]                  Move the current frame down by n (default 1).
10exit|quit|q               Exit the debugger and program.
11help|h [command]          Print the help message.
12list|l [locspec]          Show source code.
13print|p <expression>      Print a variable or expression.
14stack|bt                  Print stack trace.
15step|s                    Single step through program.
16stepi|si                  Single step a single VM instruction.
17up [n]                    Move the current frame up by n (default 1).
18
19Type help followed by a command for full documentation.
20dbg>

If you have already used a debugger before, like gdb or lldb for C/C++ programs, or delve for Go programs, the Gno debugger should look familiar; the commands are similar in their syntax and usage.

The commands can be classified in the following categories:

  • managing breakpoints: break, breakpoints, clear,
  • controlling execution: step, stepi, continue,
  • browsing code, data and stack: list, print, stack,
  • navigating the stack: up, down,
  • quitting the debugger: detach, exit.

Controlling and exploring the program state

Let's go back to our Fibonacci program, still paused. We step a first time, which instructs the GnoVM to execute a single statement and give back control to the user:

 1dbg> s
 2> main.main() main/./fib.gno:11:1
 3      7: 		return n
 4      8: 	}
 5      9: 	return fib(n-2) + fib(n-1)
 6     10: }
 7     11: 
 8=>   12: func main() {
 9     13: 	println(fib(4))
10     14: }

The first output line > main.main() main/./fib.gno:11:1 indicates the precise current location in source code, followed by a short source listing around this location. The current line is indicated by the cursor =>.

From there, we could repeat step commands to progress, but that would be too tedious. Instead, we set a breakpoint at an interesting line in the fib function, and continue to it directly:

 1dbg> b 7
 2Breakpoint 0 at main main/./fib.gno:7:1
 3dbg> c
 4> main.fib() main/./fib.gno:7:10
 5      2: package main
 6      3: 
 7      4: // fib returns the nth number in the Fibonacci sequence.
 8      5: func fib(n int) int {
 9      6: 	if n < 2 {
10=>    7: 		return n
11      8: 	}
12      9: 	return fib(n-2) + fib(n-1)
13     10: }
14     11: 
15dbg>

Note that we have used the short alias of commands: b for break and c for continue. We only need to specify the line number when setting the break point here, due to it being in the same file. Setting break points in other files requires specifying the full file path and line number.

We can now examine the call stack which indicates the successive nested function calls up to the current location:

 1dbg> stack
 20	in main.fib
 3	at main/./fib.gno:7:10
 41	in main.fib
 5	at main/./fib.gno:9:20
 62	in main.fib
 7	at main/./fib.gno:9:20
 83	in main.main
 9	at main/./fib.gno:13:2
10dbg>

We see a call stack of depth 4, with call frames (local function contexts) numbered from 0 to 3, 0 being the current call level (the deepest). This information is crucial, especially when debugging recursive functions like fib. We know that the caller and its caller were both fib.

Now we want to examine the value of the local parameter n, for each call level:

 1dbg> print n
 2(0 int)
 3dbg> up
 4> main.fib() main/./fib.gno:7:10
 5Frame 1: main/./fib.gno:9:20
 6      4: // fib returns the nth number in the Fibonacci sequence.
 7      5: func fib(n int) int {
 8      6: 	if n < 2 {
 9      7: 		return n
10      8: 	}
11=>    9: 	return fib(n-2) + fib(n-1)
12     10: }
13     11: 
14     12: func main() {
15     13: 	println(fib(4))
16dbg> print n
17(2 int)
18dbg> up
19> main.fib() main/./fib.gno:7:10
20Frame 2: main/./fib.gno:9:20
21      4: // fib returns the nth number in the Fibonacci sequence.
22      5: func fib(n int) int {
23      6: 	if n < 2 {
24      7: 		return n
25      8: 	}
26=>    9: 	return fib(n-2) + fib(n-1)
27     10: }
28     11: 
29     12: func main() {
30     13: 	println(fib(4))
31dbg> print n
32(4 int)
33dbg>

We see that the local value n is 0 at current frame 0, 2 at frame 1 and 4 at frame 2, which corresponds to the nested calls of fib expressed at line 9.

The up and down stack navigation commands enable the debugger to display the value of local function variables and parameters for the whole call chain.

In this example, the n variable is simply an integer, but the print command is also able to handle more complex expressions to uncover the content of arbitrary maps, struct, arrays, etc using the same syntax as Go. For example, print a.b[n] will print as expected, with a being a value of type:

1var a struct {
2    b []string
3}

For security reasons, the print command will only evaluate expressions with no side effects on the virtual machine state. For example, it is not possible to perform an arithmetic operation like print a + 2, or to call a function like printf f(6).

Conclusion

We have introduced the new Gno debugger and presented its main capabilities.

This is just the start of a new project, with a lot of room for improvement. The whole Gno project being open source, you are welcome not only to provide feedbacks and suggestions, but also to contribute at https://github.com/gnolang/gno.


Tags: #blog #post #tutorial #gno #debugger

Written by mvertes on 06 Aug 2024

Published by g125em6arxsnj49vx35f0n0z34putv5ty3376fg5 to gno.land's blog