Introduction to Golang assembly

Posted by bladx on Wed, 05 Jan 2022 12:34:28 +0100

When reading Golang source code, it is always stuck in its assembly code and does not read smoothly. Today, let's take a brief look at the assembly language in Golang.

Assembly Classification

Classify by Instruction Set Architecture (for CPU)

  1. X86 assembly (32bit): This architecture is often referred to as i386, x86

  2. The x86 assembly (64bit), often referred to as AMD64, Intel64, x86-64, x64, was designed by AMD, a 64-bit extension of the x86 architecture that was later made public

  3. ARM assembly, an ARM processor, is commonly used in embedded, mobile devices due to its high performance and low power consumption.

  4. ...

Categorize by assembly format (for people's reading habits)

  1. Intel Format
  2. AT&T Format

Usually we say that the golang collection belongs to plan9 style and is classified according to the second way. Its reading style (symbol) is different from Intel and AT&T. The plan9 compiler was developed by Bel Laboratories, the same group of people working on the unix operating system.

The Go assembly language is based on plan9 Assemblies But there are so many different CPU s in the real world. So the golang assembly, in the plan9 style, has multiple implementations of different instruction set architectures for the same method.

Where can I see the Golang assembly code

  1. Golang source code, such as src/runtime/asm_amd64.s, src/math/big/...
  2. Go tool compile-S main. Go, compile your own code into assembly code. For example, on my Mac Intel machine, the architecture of amd64, assembly code is generated as follows:
$ cat main.go 
package main

func main() {
        a, b := 0, 0
        println(a + b)
}
$ go tool compile -S main.go 
"".main STEXT size=66 args=0x0 locals=0x10 funcid=0x0
        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $16-0
        0x0000 00000 (main.go:3)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:3)        PCDATA  $0, $-2
        0x0004 00004 (main.go:3)        JLS     57
        0x0006 00006 (main.go:3)        PCDATA  $0, $-1
        0x0006 00006 (main.go:3)        SUBQ    $16, SP
        0x000a 00010 (main.go:3)        MOVQ    BP, 8(SP)
        0x000f 00015 (main.go:3)        LEAQ    8(SP), BP
        0x0014 00020 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:5)        PCDATA  $1, $0
        0x0014 00020 (main.go:5)        CALL    runtime.printlock(SB)
        0x0019 00025 (main.go:5)        XORL    AX, AX
...

Go Assembler Basic Syntax

1. Registers

Universal Register

Registers are related to physical machine architectures, which have different physical registers.

16 universal registers are provided on the amd64 architecture for user use

plan9 assembly language provides the following mappings for direct reference in assembly language to use physical registers.

amd64raxrbxrcxrdxrdirsirbprspr8r9r10r11r12r13r14rip
Plan9AXBXCXDXDISIBPSPR8R9R10R11R12R13R14PC

As used in the example above: SP,AX,R14,BP

Virtual Register

Go Assembler introduced four virtual registers

  • FP: Frame pointer: arguments and locals. Frame pointer, quick access to function parameters and return values
  • PC: Program counter: jumps and branches. A program counter that points to the address of the next instruction. In amd64 it's actually a rip register
  • SB: Static base pointer: global symbols. Static base address pointer, global symbol.
  • SP: Stack pointer: the highest address within the local stack frame. Stack pointer, pointing to a local variable

Usage:

  • FP:0(FP) represents the first parameter, and 8(FP) represents the second parameter (AMD64 architecture). first_arg+0(FP) means that the address of the first parameter is bound to the symbol first_arg

  • SP:localvar0-8(SP) represents the first local variable in the function in plan9. There are also SPs in physical registers, and hardware SPs really represent the top of the stack. So to distinguish whether SP refers to hardware SP or virtual registers. The plan9 code needs to be differentiated in a specific format. eg:symbol+offset(SP) represents the virtual register SP. offset(SP) means hardware SP. As in the example above, 8(SP) refers to hardware SP

  • PC: Except for individual jump management, it is generally not needed

  • SB: Indicates the global memory starting point. foo(SB) indicates that the symbol foo is used as a memory address. This form is used to declare global functions, data. foo+4(SB) indicates the address of the foo to the next 4 bytes. <> Restriction symbols can only be used in the current source file

Pictures stolen from the Internet:

2. Instructions

1. Variable declaration

Format: Declare a global variable using DATA and GLOBL

DATA symbol+offset(SB)/width, value
GLOBL symbol(SB), flag, $size

Meaning:

  • DATA part: Assign value s to bytes in symbol s and to bytes offset to offset + width.

  • GLOBL part: Must be followed by DATA to indicate that a global variable symbol of size size size is declared. flag Represents some properties of a variable, such as RODATA being read-only. Adding <> to GLOBL, such as GLOBL bio <> (SB), RODATA, $16 also means that this global variable only works in this file.

Practical examples:

// src/runtime/asm_amd64.s, argc declared here, argv is a parameter to the Go program
DATA _rt0_amd64_lib_argc<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argc<>(SB),NOPTR, $8
DATA _rt0_amd64_lib_argv<>(SB)/8, $0
GLOBL _rt0_amd64_lib_argv<>(SB),NOPTR, $8

NOPTR This is not a pointer and does not require a garbage collection scan

Local variable: It does not need to be declared in the stack frame. Rely directly on offset for removal and use. For example, 0(FP) represents the first parameter of the function and the first local variable in the localvar0-8(SP) function.

2. Function declaration

Format:

TEXT pkgname·funname(SB),flag,$framesize-argsize

Meaning:

pkgname: can be omitted, preferably omitted. Otherwise, cascade changes are needed to modify the package name.

funname:Declared function name

flag: Flags, such as NOSPLIT, we know that Go Runtime tracks the usage of each stack and dynamically increases itself. NOSPLIT flags prohibit checking and save money, but the person who writes the program should make sure the function is secure.

framesize: Function stack frame size = Local variable + Total size of calling other function parameter space

argsize: Some references say this is the parameter + return size, but there are some differences in the experiment. This should be related to the GO 1.7 update. GO1.7 Register-based Call Protocol GO 1.7 optimization

  • Before GO 1.7, parameter + return values exist in the stack frame
  • When GO 1.7 is updated, 9 universal registers are used to pass parameters and return values in preference, and the excess is left on the stack. And the return value in the register overrides the value in the parameter
  • Argument + return less than 9, argsize value is the size of the parameter
  • Return value > 9, argsize = parameter size + return value exceeds 9 parts

Practical examples:

$ cat main.go
package main

func main() {
}

func add(a int64, b int64) int64 {
        return a + b
}

$ go tool compile -S main.go
"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
...
"".add STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
        0x0000 00000 (main.go:6)        TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-16
        0x0000 00000 (main.go:6)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:6)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:6)        FUNCDATA        $5, "".add.arginfo1(SB)
        0x0000 00000 (main.go:7)        ADDQ    BX, AX
        0x0003 00003 (main.go:7)        RET
        ...

For example, there is no need to store a local variable within a function, framesize = 0, two int64 parameters, and argsize = 16

3. Common operating instructions

Autonomous Query Links

The following are common

1. Data handling
  • MOV directive: its suffix denotes the carrying length, $NUM denotes the specific number, as shown in the following example
MOVB $1, DI      	// 1 byte,  DI=1
MOVW $0x10, BX   	// 2 bytes, BX=10
MOVD $1, DX      	// 4 bytes, DX=1
MOVQ $-10, AX     // 8 bytes, AX=-10
  • LEA, loads valid addresses into the specified address register
// ret+24(FP), which represents the third function parameter, is an address
LEAQ	ret+24(FP), AX	// Move ret+24(FP) address to AX register
2. Computing instructions
  • ADD, SUB,IMULQ, such as the following examples
ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
IMULQ AX, BX   // BX *= AX
  • You can use calculation instructions to adjust stack space, and we know that SP points to the top of the stack, adjusting the values in SP is sufficient.
// Stack space: high address to low address
SUBQ $0x18, SP // Subtract SP, assign function stack frame to function
ADDQ $0x18, SP // Add SP, clear function stack frame
3. Conditional/unconditional jumps
  • JMP, JZ,JLS ...
// Unconditional jump
JMP addr   // Jump to the address, which can be the address in the code, but handwriting doesn't actually do that
JMP label  // Jump to label, you can jump to the label position within the same function
JMP 2(PC)  // Jump x lines forward/backward based on current instructions
JMP -2(PC) // Ditto

// Conditional Jump
JZ target // If zero flag is set, jump
JLS num		// If the comparison results of the previous line are smaller on the left than on the right, skip to the num address
4. Other
  • Compare: CMP, used with challenge instructions

    CMPQ	BX, $0	// Compare size with BX and 0
    JNE	3(PC)			// Left less than right executes the position of the third instruction after jumping to the current PC instruction
    
  • Bit operations: AND,OR,XOR

summary

After this article, I'm sure you can read some simple assemblers in general. A few source code compilation readings are recommended here.

  • Start point of Go program: src/runtime/asm_ Amd64. Rt0_in s Go(SB) function
  • Go Atomic Package: src/runtime/internal/atomic_ Amd64. Case function in S

Reference resources

  1. https://segmentfault.com/a/1190000039978109
  2. https://go.dev/doc/asm
  3. https://medium.com/martinomburajr/go-tools-the-compiler-part-1-assembly-language-and-go-ffc42cbf579d
  4. https://xargin.com/go-and-plan9-asm/
  5. https://kcode.icu/posts/go/2021-03-20-go-%E4%BD%BF%E7%94%A8%E7%9A%84-plan9-%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%88%9D%E6%8E%A2/
  6. https://mioto.me/2021/01/plan9-assembly/
  7. https://www.symbolcrash.com/2021/03/02/go-assembly-on-the-arm64/

Topics: Go Back-end