How Go dispatches interface methods under the register ABI
Go's move to a register-based ABI, introduced in Go 1.17 for AMD64 and extended to further architectures in Go 1.18, changed the way interface method calls are compiled and executed. This post examines the machine code generated for a simple interface method invocation under the new ABI.
Consider the following function, the inner loop of a Bubble sort implementation:
func bubbleUp(x sort.Interface) {
n := x.Len()
for i := 1; i < n; i++ {
if x.Less(i, i-1) {
x.Swap(i, i-1)
}
}
}
The function receives a value of type sort.Interface, which abstracts away the underlying data structure—be it a slice or something more exotic—behind three methods. Here is the interface definition:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
Interface representation
On a 64-bit architecture, an interface value occupies two quadwords: the first is a pointer to the interface's dispatch table (itab), and the second is a pointer to the actual runtime value. The Go runtime defines this layout as follows:
type iface struct {
tab *itab
data unsafe.Pointer
}
The itab structure is defined as:
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
The key field for this discussion is fun, a variable-sized array of function pointers that forms the dispatch table. Generated code looks up method addresses here, at fixed offsets relative to the start of the itab.
Register-based call conventions
Since Go 1.17, function arguments and return values are passed in registers on AMD64 (the internal Go ABI—not the stable platform ABI). The register allocation order is:
RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
Values that exceed a single quadword occupy multiple registers. An interface value passed as the first argument, for example, spans RAX (itab pointer) and RBX (data pointer). A second interface argument would use RBX and RCX. (Go's disassembly lists registers without the R prefix.)
Return values follow the same register order; a function returning a single quadword places it in RAX. Notably, the current register ABI defines no callee-saved registers—any value live across a call must be preserved on the stack by the caller.
Tracing the generated code
The disassembly below, produced for bubbleUp on AMD64, reveals how each interface call is compiled. GC-related PCDATA and FUNCDATA directives have been stripped for clarity.
$ go tool compile -S bubble.go
Prologue first:
"".bubbleUp STEXT size=182 args=0x10 locals=0x38 funcid=0x0 align=0x0 0x0000 00000 (bubble.go:11) TEXT "".bubbleUp(SB), ABIInternal, $56-16 0x0000 00000 (bubble.go:11) CMPQ SP, 16(R14) 0x0004 00004 (bubble.go:11) JLS 152 0x000a 00010 (bubble.go:11) SUBQ $56, SP 0x000e 00014 (bubble.go:11) MOVQ BP, 48(SP) 0x0013 00019 (bubble.go:11) LEAQ 48(SP), BP
Then the actual body:
0x0018 00024 (bubble.go:12) MOVQ AX, "".x+64(SP) 0x001d 00029 (bubble.go:12) MOVQ BX, "".x+72(SP) 0x0022 00034 (bubble.go:12) MOVQ 24(AX), CX 0x0026 00038 (bubble.go:12) MOVQ BX, AX 0x0029 00041 (bubble.go:12) CALL CX
The interface argument x arrives in AX (itab pointer) and BX (data pointer). Both are saved to the stack immediately, since the registers will be repurposed for subsequent calls.
For x.Len(), the compiler must locate the method's implementation. The address at offset 24(AX) is loaded into CX—offset 24 is where fun starts. Interface methods are ordered alphabetically by name, so the first slot is Len. The interface data pointer BX, the receiver for this call, is passed along unchanged.
0x002b 00043 (bubble.go:12) MOVQ AX, "".n+24(SP)
Len's return value arrives in AX and is immediately saved to the stack, as it must survive across later calls.
0x0030 00048 (bubble.go:12) MOVL $1, CX 0x0035 00053 (bubble.go:13) JMP 69 ----\ 0x0037 00055 (bubble.go:13) MOVQ "".i+32(SP), DX | 0x003c 00060 (bubble.go:13) LEAQ 1(DX), CX | 0x0040 00064 (bubble.go:13) MOVQ "".n+24(SP), AX | 0x0045 00069 (bubble.go:13) CMPQ AX, CX <<---/ 0x0048 00072 (bubble.go:13) JLE 142
The loop initializes i = 1 in CX and branches to the condition check comparing n (still in AX, or restored from stack) with i. The loop's i++ update lives between these jumps.
Calling x.Less(i, i-1) is the next step. Since Less is the second method in the interface, it sits at itab+32.
0x004a 00074 (bubble.go:13) MOVQ CX, "".i+32(SP) 0x004f 00079 (bubble.go:14) MOVQ "".x+64(SP), DX 0x0054 00084 (bubble.go:14) MOVQ 32(DX), SI 0x0058 00088 (bubble.go:14) LEAQ -1(CX), DI 0x005c 00092 (bubble.go:14) MOVQ DI, ""..autotmp_4+40(SP) 0x0061 00097 (bubble.go:14) MOVQ "".x+72(SP), AX 0x0066 00102 (bubble.go:14) MOVQ CX, BX 0x0069 00105 (bubble.go:14) MOVQ DI, CX 0x006c 00108 (bubble.go:14) CALL SI
The saved interface pair is reloaded into DX; the method pointer comes from 32(DX). Arguments are laid out: the receiver goes in AX, i in BX, and i-1 is computed via LEAQ into CX.
0x006e 00110 (bubble.go:14) TESTB AL, AL 0x0070 00112 (bubble.go:14) JEQ 55
A zero return from Less (i.e., false) branches back to the loop condition. Otherwise, Swap(i, i-1) is invoked.
0x0072 00114 (bubble.go:15) MOVQ "".x+64(SP), DX 0x0077 00119 (bubble.go:15) MOVQ 40(DX), SI 0x007b 00123 (bubble.go:15) MOVQ "".x+72(SP), AX 0x0080 00128 (bubble.go:15) MOVQ "".i+32(SP), BX 0x0085 00133 (bubble.go:15) MOVQ ""..autotmp_4+40(SP), CX 0x008a 00138 (bubble.go:15) CALL SI
This is the third method, found at 40(DX). Arguments are staged as before, and the call is made.
The backward jump to the loop condition follows, along with the epilogue:
0x008c 00140 (bubble.go:15) JMP 55
0x008e 00142 (bubble.go:18) MOVQ 48(SP), BP 0x0093 00147 (bubble.go:18) ADDQ $56, SP 0x0097 00151 (bubble.go:18) RET 0x0098 00152 (bubble.go:18) NOP
What the pattern shows
Every interface method invocation compiles to a similar sequence: load the itab pointer, fetch the function pointer at a fixed offset (24 bytes per method slot, starting at offset 24), pass the data pointer as the receiver, and call. This is reliable because interface methods are sorted alphabetically, giving each method a deterministic index in the dispatch table.
The shift to registers removed much of the stack shuffling that characterized the old ABI, but the fundamental mechanism—virtual dispatch through the itab—remains. What has changed is where arguments and returns travel, and how aggressively values must be spilled to the stack across call boundaries.



