动态断点

实现目标:列出断点

前一节中我们实现了动态断点的添加,为了能够支持移除断点,我们必须为断点提供一些描述信息,比如断点编号,这样用户可以借助断点编号来移除断点。

比如依次添加了3个断点,每个断点依次编号为1、2、3,当用户希望移除断点2时,可以通过执行命令clear -n 2来移除。

当然添加的断点数量多了之后,我们很难记得清楚自己添加了多少个断点,每个断点对应的指令地址是什么,添加顺序(编号)是什么,所以我们还必须提供一个列出已添加断点的功能,如执行breakpoints会列出所有已添加断点。

展示样式大致如下所示,至少要显示断点编号,对应指令地址,以及源码位置。

godbg> breakpoints
breakpoint[1] 0x4000001 main.go:10
breakpoint[2] 0x5000001 hello.go:20
breakpoint[3] 0x5000101 world.go:30

代码实现

微调代码:新增断点时记录编号及位置

我们需要对前一节添加断点的部分代码进行适当修改,在添加断点时能够同时记录断点的编号、指令地址、源码位置(源码位置我们先用空串表示)。

file: cmd/debug/break.go

package debug

var breakCmd = &cobra.Command{
    RunE: func(cmd *cobra.Command, args []string) error {
        ...
        breakpoint, err := target.NewBreakpoint(addr, orig[0], "")
        if err != nil {
            return fmt.Errorf("add breakpoint error: %v", err)
        }
        breakpoints[addr] = &breakpoint
    ...
    },
}

func init() {
    debugRootCmd.AddCommand(breakCmd)
}

file: target/breakpoint.go

func NewBreakpoint(addr uintptr, orig byte, location string) (Breakpoint, error) {
    b := Breakpoint{
        ID:       seqNo.Add(1),
        Addr:     addr,
        Orig:     orig,
        Location: location,
    }
    return b, nil
}

新增命令:breakpoints显示断点列表

我们新增一个调试命令breakpoints,用名词复数形式来隐含表示查询所有断点的意思。实现逻辑就比较简单,我们遍历所有已添加的断点,逐个输出断点信息即可。

file: cmd/debug/breakpoints.go

package debug

import (
    "fmt"
    "sort"

    "godbg/target"

    "github.com/spf13/cobra"
)

var breaksCmd = &cobra.Command{
    Use:     "breaks",
    Short:   "列出所有断点",
    Long:    "列出所有断点",
    Aliases: []string{"bs", "breakpoints"},
    Annotations: map[string]string{
        cmdGroupKey: cmdGroupBreakpoints,
    },
    RunE: func(cmd *cobra.Command, args []string) error {

        bs := target.Breakpoints{}
        for _, b := range breakpoints {
            bs = append(bs, *b)
        }
        sort.Sort(bs)

        for _, b := range bs {
            fmt.Printf("breakpoint[%d] %#x %s\n", b.ID, b.Addr, b.Location)
        }
        return nil
    },
}

func init() {
    debugRootCmd.AddCommand(breaksCmd)
}

新增断点记录在一个map[uintptr]*breakpoint结构中,这里用map主要是考虑到后续插入、删除、查询的场景,有助于提升查询效率,比如重复执行break main.go:10多次,首先将main.go:10转成指令地址,然后查询此map结构,可以以O(1)的时间复杂度来判断此断点是否已经存在。

上述map的key是断点的指令地址,value是断点描述信息struct,如果我们直接通过for-range来遍历map的kv然后输出其信息,那断点展示的顺序不一定就是按照断点编号。

为了能够保证断点展示的顺序能够按照编号有序展示,我们需要对断点切片Breakpoints实现sort.Interface{}接口,允许其通过编号进行排序。

file: target/breakpoint.go

package target

import (
    "go.uber.org/atomic"
)

var (
  // 断点编号
    seqNo = atomic.NewUint64(0)
)

// Breakpoint 断点
type Breakpoint struct {
    ID       uint64
    Addr     uintptr
    Orig     byte
    Location string
}

// Breakpoints 断点切片,实现了排序接口
type Breakpoints []Breakpoint

func (b Breakpoints) Len() int {
    return len(b)
}

func (b Breakpoints) Less(i, j int) bool {
    if b[i].ID <= b[j].ID {
        return true
    }
    return false
}

func (b Breakpoints) Swap(i, j int) {
    b[i], b[j] = b[j], b[i]
}

这样,我们既可以通过sort.Sort(bs)对现有断点按照编号进行排序,然后再遍历输出断点信息即可。

基于命令行的调试器,实际调试经历来看,查看断点列表、新增断点、删除断点,相对来说也是比较频繁的。存储所有断点信息使用map和slice相比,新增、删除、查询都更方便,编码也方便 :)

代码测试

我们先运行一个测试程序,查看其pid,然后通过godbg attach <pid>对目标进程进行调试,当调试会话准备好之后,我们通过disass反汇编查看其汇编指令列表以及指令地址,然后通过break <locspec>来添加多个断点,并通过breakpointsorbreaks来显示已添加的断点列表。

godbg> disass
...
0x4653a6 INT 0x3                                          ; add breakpoint here
0x4653a7 MOV [RSP+Reg(0)+0x40], AL
0x4653ab MOV RCX, RSP                                     ; add breakpoint here
0x4653ae INT 0x3
0x4653af AND [RAX-0x7d], CL
0x4653b2 Prefix(0xc4) Prefix(0x28) Prefix(0xc3) INT 0x3
0x4653b6 MOV EAX, [RSP+Reg(0)+0x30]
0x4653ba ADD RAX, 0x8
0x4653be INT 0x3
0x4653bf MOV [RSP+Reg(0)], EAX
0x4653c2 REX.W Op(0)
...
godbg> b 0x4653a6
break 0x4653a6
添加断点成功
godbg> b 0x4653ab
break 0x4653ab
添加断点成功
godbg> breakpoints
breakpoint[1] 0x4653a6 
breakpoint[2] 0x4653ab 
godbg>

我们可以看到添加了断点之后,breakpoints命令正常显示了断点列表。

godbg> breakpoints
breakpoint[1] 0x4653a6 
breakpoint[2] 0x4653ab

这里的编号1、2将用来作为断点标识用以移除断点,我们将在clear命令中描述这点。

results matching ""

    No results matching ""