一文教你搞懂 Go 中栈操作

Linux_stack

多任务操作系统中的每个进程都在自己的内存沙盒中运行。在32位模式下,它总是4GB内存地址空间,内存分配是分配虚拟内存给进程,当进程真正访问某一虚拟内存地址时,操作系统通过触发缺页中断,在物理内存上分配一段相应的空间再与之建立映射关系,这样进程访问的虚拟内存地址,会被自动转换变成有效物理内存地址,便可以进行数据的存储与访问了。

Kernel space:操作系统内核地址空间;

Stack:栈空间,是用户存放程序临时创建的局部变量,栈的增长方向是从高位地址到地位地址向下进行增长。在现代主流机器架构上(例如x86)中,栈都是向下生长的。然而,也有一些处理器(例如B5000)栈是向上生长的,还有一些架构(例如System Z)允许自定义栈的生长方向,甚至还有一些处理器(例如SPARC)是循环栈的处理方式;

Heap:堆空间,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减;

BBS segment:BSS段,存放的是全局或者静态数据,但是存放的是全局/静态未初始化数据;

Data segment:数据段,通常是指用来存放程序中已初始化的全局变量的一块内存区域;

Text segment:代码段,指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。

栈的相关概念

In computer science, a call stack is a stack data structure that stores information about the active subroutines of a computer program.

In computer programming, a subroutine is a sequence of program instructions that performs a specific task, packaged as a unit.

调用栈call stack,简称栈,是一种栈数据结构,用于存储有关计算机程序的活动 subroutines 信息。在计算机编程中,subroutines 是执行特定任务的一系列程序指令,打包为一个单元。

A stack frame is a frame of data that gets pushed onto the stack. In the case of a call stack, a stack frame would represent a function call and its argument data.

栈帧stack frame又常被称为帧frame是在调用栈中储存的函数之间的调用关系,每一帧对应了函数调用以及它的参数数据。

有了函数调用自然就要有调用者 caller 和被调用者 callee ,如在 函数 A 里 调用 函数 B,A 是 caller,B 是 callee。

调用者与被调用者的栈帧结构如下图所示:

Stack layout

Go 语言的汇编代码中栈寄存器解释的非常模糊,我们大概只要知道两个寄存器 BP 和 SP 的作用就可以了:

BP:基准指针寄存器,维护当前栈帧的基准地址,以便用来索引变量和参数,就像一个锚点一样,在其它架构中它等价于帧指针FP,只是在x86架构下,变量和参数都可以通过SP来索引;

SP:栈指针寄存器,总是指向栈顶;

Goroutine 栈操作

G Stack

在 Goroutine 中有一个 stack 数据结构,里面有两个属性 lo 与 hi,描述了实际的栈内存地址:

stack.lo:栈空间的低地址;

stack.hi:栈空间的高地址;

在 Goroutine 中会通过 stackguard0 来判断是否要进行栈增长:

stackguard0:stack.lo + StackGuard, 用于stack overlow的检测;

StackGuard:保护区大小,常量Linux上为 928 字节;

StackSmall:常量大小为 128 字节,用于小函数调用的优化;

StackBig:常量大小为 4096 字节;

根据被调用函数栈帧的大小来判断是否需要扩容:

当栈帧大小(FramSzie)小于等于 StackSmall(128)时,如果 SP 小于 stackguard0 那么就执行栈扩容;

当栈帧大小(FramSzie)大于 StackSmall(128)时,就会根据公式 SP - FramSzie + StackSmall 和 stackguard0 比较,如果小于 stackguard0 则执行扩容;

当栈帧大小(FramSzie)大于StackBig(4096)时,首先会检查 stackguard0 是否已转变成 StackPreempt 状态了;然后根据公式 SP-stackguard0+StackGuard <= framesize + (StackGuard-StackSmall)判断,如果是 true 则执行扩容;

需要注意的是,由于栈是由高地址向低地址增长的,所以对比的时候,都是小于才执行扩容,这里需要大家品品。

当执行栈扩容时,会在内存空间中分配更大的栈内存空间,然后将旧栈中的所有内容复制到新栈中,并修改指向旧栈对应变量的指针重新指向新栈,最后销毁并回收旧栈的内存空间,从而实现栈的动态扩容。

汇编

这里简单讲解一下后面分析中会用到的一些 Go 语言使用的 Plan 9 汇编,以免看不太明白。

汇编函数

我们先来看看 plan9 的汇编函数的定义:

function

stack frame size:包含局部变量以及额外调用函数的参数空间;

arguments size:包含参数以及返回值大小,例如入参是 3 个 int64 类型,返回值是 1 个 int64 类型,那么返回值就是 sizeof(int64) * 4;

栈调整

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsjdd.html