DEV Community

SnowballSH
SnowballSH

Posted on

Go With Assembly

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.

package main

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

func main() {
    println(add(2, 3))
}
Enter fullscreen mode Exit fullscreen mode

try online

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:

TEXT ·add(SB), $0-24
    MOVQ x+0(FP), BX
    MOVQ y+8(FP), BP
    ADDQ BP, BX
    MOVQ BX, ret+16(FP)
    RET
Enter fullscreen mode Exit fullscreen mode

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

package main

func add(x, y int64) int64

func main() {
    println(add(2, 3))
}
Enter fullscreen mode Exit fullscreen mode

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:

#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
Enter fullscreen mode Exit fullscreen mode


(Credit: 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
Go doc for asm

Top comments (0)