Vim, Go and Remote Debugging
Setting up Vim for remotely debugging Go code
Introduction
While developing Log4Shell Sentinel, I found myself having to debug my Go application in different environments, with each environment running a unique combination of a container runtime (Docker, CRI-O, Containerd) and storage driver (overlay2, aufs, etc.). I decided to use Vim (if you’re not currently using Vim, then I’d recommend that you seriously reconsider your life choices) and Fatih’s excellent vim-go plugin which gives me many of the features I would get from a full blown IDE. However, I didn’t run into many useful tutorials on setting up remote debugging, so I decided to quickly the steps I took. Although there are other Vim plugins to integrate delve - the most popular Go debugger - such as vim-delve, I’ve found Fatih’s plugin to be the best option.
Preparing the Environment
There are two sides to my setup:
- the development / Vim session with my source code
- the remote host running my Go binary
Development Side
For my development side, I simply need to install delve and the vim-go plugin. On my first run of Vim after installing the plugin, I had to run :GoInstallBinaries
to download and install Go tool binaries such as gopls
, goimports
, etc as they were not all installed on the system. Once installed, debugging a simple Go application such as this:
package main
import "fmt"
func main() {
a := 5
fmt.Println(a)
}
is as simple as moving to the line I want to run to and then issuing the following command:
:GoDebugStart
followed by this command once the debugger UI is displayed:
:GoDebugContinue
This tells the debugger to break on the current line. The Vim session will now look like this:
By default, it shows us the following four panes:
- Stacktrace
- Variables
- Go routines
- Godebug output
The default panes and pane arrangement isn’t ideal in my opinion. For example, the GoDebug
output is rarely useful and I prefer having all my tab-related panes on one side. As I’m not currently using Go routines, I’ll remove that pane as well. I’ll add the following snippet to my ~/.vimrc
file:
let g:go_debug_windows = {
\ 'vars': 'rightbelow 50vnew',
\ 'stack': 'rightbelow 10new',
\ }
" \ 'goroutines': 'rightbelow 10new',
which instructs the plugin to display a 50-character wide pane on the right for variables followed by the stack. If I needed to debug Go routines, I can uncomment the commented goroutines
line above and add it to the array. The debug window now looks like this:
However, I don’t find the Registers
section useful while debugging and found it distracting. There is no Vim variable or option to remove it so I’ll directly comment out the following lines in ~/.vim/autoload/go/debug.vim
instead:
let v += ['']
let v += ['# Registers']
if type(get(s:state, 'registers', [])) is type([])
for c in s:state['registers']
let v += [printf("%s = %s", c.Name, c.Value)]
endfor
endif
There may be a cleaner way to do this that I’m not aware of. The debug UI is now much clearer and now looks like this:
Now that the UI-side is more to my liking, it is time to configure key-bindings. By default, vim-go has typical debugger key-bindings:
- F5 = continue
- F9 = add breakpoint
- F8 = halt
- F10 = next
- F11 = step in
However, these keys are often already mapped to other system-related actions and I prefer to use more Vim-like key-bindings as shown below:
let g:go_debug_mappings = {
\ '(go-debug-continue)': {'key': 'c', 'arguments': '<nowait>'},
\ '(go-debug-next)': {'key': 'n', 'arguments': '<nowait>'},
\ '(go-debug-step)': {'key': 's'},
\ '(go-debug-print)': {'key': 'p'},
\}
Now, when the debugger is running, I have the following mappings:
- c = continue
- p = print value (it prints the value of any variable the cursor hovers over)
- n = next
- s = step in
I also added the following key-bindings to quickly invoke the debugger, stop the debugger and toggle a breakpoint:
map <leader>ds :GoDebugStart<cr>
map <leader>dt :GoDebugStop<cr>
map <leader>db :GoDebugBreakpoint<cr>
So a typical debug session would start by <leader>ds
followed by pressing c
while in debugger mode to break on the current line (or I can manually add breakpoints using <leader>db
).
Remote Side
To debug my Go binary on remote hosts, I need to:
- build my binary without removing debugging data. This is the default so nothing changes
- copy over the dlv/delve binary to the remote systems
For the later, I’ll compile delve statically to make sure that I don’t run into any GLIBC-related issues:
$ CGO_ENABLED=0 go get github.com/go-delve/delve/cmd/dlv
I then copy over both of these to the remote host.
Remote Debugging
Having copied the delve binary and my Go binary to the remote host, I can start a remote debugging session by running delve on the remote host:
$ sudo ./dlv --listen="0.0.0.0:40000" --headless=true --api-version=2 --accept-multiclient exec ./log4shell_sentinel -- -p /
API server listening at: [::]:40000
This configures delve to listen on port 40000
for connections. Using --
, I can pass parameters to my executable. On my development machine, I open my source code file in Vim and run:
:GoDebugConnect 192.168.1.110:40000
to connect to the delve debugger. I can now proceed by running c to continue execution until execution hits the line I am currently on:
Conclusion
In this short blog post, we looked at how to setup Vim for remote Go debugging. I hope you’ve found the post useful. Until next time.