golang 栈结构

程序组成

程序由代码和数据组成,数据又有静态与动态之分;
动态数据:存放在堆区和栈区;
静态数据:静态只读数据可以放在代码区,也可以放在特定的只读数据区;
可读写的已初始化的静态数据放在数据区,可读写的未初始化的静态数据放在bss区;

寄存器

伪寄存器

  • FP(Frame pointer): 表示参数以及返回值的基地址; 通过 SYMBOL+/-ffset(FP)
  • PC(Program counter): 跳转寄存器,存储下一条指令地址;
  • SB(Static base pointer): 全局静态起始地址.
  • SP(Stack pointer): 表示本地变量的起始地址;
    使用方式 symbol + offset(SP), 例如第一个变量 local0 + (0)SP , local0 只是定义一个符号,类似于 local0 := xxxx

这个四个伪寄存器在golang 汇编中经常被用到,尤其是SB和FP;
SB 全局静态起始地址, foo(SB)表示foo在内存中的地址。这个语法有两个修饰符<> 和 +N,其中N是一个整数。 foo<>(SB)表示foo是一个私有元素只能在 当前文件中可见,就像是golang 首字母小写的变量或者函数。foo+8(SB)表示相对于foo 8字节的内存地址;注意 这里是相对符号的地址
FP 用来引用程序的参数,这些引用是由编译器维护,通过该寄存器的偏移量来引用参数。在64位的机器上,0(FP)表示第一个参数,8(FP)表示第二个参数等等。为了程序的清晰与可读性,编译器强制在引用参数时使用名称。

FP、 伪SP、 硬件SP之间的关系

SP分为伪SP和硬件寄存器SP,在栈桢为0的情况下 伪SP与硬件寄存器SP相等。可以使用有无symbol来区分是哪个寄存器: 有symbol 例如 foo-8(SP)表示伪寄存器,8(SP)表示硬件寄存器。

栈结构

无参数无本地变量

无参数无本地变量栈结果是如下所示

没有参数没有本地变量

通过如下函数来验证

 1#include "textflag.h" //
 2
 3TEXT ·SpFp(SB),NOSPLIT,$0-32
 4    LEAQ (SP), AX   // 将硬件SP地址存储到AX
 5    LEAQ a+0(SP), BX  // 将伪SP地址存储到BX
 6    LEAQ b+0(FP), CX  // 将FP地址存储到CX
 7    MOVQ AX, ret+0(FP) // 将AX地址存储到第一个返回值
 8    MOVQ BX, ret+8(FP) // 将BX地址存储到第二个返回值
 9    MOVQ CX, ret+16(FP) // 将CX地址存储到第三个返回值
10    MOVQ a+0(SP), AX  // 将SP 存储的值存储到AX, 也就是该函数的返回值
11    MOVQ AX, ret+24(FP)  //将AX 放到第四个返回值
12    RET
13    
1package main
2
3import "fmt"
4
5func SpFp() (int, int, int, int) // 汇编函数声明
6func main()  {
7	a,b,c, addr  := SpFp()
8	fmt.Printf("硬SP[%d] 伪SP[%d] FP[%d] addr[%d] SpFp[%d] \n", a, b ,c,addr,SpFp )
9}
1$ ./spfp
2硬SP[824634216112] 伪SP[824634216112] FP[824634216120] addr[17385428] SpFp[17385904]

由输出可以看出在没有参数没有本地变量情况下硬件SP与伪SP相等,FP = 伪SP+8

1$ dlv exec ./fpsp
2Type 'help' for list of commands.
3(dlv) b *17385428
4Breakpoint 1 set at 0x10947d4 for main.main() ./main.go:7
5(dlv)

由断点可以看出返回值就在main.go的第7行也就是 a,b,c, addr := SpFp()

有参数无本地变量

 1#include "textflag.h" //
 2
 3TEXT ·SpFpArgs(SB),NOSPLIT,$0-24
 4    LEAQ (SP), AX
 5    LEAQ a+0(SP), BX
 6    LEAQ b+0(FP), CX
 7    MOVQ AX, ret+24(FP)
 8    MOVQ BX, ret+32(FP)
 9    MOVQ CX, ret+40(FP)
10    RET
11    
1package main
2
3import "fmt"
4
5func SpFpArgs(int, int, int) (int, int, int) // 汇编函数声明
6func main()  {
7	e,f,g   := SpFpArgs(1, 2, 3)
8	fmt.Printf("硬SP[%d] 伪SP[%d] FP[%d]\n", e, f, g)
9}
1$ ./spfp
2硬SP[824634216048] 伪SP[824634216048] FP[824634216056]

由此可以看出这种情况硬件SP与伪SP相等,FP = 伪SP+8

有本地变量

在有本地变量情况下,在X86 和 ARM 中栈结构是不同的,如下所示:

 1
 2
 3// Stack frame layout
 4//
 5// (x86)
 6// +------------------+
 7// | args from caller |
 8// +------------------+ <- frame->argp
 9// |  return address  |
10// +------------------+
11// |  caller's BP (*) | (*) if framepointer_enabled && varp < sp
12// +------------------+ <- frame->varp
13// |     locals       |
14// +------------------+
15// |  args to callee  |
16// +------------------+ <- frame->sp
17//
18// (arm)
19// +------------------+
20// | args from caller |
21// +------------------+ <- frame->argp
22// | caller's retaddr |
23// +------------------+ <- frame->varp
24// |     locals       |
25// +------------------+
26// |  args to callee  |
27// +------------------+
28// |  return address  |
29// +------------------+ <- frame->sp

我们在这里特别关注X86,会使用到BP寄存器,这个寄存器主要用来指示栈的起始位置,现在很多编译器并不需要这个,因为可以通过SP加Offset来寻找栈起始位置。在amd64平台上,会在函数返回值之后插入8byte来放置Caller BP。 在有本地变量的情况,在本地变量和参数之间会插入函数返回值和 BP 寄存器,但是BP寄存器的插入必须满足两点要求:

  1. 函数的栈帧大于0;
  2. 满足条件
1func Framepointer_enabled(goos, goarch string) bool {
2  return framepointer_enabled != 0 && goarch == "amd64" && goos != "nacl"
3}

另外此时 硬件SP与伪SP是不相同的。
硬件SP + locals = 伪SP

参考

https://9p.io/plan9/
指令查询
命令查询
Go的标准IDE:Acme文本编辑器
A Quick Guide to Go's Assembler
golang 汇编
汇编语言入门教程
[译]go 和 plan9 汇编
split stacks
How Stacks are Handled in Go
go函数调用

Posts in this Series