> ## 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.

# 调试器与GO
- URL: https://blog.hacking.pub/2020/04/09/tiaoshiqiyugo/
- Published: 2020-04-09T11:13:40.000Z
- Updated: 2020-11-26T09:51:51.000Z
- Author: chainhelen
- Tags: go, 调试器, delve, 源码剖析

## 调试器

如果把调试器看成一个产品的时候，首先他应该具体的功能有哪些？

1. 断点
2. 代码与当前断点映射（例如查看源码）
3. 断点过后继续运行（继续执行，单步，单出）
4. 打印变量

附属的功能：

1. 打印调用栈
2. 打印堆栈
3. 函数调用
4. 条件断点
5. 反汇编  
...

## 调试状态

`linux`会有trace进程的状态，对被`trace`的`进程`可以进行控制

## 基本功能

#### 断点

断点通常分为两种，硬件断点和软件断点

1. 先说硬件断点，这个我们（普通的后台开发）平时不太常用，对于x86系统来说， 是依赖寄存器DR0\~DR7和2个MSR实现的  
说直白一点，就是可以把内存地址放到寄存器内，当cpu进行 读、写、修改该地方内存时，就会触发trap
2. 而对于软件断点，依赖CPU的指令  
对于x86架构CPU来说，`int 3`指令就是暂停的系统中断指令，`0XCC` （实际上还有一种软断点编码[\[\]byte{0xcd, 0x03}](https://github.com/go-delve/delve/commit/e69d536e819bbbd3539d5748d61942330863cf11?ref=blog.hacking.pub#diff-fb7b7a020e32bf8bf477c052ac2d2857e7e587478be6039aebc7135c658417b2R579)）  
当CPU指令执行 `0xCC`，`task`（CPU的工作基本单位）就会陷入暂停状态(trap或者中断)，等待新的命令

![go-delve-debugger](https://blog.hacking.pub/qiniu/debugger/go-delve-debugger.gif)

![debugger3.svg](https://blog.hacking.pub/qiniu/debugger/debugger3.svg)

#### 代码和当前断点映射

这个需要编译器和链接器支持，编译参数 `go build -gcflags="-N -l" -o main main.go`  
保证禁用编译器优化和内联优化 ctx  
`objdump --dwarf main > log.txt` 可以看到 行号 状态机

```
 The Directory Table is empty. 

 The File Name Table (offset 0x279ad):
  Entry Dir Time    Size    Name   
  1 0   0   0   /home/chainhelen/main.go

 Line Number Statements:
  [0x000279ca]  Extended opcode 2: set Address to 0x48cf30
  [0x000279d5]  Set File Name to entry 1 in the File Name Table
  [0x000279d7]  Advance Line by 1 to 2 
  [0x000279d9]  Special opcode 9: advance Address by 0 to 0x48cf30 and Line by 5 to 7 
  [0x000279da]  Set prologue_end to true
  [0x000279db]  Special opcode 154: advance Address by 15 to 0x48cf3f and Line by 0 to 7 
  [0x000279dc]  Special opcode 145: advance Address by 14 to 0x48cf4d and Line by 1 to 8 
  [0x000279dd]  Set is_stmt to 0
  [0x000279de]  Special opcode 34: advance Address by 3 to 0x48cf50 and Line by 0 to 8 
  [0x000279df]  Set is_stmt to 1
  [0x000279e0]  Advance PC by 66 to 0x48cf92
  [0x000279e2]  Special opcode 244: advance Address by 24 to 0x48cfaa and Line by 0 to 8 
  [0x000279e3]  Special opcode 55: advance Address by 5 to 0x48cfaf and Line by 1 to 9 
  [0x000279e4]  Special opcode 102: advance Address by 10 to 0x48cfb9 and Line by -2 to 7
  [0x000279e5]  Extended opcode 1: End of Sequence

```

```
objdump --dwarf ./main | grep -C2 "DW_TAG_compile_unit\|DW_AT_producer"

```

[dwarf 知识简介](https://wiki.osdev.org/DWARF?ref=blog.hacking.pub)  
[golang行号状态机的小bug](https://www.zhihu.com/question/387920953/answer/1219734250?ref=blog.hacking.pub)  
[PrologueEnd 断点](https://golang.org/pkg/debug/dwarf/?ref=blog.hacking.pub)

#### 断点过后继续运行（继续执行，单步，单出）

主要有两个系统调用可以让进程从`trap`恢复出来， `PTRACE_CONT`、`PTRACE_SINGLESTEP`  
而对于 `PTRACE_CONT` 是让进程恢复正常的trace状态并且一直运行下去  
`PTRACE_SINGLESTEP` 是执行一条汇编语句

两个问题

1. 如果断点断住了，那么breakpoint的位置是 `int 3`，`PTRACE_CONT`是不能正常运行的，举个例子

```
(dlv) disassemble
TEXT main.main(SB) /home/chainhelen/main.go
    ...
	main.go:7	0x4aa3b8	488d6c2460		lea rbp, ptr [rsp+0x60]
=>	main.go:8	0x4aa3bd*	0f57c0			xorps xmm0, xmm0
	main.go:8	0x4aa3c0	0f11442438		movups xmmword ptr [rsp+0x38], xmm0
	main.go:8	0x4aa3c5	488d442438		lea rax, ptr [rsp+0x38]
    ...
	main.go:7	0x4aa429	e8b2e9faff		call $runtime.morestack_noctxt
	main.go:1	0x4aa42e	e96dffffff		jmp $main.main

```

实际上的状态是`0XCC`

```
(dlv) disassemble
TEXT main.main(SB) /home/chainhelen/main.go
    ...
	main.go:7	0x4aa3b8	488d6c2460		lea rbp, ptr [rsp+0x60]
=>	main.go:8	0x4aa3bd*	cc57c0			xorps xmm0, xmm0
	main.go:8	0x4aa3c0	0f11442438		movups xmmword ptr [rsp+0x38], xmm0
	main.go:8	0x4aa3c5	488d442438		lea rax, ptr [rsp+0x38]
    ...
	main.go:7	0x4aa429	e8b2e9faff		call $runtime.morestack_noctxt
	main.go:1	0x4aa42e	e96dffffff		jmp $main.main

```

如果你从这个地方直接运行 `PTRACE_CONT`，那么通常结果是出错（或者CPU跳过错误指令继续前向）

需要的是让指令继续前行（PC寄存器，IP寄存器，RIP寄存器，或者EIP寄存器）  
2\. 正常运行了，如何实现source code的单步调试  
而是源码级别的单步调试，那么需要对每一行的源码都下断点，还有注意返回帧(函数栈或者叫调用栈)

#### 打印变量

对于打印变量，对于全局变量（包括global或者package导出的）主要是符号表  
是否符号表就够了？不够！

```
main.c  
nm main

main.go  
nm main

```

局部变量怎么办，跟package无关，也就是跟导出符号无关，也就是不在符号表内  
并且对于编译器来说，所有的非static局部变量，最终都在栈上面（还有逃逸的部分在堆上），利用压栈弹栈方式  
在编译链接过程中，局部变量都已经转化成栈底（或者栈顶）偏移量

这个时候就需要dwarf记录各个变量的信息，通常非优化编译，都是可以保留的

```
// main.go
package main

import (
	"fmt"
)

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

// go build
go build -gcflags="-l -N" -o main main.go 

// objdump
objdump --dwarf main > log.txt  

274873  <2><76ed1>: Abbrev Number: 10 (DW_TAG_variable)
274874     <76ed2>   DW_AT_name        : hello
274875     <76ed8>   DW_AT_decl_line   : 8
274876     <76ed9>   DW_AT_type        : <0x37482>
274877     <76edd>   DW_AT_location    : 3 byte block: 91 b8 7f    (DW_OP_fbreg: -72)

```

这种做法是对的吗？？？哪种场景的变量不正确 [Stack unwinding](https://zhuanlan.zhihu.com/p/280929169?ref=blog.hacking.pub)

## 附属功能

#### 打印调用栈

主要指函数调用，通常我们有函数帧（栈），栈中包含了返回地址，通过返回地址来保证读取到函数栈

![test_defer3.svg](https://blog.hacking.pub/qiniu/go%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%20defer/test_defer3.svg)

(图片好像有点小错误，但是我忘记了哪个寄存器搞错了)

#### 函数调用

需要语言本身提供支持（原理上来讲不支持也可以注入的方式，但是异常复杂），因为涉及到内存（堆栈）和寄存器的变动

```
"debugCall"
"runtime.debugCall"
"runtime.debugCallV1"

```

[debugcall](https://golang.org/src/runtime/debugcall.go?ref=blog.hacking.pub)

#### 条件断点

条件断点是一件很慢的事情，普通的10000次循环，只check一个变量，延迟会达到10s  
[go-delve/delve 1549](https://github.com/go-delve/delve/issues/1549?ref=blog.hacking.pub)

#### 反汇编

指令翻译 [符号对应关系提前从二进制文件中解析出来](https://github.com/go-delve/delve/blob/v1.5.0/pkg/proc/x86%5Fdisasm.go?ref=blog.hacking.pub#L51-L70)

## go特殊的部分

#### 协程

1. cgo的协程是要做切换栈的，我们对于切换的协程是如何做到

![debugger.svg](https://blog.hacking.pub/qiniu/debugger/debugger.svg)

asm\_amd64.s 文件  
[go-delve/delve 935 cgo better](https://github.com/go-delve/delve/issues/935?ref=blog.hacking.pub)

2.切换用户无关，那么trace权限如何控制？  
lockOSThread [trace协程与线程绑定](https://go-review.googlesource.com/c/go/+/155138/2/src/syscall/exec%5Flinux.go?ref=blog.hacking.pub#28)

3.系统线程和协程绑定关系保存在当前协程的tls中（见下文）  
所有线程，Linux 下在/proc中可以获取到task  
所有协程，依赖底下几个内存变量，[官方runtime的代码](https://github.com/golang/go/blob/370682ae98d7edc3ce9772d6d1d746df93ba9e6d/src/runtime/proc.go?ref=blog.hacking.pub#L497-L506)，[dlv 解析代码](https://github.com/go-delve/delve/blob/v1.5.0/pkg/proc/goroutine%5Fcache.go?ref=blog.hacking.pub#L16-L23)，[官方runtime使用代码](https://github.com/golang/go/blob/9393b5bae5944acebed3ab6f995926b7de3ce429/src/runtime/heapdump.go?ref=blog.hacking.pub#L405-L422)

```
runtime.allglen
runtime.allgs
runtime.allg(旧版本)

```

#### 单步

###### 序章

关于协程调度的地方，序章需要跳过做一些识别

```
// main.go 

package main

import (
	"fmt"
)

func A() {
	fmt.Println("hello world")  // set breakpoint
}

func main() {
	A()
}

TEXT main.A(SB) /home/chainhelen/main.go
	main.go:7	0x4aa3a0	64488b0c25f8ffffff	mov rcx, qword ptr fs:[0xfffffff8]
	main.go:7	0x4aa3a9	483b6110		cmp rsp, qword ptr [rcx+0x10]
	main.go:7	0x4aa3ad	767a			jbe 0x4aa429
=>	main.go:7	0x4aa3af*	4883ec68		sub rsp, 0x68
	main.go:7	0x4aa3b3	48896c2460		mov qword ptr [rsp+0x60], rbp
	main.go:7	0x4aa3b8	488d6c2460		lea rbp, ptr [rsp+0x60]
	main.go:8	0x4aa3bd	0f57c0			xorps xmm0, xmm0
	main.go:8	0x4aa3c0	0f11442438		movups xmmword ptr [rsp+0x38], xmm0
	main.go:8	0x4aa3c5	488d442438		lea rax, ptr [rsp+0x38]

```

可能这个地方不够明显，断点在`0x4aa3a0` 或者 `0x4aa3af` 差别不是很大

###### 栈分裂

再来看一下栈分裂（或者翻译成分裂栈？）情况(写屏障类似)

```
package main

import (
	"fmt"
)

func A() {
	s := make([]string, 0, 0)
	s = append(s, "hello", "world") // set breakpoint
	fmt.Println(s)
}

func main() {
	A()
}

```

(流程上操作为主)

[go1.15引入的一个分裂栈问题](https://go-review.googlesource.com/c/go/+/228417?ref=blog.hacking.pub)，[解决](https://go-review.googlesource.com/c/go/+/248878/7/src/runtime/lockrank%5Foff.go?ref=blog.hacking.pub#27)

###### interface

对于interface的实现，也需要跳过

```
package main

import (
	"fmt"
)

type Iface interface {
	Blah() string
}

type A struct {
	a int
}

func (A) Blah() string {
	return "blah"
}

func main() {
	var iface Iface = A{a: 1}
	s := iface.Blah()    // line 21
	fmt.Printf("%s\n", s)
}

```

(流程上操作为主)

第二种interface的实现，组合方式

```
package main

import "fmt"

type A struct {
	a int
}

type B struct {
	*A
}

type Iface interface {
	PtrReceiver() string
	NonPtrReceiver() string
}

func (*A) PtrReceiver() string {
	return "blah"
}

func (A) NonPtrReceiver() string {
	return "blah"
}

func main() {
	var iface Iface = &B{&A{1}}
	s := iface.PtrReceiver()
	s = iface.NonPtrReceiver()
	fmt.Printf("%s\n", s)
}

```

(流程上操作为主)

###### tls

并不是指`tls`双向验证，而是`thread local storage`.  
(linux语境) M 代表的是 `task`，每一个`task`都需要一块内存来存放数据，例如 `G`结构

而且 `go` 汇编中是有一个名为 `TLS`寄存器的

![debugger2.svg](https://blog.hacking.pub/qiniu/debugger/debugger2.svg)

amd64 linux fs -8 （fs地址偏移量直接-8）  
i386 linux gs -4 （gs是ldt，需要找到对应gdt地址-4）

[golang tls](https://www.zhihu.com/question/284288720/answer/1033460480?ref=blog.hacking.pub)  
[dlv中对于这部分的注释](https://github.com/go-delve/delve/pull/1884/files?ref=blog.hacking.pub#diff-1cc68140838ee78508038d162157bc5b5f3d68e5b492a864dc7a4f9596c008c6R1031-R1033)

###### pie

地址无关，尤其是动态库，.text段被加载到虚拟内存任意位置都是可以利用当前指令的相对位置（动态重定位）

```
// main.go
package main

import (
	"fmt"
)

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

go build -buildmode=pie -gcflags="-N -l" -o main.pie ~/main.go  
objdump -s -S main.pie > main.pie.log  

go build -gcflags="-N -l" -o main.nopie ~/main.go  
objdump -s -S main.nopie > main.nopie.log  

```

分别在64bit和32bit上面执行，拿到的文件做一下对比

```
export CGO_ENABLED="0"
go build -buildmode=pie -gcflags="-N -l" -o main.pie ~/main.go 

linux x86 32位 编译不过 
linux x86 64位 能编译过 

```

```
32位
	world.go:17		0x86ef64	e80729f7ff		call $__x86.get_pc_thunk.dx
	world.go:17		0x86ef69	8d9277b20200		lea edx, ptr [edx+0x2b277]
	world.go:17		0x86ef6f	89542430		mov dword ptr [esp+0x30], edx
	world.go:17		0x86ef73	894c2434		mov dword ptr [esp+0x34], ecx
	world.go:17		0x86ef77	8400			test byte ptr [eax], al
	world.go:17		0x86ef79	eb00			jmp 0x86ef7b
	world.go:17		0x86ef7b	89442444		mov dword ptr [esp+0x44], eax
	world.go:17		0x86ef7f	c744244801000000	mov dword ptr [esp+0x48], 0x1
	world.go:17		0x86ef87	c744244c01000000	mov dword ptr [esp+0x4c], 0x1
	world.go:17		0x86ef8f	890424			mov dword ptr [esp], eax
	world.go:17		0x86ef92	c744240401000000	mov dword ptr [esp+0x4], 0x1
	world.go:17		0x86ef9a	c744240801000000	mov dword ptr [esp+0x8], 0x1
	world.go:17		0x86efa2	e8d9a2ffff		call $fmt.Println

```

一些[get\_pc\_thunk介绍](https://zhuanlan.zhihu.com/p/91420787?ref=blog.hacking.pub)，dlv没有直接解析反汇编指令跳过，对于`get_pc_thunk.dx` 需要跳过 [get\_pc\_thunk](https://github.com/go-delve/delve/pull/1884/files?ref=blog.hacking.pub#diff-1cc68140838ee78508038d162157bc5b5f3d68e5b492a864dc7a4f9596c008c6R990-R1008)

> 参考  
> [一个古老又广泛的寻址技术：段寄存器](https://zhuanlan.zhihu.com/p/69504370?from%5Fvoters%5Fpage=true&ref=blog.hacking.pub)  
> [ptrace.2](http://man7.org/linux/man-pages/man2/ptrace.2.html?ref=blog.hacking.pub)