Fun with go assembler

03 Jul 2016

Go has a neat feature where a function can be written with no body, and as long as there is a related assembler file present that defines the function, it will be compiled into your final Go binary. Neat stuff.

I got a lot of useful info here, here, here, and here.

Interestingly, I found that the compiler would trip over the NOSPLIT keyword:

$ go build
# github.com/manniwood/playground/rotl
./rotl_amd64.s:2: illegal or missing addressing mode for symbol NOSPLIT
asm: asm: assembly of ./rotl_amd64.s failed

so unlike the advice given, I omitted that keyword, and things seemed to go fine.

My objective was to play with the x86 ROL instruction to do barrell rolls of bytes, so I modified the ·add example given at https://goroutines.com/asm to see if I could use the two-argument version of ROLQ to do a left bit rotation on a 64-bit value.

The add example, from https://goroutines.com/asm, is:

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

so I figured this would allow me to do a bit rotation of x y times:

TEXT ·rotl(SB), $0
        MOVQ x+0(FP), BX
        MOVQ y+8(FP), BP
        ROLQ BP, BX
        MOVQ BX, ret+16(FP)
        RET

But no such luck:

$ go build
# github.com/manniwood/playground/rotl
asm: invalid instruction: 00010 (/home/mwood/go/src/github.com/manniwood/playground/rotl/rotl_amd64.s:5)	ROLQ	BP, BX
asm: asm: assembly of ./rotl_amd64.s failed

A bit of Googling showed me that I had to use the special CX register, which is where ROLQ looks for that argument:

TEXT ·rotl(SB), $0
        MOVQ x+0(FP), BX

        // C is the counter register, used for args
        // for things like bit shift commands
        MOVQ y+8(FP), CX
        ROLQ CX, BX
        MOVQ BX, ret+16(FP)
        RET

And that compiled!

Here are the two files I used:

main.go
rotl_amd64.s

And here are their contents:

// main.go

package main

import "fmt"

// notice no function body!
func rotl(x, y int64) int64

func main() {
	fmt.Println(rotl(2, 3))
}
// rotl_amd64.s
TEXT ·rotl(SB), $0
        MOVQ x+0(FP), BX

        // C is the counter register, used for args
        // for things like bit shift commands
        MOVQ y+8(FP), CX
        ROLQ CX, BX
        MOVQ BX, ret+16(FP)
        RET

A few notes on names.

I tried changing the name of argument y to argument z in the assembler code, and Go happily still built the binary, so it looks like the argument names are in the assembly file for the benefit of the reader, not the compiler.

I renamed the file rotl_amd64.s to foo_amd64.s and Go still happily compiled it.

I renamed the file rotl_amd64.s to rotl_foo.s and Go still happily compiled it.

But I wouldn't recommend devaiting from the naming convention set out in examples.

Final note: altough it's cool that one can use assembler to leverage x86's bit rotate instruction (nicer than having to or together a right shift and a left shift in Go code to mimic the rotation), the overhead of the function call is likely a big hit. However, for longer snippets of code where one might want to speed things up, being able to so easily use assembler is pretty spiffy.

Finally, as has been pointed out elsewhere, the easy way to look at the assembler Go will generate from a .go source file is to do the following:

$ build -gcflags -S file.go