13

Golang源码学习:使用gdb调试探究Golang函数调用栈结构

 4 years ago
source link: http://www.cnblogs.com/flhs/p/12510178.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文所使用的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)。

流程图

先给出流程图,好心里有个数:

U32ieyi.jpg!web

代码及编译

指定 -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.

内存结构对照表

rmQrYnb.jpg!web

一些结论

  • golang通过rsp加偏移量访问栈帧。
  • 被调用者的入参是位于调用者的栈中。
  • caller会为有返回值的callee,在栈中预留返回值内存空间。而callee在执行return时,会将返回值写入caller在栈中预留的空间。
  • 意外收获是了解了多值返回的实现。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK