We have learned that storing a value in an interface results in a copy of that value being created...somewhere. One of the possible locations is on the stack. To understand how a value is stored in an interface on the stack, we will take a look at the assembly. Please note:
- Assembly is not my area of expertise. Not even close. Therefore I am empathetic to people who may get stuck here. Please feel free to ping me on Gopher slack
@akutz
with any questions! - This page tries to provide as many links and answer as many questions as possible regarding the assembly.
- Lastly, assembly is platform dependent. For example, the assembly for x86 does not look like the assembly for ARM. This page is based on x86 assembly.
The example on this page is based on the source code in ex1.go:
/* line 19 */ func ex1() {
/* line 20 */ var x int64
/* line 21 */ var y interface{}
/* line 22 */ x = 2
/* line 23 */ y = x
/* line 24 */ _ = y
/* line 25 */ }
With that in mind, let's get started:
-
Build the
ex1
package with compiler flags to prevent write barriers (-wb=false
), inlining (-l
), and optimization (-N
). You would never do this in producton, but it makes walking the assembly easier:docker run -it --rm -v "$(pwd):/tmp/pkg" go-interface-values \ go build -gcflags "-wb=false -l -N" \ -o /tmp/pkg/02-interface-values.ex1 \ ./docs/02-interface-values/examples/ex1
-
Dump the symbol
ex1$
from the newly built archive:docker run -it --rm -v "$(pwd):/tmp/pkg" go-interface-values \ go tool objdump -s ex1$ /tmp/pkg/02-interface-values.ex1
👋 Alternative assembly
Please note it is also possible to dump the assembly for a single Go source file:
docker run -it --rm go-interface-values \
go tool compile -wb=false -l -N -S \
./docs/02-interface-values/examples/ex1/ex1.go
However I have found the Go compiler will produce different assembly based on go tool compile
and actually packing the archive with go build
. In order to be more aligned with package archive assembly, this page uses go build
.
-
The resulting output will be similar (but not identical) to the following:
TEXT main.ex1(SB) /go-interface-values/docs/02-interface-values/examples/ex1/ex1.go ex1.go:19 0x454c40 4883ec28 SUBQ $0x28, SP ex1.go:19 0x454c44 48896c2420 MOVQ BP, 0x20(SP) ex1.go:19 0x454c49 488d6c2420 LEAQ 0x20(SP), BP ex1.go:20 0x454c4e 48c7042400000000 MOVQ $0x0, 0(SP) ex1.go:21 0x454c56 440f117c2410 MOVUPS X15, 0x10(SP) ex1.go:22 0x454c5c 48c7042402000000 MOVQ $0x2, 0(SP) ex1.go:23 0x454c64 48c744240802000000 MOVQ $0x2, 0x8(SP) ex1.go:23 0x454c6d 488d056c410000 LEAQ 0x416c(IP), AX ex1.go:23 0x454c74 4889442410 MOVQ AX, 0x10(SP) ex1.go:23 0x454c79 488d442408 LEAQ 0x8(SP), AX ex1.go:23 0x454c7e 4889442418 MOVQ AX, 0x18(SP) ex1.go:25 0x454c83 488b6c2420 MOVQ 0x20(SP), BP ex1.go:25 0x454c88 4883c428 ADDQ $0x28, SP ex1.go:25 0x454c8c c3 RET
Here are the lines on which we want to focus:
ex1.go:20 0x454c4e 48c7042400000000 MOVQ $0x0, 0(SP) ex1.go:21 0x454c56 440f117c2410 MOVUPS X15, 0x10(SP) ex1.go:22 0x454c5c 48c7042402000000 MOVQ $0x2, 0(SP) ex1.go:23 0x454c64 48c744240802000000 MOVQ $0x2, 0x8(SP) ex1.go:23 0x454c6d 488d056c410000 LEAQ 0x416c(IP), AX ex1.go:23 0x454c74 4889442410 MOVQ AX, 0x10(SP) ex1.go:23 0x454c79 488d442408 LEAQ 0x8(SP), AX ex1.go:23 0x454c7e 4889442418 MOVQ AX, 0x18(SP)
-
ex1.go:20 0x454c4e 48c7042400000000 MOVQ $0x0, 0(SP)
ex1.go:19
- This is the file and line number of the source code that corresponds to this line of assembly.
- In this case it is line 20 from the file
ex1.go
--var x int64
.
0x454c4e
- The program counter formatted as hexadecimal.
- GNU's
objdump
tool formats this value as hexadecimal as well, but without the leading prefix0x
.
48c7042400000000
- The executable instruction formatted as hexadecimal.
- GNU's
objdump
tool formats this value as hexadecimal as well, but with spaces, ex.48 c7 04 24 00 00 00 00
.
MOVQ $0x0, 0(SP)
- The instruction
MOVQ
copies the value from one address to another, ex.MOVQ SRC, DST
. MOVQ
-
Normally
MOVQ
operates right to left,MOVQ DST SRC
, but as the Go assembly documentation states:One detail evident in the examples from the previous sections is that data in the instructions flows from left to right: MOVQ $0, CX clears CX. This rule applies even on architectures where the conventional notation uses the opposite direction.
-
The
Q
inMOVQ
stands for quadword:- On x86 and x86_64 platforms a word is 16 bits.
- On 64-bit platforms a quadword is 16x4, or 64-bits.
- Thus
MOVQ
is used when wanting to copy 8 bytes.
-
$0x0
- The
SRC
of the copy operation. - The leading
$
indicatesSRC
is not a memory address, but a literal value. - The value to copy is therefore
0x0
, or the integer value0
.
- The
0(SP)
- The
DST
of the copy operation. - The
0
indicates an offset of zero bytes from some address. - The address is indicated by
(SP)
, stack pointer, which points to the top of the current call stack frame on x86 platforms. - Therefore
0(SP)
can be translated as zero bytes from the top of the current strack frame.
- The
- The instruction
-
ex1.go:21 0x454c56 440f117c2410 MOVUPS X15, 0x10(SP)
- The assembly for line21,
var y interface{}
. MOVUPS X15 0x10(SP)
MOVUPS
- The instruction
MOVUPS
copies an unligned, packed, single-precision floating point value from one address to another, ex.MOVUPS SRC, DST
. - Like
MOVQ
, when reading Go assemblyMOVUPS
operates right-to-left,DST SRC
. - Go is using
MOVUPS
in 128-bit mode, which means the operation is copying 16 bytes.
- The instruction
X15
- The
SRC
of the copy operation. - The
X15
register is special and holds the zero value (Go application binary interface (ABI) documentation). - Because
MOVUPS
is copying 16 bytes of data and theX15
register is0
, this instruction is essentially reserving 16 bytes on the stack starting atDST
.
- The
0x10(SP)
- The
DST
of the copy operation. - The
0x10
indicates an offset of 16 bytes (0x10
is hexadecimal for 16) from some address. - The address is indicated by
(SP)
, stack pointer, which points to the top of the current call stack frame on x86 platforms. - Therefore
0x10(SP)
can be translated as 16 bytes from the top of the current strack frame.
- The
Wait, why was
y
offset by 16 bytes whenx
is only eight bytes? Find out below! 😃 - The assembly for line21,
-
ex1.go:22 0x454c5c 48c7042402000000 MOVQ $0x2, 0(SP)
- The assembly for
x = 2
MOVQ $0x2, 0(SP)
copies the literal value2
to the memory address for the variablex
.
- The assembly for
-
ex1.go:23 0x454c64 48c744240802000000 MOVQ $0x2, 0x8(SP)
- The assembly for
y = x
MOVQ $0x2, 0x8(SP)
copies the literal value2
to the memory address 8 bytes fromSP
.- Please note this is not a named variable, or rather not a named
int64
. - The Go compiler was able to determine that the only value ever assigned to
y
would be anint64
, and so an extra eight bytes was allocated on the stack in order to store theint64
value assigned toy
.
- The assembly for
-
ex1.go:23 0x454c6d 488d056c410000 LEAQ 0x416c(IP), AX
-
Still more assembly for
y = x
-
LEAQ
-
The
LEA
instruction stands for load effective address. -
The
Q
suffix indicates a quadword, aka 64 bits, aka 8 bytes. -
Like other Go assembly, the
DST SRC
syntax is flipped to beSRC DST
-
Unlike
MOV
which reads the memory at the providedSRC
address,LEA
only reads the address itself. For example, the code snippet below would result in aMOV
instruction in order to copy the value ofx
(address0(SP)
) to the address ofy
(address0x8(SP)
):x := 1 // MOVQ $0x1 0(SP) y := x // MOVQ 0(SP) 0x8(SP)
Actually, the Go compiler is pretty smart, and it would probably use
MOVQ $0x1 0x8(SP)
to assign1
toy
, but for the purposes of this example we copied the value ofx
toy
. However, this code snippet would use anLEA
since we do not need to know the value ofx
, only its address:x := 1 // MOVQ $0x1 0(SP) y := &x // LEAQ 0(SP) 0x8(SP)
-
-
LEAQ 0x416c(IP), AX
stores the address of the next CPI instruction in registerAX
. -
Ultimately what is stored in
AX
is the address oftype.int64
, a global value that specifies the internal type for anint64
.
-
-
ex1.go:23 0x454c74 4889442410 MOVQ AX, 0x10(SP)
- Still more assembly for
y = x
MOVQ AX, 0x10(SP)
copies the value in theAX
register to the memory address offset fromSP
by 16 bytes.- This assigns the address of the global value
type.int64
to the interface's firstuintptr
, the one that points to the underlying type.
- Still more assembly for
-
ex1.go:23 0x454c79 488d442408 LEAQ 0x8(SP), AX
- Still more assembly for
y = x
LEAQ 0x8(SP), AX
loads the address of the memory eight bytes fromSP
into the registerAX
.- The address loaded into
AX
points to the aforementioned, unnamed, temporary value the Go compiler created on the stack for they
interface to reference.
- Still more assembly for
-
ex1.go:23 0x454c7e 4889442418 MOVQ AX, 0x18(SP)
- Still more assembly for
y = x
MOVQ AX, 0x18(SP)
copies the value in registerAX
into the address 24 bytes fromSP
.- This assigns the address of the unamed
int65
at0x8(SP)
to the interface's seconduintptr
, the one that points to the underlying value.
- Still more assembly for
Wait a minute, isn't memory referenced by pointer allocated on the heap!? In fact Go can optimize that memory to the stack as well, and that is what happens in this example. The Go compiler was able to place the value stored in y
on the stack at address 0x8(SP)
and let the pointer at 0x18(SP)
reference 0x8(SP)
, all on the stack.
However, the fact that a "temporary" memory location was created to reference from the interface is key to understanding when storing an interface value results in a memory allocation on the heap. Still, before we answer that question, we first need to understand why the Go compiler was able to keep this value on the stack.
Keep reading to learn about escape analysis!
Next: Escape analysis