使用Delve的调试和反汇编功能阅读Go源码
2021-10-28
Delve是一个专门用于Go语言的调试器,之前整理过Delve的基本使用,使用Delve调试Go程序。
本文进一步介绍如何使用Delve阅读Go的源码并找到一些语法在Go的内部是如何实现的。这里将一下在Go里创建channel的语法为例make(chan int, 100)
,演示如何使用Delve如何找到make chan在Go内部的源码实现以及channel具体的数据结构是什么样的。
首先使用go mod创建一个Go项目:
1mkdir make-chan-showcase
2cd make-chan-showcase
3go mod init make-chan-showcase
在项目根目录中创建一个简单的main.go,并编写如下make chan的代码:
1package main
2
3import "fmt"
4
5func main() {
6 ch := make(chan int, 100)
7 ch <- 1
8 fmt.Println(<-ch)
9 close(ch)
10}
main.go中的代码十分简单,首先使用make(chan int, 100)
创建了一个缓冲区大小为100的channel,然后往channel中写入了数据1,之后从channel中将数据读取了出来,最后将channel关闭。
为了搞清楚channel的内部结构,以及make函数的内部实现,需要使用Delve进行调试和查看具体的汇编代码的功能。具体操作如下,在main.go的第6行也就是make函数调用那一行打上断点,并输入c
执行程序到这个断点上:
1dlv debug
2Type 'help' for list of commands.
3(dlv) b main.go:6
4Breakpoint 1 set at 0x10abc18 for main.main() ./main.go:6
5(dlv) c
6> main.main() ./main.go:6 (hits goroutine(1):1 total:1) (PC: 0x10abc18)
7 1: package main
8 2:
9 3: import "fmt"
10 4:
11 5: func main() {
12=> 6: ch := make(chan int, 100)
13 7: ch <- 1
14 8: fmt.Println(<-ch)
15 9: close(ch)
16 10: }
使用dlv的子命令disassemble
简写为disass
查看汇编代码:
1(dlv) disass
2TEXT main.main(SB) /Workspace/make-chan-showcase/main.go
3 main.go:5 0x10abc00 493b6610 cmp rsp, qword ptr [r14+0x10]
4 main.go:5 0x10abc04 0f86d5000000 jbe 0x10abcdf
5 main.go:5 0x10abc0a 4883ec68 sub rsp, 0x68
6 main.go:5 0x10abc0e 48896c2460 mov qword ptr [rsp+0x60], rbp
7 main.go:5 0x10abc13 488d6c2460 lea rbp, ptr [rsp+0x60]
8=> main.go:6 0x10abc18* 488d05e16e0000 lea rax, ptr [rip+0x6ee1]
9 main.go:6 0x10abc1f bb64000000 mov ebx, 0x64
10 main.go:6 0x10abc24 e81785f5ff call $runtime.makechan
11 main.go:6 0x10abc29 4889442420 mov qword ptr [rsp+0x20], rax
12 main.go:7 0x10abc2e 488d1d3b3a0200 lea rbx, ptr [rip+0x23a3b]
13 main.go:7 0x10abc35 e88687f5ff call $runtime.chansend1
14 main.go:8 0x10abc3a 48c744241800000000 mov qword ptr [rsp+0x18], 0x0
15 main.go:8 0x10abc43 488b442420 mov rax, qword ptr [rsp+0x20]
16 main.go:8 0x10abc48 488d5c2418 lea rbx, ptr [rsp+0x18]
17 main.go:8 0x10abc4d e86e91f5ff call $runtime.chanrecv1
18 main.go:8 0x10abc52 440f117c2438 movups xmmword ptr [rsp+0x38], xmm15
19 main.go:8 0x10abc58 488d4c2438 lea rcx, ptr [rsp+0x38]
20 main.go:8 0x10abc5d 48894c2430 mov qword ptr [rsp+0x30], rcx
21 main.go:8 0x10abc62 488b442418 mov rax, qword ptr [rsp+0x18]
22 main.go:8 0x10abc67 e814ddf5ff call $runtime.convT64
23 main.go:8 0x10abc6c 4889442428 mov qword ptr [rsp+0x28], rax
24 main.go:8 0x10abc71 488b4c2430 mov rcx, qword ptr [rsp+0x30]
25 main.go:8 0x10abc76 8401 test byte ptr [rcx], al
26 main.go:8 0x10abc78 488d1541740000 lea rdx, ptr [rip+0x7441]
27 main.go:8 0x10abc7f 488911 mov qword ptr [rcx], rdx
28 main.go:8 0x10abc82 488d7908 lea rdi, ptr [rcx+0x8]
29 main.go:8 0x10abc86 833d73620d0000 cmp dword ptr [runtime.writeBarrier], 0x0
30 main.go:8 0x10abc8d 7402 jz 0x10abc91
31 main.go:8 0x10abc8f eb06 jmp 0x10abc97
32 main.go:8 0x10abc91 48894108 mov qword ptr [rcx+0x8], rax
33 main.go:8 0x10abc95 eb07 jmp 0x10abc9e
34 main.go:8 0x10abc97 e8c43dfbff call $runtime.gcWriteBarrier
35 main.go:8 0x10abc9c eb00 jmp 0x10abc9e
36 main.go:8 0x10abc9e 488b442430 mov rax, qword ptr [rsp+0x30]
37 main.go:8 0x10abca3 8400 test byte ptr [rax], al
38 main.go:8 0x10abca5 eb00 jmp 0x10abca7
39 main.go:8 0x10abca7 4889442448 mov qword ptr [rsp+0x48], rax
40 main.go:8 0x10abcac 48c744245001000000 mov qword ptr [rsp+0x50], 0x1
41 main.go:8 0x10abcb5 48c744245801000000 mov qword ptr [rsp+0x58], 0x1
42 main.go:8 0x10abcbe bb01000000 mov ebx, 0x1
43 main.go:8 0x10abcc3 4889d9 mov rcx, rbx
44 main.go:8 0x10abcc6 e8d5a8ffff call $fmt.Println
45 main.go:9 0x10abccb 488b442420 mov rax, qword ptr [rsp+0x20]
46 main.go:9 0x10abcd0 e86b8ef5ff call $runtime.closechan
47 main.go:10 0x10abcd5 488b6c2460 mov rbp, qword ptr [rsp+0x60]
48 main.go:10 0x10abcda 4883c468 add rsp, 0x68
49 main.go:10 0x10abcde c3 ret
50 main.go:5 0x10abcdf 90 nop
51 main.go:5 0x10abce0 e89b1dfbff call $runtime.morestack_noctxt
52 .:0 0x10abce5 e916ffffff jmp $main.main
从汇编代码中可以看出main.go
的第6行调用了runtime.makechan
,说明make(chan int, 100)
的语法实际上会调用runtime.makechan
。
继续使用dlv在runtime.makechan
上打个断点,并输入c
将代码执行到runtime.makechan
上:
1(dlv) b runtime.makechan
2Breakpoint 2 set at 0x100414a for runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:71
3(dlv) c
4> runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:71 (hits goroutine(1):1 total:1) (PC: 0x100414a)
5Warning: debugging optimized function
6 66: }
7 67:
8 68: return makechan(t, int(size))
9 69: }
10 70:
11=> 71: func makechan(t *chantype, size int) *hchan {
12 72: elem := t.elem
13 73:
14 74: // compiler checks this but be safe.
15 75: if elem.size >= 1<<16 {
16 76: throw("makechan: invalid channel element type")
17(dlv)
这样我们就找到了runtime.makechan
位于Go源码的src/runtime/chan.go的第71行,接下来就可以继续调试或直接去查看runtime.makechan的代码了。
在第91行,声明了一个var c *hchan
, *hchan
类型的结构体指针,hchan结构体就是channel的数据结构。
1(dlv) n
2> runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:93 (PC: 0x10041ae)
3Warning: debugging optimized function
4 88: // buf points into the same allocation, elemtype is persistent.
5 89: // SudoG's are referenced from their owning thread so they can't be collected.
6 90: // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
7 91: var c *hchan
8 92: switch {
9=> 93: case mem == 0:
10 94: // Queue or element size is zero.
11 95: c = (*hchan)(mallocgc(hchanSize, nil, true))
12 96: // Race detector uses this location for synchronization.
13 97: c.buf = c.raceaddr()
14 98: case elem.ptrdata == 0:
在chan.go的第32行找到了hchan
结构体的具体定义:
1type hchan struct {
2 qcount uint // total data in the queue
3 dataqsiz uint // size of the circular queue
4 buf unsafe.Pointer // points to an array of dataqsiz elements
5 elemsize uint16
6 closed uint32
7 elemtype *_type // element type
8 sendx uint // send index
9 recvx uint // receive index
10 recvq waitq // list of recv waiters
11 sendq waitq // list of send waiters
12
13 // lock protects all fields in hchan, as well as several
14 // fields in sudogs blocked on this channel.
15 //
16 // Do not change another G's status while holding this lock
17 // (in particular, do not ready a G), as this can deadlock
18 // with stack shrinking.
19 lock mutex
20}
后边就可以通过阅读源码和查看注释,搞清楚hchan的各个字段的具体含义了:
- qcount channel中当前的元素个数
- dataqsiz 循环队列(数组)的长度,channel使用循环队列实现其缓冲区
- buf 指向循环队列(数组)的指针
- sendx 向channel执行的发送操作到了的循环数组的索引位置
- recvx 从channel执行的接收操作到了的循环数组的索引位置
- recvq 如果缓冲区为空时,所有的接收操作将会被阻塞并放到这个等待队列中(具体是将接收操作所在的G的执行情况保存创建了一个sudog结构,并挂在recvq等待队列上)
- sendq 如果缓冲区已满时,所有的发送操作将会被阻塞并放到这个等待队列中(具体是将发送操作所在的G的执行情况保存创建了一个sudog结构,并挂在sendq这个等待队列上)
本文使用的是Go 1.17.2,Go的不同版本的代码位置和具体实现可能会不同
继续调试例子中这段简单Go源码,通过dlv的调试和disassemble,还可以找到:
- 往channel中写入数据
ch <- 1
对应的实现是runtime.chansend1
,看源码调用了runtime.chansend
- 从channel中读数据
<-ch
对应的实现是runtime.chanrecv1
,看源码调用了runtime.chanrecv
- 关闭channel
close(ch)
对应实现是runtime.closechan