Golang源码学习:使用gdb调试探究Golang函数调用栈结构
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Golang源码学习:使用gdb调试探究Golang函数调用栈结构,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含4285字,纯文字阅读大概需要7分钟。
内容图文
本文所使用的golang为1.14,gdb为8.1。
一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多。所以用gdb调试一个简单的例子,一探究竟。
函数调用栈的结构(以下简称栈)
栈包含以下作用:
- 存储函数返回地址。
- 保存调用者的rbp。
- 保存局部变量。
- 为被调用函数预留返回值内存空间。
- 向被调用函数传递参数。
每个函数在执行时都需要一段内存来保存上述的内容,这段内存被称为函数的“栈帧”
一般CPU中包含两个与栈相关的寄存器:
- rsp:始终指向整个函数调用栈的栈顶
- rbp:指向栈帧的开始位置
但存储函数返回地址的内存单元的地址并不在rbp~rsp之间。而是在0x8(%rbp)的位置
栈的工作原理
栈是一种后进先出(LIFO)的结构,在Linux AMD64环境中,golang栈由高地址向低地址生长。
当发生函数调用时,由于调用者未执行完成,栈帧还要继续使用,不可以被调用者覆盖,所以要在当前栈顶外继续为被调用者划分栈帧。这个操作叫做压栈(push),并向外移动rbp、rsp,栈空间随之增长。
与之对应的,当被调用者执行完成时,其栈帧就会被收回。这个操作叫出栈(pop),并向内移动rbp、rsp,栈空间随之缩小。调用者继续执行
栈空间的生长和收缩是由编译器生成的代码自动管理的的,与堆不同(手动或者gc)。
流程图
先给出流程图,好心里有个数:
代码及编译
指定 -gcflags="-N -l" 是为了关闭编译器优化。
go build -gcflags="-N -l" -o test test.go
为了方便查看内存内容,将变量都声明为了int64。
package main
func main() {
caller()
}
func caller() {
var a int64 = 1
var b int64 = 2
callee(a, b)
}
func callee(a, b int64) (int64, int64) {
c := a + 5
d := b * 4
return c, d
}
反汇编代码
反汇编的内容为:
- 指令地址
- 指令相对于当前函数起始位置以字节为单位的偏移
- 指令内容
gdb test
断点打在caller方法上,因为主要的研究对象是caller与callee。
(gdb) b main.caller
Breakpoint 1 at 0x458360: file /root/study/test.go, line 7.
输入run 运行程序。
caller函数反汇编,/s 表示将源代码与汇编代码一起显示,如不指定则只显示汇编代码。
可使用step(s)按源码级别调试,或者stepi(si)按汇编指令级别调试。
下面是caller、callee的反汇编代码和源码注释,还有与之相关的内存结构对照表。
(gdb) disassemble /s
Dump of assembler code for function main.caller:
7 func caller() {
=> 0x0000000000458360 <+0>: mov %fs:0xfffffffffffffff8,%rcx # 将当前g的指针存入rcx
0x0000000000458369 <+9>: cmp 0x10(%rcx),%rsp # 比较g.stackguard0和rsp
0x000000000045836d <+13>: jbe 0x4583b0 <main.caller+80> # 如果rsp较小,表示栈有溢出风险,调用runtime.morestack_noctxt
0x000000000045836f <+15>: sub $0x38,%rsp # 划分0x38字节的栈空间
0x0000000000458373 <+19>: mov %rbp,0x30(%rsp) # 保存调用者main的rbp
0x0000000000458378 <+24>: lea 0x30(%rsp),%rbp # 设置此函数栈的rbp
8 var a int64 = 1
0x000000000045837d <+29>: movq $0x1,0x28(%rsp) # 局部变量a入栈
9 var b int64 = 2
0x0000000000458386 <+38>: movq $0x2,0x20(%rsp) # 局部变量b入栈
10 callee(a, b)
0x000000000045838f <+47>: mov 0x28(%rsp),%rax # 读取第一个参数到rax
0x0000000000458394 <+52>: mov %rax,(%rsp) # callee第一个参数入栈
0x0000000000458398 <+56>: movq $0x2,0x8(%rsp) # callee第二个参数入栈
0x00000000004583a1 <+65>: callq 0x4583c0 <main.callee> # 调用callee
11 }
0x00000000004583a6 <+70>: mov 0x30(%rsp),%rbp # rbp还原为main的rbp
0x00000000004583ab <+75>: add $0x38,%rsp # rsp还原为main的rsp
0x00000000004583af <+79>: retq # 返回
<autogenerated>:
0x00000000004583b0 <+80>: callq 0x451b30 <runtime.morestack_noctxt>
0x00000000004583b5 <+85>: jmp 0x458360 <main.caller>
End of assembler dump.
callee函数反汇编
(gdb) s # 单步调试进入的callee函数
main.callee (a=1, b=2, ~r2=824634073176, ~r3=0) at /root/study/test.go:13
13 func callee(a, b int64) (int64, int64) {
(gdb) disassemble /s
Dump of assembler code for function main.callee:
13 func callee(a, b int64) (int64, int64) {
=> 0x00000000004583c0 <+0>: sub $0x18,%rsp # 划分0x18大小的栈
0x00000000004583c4 <+4>: mov %rbp,0x10(%rsp) # 保存调用者caller的rbp
0x00000000004583c9 <+9>: lea 0x10(%rsp),%rbp # 设置此函数栈的rbp
0x00000000004583ce <+14>: movq $0x0,0x30(%rsp) # 初始化第一个返回值为0
0x00000000004583d7 <+23>: movq $0x0,0x38(%rsp) # 初始化第二个返回值为0
14 c := a + 5
0x00000000004583e0 <+32>: mov 0x20(%rsp),%rax # 从内存中获取第一个参数值到rax
0x00000000004583e5 <+37>: add $0x5,%rax # rax+=5
0x00000000004583e9 <+41>: mov %rax,0x8(%rsp) # 局部变量c入栈
15 d := b * 4
0x00000000004583ee <+46>: mov 0x28(%rsp),%rax # 从内存中获取第二个参数值到rax
0x00000000004583f3 <+51>: shl $0x2,%rax # rax*=2
0x00000000004583f7 <+55>: mov %rax,(%rsp) # 局部变量d入栈
16 return c, d
0x00000000004583fb <+59>: mov 0x8(%rsp),%rax # 局部变量c的值存储到rax
0x0000000000458400 <+64>: mov %rax,0x30(%rsp) # 将c赋值给第一个返回值
0x0000000000458405 <+69>: mov (%rsp),%rax # 局部变量d的值存储到rax
0x0000000000458409 <+73>: mov %rax,0x38(%rsp) # 将d赋值给第二个返回值
17 }
0x000000000045840e <+78>: mov 0x10(%rsp),%rbp # rbp还原为caller的rbp
0x0000000000458413 <+83>: add $0x18,%rsp # rsp还原为caller的rsp
0x0000000000458417 <+87>: retq # 返回
End of assembler dump.
内存结构对照表
一些结论
- golang通过rsp加偏移量访问栈帧。
- 被调用者的入参是位于调用者的栈中。
- caller会为有返回值的callee,在栈中预留返回值内存空间。而callee在执行return时,会将返回值写入caller在栈中预留的空间。
- 意外收获是了解了多值返回的实现。
原文:https://www.cnblogs.com/flhs/p/12510178.html
内容总结
以上是互联网集市为您收集整理的Golang源码学习:使用gdb调试探究Golang函数调用栈结构全部内容,希望文章能够帮你解决Golang源码学习:使用gdb调试探究Golang函数调用栈结构所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。