Note that there is an open proposal to implement register passing in Go at https://github.com/golang/go/issues/18597 . This proposal has not yet been accepted.

This passes the arguments in registers. The exact number depends on the calling convention, but for freebsd/amd64 up to 6 arguments are passed in registers, the rest on the stack.

Also we see the arguments are at the top of the stack, and the return value slot underneath that.

So arguments are passed via memory, on the stack, not in registers like other languages.

// Note: substracting z so we know which argument is which.

So return values are passed via memory, on the stack, not in registers like in most standard x86-64 calling conventions for natively compiled languages.

How does a function like FuncAdd above get called?

func DoCallAdd () int { return FuncAdd ( 1 , 2 , 3 ) }

This gives:

0x480650 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX 0x480659 483b6110 CMPQ 0x10(CX), SP 0x48065d 7641 JBE 0x4806a0 0x48065f 4883ec28 SUBQ $0x28, SP 0x480663 48896c2420 MOVQ BP, 0x20(SP) 0x480668 488d6c2420 LEAQ 0x20(SP), BP 0x48066d 48c7042401000000 MOVQ $0x1, 0(SP) 0x480675 48c744240802000000 MOVQ $0x2, 0x8(SP) 0x48067e 48c744241003000000 MOVQ $0x3, 0x10(SP) 0x480687 e8a4ffffff CALL src.FuncAdd(SB) 0x48068c 488b442418 MOVQ 0x18(SP), AX 0x480691 4889442430 MOVQ AX, 0x30(SP) 0x480696 488b6c2420 MOVQ 0x20(SP), BP 0x48069b 4883c428 ADDQ $0x28, SP 0x48069f c3 RET 0x4806a0 e80ba5fcff CALL runtime.morestack_noctxt(SB) 0x4806a5 eba9 JMP src.DoCallAdd(SB)

Woah, what is going on?

At the center of the function we see what we wanted to see:

0x48066d 48c7042401000000 MOVQ $0x1, 0(SP) // set arg x 0x480675 48c744240802000000 MOVQ $0x2, 0x8(SP) // set arg y 0x48067e 48c744241003000000 MOVQ $0x3, 0x10(SP) // set arg z 0x480687 e8a4ffffff CALL src.FuncAdd(SB) // call 0x48068c 488b442418 MOVQ 0x18(SP), AX // get return value of FuncAdd 0x480691 4889442430 MOVQ AX, 0x30(SP) // set return value of DoCallAdd

The arguments are pushed into the stack before the call, and after the call the return value is retrieved from the callee frame and copied to the caller frame. So far, so good.

However, now we know that arguments are passed on the stack, this means that any function that calls other functions now must ensure there is some stack space to pass arguments to its callees. This is what we see here:

// Before the call: make space for callee. 0x48065f 4883ec28 SUBQ $0x28, SP // After the call: restore stack pointer. 0x48069b 4883c428 ADDQ $0x28, SP

Now, what is the remaining stuff?

Because Go has exceptions (“panics”) it must preserve the ability of the runtime system to unwind the stack. So in every activation record it must store the difference between the stack pointer on entry and the stack pointer for callees. This is the “frame pointer” which is stored in this calling convention in the BP register. That is why we see:

// Store the frame pointer of the caller into a known location in // the current activation record. 0x480663 48896c2420 MOVQ BP, 0x20(SP) // Store the address of the copy of the parent frame pointer // into the new frame pointer. 0x480668 488d6c2420 LEAQ 0x20(SP), BP

This maintains the invariant of the calling convention that BP always points to a linked list of frame pointers, where each successive value of BP is 32 bytes beyond the value of the stack pointer in the current frame (SP+0x20). This way the stack can always be successfully unwound.

Finally, what about the last bit of code?

0x480650 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX 0x480659 483b6110 CMPQ 0x10(CX), SP 0x48065d 7641 JBE 0x4806a0 ... 0x4806a0 e80ba5fcff CALL runtime.morestack_noctxt(SB) 0x4806a5 eba9 JMP src.DoCallAdd(SB)

The Go runtime implements “tiny stacks” as an optimization: a goroutine always starts with a very small stack so that a running go program can have many “small” goroutines active at the same time. However that means that on the standard tiny stack it is not really possible to call many functions recursively.

Therefore, in Go, every function that needs an activation record on the stack needs first to check whether the current goroutine stack is large enough for this. It does this by comparing the current value of the stack pointer to the low water mark of the current goroutine, stored at offset 16 (0x10) of the goroutine struct, which itself can always be found at address FS:0xfffffff8.

Compare how DoCallAdd works in C or C++:

DoCallAdd: movl $3, %edx movl $2, %esi movl $1, %edi jmp FuncAdd

This passes the arguments in registers, then transfers control to the callee with a jmp — a tail call. This is valid because the return value of FuncAdd becomes the return value of DoCallAdd .

What of the stack pointer? The function DoCallAdd cannot tell us much in C because, in contrast to Go, it does not have any variables on the stack and thus does need an activation record. In general (and that is valid for Go too), if there is no need for an activation record, there is no need to set up / adjust the stack pointer.

So how would a C/C++ compiler handle an activation record? We can force one like this:

void other ( int * x ); int DoCallAddX () { int x = 123 ; other ( & x ); return x ; }

Gives us:

DoCallAddX: subq $24, %rsp // make space leaq 12(%rsp), %rdi // allocate x at address rsp+12 movl $123, 12(%rsp) // store 123 into x call other // call other(&x) movl 12(%rsp), %eax // load value from x addq $24, %rsp // restore stack pointer ret

So %rsp gets adjusted upon function entry and restored in the epilogue.

No surprise. But is there? What of exception handling?