maelvls dev blog

maelvls dev blog

Systems software engineer. I write mostly about Kubernetes and Go. About

16 Apr2020

Deep Dive into Go Assembly

Last month, Sylvain Wallez, author of the excellent “Go: the Good, the Bad and the Ugly”, wrote:

Once again, my beloved language takes a hit. As time passes, I come to realize Go has its strenghts (ease of adoption, no overly abstract REST framework, readability, fast compilation) but also its weaknesses. I can think of the weirdness of variable scoping (scopelint really helps) and also variable shadowing, weird syntax around variable assignment, go help is unreadable and just not up to the standard (compared to git help for example), the ever-changing behaviour of GO111MODULE (that’s a temporary issue), go get that do too many things and changes go.mod when you just want to install some tool, but also go install & go run that don’t support versions (e.g. ). Wait, also the “semantic import versioning” that everybody seems to avoid (including Google itself in protobuf).

But the issue Sylvain raised really bothered me: it felt like a bug.

Given v a pointer to an interface, v != nil when v is nil.

Oooh, let’s see.

Boxed type, fat pointer:

Do Go use boxed types for map[string]string? No! It doesn’t use interface{} boxing (boxed = fat pointer = one more level of indirection, less locality – same problem as linked lists).

Here is a short program:

package main

import (
	"fmt"
	"strconv"
)

type abstract interface {
	f()
}

type concrete struct{}

func (concrete) f() {
	i := 0
	i++
	fmt.Print(strconv.Itoa(i))
}

func main() {
	var v1 abstract = &concrete{}
	v1.f()

	v2 := concrete{}
	v2.f()
}

Let’s go over what f1 and f2 do.

# go build . -o ./main
# go tool objdump -S ./main > main.s


