一文教你搞懂 Go 中栈操作 (5)

这里的伪代码会相对复杂一些,由于 G 里面的 stackguard0 在抢占的时候可能会赋值成 StackPreempt,所以明确有没有被抢占,那么需要将 stackguard0 和 StackPreempt进行比较。然后将执行比较: SP-stackguard+StackGuard <= framesize + (StackGuard-StackSmall),两边都加上 StackGuard 是为了保证左边的值是正数。

希望在理解完上面的代码之前不要继续往下看。

主要注意的是,在一些函数的执行代码中,编译器很智能的加上了NOSPLIT标记,打了这个标记之后就会禁用栈溢出检测,可以在如下代码中发现这个标记的踪影:

代码位置:cmd/internal/obj/x86/obj6.go

... if ctxt.Arch.Family == sys.AMD64 && autoffset < objabi.StackSmall && !p.From.Sym.NoSplit() { leaf := true LeafSearch: for q := p; q != nil; q = q.Link { ... } if leaf { p.From.Sym.Set(obj.AttrNoSplit, true) } } ...

大致代码逻辑应该是:当函数处于调用链的叶子节点,且栈帧小于StackSmall字节时,则自动标记为NOSPLIT。同样的,我们在写代码的时候也可以自己在函数上面加上//go:nosplit强制指定NOSPLIT属性。

栈溢出实例

下面我们写一个简单的例子:

func main() { a, b := 1, 2 _ = add1(a, b) _ = add2(a, b) _ = add3(a, b) } func add1(x, y int) int { _ = make([]byte, 20) return x + y } func add2(x, y int) int { _ = make([]byte, 200) return x + y } func add3(x, y int) int { _ = make([]byte, 5000) return x + y }

然后打印出它的汇编:

$ GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go

上面这个例子用三个方法调用解释了上面所说的三种情况:

main 函数

0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $48-0 0x0000 00000 (main.go:3) MOVQ (TLS), CX 0x0009 00009 (main.go:3) CMPQ SP, 16(CX) // SP < stackguard 则跳到 129执行 0x0009 00009 (main.go:3) CMPQ SP, 16(CX) 0x000d 00013 (main.go:3) PCDATA $0, $-2 0x000d 00013 (main.go:3) JLS 129 ... 0x0081 00129 (main.go:3) CALL runtime.morestack_noctxt(SB)

首先,我们从 TLS ( thread local storage) 变量中加载一个值至 CX 寄存器,然后将 SP 和 16(CX) 进行比较,那什么是 TLS?16(CX) 又代表什么?

其实TLS是一个伪寄存器,表示的是thread-local storage,它存放了 G 结构体。我们看一看 runtime 源代码中对于 G 的定义:

type g struct { stack stack // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink ... } type stack struct { lo uintptr hi uintptr }

可以看到 stack 占了 16bytes,所以 16(CX) 对应的是 g.stackguard0。所以 CMPQ SP, 16(CX)这一行代码实际上是比较 SP 和 stackguard 大小。如果 SP 小于 stackguard ,那么说明到了增长的阈值,会执行 JLS 跳到 129 行,调用 runtime.morestack_noctxt 执行下一步栈扩容操作。

add1

0x0000 00000 (main.go:10) TEXT "".add1(SB), NOSPLIT|ABIInternal, $32-24

我们看到 add1 的汇编函数,可以看到它的栈大小只有 32 ,没有到达 StackSmall 128 bytes 的大小,并且它又是一个 callee 被调用者,所以可以发它加上了NOSPLIT标记,也就印证了我上面结论。

add2

"".add2 STEXT size=148 args=0x18 locals=0xd0 0x0000 00000 (main.go:15) TEXT "".add2(SB), ABIInternal, $208-24 0x0000 00000 (main.go:15) MOVQ (TLS), CX // AX = SP - 208 + 128 = SP -80 0x0009 00009 (main.go:15) LEAQ -80(SP), AX // 栈大小大于StackSmall =128, 计算 SP - FramSzie + StackSmall 并放入AX寄存器 0x000e 00014 (main.go:15) CMPQ AX, 16(CX) // AX < stackguard 则跳到 138 执行 0x0012 00018 (main.go:15) PCDATA $0, $-2 0x0012 00018 (main.go:15) JLS 138 ... 0x008a 00138 (main.go:15) CALL runtime.morestack_noctxt(SB)

add2 函数的栈帧大小是 208,大于 StackSmall 128 bytes ,所以可以看到首先从 TLS 变量中加载一个值至 CX 寄存器。

然后执行指令 LEAQ -80(SP), AX,但是这里为什么是 -80 其实当时让我蛮疑惑的,但是需要注意的是这里的计算公式是: SP - FramSzie + StackSmall,直接代入之后会发现它就是 -80,然后将这个数值加载到 AX 寄存器中。

最后调用 CMPQ AX, 16(CX),16(CX) 我们在上面已经讲过了是等于 stackguard0 ,所以这里是比较 AX 与 stackguard0 的小大,如果小于则直接跳转到 138 行执行 runtime.morestack_noctxt。

add3

"".add3 STEXT size=157 args=0x18 locals=0x1390 0x0000 00000 (main.go:20) TEXT "".add3(SB), ABIInternal, $5008-24 0x0000 00000 (main.go:20) MOVQ (TLS), CX 0x0009 00009 (main.go:20) MOVQ 16(CX), SI // 将 stackguard 赋值给 SI 0x000d 00013 (main.go:20) PCDATA $0, $-2 0x000d 00013 (main.go:20) CMPQ SI, $-1314 // 将 stackguard < stackPreempt 则跳转到 147 执行 0x0014 00020 (main.go:20) JEQ 147 0x0016 00022 (main.go:20) LEAQ 928(SP), AX // AX = SP +928 0x001e 00030 (main.go:20) SUBQ SI, AX // AX -= stackguard 0x0021 00033 (main.go:20) CMPQ AX, $5808 // framesize + 928 -128 = 5808,比较 AX < 5808,则执行147 0x0027 00039 (main.go:20) JLS 147 ... 0x0093 00147 (main.go:20) CALL runtime.morestack_noctxt(SB)

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

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