标签归档:golang

Delve 调试 GO 程序

目前 Go 语言支持 GDB、LLDB 和 Delve 几种调试器。其中 GDB 是最早支持的调试工具,LLDB 是 macOS 系统推荐的标准调试工具。但是 GDB 和 LLDB 对 Go 语言的专有特性都缺乏很大支持,而只有 Delve 是专门为 Go 语言设计开发的调试工具。而且 Delve 本身也是采用 Go 语言开发,对 Windows 平台也提供了一样的支持。本节我们基于 Delve 简单解释如何调试 Go 汇编程序。

安装

Github地址: https://github.com/go-delve/delve/ 如果是在本地调试,直接通过go install命令将其安装到本地的$GOPATH/bin下即可

go install github.com/go-delve/delve/cmd/dlv@latest

容器环境下由于不一定支持 go,需要先安装 go 语言环境,会比较麻烦,可以直接将本地下载好的dlv二进制命令复制到$PATH的某一个目录中。安装好后,通过执行dlv version验证是否可用。

注意

使用go build编译时,默认会进行一些优化,会影响调试过程。 因此,对于对于需要进行调试的程序,在编译时最好设置-gcflags把优化关掉。

  • 在>= 1.10的 golang 版本中,设置-gcflags="all=-N -l"
  • 在<1.10版本的 golang 中,设置-gcflags="-N -l" 其中:
  • -N表示不优化代码,并生成调试信息。
  • -l表示禁用函数内联优化 如下面命令将当前文件夹下的代码编译成可执行文件main,并禁止优化:
    go build -gcflags="all=-N -l" -o main .

    基本使用

    Delve 的基本命令:

    
    (dlv) break main.main  # 设置断点
    (dlv) continue        # 继续执行
    (dlv) next           # 下一步
    (dlv) step           # 步入
    (dlv) print 变量名    # 打印变量

(dlv) b main.main # 在main函数设置断点 (dlv) bp 文件名:行号 # 在特定行设置断点 (dlv) c # continue 继续运行 (dlv) n # next 下一步 (dlv) s # step 步入 (dlv) p 变量名 # print 打印变量 (dlv) vars # 显示包级变量 (dlv) locals # 显示本地变量 (dlv) bt # 显示调用栈


## 常用命令
### `attach`
用于调试一个已经存在的进程,这个命令一般在调试 web 程序时使用。如下:
- 使用`lsof`命令查看占用某个端口的进程 pid
- 使用`dlv attach $pid`启动调试该进程
比如假定某个 web 程序的 http端口为 8080,进程号为10001

lsof -i:8080 dlv attach 10001

### `exec`
如果没有现成的进程,而只有一个二进制可执行文件,可以使用`dlv exec`命令启动一个进程并进行调试,如上面生成的`main`

dlv exec ./main

如果 main 程序本身需要一些参数,可以通过`--`来传递参数,比如给上面的 main 传入一个 conf 参数

dlv exec ./main — conf=./conf.yaml

### `debug`
用于直接调试源码文件,这个命令连手动编译都省了,只需要指定调试的包名和源码所在的文件夹即可。

dlv debug .

### `version`
用于查看`dlv`版本号,如下:

dlv version

WARN[0000] CGO_CFLAGS already set, Cgo code could be optimized. layer=dlv

Delve Debugger

Version: 1.21.0

Build: $Id: fec0d226b2c2cce1567d5f59169660cf61dc1efe $


## 调试指令
通过`exec`、`debug`、`attach`进入 dlv 的调试终端后,需要执行调试指令才能完成诸如下一步、退出当前函数、查看变量值等目的。
### 断点管理
主要是断点的增删改查

|指令|缩写|作用|
|---|---|---|
|break|b|添加断点|
|breakpoints|bp|用于查看设置的所有断点,每个断点都有一个编号|
|clear||如clear 1,表示删除编号为 1 的断点|
|clearall||删除所有断点|
|toggle||用于启用/禁用断点。如toggle 1|
|condition|cond|用于设置条件断点,如cond 2 i == 10指定断点 2 在 i等于 10 时执行|

需要注意的是,使用 `break` 创建断点时,有几种方法:

- `b 包名.方法名`: 在指定包的函数中设置断点,如`b main.main`。如果函数名全局唯一,则不用指定包名
- `b 文件名:行数`: 在指定的 go 文件的指定行设置断点,如`b main.go:14`

### 调试执行

主要用于控制程序的执行

|指令|缩写|作用|
|---|---|---|
|next|n|执行到下一行,如果是函数,不会进入函数|
|continue|c|执行到下一个断点处或结束执行|
|step|s|执行到下一步,如果是函数,会进入函数内部|
|stepout|so|跳出当前函数|
|restart|r|重新执行程序,断点会保留。注意无法用于 attach 的进程|
|step-instruction|si|执行到下一行机器码,一般在查看汇编代码时使用|
|rebuild||重新编译程序并执行,断点会保留。无法用于 attach 和 exec|

