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
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, andsecond_arg+8(FP)references the second parameterfirst_argandsecond_arghave no meaning, similar to comments+8is the offset relative to the FP register
- SB 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) namedgogois defined in the code section, and the function belongs to theruntimepackage.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

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.deferprocStackandruntime.deferreturnin 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_smallin 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.newobjectis called, so it is allocated on the heap.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub