First Foray into Delve

24 Jul 2016

I started playing with the Go debuger Delve, and decided to capture some rough notes here.

Our sample file:

package main

import "fmt"

func main() {
  fmt.Println("Hello")
  a := 2
  b := 3
  c := a + b
  fmt.Printf("a + b is %d\n", c)
  for i := 0; i < 10; i++ {
    fmt.Printf("i == %d\n", i)
  }
  fmt.Println("We are near the end now.")
  fmt.Println("This is the end.")
}

A run of our program:

$ go build
$ ./hello 
Hello
a + b is 5
i == 0
i == 1
i == 2
i == 3
i == 4
i == 5
i == 6
i == 7
i == 8
i == 9
We are near the end now.
This is the end.

Let's install Delve.

$ go get github.com/derekparker/delve/cmd/dlv

That was easy.

If we launch on our compiled binary, everything is in assemlber! Very cool, but I was hoping to see golang:

$ dlv exec ./hello 
Type 'help' for list of commands.
(dlv) list
> _rt0_amd64_linux() /home/mwood/golang/go/src/runtime/rt0_linux_amd64.s:8 (PC: 0x456700)
     3:	// license that can be found in the LICENSE file.
     4:	
     5:	#include "textflag.h"
     6:	
     7:	TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
=>   8:		LEAQ	8(SP), SI // argv
     9:		MOVQ	0(SP), DI // argc
    10:		MOVQ	$main(SB), AX
    11:		JMP	AX
    12:	
    13:	// When building with -buildmode=c-shared, this symbol is called when the shared
(dlv) 

I wonder if I can get golang if I run dlv against the source?

$ rm hello
$ dlv debug
Type 'help' for list of commands.
(dlv) list
> _rt0_amd64_linux() /home/mwood/golang/go/src/runtime/rt0_linux_amd64.s:8 (PC: 0x456870)
     3:	// license that can be found in the LICENSE file.
     4:	
     5:	#include "textflag.h"
     6:	
     7:	TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
=>   8:		LEAQ	8(SP), SI // argv
     9:		MOVQ	0(SP), DI // argc
    10:		MOVQ	$main(SB), AX
    11:		JMP	AX
    12:	
    13:	// When building with -buildmode=c-shared, this symbol is called when the shared

No, same thing! I wonder how to list go source?

Oh, this blog entry says that I would want to set a breakpoint at main.main and then continue until I get there:

$ dlv debug
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x40101b for main.main() ./main.go:5
(dlv) continue
> main.main() ./main.go:5 (hits goroutine(1):1 total:1) (PC: 0x40101b)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
=>   5:	func main() {
     6:		fmt.Println("Hello")
     7:		a := 2
     8:		b := 3
     9:		c := a + b
    10:		fmt.Printf("a + b is %d\n", c)

Oh, and now we have the source of my program! Nice! So my earlier attempts must have started at the real beginning of the program, in the assembler routines before main.main is called. Makes sense.

Ah, and then I can type next to go to the next source line. When I type next inside the loop, I correctly loop around. I can also print the value of i when I am inside the loop:

This is after doing next a few times:

(dlv) next
> main.main() ./main.go:12 (PC: 0x401276)
     7:		a := 2
     8:		b := 3
     9:		c := a + b
    10:		fmt.Printf("a + b is %d\n", c)
    11:		for i := 0; i < 10; i++ {
=>  12:			fmt.Printf("i == %d\n", i)
    13:		}
    14:		fmt.Println("We are near the end now.")
    15:		fmt.Println("This is the end.")
    16:	}
(dlv) print i
2

Now, when I decide I want to break out of the loop, I set a breakpoint at line 14, outside the loop, and continue until I get to it:

(dlv) break 14
Breakpoint 2 set at 0x4013a4 for main.main() ./main.go:14
(dlv) continue
i == 3
i == 4
i == 5
i == 6
i == 7
i == 8
i == 9
> main.main() ./main.go:14 (hits goroutine(1):1 total:1) (PC: 0x4013a4)
     9:		c := a + b
    10:		fmt.Printf("a + b is %d\n", c)
    11:		for i := 0; i < 10; i++ {
    12:			fmt.Printf("i == %d\n", i)
    13:		}
=>  14:		fmt.Println("We are near the end now.")
    15:		fmt.Println("This is the end.")
    16:	}
