Go With Assembly
Tags: go, assembly
Published: 2021-04-19 02:40:21

Recently I just found out that you can use Go with Assembly. The Assembly isn't x86_64 or ARM, it is Go's special Assembly format, using syntax inspired by the Plan 9 Assembler.

This post is a short introduction for it.

Lets say we have some Go code.

```go
package main

func add(x, y int64) int64 {
    return x + y
}

func main() {
    println(add(2, 3))
}
```
[try online](https://goplay.space/#trWiW4KyoNe)

Pretty simple, right? Prints `"5\n"` to stderr.

Now we want to power it up with Go's Assembler.
Create a file called `add.s` in the same directory with `main.go`. Use this code:

```s
TEXT ·add(SB), $0-24
    MOVQ x+0(FP), BX
    MOVQ y+8(FP), BP
    ADDQ BP, BX
    MOVQ BX, ret+16(FP)
    RET
```

Let me explain the code:

`TEXT` tells the assembler that this is a function.
The syntax to declare a function is

```TEXT package_name·function_name(SB), $frame_size-argument_size```

Note that the dot is `·`(Unicode 0xB7), not `.`.
In this example, `TEXT ·add(SB), $0-24` declares a function called `main·add` with frame size of 0 (registers are enough) and argument size of 24 (3 * 8).

`MOVQ x+0(FP), BX` moves `*x` to the `BX` register.
`MOVQ a, b` moves a **64-bit** value `a` to `b`. `Q` stands for quadword.
`x+0(FP)` accesses the argument `x`. `symbol+offset(register)` accesses the symbol from register using the offset. `FP` is the register for the function arguments.

Similarly, `MOVQ y+8(FP), BP` moves `*y` to the `BP` register.

`ADDQ BP, BX` adds BP and BX, storing the result to `BX`.

`MOVQ BX, ret+16(FP)` moves the result to the return register. +16 because we have a size of 24 and each argument used 8, so ret needs to use 16-23.

`RET` simply returns the last result `ret`.

With all those complex assembly explained, we can use it now.

Change the `main.go` to

```go
package main

func add(x, y int64) int64

func main() {
    println(add(2, 3))
}
```

Make sure `add.s` is in the same directory as `main.go`.

Compile using `go build` or run with `go run`. You will still see 5 for the result!

---

In case you are wondering, you can print hello world with this:

```s
#include "textflag.h"

DATA world<>+0(SB)/8, $"hello wo"
DATA world<>+8(SB)/4, $"rld "

GLOBL world<>+0(SB), RODATA, $12
	
TEXT ·hello(SB),$88-0
	SUBQ	$88, SP
	MOVQ	BP, 80(SP)
	LEAQ	80(SP), BP

	LEAQ	world<>+0(SB), AX 
	MOVQ	AX, my_string+48(SP)        
	MOVQ	$11, my_string+56(SP)
	MOVQ	$0, autotmp_0+64(SP)
	MOVQ	$0, autotmp_0+72(SP)
	LEAQ	type·string(SB), AX
	MOVQ	AX, (SP)
	LEAQ	my_string+48(SP), AX        
	MOVQ	AX, 8(SP)

	CALL	runtime·convT2E(SB)           
	MOVQ	24(SP), AX
	MOVQ	16(SP), CX                    
	MOVQ	CX, autotmp_0+64(SP)        
	MOVQ	AX, autotmp_0+72(SP)
	LEAQ	autotmp_0+64(SP), AX        
	MOVQ	AX, (SP)                      
	MOVQ	$1, 8(SP)                      
	MOVQ	$1, 16(SP)

	CALL	fmt·Println(SB)

	MOVQ 80(SP), BP
	ADDQ $88, SP
	RET
``` (Credit: [davidwong.fr/goasm/hello](https://davidwong.fr/goasm/hello))

---

According to some benchmark, using Go's assembler makes Go faster.
However, I don't recommend using it in real projects.

First, you lose Go's automatic garbage collecting. It will result in heavy memory usage if you do a lot of arrays using Assembly.

Second, you lose Go's simplicity. Go is aimed for simple and efficient, but Assembly is clearly not simple at all. A small add function turns into 6 lines of assembly code... You won't be able to complete a project using it.

Third, you lose the ability to debug and manage your project. Go's assembler provides bad error messages and the worst error I have encountered is `unexpected return pc` followed by 30 binary numbers. If you encounter a bug, you will probably spend 1 hour to fix it!

Despite all of that, Assembly in Go is still very interesting! :)

---

Resources:

[davidwong.fr/goasm](https://davidwong.fr/goasm)
[Go doc for asm](https://golang.org/doc/asm)