Link Search Menu Expand Document

Debugging bazel/golang binaries from the command line

A mini-tutorial.

Debugging golang binaries using the delve command-line interface is a useful skill and surprisingly enjoyable interface. In this page we’ll go over the basics of dlv exec.

Installing Delve

Refer to the official instructions to install. Options include:

  • go get github.com/go-delve/delve/cmd/dlv
  • > Go: Install/Update tools (contributed by the vscode-go extension)

Building Binaries

To debug your binaries, build the go_binary or go_test targets with debugging symbols. Setting the dbg compilation mode affects the generated file path and (not surprisingly) the file size:

$ bazel build  @bazel_gazelle//label:go_default_test
Target @bazel_gazelle//label:go_default_test up-to-date:
  bazel-bin/external/bazel_gazelle/label/darwin_amd64_stripped/go_default_test
$ du -sh bazel-bin/external/bazel_gazelle/label/darwin_amd64_stripped/go_default_test
3.0M	bazel-bin/external/bazel_gazelle/label/darwin_amd64_stripped/go_default_test
$ bazel build -c dbg @bazel_gazelle//label:go_default_test
Target @bazel_gazelle//label:go_default_test up-to-date:
  bazel-bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test
$ du -sh bazel-bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test
3.9M	bazel-bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test

(here we’re arbitrarily choosing a test target in the @bazel_gazelle workspace)

Executing Delve

Not that we have a suitable binary, we can use the dlv exec subcommand to start a delve repl that runs the golang binary in debug mode:

$ dlv exec --api-version=2 bazel-bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test
Type 'help' for list of commands.
(dlv)

The dlv process is now waiting for additional commands. A handy way to get oriented is to set a breakpoint at the main function in the main package.

(dlv) b main.main
Breakpoint 1 set at 0x114854b for main.main() bazel-out/darwin-dbg/bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test%/testmain.go:59

Ok, now let’s continue (c) to that point:

(dlv) c
> main.main() bazel-out/darwin-dbg/bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test%/testmain.go:59 (hits goroutine(1):1 total:1) (PC: 0x114854b)
    54:			}
    55:		}
    56:		return tests
    57:	}
    58:
=>  59:	func main() {
    60:		if shouldWrap() {
    61:			err := wrap("github.com/bazelbuild/bazel-gazelle/label")
    62:			if xerr, ok := err.(*exec.ExitError); ok {
    63:				os.Exit(xerr.ExitCode())
    64:			} else if err != nil {

Awesome! We are now debugging and get a contextual source window into our current breakpoint location. We can step over the next few lines with the next command (n):

(dlv) n
> main.main() bazel-out/darwin-dbg/bin/external/bazel_gazelle/label/darwin_amd64_debug/go_default_test%/testmain.go:76 (PC: 0x11486a2)
    71:
    72:		// Check if we're being run by Bazel and change directories if so.
    73:		// TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent proxy.
    74:		testSrcdir := os.Getenv("TEST_SRCDIR")
    75:		testWorkspace := os.Getenv("TEST_WORKSPACE")
=>  76:		if testSrcdir != "" && testWorkspace != "" {
    77:			abs := filepath.Join(testSrcdir, testWorkspace, "external/bazel_gazelle/label")
    78:			err := os.Chdir(abs)
    79:			// Ignore the Chdir err when on Windows, since it might have have runfiles symlinks.
    80:			// https://github.com/bazelbuild/rules_go/pull/1721#issuecomment-422145904
    81:			if err != nil && runtime.GOOS != "windows" {

We can inspect local variables:

(dlv) locals
testSrcdir = ""
testWorkspace = ""

Let’s check what functions are defined (funcs) that include Test and set a breakpoint at one of the test entrypoints:

(dlv) funcs Test
github.com/bazelbuild/bazel-gazelle/label.TestImportPathToBazelRepoName
github.com/bazelbuild/bazel-gazelle/label.TestLabelString
github.com/bazelbuild/bazel-gazelle/label.TestParse
testing.listTests
testing.newTestContext
testing.runTests
testing.runTests.func1
testing.runTests.func1.1
testing/internal/testdeps.(*TestDeps).ImportPath
...
(dlv) b github.com/bazelbuild/bazel-gazelle/label.TestLabelString
Breakpoint 2 set at 0x111b82b for github.com/bazelbuild/bazel-gazelle/label.TestLabelString() external/bazel_gazelle/label/label_test.go:23
(dlv) c

At this point, delve fails to print source code as it cannot find the source file (which exists in an external workspace). Here’s a hack to support that:

ln -s $(bazel info output_base)/external external

We can also use the edit command to open in the editor to see where we are:

(dlv) ed

Let’s relist the source code:

(dlv) l
> github.com/bazelbuild/bazel-gazelle/label.Label.String() external/bazel_gazelle/label/label.go:127 (PC: 0x111ac8b)
   122:			Name:     name,
   123:			Relative: relative,
   124:		}, nil
   125:	}
   126:
=> 127:	func (l Label) String() string {
   128:		if l.Relative {
   129:			return fmt.Sprintf(":%s", l.Name)
   130:		}
   131:
   132:		var repo string

OK, that looks better.

At this point, we can set a breakpoint at line 48 in the current file, continue to it, and print the value of the spec variable:

(dlv) b 48
(dlv) c
(dlv) p spec
(dlv) p spec
struct { github.com/bazelbuild/bazel-gazelle/label.l github.com/bazelbuild/bazel-gazelle/label.Label; github.com/bazelbuild/bazel-gazelle/label.want string } {
	l: github.com/bazelbuild/bazel-gazelle/label.Label {Repo: "", Pkg: "", Name: "foo", Relative: false},
	want: "//:foo",}

Let’s step into the function:

(dlv) s
> github.com/bazelbuild/bazel-gazelle/label.Label.String() external/bazel_gazelle/label/label.go:127 (PC: 0x111ac8b)
   122:			Name:     name,
   123:			Relative: relative,
   124:		}, nil
   125:	}
   126:
=> 127:	func (l Label) String() string {
   128:		if l.Relative {
   129:			return fmt.Sprintf(":%s", l.Name)
   130:		}
   131:
   132:		var repo string

OK, you get the idea…. this is just the tip of the iceberg here. Use the help menu to get a better sense of the possibilities.

Happy $@#!&! debugging!

Editor Support

The dlv edit command (ed) uses the DELVE_EDITOR or EDITOR environment variables to open an editor at the specified position. Delve edit uses a format compatible with vim. Here’s a bash script delve-edit-vscode you can use as a shim:

#!/bin/bash
set -euo pipefail

# convert args from vim-style '+22 /path/to/file' to vscode-style
# '/path/to/file:22'
line="$1"
file="$2"

if [[ $file == external/* ]]; then
    file="$(bazel info output_base)/${file}"
fi

code --goto "${file}:${line:1}"
chmod +x ~/bin/delve-edit-vscode
export DELVE_EDITOR=~/bin/delve-edit-vscode

See Also


Copyright © 2020 Stack.Build.