TEXT main.concrete.f(SB) /Users/mvalais/code/go-atomic-issue-wallez-tweet/main.go
func (concrete) f() {
  0x10994c0		65488b0c2530000000	MOVQ GS:0x30, CX
  0x10994c9		483b6110		CMPQ 0x10(CX), SP
  0x10994cd		0f869c000000		JBE 0x109956f
  0x10994d3		4883ec58		SUBQ $0x58, SP
  0x10994d7		48896c2450		MOVQ BP, 0x50(SP)
  0x10994dc		488d6c2450		LEAQ 0x50(SP), BP
	fmt.Print(strconv.Itoa(i))
  0x10994e1		48c7042401000000	MOVQ $0x1, 0(SP)
	return FormatInt(int64(i), 10)
  0x10994e9		48c74424080a000000	MOVQ $0xa, 0x8(SP)
  0x10994f2		e8097ffcff		CALL strconv.FormatInt(SB)
  0x10994f7		488b442410		MOVQ 0x10(SP), AX
  0x10994fc		488b4c2418		MOVQ 0x18(SP), CX
	fmt.Print(strconv.Itoa(i))
  0x1099501		48890424		MOVQ AX, 0(SP)
  0x1099505		48894c2408		MOVQ CX, 0x8(SP)
  0x109950a		e8a1f7f6ff		CALL runtime.convTstring(SB)
  0x109950f		488b442410		MOVQ 0x10(SP), AX
  0x1099514		0f57c0			XORPS X0, X0
  0x1099517		0f11442440		MOVUPS X0, 0x40(SP)
  0x109951c		488d0dfd1a0100		LEAQ runtime.types+72160(SB), CX
  0x1099523		48894c2440		MOVQ CX, 0x40(SP)
  0x1099528		4889442448		MOVQ AX, 0x48(SP)
	return Fprint(os.Stdout, a...)
  0x109952d		488b05c4ea0d00		MOVQ os.Stdout(SB), AX
  0x1099534		488d0d251b0500		LEAQ go.itab.*os.File,io.Writer(SB), CX
  0x109953b		48890c24		MOVQ CX, 0(SP)
  0x109953f		4889442408		MOVQ AX, 0x8(SP)
  0x1099544		488d442440		LEAQ 0x40(SP), AX
  0x1099549		4889442410		MOVQ AX, 0x10(SP)
  0x109954e		48c744241801000000	MOVQ $0x1, 0x18(SP)
  0x1099557		48c744242001000000	MOVQ $0x1, 0x20(SP)
  0x1099560		e8cb97ffff		CALL fmt.Fprint(SB)
  0x1099565		488b6c2450		MOVQ 0x50(SP), BP
  0x109956a		4883c458		ADDQ $0x58, SP
  0x109956e		c3			RET
func (concrete) f() {
  0x109956f		e8ac7ffbff		CALL runtime.morestack_noctxt(SB)
  0x1099574		e947ffffff		JMP main.concrete.f(SB)

  0x1099579		cc			INT $0x3
  0x109957a		cc			INT $0x3
  0x109957b		cc			INT $0x3
  0x109957c		cc			INT $0x3
  0x109957d		cc			INT $0x3
  0x109957e		cc			INT $0x3
  0x109957f		cc			INT $0x3

TEXT main.main(SB) /Users/mvalais/code/go-atomic-issue-wallez-tweet/main.go
func main() {
  0x1099580		65488b0c2530000000	MOVQ GS:0x30, CX
  0x1099589		483b6110		CMPQ 0x10(CX), SP
  0x109958d		7631			JBE 0x10995c0
  0x109958f		4883ec10		SUBQ $0x10, SP
  0x1099593		48896c2408		MOVQ BP, 0x8(SP)
  0x1099598		488d6c2408		LEAQ 0x8(SP), BP
	v.f()
  0x109959d		488d059c1a0500		LEAQ go.itab.*main.concrete,main.abstract(SB), AX
  0x10995a4		8400			TESTB AL, 0(AX)
  0x10995a6		488d05eba40f00		LEAQ runtime.zerobase(SB), AX
  0x10995ad		48890424		MOVQ AX, 0(SP)
  0x10995b1		e81a000000		CALL main.(*concrete).f(SB)
}
  0x10995b6		488b6c2408		MOVQ 0x8(SP), BP
  0x10995bb		4883c410		ADDQ $0x10, SP
  0x10995bf		c3			RET
func main() {
  0x10995c0		e85b7ffbff		CALL runtime.morestack_noctxt(SB)
  0x10995c5		ebb9			JMP main.main(SB)

  0x10995c7		cc			INT $0x3
  0x10995c8		cc			INT $0x3
  0x10995c9		cc			INT $0x3
  0x10995ca		cc			INT $0x3
  0x10995cb		cc			INT $0x3
  0x10995cc		cc			INT $0x3
  0x10995cd		cc			INT $0x3
  0x10995ce		cc			INT $0x3
  0x10995cf		cc			INT $0x3

TEXT main.(*concrete).f(SB) <autogenerated>

  0x10995d0		65488b0c2530000000	MOVQ GS:0x30, CX
  0x10995d9		483b6110		CMPQ 0x10(CX), SP
  0x10995dd		7631			JBE 0x1099610
  0x10995df		4883ec08		SUBQ $0x8, SP
  0x10995e3		48892c24		MOVQ BP, 0(SP)
  0x10995e7		488d2c24		LEAQ 0(SP), BP
  0x10995eb		488b5920		MOVQ 0x20(CX), BX
  0x10995ef		4885db			TESTQ BX, BX
  0x10995f2		7523			JNE 0x1099617
  0x10995f4		48837c241000		CMPQ $0x0, 0x10(SP)
  0x10995fa		740e			JE 0x109960a
  0x10995fc		e8bffeffff		CALL main.concrete.f(SB)
  0x1099601		488b2c24		MOVQ 0(SP), BP
  0x1099605		4883c408		ADDQ $0x8, SP
  0x1099609		c3			RET
  0x109960a		e8c1ddf6ff		CALL runtime.panicwrap(SB)
  0x109960f		90			NOPL
  0x1099610		e80b7ffbff		CALL runtime.morestack_noctxt(SB)
  0x1099615		ebb9			JMP main.(*concrete).f(SB)
  0x1099617		488d7c2410		LEAQ 0x10(SP), DI
  0x109961c		48393b			CMPQ DI, 0(BX)
  0x109961f		75d3			JNE 0x10995f4
  0x1099621		488923			MOVQ SP, 0(BX)
  0x1099624		ebce			JMP 0x10995f4
package main

import (
	"fmt"
	"strconv"
)

type abstract interface {
	f()
}

type concrete struct{}

func (concrete) f() {
	i := 0
	i++
	fmt.Print(strconv.Itoa(i))
}

func main() {
	var v1 abstract = &concrete{}
	v1.f()

	v2 := concrete{}
	v2.f()
}

Review of what happens in v1.f()

  v1.f()
  0x109959d		488d059c1a0500		LEAQ go.itab.*main.concrete,main.abstract(SB), AX
  0x10995a4		8400			TESTB AL, 0(AX)
  0x10995a6		488d05eba40f00		LEAQ runtime.zerobase(SB), AX
  0x10995ad		48890424		MOVQ AX, 0(SP)
  0x10995b1		e81a000000		CALL main.(*concrete).f(SB)

Let’s read that line by line.

  0x109959d		488d059c1a0500		LEAQ go.itab.*main.concrete,main.abstract(SB), AX

We first load the tab field of the itab struct into the address pointed by the stack pointer (SP). This itab refers to the field ‘itab’ of the iface struct (see iface in runtime2.go):

type iface struct {
  tab  *itab
  data unsafe.Pointer
}
0x10995a4		8400			TESTB AL, 0(AX)

Here, we test that AL, which contains

0x10995a6		488d05eba40f00		LEAQ runtime.zerobase(SB), AX
0x10995ad		48890424		MOVQ AX, 0(SP)

Now that the tab address is contained in the address pointed by the SP, it’s time to find out which concrete implementation of f() should be called:

0x10995b1		e81a000000		CALL main.(*concrete).f(SB)

And here is the dispath function main.(*concrete).f:

TEXT main.(*concrete).f(SB) <autogenerated>

0x10995d0		65488b0c2530000000	MOVQ GS:0x30, CX
0x10995d9		483b6110		CMPQ 0x10(CX), SP
0x10995dd		7631			JBE 0x1099610

That’s the stack-split prologue that deals with growing the stack when the goroutine doesn’t have enough stack space. Basicall, JBE means ‘jump to the stack-split epilogue if the value contained in GS:0x30 is lower or equal to the SP (stack pointer). Since the stack grows backwards, meaning that SP will decrease as the stack grows).

0x10995df		4883ec08		SUBQ $0x8, SP
0x10995e3		48892c24		MOVQ BP, 0(SP)
0x10995e7		488d2c24		LEAQ 0(SP), BP
0x10995eb		488b5920		MOVQ 0x20(CX), BX
0x10995ef		4885db			TESTQ BX, BX
0x10995f2		7523			JNE 0x1099617
0x10995f4		48837c241000		CMPQ $0x0, 0x10(SP)
0x10995fa		740e			JE 0x109960a
0x10995fc		e8bffeffff		CALL main.concrete.f(SB)
0x1099601		488b2c24		MOVQ 0(SP), BP
0x1099605		4883c408		ADDQ $0x8, SP
0x1099609		c3			RET
0x109960a		e8c1ddf6ff		CALL runtime.panicwrap(SB)
0x109960f		90			NOPL
0x1099610		e80b7ffbff		CALL runtime.morestack_noctxt(SB)
0x1099615		ebb9			JMP main.(*concrete).f(SB)
0x1099617		488d7c2410		LEAQ 0x10(SP), DI
0x109961c		48393b			CMPQ DI, 0(BX)
0x109961f		75d3			JNE 0x10995f4
0x1099621		488923			MOVQ SP, 0(BX)
0x1099624		ebce			JMP 0x10995f4

Review of what happens in v2.f()

	v2.f()
  0x10995b6		e805ffffff		CALL main.concrete.f(SB)

Notes

The AX and AL registers

From intel64 (Figure 3-5. Alternate General-Purpose Register Names, p. 3-12 Vol. 1):

| 0000 0001 0010 0011 0100 0101 0110 0111 | ------> EAX

|                     0100 0101 0110 0111 | ------> AX

|                               0110 0111 | ------> AL

|                     0100 0101           | ------> AH

(from a Stackoverflow post)

From intel64 (p. 3-16, Vol. 1):

AF (bit 4) Auxiliary Carry flag — Set if an arithmetic operation generates a carry or a borrow out of bit 3 of the result; cleared otherwise. This flag is used in binary-coded decimal (BCD) arithmetic.

The GS register

In the x86-64 intel64 (p. 3-14, Vol. 1):

The DS, ES, FS, and GS registers point to four data segments. The availability of four data segments permits efficient and secure access to different types of data structures. For example, four separate data segments might be created: one for the data structures of the current module, another for the data exported from a higher-level module, a third for a dynamically created data structure, and a fourth for data shared with another program. To access additional data segments, the application program must load segment selectors for these segments into the DS, ES, FS, and GS registers, as needed.

The ‘Q’ suffix in MOVQ and LEAQ

I could not find that in the Intel 64 PDF. Apparently, these prefixes were introduced with the assembler (as). See binutils’s as i386mnemonics:

Instruction mnemonics are suffixed with one character modifiers which specify the size of operands. The letters ‘b’, ‘w’, ‘l’ and ‘q’ specify byte, word, long and quadruple word operands.

What are SP and SB

From https://golang.org/doc/asm#symbols:

  • FP: Frame pointer: arguments and locals.
  • PC: Program counter: jumps and branches.
  • SB: Static base pointer: global symbols.
  • SP: Stack pointer: top of stack.

References

	v2.f()
  0x10995b6		e805ffffff		CALL main.concrete.f(SB)
📝 Edit this page