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))
}
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
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))
}
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
(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:
Top comments (0)