7 23
使用GDB工具调试Go程序

对于程序的调试相信程序员并不陌生,不过对于大部分Java和Python的程序员来说,大部分都是依赖IDE进行的。而且目前类似Eclipse和IntelliJ等工具都提供了很方便的调试工具。

不过除了通过IDE,我们其实还可以通过GDB这个工具来辅助我们的代码调试。以看到一些内部的运行机理,对于学习还是挺有好处的。 GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。

详细的GDB命令参数可以参考这篇文章 点击这里

这里我主要介绍下如果使用gdb来debug我们的go代码, 我们先写一段简单的代码(命名为main.go),如下:


package main

import (
    "fmt"
)

type Person struct {
    name string
    age  uint
}

func (p *Person) Say() {
    fmt.Printf("%s who is %d years old is saying", p.name, p.age)
}

func main() {
    b := "my name is lihaoquan"
    a := []string{"abc", "def"}
    p := &Person{
        name: "Tom",
        age:  20,
    }
    fmt.Println(b)
    fmt.Println(a)
    p.Say()
}

上面的代码,我们定义了一个字符串变量,一个slice变量,一个包含接收方法的结构体。

在调试前,我们先进行程序的编译,并且关闭内联优化

$ go build -gcflags="-N -l" -o test main.go

生成静态链接文件 test 后,我们便可以使用gdb对它进行debug.

$ gdb test

(gdb) _

这时,我们尝试使用命令 r 来运行调试 :

my name is lihaoquan
[abc def]
Tom who is 20 years old is saying[Inferior 1 (process 2455) exited normally]

我们发现,我们使用 r 命令的时候,程序已经执行完毕了,我们并没有进入到调试的环境。

原因是我们并没有设置断点

我们回到gdb的命令行中,并设置具体的断点+行号

(gdb) b 17

这是gdb会输出具体的行号位置信息

Breakpoint 1 at 0x2242: file /Users/lihaoquan/../../main.go, line 17.

我们在执行 r, 让调试器运行到我们的断点位置

17        b := "my name is lihaoquan"

好了, 我们的debug已经ok, 接下来我们可以使用 n 来让程序继续走下去

(gdb) n
18        a := []string{"abc", "def"}
(gdb) n
21            age:  20,
(gdb) n
20            name: "Tom",
(gdb) n
21            age:  20,
(gdb) n
19        p := &Person{
(gdb) n
23        fmt.Println(b)
(gdb) n
my name is lihaoquan
24        fmt.Println(a)

如果我们想对某个变量进行调试输出,可以使用参数 p , p 的意思是 print

(gdb) p b
$1 = 0x11c1f0 "my name is lihaoquan"

最后,我们可以使用上述的几个基本命令参数调试我们其他的变量。对应其它更复杂的程序,其实原理都是一样的。

接下来,这边参考《go学习笔记》的知识点,简单介绍一下如何使用gdb来入门学习go的源码。

还是一个简单的代码段

package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}

接着我们需要编译它,在我们进行代码调试的时候,尽量使用 -gcflags="-N -l" 参数来关闭编译器的代码优化和函数内联,因为这些优化,会规避了一些小函数和局部变量, 这样不利于我们观察debug信息。

go build -gcflags="-N -l" -o test

如果我们在平台使用交叉编译, 需要设置GOOS环境变量

$ gdb test

GNU gdb (GDB) 7.10.1
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin15.2.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...done.
warning: Unsupported auto-load script at offset 0 in section .debug_gdb_scripts
of file /Users/lihaoquan/GoProjects/Playground/src/study2016/gosc/basic/test.
Use `info auto-load python-scripts [REGEXP]' to list them.
(gdb)

接着,我们使用 info files 查看链接文件信息

(gdb) info files
Symbols from "/Users/lihaoquan/GoProjects/src/gosc/basic/test".
Local exec file:
    `/Users/lihaoquan/GoProjects/src/gosc/basic/test', file type mach-o-x86-64.
    Entry point: 0x51ef0
    0x0000000000002000 - 0x000000000007c730 is .text
    0x000000000007c740 - 0x00000000000af434 is __TEXT.__rodata
    0x00000000000af434 - 0x00000000000afee8 is __TEXT.__typelink
    0x00000000000afee8 - 0x00000000000aff28 is __TEXT.__itablink
    0x00000000000aff28 - 0x00000000000aff28 is __TEXT.__gosymtab
    0x00000000000aff40 - 0x00000000000f355e is __TEXT.__gopclntab
    0x00000000000f355e - 0x00000000000f355e is __TEXT.__symbol_stub1
    0x00000000000f4000 - 0x00000000000f4000 is __DATA.__nl_symbol_ptr
    0x00000000000f4000 - 0x00000000000f6048 is __DATA.__noptrdata
    0x00000000000f6060 - 0x00000000000f7860 is .data
    0x00000000000f7860 - 0x0000000000112150 is .bss
    0x0000000000112160 - 0x0000000000116da0 is __DATA.__noptrbss
(gdb)

我们可以看到我们的代码的Entry point在内存地址 0x51ef0 中,我们使用gdb访问它:

db)
(gdb) b *0x51ef0
Breakpoint 1 at 0x51ef0: file /usr/local/go/src/runtime/rt0_darwin_amd64.s, line 8.

这时候, gdb告诉咱们, 它已经找到真正入口,就在rt0_darwin_amd64.s中,而且具体位置是第8行。

我们翻开go的源码,来看下rt0_darwin_amd64.s的内容

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "textflag.h"

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
    LEAQ    8(SP), SI // argv
    MOVQ    0(SP), DI // argc
    MOVQ    $main(SB), AX
    JMP    AX

// When linking with -shared, this symbol is called when the shared library
// is loaded.
TEXT _rt0_amd64_darwin_lib(SB),NOSPLIT,$0x48
    MOVQ    BX, 0x18(SP)
    MOVQ    BP, 0x20(SP)
    MOVQ    R12, 0x28(SP)
    MOVQ    R13, 0x30(SP)
    MOVQ    R14, 0x38(SP)
    MOVQ    R15, 0x40(SP)

    MOVQ    DI, _rt0_amd64_darwin_lib_argc<>(SB)
    MOVQ    SI, _rt0_amd64_darwin_lib_argv<>(SB)

    // Synchronous initialization.
    MOVQ    $runtime·libpreinit(SB), AX
    CALL    AX

    // Create a new thread to do the runtime initialization and return.
    MOVQ    _cgo_sys_thread_create(SB), AX
    TESTQ    AX, AX
    JZ    nocgo
    MOVQ    $_rt0_amd64_darwin_lib_go(SB), DI
    MOVQ    $0, SI
    CALL    AX
    JMP    restore

nocgo:
    MOVQ    $8388608, 0(SP)                    // stacksize
    MOVQ    $_rt0_amd64_darwin_lib_go(SB), AX
    MOVQ    AX, 8(SP)                          // fn
    MOVQ    $0, 16(SP)                         // fnarg
    MOVQ    $runtime·newosproc0(SB), AX
    CALL    AX

restore:
    MOVQ    0x18(SP), BX
    MOVQ    0x20(SP), BP
    MOVQ    0x28(SP), R12
    MOVQ    0x30(SP), R13
    MOVQ    0x38(SP), R14
    MOVQ    0x40(SP), R15
    RET

TEXT _rt0_amd64_darwin_lib_go(SB),NOSPLIT,$0
    MOVQ    _rt0_amd64_darwin_lib_argc<>(SB), DI
    MOVQ    _rt0_amd64_darwin_lib_argv<>(SB), SI
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

DATA _rt0_amd64_darwin_lib_argc<>(SB)/8, $0
GLOBL _rt0_amd64_darwin_lib_argc<>(SB),NOPTR, $8
DATA _rt0_amd64_darwin_lib_argv<>(SB)/8, $0
GLOBL _rt0_amd64_darwin_lib_argv<>(SB),NOPTR, $8

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

从源码看到,从第8行开始,汇编程序会执行 MOVQ $main(SB), AX,也就是

...

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

这个代码段,程序会进一步调用 $runtime·rt0_go , 我们再使用gdb进入$runtime·rt0_go了解下:

源码文件中的“·” 符号编译后会变成正常的“.”

(gdb) b runtime.rt0_go
Breakpoint 2 at 0x4e630: file /usr/local/go/src/runtime/asm_amd64.s, line 12.

asm_amd64.s的源码片段如下:

TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // copy arguments forward on an even stack
    MOVQ    DI, AX        // argc
    MOVQ    SI, BX        // argv
    SUBQ    $(4*8+7), SP        // 2args 2auto
    ANDQ    $~15, SP
    MOVQ    AX, 16(SP)
    MOVQ    BX, 24(SP)
    
    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    MOVQ    $runtime·g0(SB), DI
    LEAQ    (-64*1024+104)(SP), BX
    MOVQ    BX, g_stackguard0(DI)
    MOVQ    BX, g_stackguard1(DI)
    MOVQ    BX, (g_stack+stack_lo)(DI)
    MOVQ    SP, (g_stack+stack_hi)(DI)

    // find out information about the processor we're on
    MOVQ    $0, AX
    CPUID
    MOVQ    AX, SI
    CMPQ    AX, $0
    JE    nocpuinfo

    // Figure out how to serialize RDTSC.
    // On Intel processors LFENCE is enough. AMD requires MFENCE.
    // Don't know about the rest, so let's do MFENCE.
    CMPL    BX, $0x756E6547  // "Genu"
    JNE    notintel
    CMPL    DX, $0x49656E69  // "ineI"
    JNE    notintel
    CMPL    CX, $0x6C65746E  // "ntel"
    JNE    notintel
    MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

    // Load EAX=1 cpuid flags
    MOVQ    $1, AX
    CPUID
    MOVL    CX, runtime·cpuid_ecx(SB)
    MOVL    DX, runtime·cpuid_edx(SB)

    // Load EAX=7/ECX=0 cpuid flags
    CMPQ    SI, $7
    JLT    no7
    MOVL    $7, AX
    MOVL    $0, CX
    CPUID
    MOVL    BX, runtime·cpuid_ebx7(SB)
