1
- # 3.9 Delve调试器
1
+ # 3.9 Delve 调试器
2
2
3
- 目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具,LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持,而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序 。
3
+ 目前 Go 语言支持 GDB、LLDB 和 Delve 几种调试器。其中 GDB 是最早支持的调试工具,LLDB 是 macOS 系统推荐的标准调试工具。但是 GDB 和 LLDB 对 Go 语言的专有特性都缺乏很大支持,而只有 Delve 是专门为 Go 语言设计开发的调试工具。而且 Delve 本身也是采用 Go 语言开发,对 Windows 平台也提供了一样的支持。本节我们基于 Delve 简单解释如何调试 Go 汇编程序 。
4
4
5
- ## 3.9.1 Delve入门
5
+ ## 3.9.1 Delve 入门
6
6
7
- 首先根据官方的文档正确安装Delve调试器。我们会先构造一个简单的Go语言代码,用于熟悉下Delve的简单用法 。
7
+ 首先根据官方的文档正确安装 Delve 调试器。我们会先构造一个简单的 Go 语言代码,用于熟悉下 Delve 的简单用法 。
8
8
9
- 创建main.go文件,main函数先通过循初始化一个切片 ,然后输出切片的内容:
9
+ 创建 main.go 文件,main 函数先通过循初始化一个切片 ,然后输出切片的内容:
10
10
11
11
``` go
12
12
package main
@@ -17,22 +17,22 @@ import (
17
17
18
18
func main () {
19
19
nums := make ([]int , 5 )
20
- for i := 0 ; i < len (nums); i++ {
20
+ for i := 0 ; i <len (nums); i++ {
21
21
nums[i] = i * i
22
22
}
23
23
fmt.Println (nums)
24
24
}
25
25
```
26
26
27
- 命令行进入包所在目录,然后输入` dlv debug ` 命令进入调试:
27
+ 命令行进入包所在目录,然后输入 ` dlv debug ` 命令进入调试:
28
28
29
29
```
30
30
$ dlv debug
31
31
Type 'help' for list of commands.
32
32
(dlv)
33
33
```
34
34
35
- 输入help命令可以查看到Delve提供的调试命令列表 :
35
+ 输入 help 命令可以查看到 Delve 提供的调试命令列表 :
36
36
37
37
```
38
38
(dlv) help
@@ -78,14 +78,14 @@ Type help followed by a command for full documentation.
78
78
(dlv)
79
79
```
80
80
81
- 每个Go程序的入口是main.main函数,我们可以用break在此设置一个断点 :
81
+ 每个 Go 程序的入口是 main.main 函数,我们可以用 break 在此设置一个断点 :
82
82
83
83
```
84
84
(dlv) break main.main
85
85
Breakpoint 1 set at 0x10ae9b8 for main.main() ./main.go:7
86
86
```
87
87
88
- 然后通过breakpoints查看已经设置的所有断点 :
88
+ 然后通过 breakpoints 查看已经设置的所有断点 :
89
89
90
90
```
91
91
(dlv) breakpoints
@@ -95,9 +95,9 @@ Breakpoint unrecovered-panic at 0x102a380 for runtime.startpanic()
95
95
Breakpoint 1 at 0x10ae9b8 for main.main() ./main.go:7 (0)
96
96
```
97
97
98
- 我们发现除了我们自己设置的main.main函数断点外,Delve内部已经为panic异常函数设置了一个断点 。
98
+ 我们发现除了我们自己设置的 main.main 函数断点外,Delve 内部已经为 panic 异常函数设置了一个断点 。
99
99
100
- 通过vars命令可以查看全部包级的变量 。因为最终的目标程序可能含有大量的全局变量,我们可以通过一个正则参数选择想查看的全局变量:
100
+ 通过 vars 命令可以查看全部包级的变量 。因为最终的目标程序可能含有大量的全局变量,我们可以通过一个正则参数选择想查看的全局变量:
101
101
102
102
```
103
103
(dlv) vars main
@@ -107,7 +107,7 @@ runtime.mainStarted = true
107
107
(dlv)
108
108
```
109
109
110
- 然后就可以通过continue命令让程序运行到下一个断点处 :
110
+ 然后就可以通过 continue 命令让程序运行到下一个断点处 :
111
111
112
112
```
113
113
(dlv) continue
@@ -119,14 +119,14 @@ runtime.mainStarted = true
119
119
6:
120
120
=> 7: func main() {
121
121
8: nums := make([]int, 5)
122
- 9: for i := 0; i < len(nums); i++ {
122
+ 9: for i := 0; i <len(nums); i++ {
123
123
10: nums[i] = i * i
124
124
11: }
125
125
12: fmt.Println(nums)
126
126
(dlv)
127
127
```
128
128
129
- 输入next命令单步执行进入main函数内部 :
129
+ 输入 next 命令单步执行进入 main 函数内部 :
130
130
131
131
```
132
132
(dlv) next
@@ -137,15 +137,15 @@ runtime.mainStarted = true
137
137
6:
138
138
7: func main() {
139
139
=> 8: nums := make([]int, 5)
140
- 9: for i := 0; i < len(nums); i++ {
140
+ 9: for i := 0; i <len(nums); i++ {
141
141
10: nums[i] = i * i
142
142
11: }
143
143
12: fmt.Println(nums)
144
144
13: }
145
145
(dlv)
146
146
```
147
147
148
- 进入函数之后可以通过args和locals命令查看函数的参数和局部变量 :
148
+ 进入函数之后可以通过 args 和 locals 命令查看函数的参数和局部变量 :
149
149
150
150
```
151
151
(dlv) args
@@ -154,9 +154,9 @@ runtime.mainStarted = true
154
154
nums = []int len: 842350763880, cap: 17491881, nil
155
155
```
156
156
157
- 因为main函数没有参数,因此args命令没有任何输出。而locals命令则输出了局部变量nums切片的值 :此时切片还未完成初始化,切片的底层指针为nil ,长度和容量都是一个随机数值。
157
+ 因为 main 函数没有参数,因此 args 命令没有任何输出。而 locals 命令则输出了局部变量 nums 切片的值 :此时切片还未完成初始化,切片的底层指针为 nil ,长度和容量都是一个随机数值。
158
158
159
- 再次输入next命令单步执行后就可以查看到nums切片初始化之后的结果了 :
159
+ 再次输入 next 命令单步执行后就可以查看到 nums 切片初始化之后的结果了 :
160
160
161
161
```
162
162
(dlv) next
@@ -166,7 +166,7 @@ nums = []int len: 842350763880, cap: 17491881, nil
166
166
6:
167
167
7: func main() {
168
168
8: nums := make([]int, 5)
169
- => 9: for i := 0; i < len(nums); i++ {
169
+ => 9: for i := 0; i <len(nums); i++ {
170
170
10: nums[i] = i * i
171
171
11: }
172
172
12: fmt.Println(nums)
@@ -177,9 +177,9 @@ i = 17601536
177
177
(dlv)
178
178
```
179
179
180
- 此时因为调试器已经到了for语句行,因此局部变量出现了还未初始化的循环迭代变量i 。
180
+ 此时因为调试器已经到了 for 语句行,因此局部变量出现了还未初始化的循环迭代变量 i 。
181
181
182
- 下面我们通过组合使用break和condition命令 ,在循环内部设置一个条件断点,当循环变量i等于3时断点生效 :
182
+ 下面我们通过组合使用 break 和 condition 命令 ,在循环内部设置一个条件断点,当循环变量 i 等于 3 时断点生效 :
183
183
184
184
```
185
185
(dlv) break main.go:10
@@ -188,7 +188,7 @@ Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
188
188
(dlv)
189
189
```
190
190
191
- 然后通过continue执行到刚设置的条件断点 ,并且输出局部变量:
191
+ 然后通过 continue 执行到刚设置的条件断点 ,并且输出局部变量:
192
192
193
193
```
194
194
(dlv) continue
@@ -197,7 +197,7 @@ Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
197
197
6:
198
198
7: func main() {
199
199
8: nums := make([]int, 5)
200
- 9: for i := 0; i < len(nums); i++ {
200
+ 9: for i := 0; i <len(nums); i++ {
201
201
=> 10: nums[i] = i * i
202
202
11: }
203
203
12: fmt.Println(nums)
@@ -210,9 +210,9 @@ i = 3
210
210
(dlv)
211
211
```
212
212
213
- 我们发现当循环变量i等于3时,nums切片的前3个元素已经正确初始化 。
213
+ 我们发现当循环变量 i 等于 3 时,nums 切片的前 3 个元素已经正确初始化 。
214
214
215
- 我们还可以通过stack查看当前执行函数的栈帧信息 :
215
+ 我们还可以通过 stack 查看当前执行函数的栈帧信息 :
216
216
217
217
```
218
218
(dlv) stack
@@ -225,7 +225,7 @@ i = 3
225
225
(dlv)
226
226
```
227
227
228
- 或者通过goroutine和goroutines命令查看当前Goroutine相关的信息 :
228
+ 或者通过 goroutine 和 goroutines 命令查看当前 Goroutine 相关的信息 :
229
229
230
230
```
231
231
(dlv) goroutine
@@ -247,13 +247,13 @@ Goroutine 1:
247
247
(dlv)
248
248
```
249
249
250
- 最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法 。
250
+ 最后完成调试工作后输入 quit 命令退出调试器。至此我们已经掌握了 Delve 调试器器的简单用法 。
251
251
252
252
## 3.9.2 调试汇编程序
253
253
254
- 用Delve调试Go汇编程序的过程比调试Go语言程序更加简单 。调试汇编程序时,我们需要时刻关注寄存器的状态,如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态 。
254
+ 用 Delve 调试 Go 汇编程序的过程比调试 Go 语言程序更加简单 。调试汇编程序时,我们需要时刻关注寄存器的状态,如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器 SP 的状态 。
255
255
256
- 为了编译演示,我们重新实现一个更简单的main函数 :
256
+ 为了编译演示,我们重新实现一个更简单的 main 函数 :
257
257
258
258
``` go
259
259
package main
@@ -263,9 +263,9 @@ func main() { asmSayHello() }
263
263
func asmSayHello ()
264
264
```
265
265
266
- 在main函数中调用汇编语言实现的asmSayHello函数输出一个字符串 。
266
+ 在 main 函数中调用汇编语言实现的 asmSayHello 函数输出一个字符串 。
267
267
268
- asmSayHello函数在main_amd64.s文件中实现 :
268
+ asmSayHello 函数在 main_amd64.s 文件中实现 :
269
269
270
270
```
271
271
#include "textflag.h"
@@ -286,7 +286,7 @@ TEXT ·asmSayHello(SB), $16-0
286
286
RET
287
287
```
288
288
289
- 参考前面的调试流程,在执行到main函数断点时,可以disassemble反汇编命令查看main函数对应的汇编代码 :
289
+ 参考前面的调试流程,在执行到 main 函数断点时,可以 disassemble 反汇编命令查看 main 函数对应的汇编代码 :
290
290
291
291
```
292
292
(dlv) break main.main
@@ -315,11 +315,11 @@ TEXT main.main(SB) /path/to/pkg/main.go
315
315
(dlv)
316
316
```
317
317
318
- 虽然main函数内部只有一行函数调用语句 ,但是却生成了很多汇编指令。在函数的开头通过比较rsp寄存器判断栈空间是否不足,如果不足则跳转到0x1050139地址调用runtime.morestack函数进行栈扩容,然后跳回到main函数开始位置重新进行栈空间测试。而在asmSayHello函数调用之前,先扩展rsp空间用于临时存储rbp寄存器的状态,在函数返回后通过栈恢复rbp的值并回收临时栈空间。通过对比Go语言代码和对应的汇编代码,我们可以加深对Go汇编语言的理解 。
318
+ 虽然 main 函数内部只有一行函数调用语句 ,但是却生成了很多汇编指令。在函数的开头通过比较 rsp 寄存器判断栈空间是否不足,如果不足则跳转到 0x1050139 地址调用 runtime.morestack 函数进行栈扩容,然后跳回到 main 函数开始位置重新进行栈空间测试。而在 asmSayHello 函数调用之前,先扩展 rsp 空间用于临时存储 rbp 寄存器的状态,在函数返回后通过栈恢复 rbp 的值并回收临时栈空间。通过对比 Go 语言代码和对应的汇编代码,我们可以加深对 Go 汇编语言的理解 。
319
319
320
- 从汇编语言角度深刻Go语言各种特性的工作机制对调试工作也是一个很大的帮助。如果希望在汇编指令层面调试Go代码,Delve还提供了一个step-instruction单步执行汇编指令的命令 。
320
+ 从汇编语言角度深刻 Go 语言各种特性的工作机制对调试工作也是一个很大的帮助。如果希望在汇编指令层面调试 Go 代码,Delve 还提供了一个 step-instruction 单步执行汇编指令的命令 。
321
321
322
- 现在我们依然用break命令在asmSayHello函数设置断点,并且输入continue命令让调试器执行到断点位置停下 :
322
+ 现在我们依然用 break 命令在 asmSayHello 函数设置断点,并且输入 continue 命令让调试器执行到断点位置停下 :
323
323
324
324
```
325
325
(dlv) break main.asmSayHello
@@ -340,7 +340,7 @@ Breakpoint 2 set at 0x10501bf for main.asmSayHello() ./main_amd64.s:10
340
340
(dlv)
341
341
```
342
342
343
- 此时我们可以通过regs查看全部的寄存器状态 :
343
+ 此时我们可以通过 regs 查看全部的寄存器状态 :
344
344
345
345
```
346
346
(dlv) regs
@@ -366,7 +366,7 @@ Breakpoint 2 set at 0x10501bf for main.asmSayHello() ./main_amd64.s:10
366
366
(dlv)
367
367
```
368
368
369
- 因为AMD64的各种寄存器非常多 ,项目的信息中刻意省略了非通用的寄存器。如果再单步执行到13行时,可以发现AX寄存器值的变化 。
369
+ 因为 AMD64 的各种寄存器非常多 ,项目的信息中刻意省略了非通用的寄存器。如果再单步执行到 13 行时,可以发现 AX 寄存器值的变化 。
370
370
371
371
```
372
372
(dlv) regs
@@ -377,14 +377,14 @@ Breakpoint 2 set at 0x10501bf for main.asmSayHello() ./main_amd64.s:10
377
377
(dlv)
378
378
```
379
379
380
- 因此我们可以推断汇编程序内部定义的` text<> ` 数据的地址为0x00000000010a4060。我们可以用过print命令来查看该内存内的数据 :
380
+ 因此我们可以推断汇编程序内部定义的 ` text<> ` 数据的地址为 0x00000000010a4060。我们可以用过 print 命令来查看该内存内的数据 :
381
381
382
382
```
383
383
(dlv) print *(*[5]byte)(uintptr(0x00000000010a4060))
384
384
[5]uint8 [72,101,108,108,111]
385
385
(dlv)
386
386
```
387
387
388
- 我们可以发现输出的` [5]uint8 [72,101,108,108,111] ` 刚好是对应“Hello”字符串。通过类似的方法,我们可以通过查看SP对应的栈指针位置 ,然后查看栈中局部变量的值。
388
+ 我们可以发现输出的 ` [5]uint8 [72,101,108,108,111] ` 刚好是对应 “Hello” 字符串。通过类似的方法,我们可以通过查看 SP 对应的栈指针位置 ,然后查看栈中局部变量的值。
389
389
390
- 至此我们就掌握了Go汇编程序的简单调试技术 。
390
+ 至此我们就掌握了 Go 汇编程序的简单调试技术 。
0 commit comments