3 ms·
Let's take a look at Linux. Other systems are similar. ; go tool objdump -s main.main a TEXT main.main(SB) /private/tmp/a/a.go a.go:9 0x400c10 644
by 4ad 12y ago
Let's take a look at Linux. Other systems are similar.
; go tool objdump -s main.main a
TEXT main.main(SB) /private/tmp/a/a.go
a.go:9 0x400c10 64488b0c25f0ffffff FS MOVQ FS:0xfffffff0, CX
a.go:9 0x400c19 483b21 CMPQ 0(CX), SP
a.go:9 0x400c1c 7707 JA 0x400c25
a.go:9 0x400c1e e8ddf90100 CALL runtime.morestack00_noctxt(SB)
a.go:9 0x400c23 ebeb JMP main.main(SB)
a.go:10 0x400c25 e8d6ffffff CALL main.foo(SB)
a.go:11 0x400c2a c3 RET
a.go:11 0x400c2b 0000 ADDL AL, 0(AX)
a.go:11 0x400c2d 0000 ADDL AL, 0(AX)
a.go:11 0x400c2f 00 ?
On linux/amd64 we can use the Local Executables TLS access procedure. In particular, we use a negative offset from the FS segment register to get a TLS slot (our job is simpler because we are always the main executable).
MOVQ FS:0xfffffff0, CX
We make use of two TLS variables, g and m (soon we will only use one), a pointer to g is at -16(FS). We access it in this first instruction.
g is an instance of struct G, see go/src/pkg/runtime/runtime.h:/struct.G. It contains many things, but it starts like this:
struct G
{
uintptr stackguard0;
uintptr stackbase;
...
In particular the first word (at offset zero) is the stackguard, which indicates the stack limit (it is also used for voluntary preemption, but that doesn't matter here).
This instruction in the stack check preamble:
CMPQ 0(CX), SP
Compares the current stack pointer with the stackguard. In most cases we have enough stack, so the next instruction just skips past the preamble to the real function code.
JA 0x400c25
When we don't have enough stack, we call a function in the runtime (one of the runtime.morestack functions). This function allocates a new stack segment (from the heap). Currently we use contiguous stacks, so if we have complete type information in the current stack we can just copy the old stack to the new stack segment fixing any pointers as dictated by the type information, and then we switch the stack pointer.
If we don't have enough type information (or in previous Go versions), we use segmented stacks. We allocate a new stack segment, but we don't copy the stack; we just switch the stack pointer and we take care to be able to do the reverse operation when we return from the function.
Take a look at the next instruction after the call to runtime.morestack.
JMP main.main(SB)
We just jump to the beggining of the function like nothing has happened. Then the algorithm repeats, but we won't fails the stack limit check again, so it will skip it. Why it jumps to the begining of the function instead of just continuing in the body of the function is left as an exercise to the reader.
We used the Local Executables TLS access model here, sometimes we have to use the Initial Executable model. If we ever allow Go programs to be loaded by C programs as dynamic objects, we would have to use more complicated models.
On ARM we just use a register instead of using any form of TLS. On most systems Go binaries set the FSbase register to some value on the heap, but when we use cgo, or on platforms that don't support static binaries we don't touch FSbase, as it was already set up by libc.
Functions that use little stack (under 120 bytes) can be excepted from this stack check.
- zwieback 12y agoThanks, nice writeup.