(dlv) print i
10

Nice.

And, as expected, if, at the start of main, I feel like stepping into fmt.Printf, instead of nexting past it, I can do that too:

(dlv) next
> main.main() ./main.go:6 (PC: 0x40102d)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
     5:	func main() {
=>   6:		fmt.Println("Hello")
     7:		a := 2
     8:		b := 3
     9:		c := a + b
    10:		fmt.Printf("a + b is %d\n", c)
    11:		for i := 0; i < 10; i++ {
(dlv) step
> runtime.convT2E() /home/mwood/golang/go/src/runtime/iface.go:128 (PC: 0x40bfd3)
   123:		tab := getitab(inter, t, false)
   124:		atomicstorep(unsafe.Pointer(cache), unsafe.Pointer(tab))
   125:		return tab
   126:	}
   127:	
=> 128:	func convT2E(t *_type, elem unsafe.Pointer, x unsafe.Pointer) (e eface) {
   129:		if raceenabled {
   130:			raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&t)), funcPC(convT2E))
   131:		}
   132:		if msanenabled {
   133:			msanread(elem, t.size)

So, can we get inside a goroutine easily?

Here is a new program:

package main

import "fmt"

func main() {
  fmt.Println("Hello")
  done := make(chan struct{})
  go func() {
    a := 4
    b := 5
    c := a + b
    fmt.Printf("in anonymous go func, a + b is %d\n", c)
    done <- struct{}{}
  }()
  <-done // wait for goroutine to finish
  a := 2
  b := 3
  c := a + b
  fmt.Printf("a + b is %d\n", c)
  for i := 0; i < 10; i++ {
    fmt.Printf("i == %d\n", i)
  }
  fmt.Println("We are near the end now.")
  fmt.Println("This is the end.")
}          

I ran dlv debug and set a breakpoint at main, and then...

(dlv) next
> main.main() ./main.go:6 (PC: 0x40102d)
     1:	package main
     2:	
     3:	import "fmt"
     4:	
     5:	func main() {
=>   6:		fmt.Println("Hello")
     7:		done := make(chan struct{})
     8:		go func() {
     9:			a := 4
    10:			b := 5
    11:			c := a + b
(dlv) break 11
Breakpoint 2 set at 0x4016f1 for main.main.func1() ./main.go:11
(dlv) continue
Hello
> main.main.func1() ./main.go:11 (hits goroutine(5):1 total:1) (PC: 0x4016f1)
     6:		fmt.Println("Hello")
     7:		done := make(chan struct{})
     8:		go func() {
     9:			a := 4
    10:			b := 5
=>  11:			c := a + b
    12:			fmt.Printf("in anonymous go func, a + b is %d\n", c)
    13:			done <- struct{}{}
    14:		}()
    15:		<-done // wait for goroutine to finish
    16:		a := 2

So it looks like there is no problem getting inside a go routine, which is nice.

If I change my program to have a named function, it might be easier to give that function name as a break point the first time it is called.

package main

import "fmt"

func main() {
  fmt.Println("Hello")
  done := make(chan struct{})
  go DoStuff(done)
  <-done // wait for goroutine to finish
  a := 2
  b := 3
  c := a + b
  fmt.Printf("a + b is %d\n", c)
  for i := 0; i < 10; i++ {
    fmt.Printf("i == %d\n", i)
  }
  fmt.Println("We are near the end now.")
  fmt.Println("This is the end.")
}

func DoStuff(done chan<- struct{}) {
  a := 4
  b := 5
  c := a + b
  fmt.Printf("in anonymous go func, a + b is %d\n", c)
  done <- struct{}{}
}
$ dlv debug
Type 'help' for list of commands.
(dlv) break main.DoStuff
Breakpoint 1 set at 0x4016d8 for main.DoStuff() ./main.go:21
(dlv) continue
Hello
> main.DoStuff() ./main.go:21 (hits goroutine(5):1 total:1) (PC: 0x4016d8)
    16:		}
    17:		fmt.Println("We are near the end now.")
    18:		fmt.Println("This is the end.")
    19:	}
    20:	
=>  21:	func DoStuff(done chan<- struct{}) {
    22:		a := 4
    23:		b := 5
    24:		c := a + b
    25:		fmt.Printf("in anonymous go func, a + b is %d\n", c)
    26:		done <- struct{}{}

That worked quite nicely!