> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# godbg 介绍
- URL: https://blog.hacking.pub/2019/09/01/godbg-jie-shao/
- Published: 2019-09-01T16:43:33.000Z
- Updated: 2019-09-01T16:44:03.000Z
- Author: chainhelen
- Tags: go, golang, 调试器, godbg, go语言, go源码, delve, dlv

`Start a new project, the debugger on linux platform for go.`  
`This project is inspired by dlv` [github](https://github.com/chainhelen/godbg?ref=blog.hacking.pub)地址

#### 背景

没想到这个项目最终还是沦为为一个玩具。初次有这个想法是因为发现 `gdb`调试`golang`代码是可以进入runtime的指令，而`dlv`却不可以。例如像`map`这种的初始化

```
s := make(map[int]string, 20)

```

主要是`dlv`将`runtime`的代码都`skip`掉了，表示对`golanger`无需关心底层的逻辑，只需要关心自己编写的代码即可。

先看一下效果

![display.gif](https://github.com/chainhelen/godbg/blob/master/file/display.gif?raw=true)

#### 实现

代码本身分为5个步骤，代码是在`main.go`

> 1. 获取生成含.debug可执行文件的路径
> 2. 编译创建可执行文件
> 3. 分析可执行文件里面的\_zdebug的字段，以便后续调试使用
> 4. 执行可执行文件，并且ptrace
> 5. 处理用户所有的输入

> 注意两点：
> 
> 1．本身`linux`里面并没有多线程的概念，都是`task`；没有做上下文切换，所以暂时不支持多协程，全局唯一的`pid`；
> 
> 2．为了`ptrace`生效，需要将协程和`task`一一绑定，`runtime.LockOSThread()`；

前两个步骤没什么好讲的，主要从分析二进制文件开始

#### 分析

分为两个部分，第一个部分是`line`和`info`两个节区的结合，记录出行信息的表，对应每一行汇编是源码中哪一行生成的，主要是为后续`list`和`breakpoint`服务的 ；第二部分是`FrameSection`，主要是为打印变量时候，算调用栈帧用的 ；

1. `line` 和 `info`识别`dwarf`里面`compileunit`的`tag`，每个编译单元都是记录当前单元的所有行信息，一个编译单元可以理解为一个`package`
2. `line`和`info`识别`dwarf`里面`SubProgram`的`tag`，这个可以理解是每一个函数包含哪些指令范围，这样在程序运行时候拿到`pc`寄存器值进行反查就能知道当前运行到哪一个函数了
3. `line`和`info`识别`dwarf`里面的`Variable`的tag，是为了找到局部变量的定义、名称、结构以及数据内存地址等等
4. `frame`识别里面的`CIE`（全称:`CommonInfomationEntry`）和`FDE`（全称`FrameDescriptionEntry`，`FDE`对应一定指向一个公用的`CIE`），在程序运行时可以按照`CIE`和`FDE`的规则拿到`CFA`的计算规则

#### 处理

**断点**：

设置断点其实没有什么很深的东西，主要是利用行信息表查出对应的内存块，`ptrace`将需要下断点的地方内存指令换成软断点`0xcc`；当进程执行到`0xcc`会触发一个`trap`停止执行后续指令；

这个会出现一个问题，就是当期望恢复断点继续执行的时候，由于破坏了原来的指令则无法继续`continue`，需要将指令暂时（注意是暂时，如果不是暂时那么断点就失效了）恢复，并且`pc`寄存器数值减少一个指令重新执行；

还有就是`golang`是具有`gc`高级特效的语言，许多地方增加了`checkpoint`，例如每次函数调用的时候，都会去`check`当前栈是否需要扩容，如果你的断点下在了`checkpoint`的地方，那么很有可能会在同一行代码（`checkpoint`也对应用户代码的行表）多次触发，看起来就像是`continue`一直在同一行卡住，解决这个问题需要主要一下`PrologueEnd`（编译器生成debug信息提供的）

断点列表、显示源码的原理都需要记载在内存里面对应关系就可以做到了；

**继续**:

`continue`上面讲过了，需要将断点暂时恢复一下；而`next`和`stepin`，正常来讲应该是内部断点的实现方式，这里只用到了一个简单逻辑（暴露玩具的本质）；`next`就是在当前`pc`值（如果当前是断点，应该是`pc`\-1）过后不停地`PtraceSingleStep`直到发现 1.文件名或者行数变了 2.碰到别的断点 3.如果当前是函数调用记录一个`calling`状态继续`PtraceSingleStep`直到`stepout`；

**列举**:

`list`原理跟`breakpoint`类似

**重启**:

原理就是将程序原来的`kill`掉，重新执行一个

**反汇编**:

就是上面根据`SubProgram`讲到的`pc`找到对应的指令范围，然后反汇编就可以了，注意断点的部分需要特殊处理（因为已经被替换掉了，否则反汇编会报错）

**变量**:

核心是要计算出来`cfa`(`callframeaddress`)，[http://dwarfstd.org/doc/Dwarf3.pdf大概在122页左右；](http://dwarfstd.org/doc/Dwarf3.pdf%E5%A4%A7%E6%A6%82%E5%9C%A8122%E9%A1%B5%E5%B7%A6%E5%8F%B3%EF%BC%9B?ref=blog.hacking.pub)

计算过程相当繁琐，例如这样能发现`delve`某些地方解析也是不对的，[https://github.com/go-delve/delve/pull/1679](https://github.com/go-delve/delve/pull/1679?ref=blog.hacking.pub)；

---

> 参考
> 
> [https://www.cnblogs.com/tsecer/p/10485670.html](https://www.cnblogs.com/tsecer/p/10485670.html?ref=blog.hacking.pub) 其实没有完全看懂
> 
> [http://ucla.jamesyxu.com/?p=231](http://ucla.jamesyxu.com/?p=231&ref=blog.hacking.pub) 也是完全没有看懂
> 
> [https://www.corsix.org/content/cfa-rsp-x86-64](https://www.corsix.org/content/cfa-rsp-x86-64?ref=blog.hacking.pub) 又是完全看不懂
> 
> [http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch10s07.html](http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch10s07.html?ref=blog.hacking.pub) 好像看懂了
> 
> [http://dwarfstd.org/doc/Dwarf3.pdf](http://dwarfstd.org/doc/Dwarf3.pdf?ref=blog.hacking.pub) 这本书很重要，结合dlv才能看得懂