NOTE

3.5 Plan 9 Assembly

1. What Plan 9 assembly is 2. Registers 3. Instruction format 4. Basic instructions 5. Advanced instructions 6. Typical function stack frame 7. Usage 8. References

GoCreated Updated 3 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. What Is Plan 9 Assembly?

  • Pseudo-assembly
    • Like assembly, it still needs to be converted into machine instructions by an assembler before the CPU can execute it
    • The reason it is “pseudo” is that disassembling machine instructions cannot produce Go assembly; it can only produce platform-specific assembly

2. Registers

  • General-purpose registers The correspondence with AMD64 machine registers is as follows: Generally the first six are used to pass parameters, while BP and SP are used to manage the top and bottom of the stack.
  • Virtual registers No hardware register corresponds to them; they are generally used to store memory addresses.
    • SB register
      • Static base pointer: global symbols.
      • Stores the starting address of the program’s address space, i.e. the starting address of the code section
      • Used to locate global symbols
    • PC register
      • Program counter: jumps and branches.
      • Points to the address of the next instruction to be executed
    • SP register
      • Stack pointer: top of stack.
      • Points to the starting position of local variables in the current stack frame, using a form such as symbol+offset(SP). The symbol itself has no particular meaning
      • Used to reference function-local variables
    • FP register
      • Frame pointer: arguments and locals.
      • Points to the stack frame of the calling function (caller), not the called function (callee), using a form such as symbol+offset(FP). The symbol itself has no particular meaning
      • Used to reference function input parameters
      • first_arg+0(FP) references the first parameter passed by the caller, and second_arg+8(FP) references the second parameter
        • first_arg and second_arg have no meaning, similar to comments
        • +8 is the offset relative to the FP register

3. Instruction Format

3.1. Opcodes

AT&T-style register opcodes generally use lowercase letters and put a % before register names, while Go assembly uses uppercase letters and does not put % before register names.

# AT&T format
mov %rbp,%rsp

# Go assembly format
MOVQ BP,SP

3.2. Operands

AT&T format determines the operand width from the register name, while Go assembly adds a suffix after the opcode: B (8-bit), W (16-bit), D (32-bit), or Q (64-bit).

4. Basic Instructions

4.1. Stack Adjustment

Plan 9 does not provide push and pop; it implements them by operating on the SP register.

SUBQ $0x18, SP // The stack grows from high addresses to low addresses; subtracting from SP is equivalent to allocating a stack frame for the function
ADDQ $0x18, SP // The stack grows from high addresses to low addresses; adding to SP is equivalent to clearing the function's stack frame

4.2. Data Movement

Constants in Plan 9 assembly are represented with $num. They can be negative and are decimal by default. $0x123 can be used to represent a hexadecimal number.

// The amount moved is determined by the MOV suffix
MOVB $1, DI      // 1 byte
MOVW $0x10, BX   // 2 bytes
MOVD $1, DX      // 4 bytes
MOVQ $-10, AX    // 8 bytes

4.3. Arithmetic

Like data-movement instructions, the instruction suffix can be changed to correspond to operands of different widths, for example ADDQ/ADDW/ADDL/ADDB.

ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
IMULQ AX, BX   // BX *= AX

4.4. Conditional / Unconditional Jumps

// Unconditional jump
JMP addr   // Jump to an address. The address can be an address in the code, although this generally will not appear in handwritten code
JMP label  // Jump to a label; it can jump to a label within the same function
JMP 2(PC)  // Based on the current instruction, jump x lines forward/backward
JMP -2(PC) // Same as above

// Conditional jump
JNZ target // Jump if the zero flag has been set

5. Advanced Instructions

5.1. Variable Declarations

Generally read-only values stored in the .rodata or .data sections. At the application level, these correspond to initialized global const, var, and static variables/constants.

  • DATA instruction
// offset indicates the offset relative to the symbol
DATA    symbol+offset(SB)/width, value
  • GLOBAL instruction
// Compared with DATA, this adds a flag and the total size of the variable
GLOBL divtab(SB), RODATA, $64

5.2. Function Declarations

// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
......
  • TEXT runtime·gogo(SB): indicates that a global function (symbol) named gogo is defined in the code section, and the function belongs to the runtime package.
  • NOSPLIT: tells the compiler not to insert code in this function to check whether the stack overflows.
  • $16-8: the number 16 means this function’s stack frame size is 16 bytes; 8 means this function’s parameters and return values together need 8 bytes of memory.
    • There is no return value here, and there is one pointer parameter, which occupies 8 bytes
    • In Go, function-call parameters and return values are placed on the stack, and this part of stack memory is reserved by the caller rather than the called function, so the function definition needs to state how much space must be reserved in the caller’s stack frame

6. Typical Function Stack Frame

Go stack frame

7. How to Use It

7.1. Determine Which runtime Functions Application-Level Code Is Translated Into

7.1.1. defer

package asm1

import (
	"fmt"
	"testing"
)

func testDefer() {
	defer func() {
		fmt.Println("defer")
	}()
}

func TestAsm1(t *testing.T) {
	testDefer()
}
  • go tool compile -S ams1_test.go | grep -i "ams1_test.go:9"
