go gdb

本文转自 https://www.oschina.net/translate/using-gdb-debugger-with-go

排除应用程序故障是比较复杂的,特别是处理像 Go 这样的高并发语言。它更容易在具体位置使用 print 打印语句来确定程序状态,但是这个方法很难根据条件发展去动态响应你的代码。

调试器提供了一个强大得令人难以置信的故障排除机制。添加排除故障的代码可以巧妙地影响到应用程序该如何运行。调试器可以给正在迷茫的你更精确的看法。

已经有许多 Go 的调试器存在了,其中一些调试器的不好之处是通过在编译时注入代码来提供一个交互终端。gdb 调试器则允许你调试已经编译好的二进制文件,只要他们已经与 debug 信息连接,并不用修改源代码。这是个相当不错的特性,因此你可以从你的部署环境中取一个产品然后灵活地调试它。你可以从Golang 官方文档中阅读更多关于 gdb 的信息,那么这篇指南将简单讲解使用 gdb 调试器来调试 Go 应用程序的基本用法。

开始 gdb 调试

为了实验 gdb 我使用了一个测试程序,完整的源代码可以在gdb_sandbox_on_Github上查看。让我们从一个非常简单的程序开始吧:

package main

import ( 
    "fmt" 
)

func main() { 
    for i := 0; i < 5; i++ {
        fmt.Println("looping") 
    } 
    fmt.Println("Done") 
}

我们可以运行这段代码并看到它输出内容的和我们想象的一样:

$ go run main.go
looping
looping
looping
looping
looping
Done

我们来调试这个程序吧。首先,使用 go build 编译成二进制文件,接着使用这个二进制文件的路径做为参数运行** gdb。根据你的设定,你也可以使用 source **命令来获取 Go 运行时(Go runtime)的支持。现在我们已经在 gdb 的命令行中了,我们可以在运行我们的二进制文件前为它设置断点。

$ go build -gcflags "-N -l" -o gdb_sandbox main.go 
$ gdb gdb_sandbox
(gdb) source /usr/local/src/go/src/runtime/runtime-gdb.py
Loading Go Runtime support.

第一关,我们在 for 循环里面设置一个断点(b)来查看执行每次循环时我们的代码会各有什么状态。我们可以使用print(p)命令来检查当前内容的一个变量,还有 list(l)和 backtrace(bt)命令查看当前步骤周围的代码。程序运行时可以使用 next(n)执行下一步或者使用 breakpoint(c)执行到下一个断点。