### 参数管理

主要用于对设置和查看变量、参数等

|指令|缩写|作用|
|---|---|---|
|print|p|查看变量或表达式的值|
|whatis||查看变量类型|
|args||查看函数的入参|
|locals||查看函数的局部变量|
|vars||查看全局变量|
|set||设置某个变量的值|
|display||将变量加入/移除监控列表、或查看监控列表|

注意上述locals/vars/display都支持指定正则,具体用法可以使用`h locals`查看。

### 其他

|指令|缩写|作用|
|---|---|---|
|exit|q|退出调试会话|
|disassemble|disass|用于查看指定函数的汇编代码|
|funcs||查看函数,同样支持正则|
|help|h|查看使用手册|
|list|ls/l|查看源代码|

## 总结

本文介绍了`dlv`工具的基本使用姿势,掌握该工具,不仅能够辅助我们进行调试,以后想学习一些项目的原理时,也能够很快摸清楚调用链路。

后续我的其他分析原理性的文章中,也会使用使用这一工具对源代码进行分析。

## References
- [3.9 Delve 调试器](https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html)
- [golang调试利器 dlv 的使用](https://www.cnblogs.com/hanzeng1993/p/18101986)
- [go-delve/delve](https://github.com/go-delve/delve)

使用 GDB 调试 Go 程序

进入调试

使用前,请先确保机器上已经安装 GDB

[root@localhost code]# which gdb /usr/bin/gdb

准备就绪后,以下列测试程序为例

package main

import "fmt"

func main(){
  msg := "hello, world"
  fmt.Println(msg)
}

然后执行 如下命令进行编译,里面有好多个参数,有疑问的可以自行搜索引擎

# 关闭内联优化,方便调试
$ go build -gcflags "-N -l" demo.go

# 发布版本删除调试符号
go build -ldflags “-s -w”

最后使用 GDB 命令进入调试界面

# tui 界面
$ gdb -tui demo 
# 纯终端界面 
$ gdb demo

进入 后就可以打断点调试了

(gdb) b main.main   # 在 main 包里的 main 函数 加断点
Breakpoint 1 at 0x4915c0: file /home/wangbm/code/demo.go, line 5.
(gdb) run  # 执行进程
Starting program: /home/wangbm/code/demo 
Breakpoint 1, main.main () at /home/wangbm/code/demo.go:5
(gdb) 

# 注,如果需要带参数运行,比如正常这样运行:
./app -a 1 --debug
# 在 gdb 中就这样运行
gdb app
(gdb) run -a 1 --debug

调试指令

要熟练使用 GDB ,得熟悉的掌握它的指令,这里列举一下

  • r:run,执行程序
  • n:next,下一步,不进入函数
  • s:step,下一步,会进入函数
  • b:breakponit,设置断点
  • l:list,查看源码
  • c:continue,继续执行到下一断点
  • bt:backtrace,查看当前调用栈
  • p:print,打印查看变量
  • q:quit,退出 GDB
  • whatis:查看对象类型
  • info breakpoints:查看所有的断点
  • info locals:查看局部变量
  • info args:查看函数的参数值及要返回的变量值
  • info frame:堆栈帧信息
  • info goroutines:查看 goroutines 信息。在使用前 ,需要注意先执行 source /usr/local/go/src/runtime/runtime-gdb.py
  • goroutine 1 bt:查看指定序号的 goroutine 调用堆栈
  • 回车:重复执行上一次操作

其中有几个指令的使用比较灵活

# 查看指定行数上下5行
(gdb) l 8

# 查看指定范围的行数
(gdb) l 5:8

# 查看指定文件的行数上下5行
l demo.go:8

# 可以查看函数,记得加包名
l main.main

把上面的 l 换成 b ,大多数也同样适用

# 在指定行打断点 
(gdb) b 8  
# 在指定指定文件的行打断点 
b demo.go:8  
# 在指定函数打断点,记得加包名 
b main.main

还有 p – print,打印变量

# 查看变量
(gdb) p var

# 查看对象长度或容量
(gdb) p $len(var)
(gdb) p $cap(var)

# 查看对象的动态类型
(gdb) p $dtype(var)
(gdb) iface var

# 举例如下
(gdb) p i
$4 = {str = "cbb"}
(gdb) whatis i
type = regexp.input
(gdb) p $dtype(i)
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) iface i
regexp.input: struct regexp.inputBytes *

以上就是关于 GDB 的使用方法,非常简单,可以自己手动敲下体验一下。

References