0x0024 00036 (ams1_test.go:9)   PCDATA  $0, $0
0x0024 00036 (ams1_test.go:9)   PCDATA  $1, $0
0x0024 00036 (ams1_test.go:9)   MOVL    $0, ""..autotmp_1+8(SP)
0x002c 00044 (ams1_test.go:9)   PCDATA  $0, $1
0x002c 00044 (ams1_test.go:9)   LEAQ    "".testDefer.func1·f(SB), AX
0x0033 00051 (ams1_test.go:9)   PCDATA  $0, $0
0x0033 00051 (ams1_test.go:9)   MOVQ    AX, ""..autotmp_1+32(SP)
0x0038 00056 (ams1_test.go:9)   PCDATA  $0, $1
0x0038 00056 (ams1_test.go:9)   LEAQ    ""..autotmp_1+8(SP), AX
0x003d 00061 (ams1_test.go:9)   PCDATA  $0, $0
0x003d 00061 (ams1_test.go:9)   MOVQ    AX, (SP)
0x0041 00065 (ams1_test.go:9)   CALL    runtime.deferprocStack(SB)
0x0046 00070 (ams1_test.go:9)   TESTL   AX, AX
0x0048 00072 (ams1_test.go:9)   JNE     90
0x005a 00090 (ams1_test.go:9)   XCHGL   AX, AX
0x005b 00091 (ams1_test.go:9)   CALL    runtime.deferreturn(SB)
0x0060 00096 (ams1_test.go:9)   MOVQ    64(SP), BP
0x0065 00101 (ams1_test.go:9)   ADDQ    $72, SP
0x0069 00105 (ams1_test.go:9)   RET
0x006a 00106 (ams1_test.go:9)   NOP
0x0000 00000 (ams1_test.go:9)   TEXT    "".testDefer.func1(SB), ABIInternal, $88-0
0x0000 00000 (ams1_test.go:9)   MOVQ    TLS, CX
0x0009 00009 (ams1_test.go:9)   MOVQ    (CX)(TLS*2), CX
0x0010 00016 (ams1_test.go:9)   CMPQ    SP, 16(CX)
0x0014 00020 (ams1_test.go:9)   JLS     134
0x0016 00022 (ams1_test.go:9)   SUBQ    $88, SP
0x001a 00026 (ams1_test.go:9)   MOVQ    BP, 80(SP)
0x001f 00031 (ams1_test.go:9)   LEAQ    80(SP), BP
0x0024 00036 (ams1_test.go:9)   FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0024 00036 (ams1_test.go:9)   FUNCDATA        $1, gclocals·568470801006e5c0dc3947ea998fe279(SB)
0x0024 00036 (ams1_test.go:9)   FUNCDATA        $2, gclocals·bfec7e55b3f043d1941c093912808913(SB)
0x0024 00036 (ams1_test.go:9)   FUNCDATA        $3, "".testDefer.func1.stkobj(SB)
0x0086 00134 (ams1_test.go:9)   PCDATA  $1, $-1
0x0086 00134 (ams1_test.go:9)   PCDATA  $0, $-1
0x0086 00134 (ams1_test.go:9)   CALL    runtime.morestack_noctxt(SB)
0x008b 00139 (ams1_test.go:9)   JMP     0
  • Then look for runtime.deferprocStack and runtime.deferreturn in the runtime source code.

7.1.2. map

package asm1

import (
	"fmt"
	"testing"
)

func TestAsm2(t *testing.T) {
	testMap()
}

func testMap() {
	var a = map[int]int{}
	a[1] = 1
	fmt.Println(a)
}
  • go tool compile -S ams2_test.go | grep -i "ams2_test.go:13"
0x0028 00040 (ams2_test.go:13)  PCDATA  $0, $0
0x0028 00040 (ams2_test.go:13)  PCDATA  $1, $0
0x0028 00040 (ams2_test.go:13)  CALL    runtime.makemap_small(SB)
0x002d 00045 (ams2_test.go:13)  PCDATA  $0, $1
0x002d 00045 (ams2_test.go:13)  MOVQ    (SP), AX
0x0031 00049 (ams2_test.go:13)  PCDATA  $1, $1
0x0031 00049 (ams2_test.go:13)  MOVQ    AX, ""..autotmp_22+64(SP)
  • Then look for runtime.makemap_small in the runtime source code.

7.2. Check Whether Memory Is Allocated on the Heap

7.2.1. new

package asm1

import (
	"fmt"
	"testing"
)

func TestAsm3(t *testing.T) {
	testNew()
}

func testNew() {
	var a = new([]int)
	fmt.Println(a)
}
  • go tool compile -S asm3_test.go | grep -i "asm3_test.go:13"
0x0024 00036 (asm3_test.go:13)  PCDATA  $0, $1
0x0024 00036 (asm3_test.go:13)  PCDATA  $1, $0
0x0024 00036 (asm3_test.go:13)  LEAQ    type.[]int(SB), AX
0x002b 00043 (asm3_test.go:13)  PCDATA  $0, $0
0x002b 00043 (asm3_test.go:13)  MOVQ    AX, (SP)
0x002f 00047 (asm3_test.go:13)  CALL    runtime.newobject(SB)
0x0034 00052 (asm3_test.go:13)  PCDATA  $0, $1
0x0034 00052 (asm3_test.go:13)  MOVQ    8(SP), AX
  • You can see that runtime.newobject is called, so it is allocated on the heap.

8. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub