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寄存器的插入必须满足两点要求:
- 函数的栈帧大于0;
- 满足条件
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函数调用