Debugging Go Program

概述

定位Go程序的错误,通常有两种方式:

  • 打印日志
  • 调试

Go是编译型语言,且IDE对调试的支持不太好,绝大多数Go的初学者调试Go程序都是通过log.Printf等打印日志方式定位问题。通常过程如下:

  1. 程序panic或者报错
  2. 修改Go程序,添加打印调试日志代码
  3. 编译Go程序
  4. 重复错误出现时的操作,查看日志
    1. 如果定位问题原因,修复程序错误,删除打印debug日志代码,返回第3步的操作
    2. 如果未定位问题原因,返回到第2步的操作

如果程序比较复杂,需要反复增加日志输出才能找到问题原因。熟练的使用调试器能够提高我们面对这样问题的灵活性。本文重点总结介绍调试相关知识,具体调试操作网上相关资料已经很全面(见最后一章参考),不作为重点。

使用GDB调试Go程序

简介

GDB不能很好的理解Go程序。Go程序和GDBstack managementthreadingruntime模型差异很大,并可能导致调试器输出不正确的结果。因此,虽然GDB在某些场景下有用,比如调试Cgo代码、调试runtime,但是对于Go程序来说,尤其是高并发程序,GDB不是一个可靠的调试器。而且,对于Go语言项目本身来说,解决这些的问题很困难,也不是一个高优先级的事情。

当你在LinuxmacOSFreeBSDNetBSD上使用gc工具链编译和连接Go程序的时候,产生的二进制包含DWARFv4调试信息,最近版本的GDB调试器可以利用这些信息观察一个运行的进程或者core dump

可以通过-w标记告诉连接器去掉这些调试信息,比如:

1
go build -ldflags "-w" .

gc编译器生成的程序包含函数内联和变量注册。这些优化可能会让gdb调试更加困难。如果你需要禁用这些优化,使用下面的参数构建程序:

1
go build -gcflags "all=-N -l" .

如果你想要使用gdb检查一个core dump,你可以在程序崩溃的时候触发一个dump。在支持dump的OS上,使用GOTRACEBACK=crash环境变量(参考runtime package documentation)。

Go 1.11版本中,由于编译器会产生更多更准确的调试信息,为了减少二进制的大小,DWARF调试信息编译时候会默认被压缩。这对于大多数ELF工具来说这是透明的,也得到Delve支持。但是macOS和Windows上一些工具不支持。如果要禁用DWARF压缩,可以在编译的时候传入参数-ldflags "-compressdwarf=false"

Go 1.11添加了一个实验性的功能,允许在调试器中调用函数。目前这个特性仅得到Delve(version 1.1.0及以上)的支持。

常用命令和教程

可以参考下面几篇文章,这里不做赘述:

使用LLDB调试Go程序

简介

Mac下如果你安装XCode,应该会自动安装了LLDB,LLDB是XCode的默认调试器。LLDB的安装方法可以参考这里

GDB的命令格式非常自由,和GDB的命令不同,LLDB命令格式非常结构化(“严格”的婉转说法)。LLDB的命令格式如下:

1
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

解释一下:

  • <command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  • <action>:执行命令的操作
  • <options>:命令选项。需要注意的是,如果aguments的第一个字母是”-“,<options><arguments>中间必须以”–”分隔开。所以如果你想启动一个程序,并给这个程序传入-program_arg value参数,可以输入(lldb) process launch --stop-at-entry -- -program_arg value
  • <arguement>:命令的参数
  • []:表示命令是可选的,可以有也可以没有

LLDB也减少了gdb中一些命令的特殊写法,让用户更加容易理解命令的意图。可以阅读LLDB文档中下面一段文字了解细节:

We also tried to reduce the number of special purpose argument parsers, which sometimes forces the user to be a little more explicit about stating their intentions.

……

LLDB的命令同样给很多命令提供了缩写形式,可以通过(lldb) help查看所有的缩写命令。

gdb和LLDB的命令之间的差别可以访问这里查看。

常用命令

使用LLDB需要熟悉的常用命令如下:

帮助

(lldb) help help
Show a list of all debugger commands, or give details about a specific command.

Syntax: help []

使用LLDB加载一个程序

1
2
3
4
5
6
$lldb /binary-path
Current executable set to '/binary-path'(x86_64).

$lldb
(lldb) file /binary-path
Current executable set to '/binary-path'(x86_64).

设置断点(breakpoints)

常见的设置断点的命令如下:

1
2
(lldb) breakpoint set --file source-file.go --line 11
Breakpoint 1: where = sample1`github.com/ethancai/go-debug-practice/sample1/model.(*MyStruct).Print + 19 at my_struct.go:11, address = 0x00000000010b2713

breakpoint命令会创建一个逻辑的断点,一个逻辑的断点可以对应一个或者多个位置location。比如,通过selector设置的断点对应所有实现了selector的方法。

breakpoint命令:

1
2
3
4
5
6
(lldb) help breakpoint
Commands for operating on breakpoints (see 'help b' for shorthand.)

Syntax: breakpoint <subcommand> [<command-options>]

...

设置观察点(Watchpoints)

watchpoint命令:

1
2
3
4
5
6
(lldb) help watchpoint
Commands for operating on watchpoints.

Syntax: watchpoint <subcommand> [<command-options>]

...

运行程序或者附着程序

process命令:

1
2
3
4
5
6
(lldb) help process
Commands for interacting with processes on the current platform.

Syntax: process <subcommand> [<subcommand-options>]

...

控制程序执行或者检查Thread状态

thread命令

1
2
3
4
5
6
(lldb) help thread
Commands for operating on one or more threads in the current process.

Syntax: thread <subcommand> [<subcommand-options>]

...

检查堆栈结构(Stack Frame)状态

frame命令

1
2
3
4
5
6
(lldb) help frame
Commands for selecting and examing the current thread's stack frames.

Syntax: frame <subcommand> [<subcommand-options>]

...

expression命令

1
2
3
4
5
6
7
(lldb) help expression
Evaluate an expression on the current thread. Displays any returned value with LLDB's
default formatting. Expects 'raw' input (see 'help raw-input'.)

Syntax: expression <cmd-options> -- <expr>

...

操作教程

可以参考下面几篇文章:

使用Delve调试Go程序

可以参考下面几篇文章:

不要使用调试器

对于调试器,一众计算机大牛都给出了明确而且强烈的建议:不要使用调试器。

调试技术是一众纯手工的技术,诞生于计算机程序的规模还不是很大的时期。在当今软件规模不断扩展的情况下,调试无法解决软件质量问题。深入的思考、合理的架构、优美的代码、充分的单元测试才是提高软件质量的正确方向。调试应该仅作为调查问题最后一种办法。

参考