本文所使用的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在栈中预留的空间。
  • 意外收获是了解了多值返回的实现。

Golang源码学习:使用gdb调试探究Golang函数调用栈结构的更多相关文章

  1. Golang源码学习:调度逻辑(二)main goroutine的创建

    接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...

  2. nginx源码分析--使用GDB调试(strace、 pstack )

    nginx源码分析--使用GDB调试(strace.  pstack ) http://blog.csdn.net/scdxmoe/article/details/49070577

  3. BIND9源码学习笔记1---gdb调试篇

    学习bind9源码之前,首先要知道如何用gdb来调试bind.BIND9的源码我是先看代码弄懂它的架构,像什么event-drive,epoll等, 再去看它的业务流程.看业务流程的时候要追踪它的数据 ...

  4. Golang源码学习:调度逻辑(一)初始化

    本文所使用的Golang为1.14,dlv为1.4.0. 源代码 package main import "fmt" func main() { fmt.Println(" ...

  5. Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

    本文内容主要分为三部分: main goroutine 的调度运行 非 main goroutine 的退出流程 工作线程的执行流程与调度循环. main goroutine 的调度运行 runtim ...

  6. Golang源码学习:调度逻辑(四)系统调用

    Linux系统调用 概念:系统调用为用户态进程提供了硬件的抽象接口.并且是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核唯一的合法入口.保证系统的安全和稳定. 调用号:在Linux中,每个系统 ...

  7. Golang源码学习:监控线程

    监控线程是在runtime.main执行的时候在系统栈中创建的,监控线程与普通的工作线程区别在于,监控线程不需要绑定p来运行. 监控线程的创建与启动 简单的调用图 先给出个简单的调用图,好心里有数,逐 ...

  8. Golang源码探索(一) 编译和调试源码(转)

    GO可以说是近几年最热门的新兴语言之一了, 一般人看到分布式和大数据就会想到GO,这个系列的文章会通过研究golang的源代码来分析内部的实现原理,和CoreCLR不同的是, golang的源代码已经 ...

  9. Vue源码学习(一):调试环境搭建

    最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...

随机推荐

  1. 日志分析工具ELK(三)

    目前官网更新特别快,不到半年时间就更新了好几个版本,目前最新的是5.1 以下安装配置使用4.5版本的 https://www.elastic.co/guide/en/kibana/4.5/index. ...

  2. Scala教程之:深入理解协变和逆变

    文章目录 函数的参数和返回值 可变类型的变异 在之前的文章中我们简单的介绍过scala中的协变和逆变,我们使用+ 来表示协变类型:使用-表示逆变类型:非转化类型不需要添加标记. 假如我们定义一个cla ...

  3. ip-端口-协议等基本概念

    互联网上的计算机,都会有一个唯一的32位的地址——ip地址.我们访问服务器,就必须通过这个ip地址.   局域网里也有预留的ip地址:192/10/172开头.局域网里的ip地址也是唯一的.   NA ...

  4. 手把手教你用Rancher创建产品质量数据库设置

    目标:在本文中,我们将介绍如何运行一个分布式产品质量数据库设置,它由Rancher进行管理,并且保证持久性.为了部署有状态的分布式Cassandra数据库,我们将使用Stateful Sets (有状 ...

  5. 疯子的算法总结(九) 图论中的矩阵应用 Part 1+POJ3613 Cow Relays

    图的存储有邻接矩阵,那么他就具备一些矩阵的性质,设有一个图的demo[100][100];那么demo[M][N]就是M—>N的距离,若经过一次松弛操作demo[M][N]=demo[M][K] ...

  6. matlab-均值滤波

    均值滤波 主要思想为邻域平均法,即用几个像素灰度的平均值来代替每个像素的灰度.有效抑制加性噪声.缺点:容易引起图像模糊,可以对其进行改进,主要避开对景物边缘的平滑处理. 均值滤波器的缺点是存在着边缘模 ...

  7. GIL-Guilds(黑白灰染色)

    传送门门门门门咩咩咩咩咩咩咩咩咩咩咩咩 \(这题真是扯谈!!!\) \(灰色很高级是吧,但是题目没要你把颜色全部用上去啊!!!\) \(黑色或者白色只有一个条件,但灰色需要和所有三种颜色都相邻.这么难 ...

  8. dbcp数据源连接池

    一.数据源连接池 我们之前利用jdbc连接数据库,每次都要创建连接对象,销毁连接对象,如果并发访问量比较大,这样肯定比较辣 浪费数据库的效率,我们可以像之前mybatis中缓存查询到的数据一样,可以把 ...

  9. 你知道Spring是怎么解析配置类的吗?

    彻底读懂Spring(二)你知道Spring是怎么解析配置类的吗? 推荐阅读: Spring官网阅读系列 彻底读懂Spring(一)读源码,我们可以从第一行读起 Spring执行流程图如下: 如果图片 ...

  10. 动态代理学习(一)自己动手模拟JDK动态代理

    最近一直在学习Spring的源码,Spring底层大量使用了动态代理.所以花一些时间对动态代理的知识做一下总结. 我们自己动手模拟一个动态代理 对JDK动态代理的源码进行分析 文章目录 场景: 思路: ...