dlv 源码阅读

一. 目录介绍

1._fixtures
这个文件夹下面主要放一些用于测试的应用程序源码

2.assets
存放了4个代表delve的图标

3.cmd
这个是server/client的命令行入口
主要从commands.go这个文件体现,主要使用spf13/cobra来做命令行工具(相信喜欢使用vim的人都应该听过spf13)
主要命令有debugtraceattach等,但是这些命令核心都是在使用gobuildexecute,所以基本看这两个函数即可
gobuild 主要是对需要调试源码的进行编译
execute 主要是对已经编译好的二进制进行运行、attach,用于调试(已经运行的进程,就直接attach)

4.Documentation
主要放dlv的文档,另外关于cmd的文档都是通过script/gen-usage-docs.go生成的
就是说大部分都是cmd目录的help注释

5.service
service入口是/service/rpccommon/server.go,主要是由cmd来调用serverRun()、Close()运行的
server本身是一个interface,用于做跨平台,对于不同服务采用不同的debug_*.go或者pkg

6.pkg
pkg属于内部的package,对外暴露最核心的文件是proc.go,对应的interfaceProcess,而实现跨平台,内部/pkg/native/proc.go有对应的实现

二. 断点

对于dlv而言,是怎么实现断点的呢?拿linux系统来说,有ptrace可以实现对进程的监控
著名命令strace就是利用ptrace实现的

对于计算机来说,异常控制流(ECF)几乎是无处不在的
按照深入理解计算机系统一书所言,我们把异常分成中断陷阱系统调用故障终止

中断(主要指硬件中断),来自I/O设备的信号,异步,总是返回到下一条指令
陷阱(包括系统调用),有意的异常,同步,总是返回到下一条指令
故障,潜在可恢复的错误,同步,可能会返回到当前指令(注意跟上面不同)
终止,不可恢复的错误,同步,不会返回

差异点:

系统调用和普通函数调用
普通的函数调用是在用户模式下,用户模式限制了函数可以执行的指令的类型,而且只能访问与调用函数相同的栈
系统调用运行在内核模式,可以访问内核中的栈
故障 最典型的事例就是缺页异常

而断点的实行就是利用了陷阱 int 3
当设置某一指令为断点时,调试器就会把该处指令替换成int 3,系统执行到该处就会中断,恢复int 3之前的指令,将现场返回给用户(这个信号会被tracer捕获到,并且traced的进程会停止)

被跟踪进程收到任何信号(除SIGKILL)都会停止,将信号转给跟踪器(触发wait)
PTRACE_SYSCALL:跟踪系统调用,每次系统调用会收到一个SIGTRAP  
PTRACE_SINGLESTEP:跟踪单步,每执行完一个指令收到一个SIGTRAP  
PTRACE_CONT:继续  
断点(int 3 指令)会触发一个SIGTRAP

dlv 对应的下断点文件在/pkg/proc/native/ptrace_*.go
本质上也是调用了官方库golang.org/x/sys/unix

三. 单步

关键在于调试信息,使用的是dwarf的一套规范(debug with attribute record format)
还有一部分的符号表信息来源与 /pkg/proc/bininfo.go的 gosym.go查看符号表
所谓的当前行单步的实现,就是优先找到当前行所在的function区域
然后对该function源码的每一行找到对应的pc指令位置,进行下断点

四.变量

打印变量,非常依赖dwarf提供的信息
对于拥有词法作用域的go语言来说,ex:

  1 package main
  2 
  3 import "fmt"
  4 
  5 func main() {
  6     w := 1
  7     s := 2
  8 
  9     func() {
 10         fmt.Printf("%d\n", w)
 11     }()
 12     fmt.Printf("%d\n", s)
 13 }


如果当前断点下在10 line,执行print w是可以显示1,但是s就不可以,因为依赖的生成的dwarf并未没有s 的信息

参考
DWARF, 说不定你也需要它哦
中断、异常、信号
go语言词法作用域调试信息