no7:
    // Detect AVX and AVX2 as per 14.7.1  Detection of AVX2 chapter of [1]
    // [1] 64-ia-32-architectures-software-developer-manual-325462.pdf
    // http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-manual-325462.pdf
    MOVL    runtime·cpuid_ecx(SB), CX
    ANDL    $0x18000000, CX // check for OSXSAVE and AVX bits
    CMPL    CX, $0x18000000
    JNE     noavx
    MOVL    $0, CX
    // For XGETBV, OSXSAVE bit is required and sufficient
    XGETBV
    ANDL    $6, AX
    CMPL    AX, $6 // Check for OS support of YMM registers
    JNE     noavx
    MOVB    $1, runtime·support_avx(SB)
    TESTL   $(1<<5), runtime·cpuid_ebx7(SB) // check for AVX2 bit
    JEQ     noavx2
    MOVB    $1, runtime·support_avx2(SB)
    JMP     nocpuinfo
noavx:
    MOVB    $0, runtime·support_avx(SB)
noavx2:
    MOVB    $0, runtime·support_avx2(SB)
nocpuinfo:    
    
    // if there is an _cgo_init, call it.
    MOVQ    _cgo_init(SB), AX
    TESTQ    AX, AX
    JZ    needtls
    // g0 already in DI
    MOVQ    DI, CX    // Win64 uses CX for first parameter
    MOVQ    $setg_gcc<>(SB), SI
    CALL    AX

    // update stackguard after _cgo_init
    MOVQ    $runtime·g0(SB), CX
    MOVQ    (g_stack+stack_lo)(CX), AX
    ADDQ    $const__StackGuard, AX
    MOVQ    AX, g_stackguard0(CX)
    MOVQ    AX, g_stackguard1(CX)

#ifndef GOOS_windows
    JMP ok
#endif
needtls:
#ifdef GOOS_plan9
    // skip TLS setup on Plan 9
    JMP ok
#endif
#ifdef GOOS_solaris
    // skip TLS setup on Solaris
    JMP ok
#endif

    LEAQ    runtime·m0+m_tls(SB), DI
    CALL    runtime·settls(SB)

    // store through it, to make sure it works
    get_tls(BX)
    MOVQ    $0x123, g(BX)
    MOVQ    runtime·m0+m_tls(SB), AX
    CMPQ    AX, $0x123
    JEQ 2(PC)
    MOVL    AX, 0    // abort
ok:
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)
    LEAQ    runtime·g0(SB), CX
    MOVQ    CX, g(BX)
    LEAQ    runtime·m0(SB), AX

    // save m->g0 = g0
    MOVQ    CX, m_g0(AX)
    // save m0 to g0->m
    MOVQ    AX, g_m(CX)

    CLD                // convention is D is always left cleared
    CALL    runtime·check(SB)

    MOVL    16(SP), AX        // copy argc
    MOVL    AX, 0(SP)
    MOVQ    24(SP), AX        // copy argv
    MOVQ    AX, 8(SP)
    CALL    runtime·args(SB)
    CALL    runtime·osinit(SB)
    CALL    runtime·schedinit(SB)

    // create a new goroutine to start program
    MOVQ    $runtime·mainPC(SB), AX        // entry
    PUSHQ    AX
    PUSHQ    $0            // arg size
    CALL    runtime·newproc(SB)
    POPQ    AX
    POPQ    AX

    // start this M
    CALL    runtime·mstart(SB)

    MOVL    $0xf1, 0xf1  // crash
    RET

DATA    runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL    runtime·mainPC(SB),RODATA,$8

这个程序基本完成引导的工作,最终去运行 runtime·main(SB) 。后续的内容我们再使用gdb debug一下:

(gdb) b runtime.main
Breakpoint 3 at 0x28470: file /usr/local/go/src/runtime/proc.go, line 106.

上面我们使用gdb基本可以查看到整个go的引导过程。当然啦,除了引导的部分,go程序还有初始化的部分。这个我们可以再去根据上面演示的线索查找 方法去研究,《Go学习笔记》给出了我们查看初始化源码的几个tips:

  • runtime.args

  • runtime.osinit

  • runtime.schedinit

我们有情趣的话,可以配合gdb去debug下,这有助于我们了解初始化的过程

结论:

  • 所有的init函数都在同一个goroutine内执行

  • 所有的init函数结束后才会执行 main.main 函数