(gdb) b main.go:9 
Breakpoint 1 at 0x400d35: file /home/bfosberry/workspace/gdb_sandbox/main.go, line 9. 
(gdb) run 
Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox Breakpoint 1, main.main () at 
/home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9 
9         fmt.Println("looping") 
(gdb) l 
4         "fmt" 
5         ) 
6  
7 func main() {
8         for i := 0; i < 5; i++ { 
9         fmt.Println("looping") 
10        }` 
11        fmt.Println("Done") 
12 } 
(gdb) p i 
$1 = 0 
(gdb) n 
looping 
Breakpoint 1, main.main () at 
/home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9 
9        fmt.Println("looping") 
(gdb) p i 
$2 = 1 
(gdb) bt
# 0 main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9

我们的断点可以设置在关联文件的行号中、GOPATH里的文件的行号或一个包里的函数。如下也是一个有效的断点:

(gdb) b github.com/bfosberry/gdb_sandbox/main.go:9
(gdb) b 'main.main'

Structs

我们可以用稍微复杂一点的代码来实例演示如何调试。我们将使用f函数生成一个简单的pair,x和y,当x相等时y=f(x),否则=x。

type pair struct { 
    x int 
    y int 
}

func handleNumber(i int) *pair { 
    val := i 
    if i%2 == 0 { 
        val = f(i) 
    } 
    return &pair{ 
       x: i, 
       y: val, 
    } 
}

func f(int x) int { 
    return x*x + x 
}

也可以在循环中改变代码来访问这些新函数。

 p := handleNumber(i)
 fmt.Printf("%+v\n", p)
 fmt.Println("looping")

因为我们需要调试的是变量 y。我们可以在y被设置的地方放置断点然后单步执行。可以使用 **info args 查看函数的参数,在 bt **之前可以返回当前回溯。

(gdb) b 'main.f' 
(gdb) run 
Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox

Breakpoint 1, main.f (x=0, ~anon1=833492132160) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33 
33       return x*x + x 
(gdb) info args 
x = 0 
(gdb) continue 
Breakpoint 1, main.f (x=0, ~anon1=833492132160) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33 
33       return x*x + x 
(gdb) info args 
x = 2 
(gdb) bt
#0 main.f (x=2, ~anon1=1) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33
#1 0x0000000000400f0e in main.handleNumber (i=2, ~anon1=0x1)
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:24
#2 0x0000000000400c47 in main.main ()
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:14

因为我们在变量 y 是在函数 f 中被设定的这样一个条件下,我们可以跳到这个函数的上下文并检查堆区的代码。应用运行时我们可以在一个更高的层次上设置断点并检查其状态。

(gdb) b main.go:26 
Breakpoint 2 at 0x400f22: file 
/home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 26. 
(gdb) continue 
Continuing.
Breakpoint 2, main.handleNumber (i=2, ~anon1=0x1) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:28 
28             y: val, 
(gdb) l 
23         if i%2 == 0 { 
24             val = f(i) 
25         } 
26         return &pair{ 
27             x: i, 
28             y: val, 
29         } 
30     } 
31  
32 func f(x int) int { 
(gdb) p val 
$1 = 6 
(gdb) p i 
$2 = 2

如果我们在这个断点处继续住下走我们将越过在这个函数中的断点1,而且将立即触发在 HandleNumer 函数中的断点,因为函数 f 只是对变量 i 每隔一次才执行。我们可以通过暂时使断点 2不工作来避免这种情况的发生。

(gdb) disable breakpoint 2 
(gdb) continue 
Continuing. 
&{x:2 y:6} 
looping 
&{x:3 y:3} 
looping 
[New LWP 15200] 
[Switching to LWP 15200]
Breakpoint 1, main.f (x=4, ~anon1=1) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33 
33         return x*x + x 
(gdb)

我们也可以分别使用 clear 和 delete breakpoint NUMBER 来清除和删除断点。动态产生和系住断点,我们可以有效地在应用流中来回移动。

Slices and Pointers

上例程序太简单了,只用到了整数型和字符串,所以我们将写一个稍微复杂一点的。首先添加一个slice(切片类型)的指针到 main 函数,并保存生成的 pair,我们后面将用到它。

 var pairs []*pair
    for i := 0; i < 10; i++ {
        p := handleNumber(i)
        fmt.Printf("%+v\n", p)
        pairs = append(pairs, p)
        fmt.Println("looping")
        }

现在我们来检查生成出来的 slice 或 pairs,首先我们用转换成数组来看一下这个 slice。因为 handleNumber 返回的是一个** pair* 类型,我们需要引用这个指针来访问 struct(结构)的属性。

(gdb) b main.go:18 
Breakpoint 1 at 0x400e14: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 18. 
(gdb) run 
Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox &{x:0 y:0}

Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18 
18         fmt.Println("looping") 
(gdb) p pairs 
$1 = []*main.pair = {0xc82000a3a0} 
(gdb) p pairs[0] 
Structure has no component named operator[]. 
(gdb) p pairs.array 
$2 = (struct main.pair **) 0xc820030028 
(gdb) p pairs.array[0] 
$3 = (struct main.pair *) 0xc82000a3a0 
(gdb) p *pairs.array[0] 
$4 = {x = 0, y = 0} 
(gdb) p (*pairs.array[0]).x 
$5 = 0 
(gdb) p (*pairs.array[0]).y 
$6 = 0 
(gdb) continue 
Continuing. 
looping 
&{x:1 y:1}

Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18 
18         fmt.Println("looping") 
(gdb) p (pairs.array[1][5]).y 
$7 = 1 
(gdb) continue 
Continuing. 
looping 
&{x:2 y:6}

Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18 
18         fmt.Println("looping") 
(gdb) p (pairs.array[2][6]).y 
$8 = 6 
(gdb)

你会发现这里** gdb** 并不确定 pairs 是一个 slice 类型,我们不能直接访问它的属性,为了访问它的成员我们需要使用 pairs.array 来转换成数组,然后我们就可以检查 slice 的 length(长度)和 capacity(容量):

(gdb) p $len(pairs)
$12 = 3
(gdb) p $cap(pairs)
$13 = 4

这时我们可以让它循环几次,并透过这个 slice 不用的成员方法监听增加的 xy 的值,要注意的是,这里的 struct 属性可以通过指针访问,所以 p pairs.array[2].y 一样可行。

Goroutines

现在我们已经可以访问 struct 和 slice 了,下面再来更加复杂一点的程序吧。让我们添加一些goroutines 到 mian 函数,并行处理每一个数字,返回的结果存入信道(chan)中:

pairs := []*pair{}
    pairChan := make(chan *pair)
    wg := sync.WaitGroup{}
        for i := 0; i < 10; i++ {
          wg.Add(1)
          go func(val int) {
            p := handleNumber(val)
            fmt.Printf("%+v\n", p)
            pairChan <- p
            wg.Done()
            }(i)
    }
    go func() {
            for p := range pairChan {
              pairs = append(pairs, p)
            }
    }()
    wg.Wait()
    close(pairChan)

如果我等待 WaitGroup 执行完毕再检查 pairs slice 的结果,我们可以预期到内容是完全相同的,虽然它的排序可能有些出入。gdb 真正的威力来自于它可以在 goroutines 正在运行时进行检查:

(gdb) b main.go:43 
Breakpoint 1 at 0x400f7f: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 43. 
(gdb) run 
Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox

Breakpoint 1, main.handleNumber (i=0, ~r1=0x0) 
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:43 
43         y: val, 
(gdb) l 
38     if i%2 == 0 { 
39         val = f(i) 
40     } 
41     return &pair{ 
42         x: i, 
43         y: val, 
44     } 
45 } 
46  
47 func f(x int) int { 
(gdb) info args 
i = 0 
~r1 = 0x0 
(gdb) p val 
$1 = 0

你会发现我们在 goroutine 要执行的代码段中放置了一个断点,从这里我们可以检查到局部变量,和进程中的其它 goroutines:

(gdb) info goroutines 
  1 waiting runtime.gopark 
  2 waiting runtime.gopark 
  3 waiting runtime.gopark 
  4 waiting runtime.gopark 
* 5 running main.main.func1 
  6 runnable main.main.func1 
  7 runnable main.main.func1 
  8 runnable main.main.func1 
  9 runnable main.main.func1 
* 10 running main.main.func1 
  11 runnable main.main.func1 
  12 runnable main.main.func1 
  13 runnable main.main.func1 
  14 runnable main.main.func1 
  15 waiting runtime.gopark 
(gdb) goroutine 11 bt
#0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19
#1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696
#2 0x0000000000000006 in ?? ()
#3 0x000000c82001a180 in ?? ()
#4 0x000000c82000a3a0 in ?? ()
#5 0x0000000000000000 in ?? ()
(gdb) goroutine 11 l 
48         return x*x + x 
49     } 
(gdb) goroutine 11 info args 
val = 6 
pairChan = 0xc82001a180 
&wg = 0xc82000a3a0 
(gdb) goroutine 11 p val 
$2 = 6

在这里我们做的第一件事就是列出所有正在运行的 goroutine,并确定我们正在处理的那一个。然后我们可以看到一些回溯,并发送任何调试命令到 goroutine。这个回溯和列表清单并不太准确,如何让回溯更准确,goroutine 上的 info args 显示了我们的局部变量,以及主函数中的可用变量,goroutine 函数之外的使用前缀&

结论

当调试应用时,gdb 的强大令人难以置信。但它仍然是一个相当新的事物,并不是所有的地方工作地都很完美。使用最新的稳定版 gdb,go 1.5 beta2,有不少地方有突破:

Interfaces

根据 go 博客上的文章, go 的 interfaces 应该已经支持了,这允许在** gdb **中动态的投影其基类型。这应该算一个突破。

Interface{} 类型

目前没有办法转换 interface{} 为它的类型。

列出 goroutine 的不同点

在其他 goroutine 中列出周边代码会导致一些行数的漂移,最终导致** gdb** 认为当前的行数超出文件范围并抛出一个错误:

(gdb) info goroutines 
  1 waiting runtime.gopark 
  2 waiting runtime.gopark 
  3 waiting runtime.gopark 
  4 waiting runtime.gopark 
* 5 running main.main.func1 
  6 runnable main.main.func1 
  7 runnable main.main.func1 
  8 runnable main.main.func1 
  9 runnable main.main.func1 
* 10 running main.main.func1 
  11 runnable main.main.func1 
  12 runnable main.main.func1 
  13 runnable main.main.func1 
  14 runnable main.main.func1 
  15 waiting runtime.gopark 
(gdb) goroutine 11 bt
#0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)
    at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19
#1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696
#2 0x0000000000000006 in ?? ()
#3 0x000000c82001a180 in ?? ()
#4 0x000000c82000a3a0 in ?? ()
#5 0x0000000000000000 in ?? ()
(gdb) goroutine 11 l 
48         return x*x + x 
49     } 
(gdb) goroutine 11 l 
Python Exception <class 'gdb.error'> Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.: 
Error occurred in Python command: Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.

Goroutine 调试还不稳定

处理 goroutines 往往不稳定;我遇到过执行简单命令产生错误的情况。现阶段你应该做好处理类似问题的准备。

gdb 支持 Go 的配置非常麻烦

运行 gdb 支持 Go 调试的配置非常麻烦,获取正确的路径结合与构建 flags,还有 gdb 自动加载功能好像都不能正常的工作。首先,通过一个 gdb 初始化文件加载 Go 运行时支持就会产生初始化错误。这就需要手动通过一个源命令去加载,调试 shell 需要像指南里面描述的那样去进行初始化。

我什么时候该使用一个调试器?

所以什么情况下使用 gdb 更有用?使用 print 语言和调试代码是更有针对性的方法。

  • 当不适合修改代码的时候

  • 当调试一个问题,但是不知道源头,动态断点或许更有效

  • 当包含许多 goroutines 时,暂停然后审查程序状态会更好

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容

  • Go入门 Go介绍 部落图鉴之Go:爹好还这么努力? 环境配置 安装 下载源码编译安装 下载相应平台的安装包安装 ...
    齐天大圣李圣杰阅读 4,531评论 0 26
  • 程序调试的基本思想是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环过程,根据现象如何假设错误原...
    Manfred_Zone阅读 16,386评论 0 26
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 18,304评论 1 47
  • 旅行归来后忙碌的工作压得我喘不过气来,已经有五天不给老爸打电话了,为避免再次被猜疑马上拨出了那个熟悉的号码。 果不...
    向行阅读 239评论 4 3
  • 折叠翻转时间: 第一空间,凌晨六点到次日同时刻,然后地下休眠24小时; 第二空间,凌晨六点到晚上十点,然后地下休眠...
    八千春秋阅读 191评论 0 1