Getting Started with Golang Assembly

Author

Reads 1K

Man Working on Computers Coding
Credit: pexels.com, Man Working on Computers Coding

Golang Assembly can seem intimidating at first, but it's actually quite accessible.

To get started, you'll need to have a basic understanding of the Golang language and its syntax, as well as a way to assemble and run your code.

The Golang documentation provides a good starting point for learning about the assembly process, including how to use the `go build` command to assemble and link your code.

In the article section "Golang's Assembly Process", we explore the step-by-step process of assembling and linking a simple Golang program, including the use of the `go build` command.

Setup and Basics

Setting up a Go development environment involves installing the Go toolchain, which includes the Go compiler, linker, and other tools.

The Go compiler is called `go build`, and it's used to compile Go source code into machine code.

You can install the Go toolchain using a package manager like Homebrew on macOS or apt-get on Linux.

To verify that Go is installed correctly, run `go version` in your terminal, which should display the version of the Go toolchain.

The Setup

A minimalist interior setup featuring a laptop, coffee cup, and pen on a marble table by a window.
Credit: pexels.com, A minimalist interior setup featuring a laptop, coffee cup, and pen on a marble table by a window.

So you've decided to start your new project, but before you can dive into the fun stuff, you need to set it up properly. This involves choosing the right location and climate for your plants.

The ideal location for a garden is one that receives at least 6 hours of direct sunlight a day. This is crucial for photosynthesis to occur.

You'll also need to consider the climate in your area. If you live in a region with cold winters, you may want to choose plants that are hardy and can withstand frost.

The type of soil you have in your garden also plays a significant role in the health of your plants. If your soil is poor in nutrients, you may need to add organic matter like compost to improve its quality.

It's also important to prepare your garden bed before planting. This involves removing any debris, loosening the soil, and adding any necessary amendments.

Constants

Credit: youtube.com, Programming Basics: Variables, Literals and Constants

The assembler takes its guidance from the Plan 9 assemblers, but it's a distinct program, so there are some differences.

One key difference is in constant evaluation. The assembler parses constant expressions using Go's operator precedence, not the C-like precedence of the original.

This means that expressions like 3&1<<2 are parsed as (3&1)<<2, not 3&(1<<2), resulting in a value of 4.

Constants are always evaluated as 64-bit unsigned integers. This means that -2 is not the integer value minus two, but the unsigned 64-bit integer with the same bit pattern.

Division or right shift where the right operand's high bit is set is rejected to avoid ambiguity.

Version

In Go version, declaring an assembly function is more complex than a standard function. It requires two separate steps: a C function declaration in Go code and the actual assembly implementation in another .s file.

The syntax for a Go function declaration is different from a standard function, and it's not immediately clear what's going on. I've seen this firsthand and it can be frustrating.

Declaring an assembly function in Go involves using the equivalent of a C function declaration, which is a different syntax than a standard function.

Architecture Specifics

Credit: youtube.com, High performance and scaling techniques in Golang using Go Assembly

Architecture Specifics are crucial to understand when working with Go assembly.

The Go assembler doesn't list all instructions for each machine, so you'll need to check the source for the obj support library for that architecture. For example, the ARM architecture's instructions are listed in the src/cmd/internal/obj/arm directory.

You'll notice that data in the instructions flows from left to right, regardless of the conventional notation used by the architecture. This is evident in examples like MOVQ$0,CX, which clears the CX register.

In Go assembly, registers R10 and R11 are reserved by the compiler and linker. The assembler also supports various addressing modes specific to each architecture. For instance, on the 386 and AMD64 architectures, you can use (R1) for register indirect, 4(R1) for register indirect with offset, and $foo(SB) for absolute address.

Here are some architecture-specific details for the 386 and AMD64 architectures:

  • 16(R1): The location at R1 plus 16.
  • (R1): Alias for 0(R1).

Keep in mind that Go assembly doesn't have a one-to-one correspondence with native assembly, so you'll need to check the Go assembler documentation for every platform you work on.

64-Bit Intel 386 (AMD64)

Credit: youtube.com, x86 i386 i686 amd64 i5 i7 architecture processor confusion

The 64-bit Intel 386, also known as AMD64, is an architecture that behaves largely the same as its 32-bit counterpart at the assembler level.

Assembly code to access the m and g pointers on the 64-bit version is the same as on the 32-bit 386, except it uses MOVQ rather than MOVL.

Register BP is callee-save, which means it's automatically saved and restored when the frame size is larger than zero.

Using BP as a general-purpose register is allowed, but be aware that it can interfere with sampling-based profiling.

R10 points to the g (goroutine) structure, and within assembler source code, this pointer must be referred to as g, not R10.

Here are some useful aliases for memory locations:

  • 16(R1): The location at R1 plus 16.
  • (R1): Alias for 0(R1).

Ppc64

The PPC64 architecture is used by the Go language, specifically for the GOARCH values ppc64 and ppc64le.

This assembler is utilized for these values, which is a crucial detail for developers working with the Go language on these architectures.

The PPC64 assembler is referenced in the Go PPC64 Assembly Instructions Reference Manual, a valuable resource for those looking to dive deeper into the specifics of this architecture.

PPC64 is used for 64-bit PowerPC processors, which is essential to understand when working with this architecture.

Mips, Mips64

Credit: youtube.com, 1 2 1 Highlights of MIPS64 Architecture

General purpose registers are named R0 through R31, and floating point registers are F0 through F31.

The MIPS architecture has specific register usage, with R30 reserved to point to g and R23 used as a temporary register.

R30 is reserved to point to g.

In a TEXT directive, the frame size $-4 for MIPS or $-8 for MIPS64 instructs the linker not to save LR.

The value of GOMIPS environment variable (hardfloat or softfloat) is made available to assembly code by predefining either GOMIPS_hardfloat or GOMIPS_softfloat.

For the hardware register, use R29 when referring to SP.

The value of GOMIPS64 environment variable (hardfloat or softfloat) is made available to assembly code by predefining either GOMIPS64_hardfloat or GOMIPS64_softfloat.

Architecture Specific Details

Architecture-specific details can be a bit overwhelming, but don't worry, I've got you covered. To see what instructions are defined for a given machine, say ARM, look in the source for the obj support library for that architecture, located in the directory src/cmd/internal/obj/arm.

A programmer sits thoughtfully at his desk with computer screens and office setups, reflecting modern workplace dynamics.
Credit: pexels.com, A programmer sits thoughtfully at his desk with computer screens and office setups, reflecting modern workplace dynamics.

In that directory is a file a.out.go; it contains a long list of constants starting with A, like AAND, which represents the bitwise and instruction, and AND, which is written in assembly source as AND.

Each instruction begins with an initial capital A in this list, so the sequence of the A names has nothing to do with the actual encoding of the machine instructions. The cmd/internal/obj package takes care of that detail.

The architectures share syntax for common addressing modes such as (R1) (register indirect), 4(R1) (register indirect with offset), and $foo(SB) (absolute address).

For the 386 and AMD64 architectures, the instructions are listed in cmd/internal/obj/x86/a.out.go. The assembler also supports some (not necessarily all) addressing modes specific to each architecture.

Here are some key addressing modes for the 386 and AMD64 architectures:

  • 16(R1): The location at R1 plus 16.
  • (R1): Alias for 0(R1).

The runtime pointer to the g structure is maintained through the value of an otherwise unused register in the MMU. In the runtime package, assembly code can include go_tls.h, which defines an OS- and architecture-dependent macro get_tls for accessing this register.

The registers R10 and R11 are reserved by the compiler and linker.

Explore further: Golang Runtime

Advanced Topics

Credit: youtube.com, dotGo 2019 - Michael McLoughlin - Better x86 Assembly Generation with Go

Golang's assembly language is a low-level, symbolic representation of machine code. It's a crucial part of the language, allowing developers to write efficient and optimized code.

In Golang, assembly code is generated by the compiler for performance-critical functions. This is evident in the example of the `runtime.Memmove` function, which is implemented in assembly to ensure maximum efficiency.

Writing assembly code directly can be challenging, especially for complex operations. However, the Go language provides a set of pre-built functions, such as `runtime.Memmove`, that simplify this process.

By using these pre-built functions, developers can focus on writing high-level code without worrying about the intricacies of assembly language.

Bridging and ASM

Bridging and ASM can be a bit tricky, but it's actually quite straightforward once you understand the process.

To compile a program that uses assembly code, you need to create a Go version of the function declaration in a .go file alongside the .s file that contains the assembly implementation.

CSS code displayed on a computer screen highlighting programming concepts and technology.
Credit: pexels.com, CSS code displayed on a computer screen highlighting programming concepts and technology.

Think of it like defining an interface in Go and implementing it in assembly in another file.

The assembly code depends on the data declaration above, which is crucial for its functionality.

If you're having trouble understanding the assembly code, you can take a look at the source files provided, or check out the GitHub repository for this blog.

Benchmarks

Benchmarks are an essential part of measuring the performance of code. The author of the code in question wrote up some benchmarks to compare three versions of the code.

The three versions compared were the original Go version, the same Go code with //go:noinline to try to control for inlining, and the assembly version. The answer was 4.6ns.

The author noted that if it took them 4 hours to write this code, it would have to run 3.13043478 × 10 times to be worthwhile. This is a crucial point to consider when deciding whether to optimize code.

Here's a breakdown of the versions compared:

  1. The original Go version
  2. The same Go code with //go:noinline to try to control for inlining
  3. The assembly version

The author concluded that saving 15 or 20 nanoseconds per request is not useful, especially when considering the programmer overhead of assembly code.

Calvin Connelly

Senior Writer

Calvin Connelly is a seasoned writer with a passion for crafting engaging content on a wide range of topics. With a keen eye for detail and a knack for storytelling, Calvin has established himself as a versatile and reliable voice in the world of writing. In addition to his general writing expertise, Calvin has developed a particular interest in covering important and timely subjects that impact society.

Love What You Read? Stay Updated!

Join our community for insights, tips, and more.