Why watch the Plan9 compilation? If you are a Go developer, it is essential to learn and understand Plan9, because it can solve your understanding of a piece of code (why not? Is that ok?) .

Plan9 is different from AT&T and Intel assemblers, but it is helpful to understand Plan9 if you know the syntax of these two assemblers.

confusion

// Why does this function return -1
func demo1(a) int {
	ret := - 1
	defer func(a) {
		ret = 1} ()return ret
}
// output: -1


// Why does this function return 1
func demo2(a) (ret int) {
	defer func(a) {
		ret = 1} ()return ret
}
// output: 1
Copy the code

I’m sure most of you have seen a similar solution, demo1 is caused by temporary variables, demo2 has no temporary variables, this is the final result.

What is going on at the assembly level? This article will explore this question. (The platform used in this article is MacOS AMD64.) Instruction sets and registers vary from platform to platform.

basis

Universal register

Here is how the names of general-purpose registers correspond in IA64 and PLAN9:

IA64 RAX RBX RCX RDX RDI RSI RBP RSP R8 R9 R10 R11 R12 R13 R14 RIP
Plan9 AX BX CX DX DI SI BP SP R8 R9 R10 R11 R12 R13 R14 PC

The general purpose registers are AX, BX, CX, DX, DI, SI, R8~R15. Although BP and SP can also be used, BP and SP are used to manage the top and bottom of the stack. It is best not to use them for calculation.

The operand orientation of the Plan9 assembly is the opposite of that of Intel assembly, similar to AT&T.

Pseudo register

Go assembly introduces four pseudo registers, officially defined as follows:

  • FP: Frame pointer: arguments and locals.
  • PC: Program counter: jumps and branches.
  • SB: Static base pointer: global symbols.
  • SP: Stack pointer: top of stack.

The following are some descriptions for FP and SP:

  • FP: the use form is as followssymbol+offset(FP)By referring to the function’s input parameters. Eg:first_arg+0(FP).second_arg+8(FP)
  • SP: SP has a corresponding register, so to distinguish whether SP refers to hardware SP or virtual register, need to distinguish in a specific format. Eg:symbol+offset(SP)Is the pseudo register SP. Eg:offset(SP)Indicates hardware SP.

Variable declarations

Use DATA with GLOBL to define a variable.

DATA	symbol+offset(SB)/width, value
Copy the code

Declare the variable as global with the GLOBL directive, taking two additional arguments, flag and the total size of the variable.

GLOBL divtab(SB), RODATA, $8
Copy the code

GLOBL must be followed by the DATA directive. Here is a complete example of a global variable that defines multiple readOnly’s:

DATA pi+0(SB)/8, $3.1415926
GLOBL pi(SB), RODATA, $8
Copy the code

To define an array or string in a global variable, add the <> symbol to make the variable operate with an offset. Example:

DATA array<>+0(SB)/8, $1
DATA array<>+8(SB)/8, $2
Copy the code

It is generally recommended to use <> directly for variable declarations.

Function declaration

// Function declaration
// This declaration is usually written in any.go file, for example, add.go
func add(a, b int) int

// Function implementation
// This implementation is usually written in an _{Arch}.s file with the same name as the declaration, for example, add_amd64.sThe TEXT pkgname, add (SB), NOSPLIT, $0- 16
    MOVQ a+0(FP), AX
    MOVQ a+8(FP), BX
    ADDQ AX, BX
    MOVQ BX, ret+16(FP)
    RET
Copy the code

Pkgname can not be written, generally is not written, you can refer to the go source, in addition to add before · not.

Parameters and return value size | TEXT pkgname, add (SB), NOSPLIT, 16 $0 - | | | package name function name Stack frame size (local variable + may need additional calls the function of the total size of the parameter space, but not call other functions of ret address size)Copy the code

The above used RODATA, NOSPLIT flag, and other values, can refer to: golang.org/doc/asm#dir… .

It is important to note that all SP are currently hardware registers SP, with or without symbol, for compiling code that prints go tool compile-s/go tool objdump.

The following is the schematic diagram of the stack structure in the figure above. Since there are no temporary variables, the pseudo-SP and the hardware SP are in the same position.

+------------------+
| return parameter |
+------------------+
|   parameter b    | 
+------------------+
|   parameter a    | <-- pseudo FP addr
+------------------+
| caller ret addr  | <-- pseudo SP addr and hardware SP addr
+------------------+
Copy the code

Analysis of the

Compile/decompile

A lot of times we can’t determine how a piece of code is executed, we need to generate assembly, disassembly to study

/ / compile
go build -gcflags="-S"
go tool compile -S hello.go
go tool compile -N -S hello.go // Disable optimization
/ / decompiling
go tool objdump <binary>
Copy the code

Now that you’ve covered the basics, it’s time to dig into the differences between these two pieces of code.

// go tool compile -S demo1.go
func demo1(a) int {
	ret := - 1
	defer func(a) {
		ret = 1} ()return ret
}

// Compile demo1 part of the code
"".demo1 STEXT size=158 args=0x8 locals=0x30
        0x0000 00000 (.\scratch.go:5)   TEXT    "".demo1(SB), ABIInternal, $48- 8 -

        // Stack initialization, GC related tags, etc..// 15(SP) does not know why this is happening, but I guess it is because of the deferreturn.
        0x002c 00044 (.\scratch.go:5)   MOVB    $0.""..autotmp_3+15(SP)
        
        // address 56(SP) is assigned with a 0, which is actually the return address
        0x0031 00049 (.\scratch.go:5)   MOVQ    $0."".~r0+56(SP)
        
        // 16(SP) temp ret writes -1 to the stack.
        0x003a 00058 (.\scratch.go:6)   MOVQ    $- 1."".ret+16(SP)

        // Guess about deferreturn.
        0x0043 00067 (.\scratch.go:7)   LEAQ    "". Not. Func1 f. (SB), AX0x004a 00074 (.\scratch.go:7)   MOVQ    AX, ""..autotmp_4+32(SP)

        // add 16(SP) to AX register where -1 is stored
        0x004f 00079 (.\scratch.go:7)   LEAQ    "".ret+16(SP), AX

        // Add 16(SP) to 24(SP)
        0x0054 00084 (.\scratch.go:7)   MOVQ    AX, ""..autotmp_5+24(SP)
        0x0059 00089 (.\scratch.go:7)   MOVB    $1.""..autotmp_3+15(SP)

        // add the value of 16(SP) to the AX register
        0x005e 00094 (.\scratch.go:10)  MOVQ    "".ret+16(SP), AX
        
        // The value of AX is 56(SP), 56(SP) is the return address, so the current return value is -1
        // This is also the last operation 56(SP), so the final return value is -1
        0x0063 00099 (.\scratch.go:10)  MOVQ    AX, "".~r0+56(SP)
        0x0068 00104 (.\scratch.go:10)  MOVB    $0.""..autotmp_3+15(SP)

        // The value of 24(SP) is given to AX. 24(SP) stores the address of 16(SP), which is the address of the temporary variable
        0x006d 00109 (.\scratch.go:10)  MOVQ    ""..autotmp_5+24(SP), AX

        // Assign AX to 0(SP), i.e. assign 16(SP) to 0(SP)
        // 0(SP) can be used as an input to call demo1.func1
        0x0072 00114 (.\scratch.go:10)  MOVQ    AX, (SP)
        0x0076 00118 (.\scratch.go:10)  PCDATA  $1, $1

        / / call not func1
        0x0076 00118 (.\scratch.go:10)  CALL    "".demo1.func1(SB)
        0x007b 00123 (.\scratch.go:10)  MOVQ    40(SP), BP
        0x0080 00128 (.\scratch.go:10)  ADDQ    $48, SP
        0x0084 00132 (.\scratch.go:10)  RET
        0x0085 00133 (.\scratch.go:10)  CALL    runtime.deferreturn(SB)
        0x008a 00138 (.\scratch.go:10)  MOVQ    40(SP), BP
        0x008f 00143 (.\scratch.go:10)  ADDQ    $48, SP
        0x0093 00147 (.\scratch.go:10)  RET
        0x0094 00148 (.\scratch.go:10)  NOP
        0x0094 00148 (.\scratch.go:5)   PCDATA  $1, $- 1
        0x0094 00148 (.\scratch.go:5)   PCDATA  $0, $2 -
        0x0094 00148 (.\scratch.go:5)   CALL    runtime.morestack_noctxt(SB)
        0x0099 00153 (.\scratch.go:5)   PCDATA  $0, $- 1
        0x0099 00153 (.\scratch.go:5)   JMP     0

"".demo1.func1 STEXT nosplit size=13 args=0x8 locals=0x0
        $0-8 here is the function after defer in the go code with only one argument and no return value
        0x0000 00000 (.\scratch.go:8)   TEXT    "".demo1.func1(SB), NOSPLIT|ABIInternal, $0- 8 -
        0x0000 00000 (.\scratch.go:8)   FUNCDATA        $0, gclocals1a65e721a2ccc325b382662e7ffee780(SB)
        0x0000 00000 (.\scratch.go:8)   FUNCDATA        $1, gclocals69c1753bd5f81501d95132d08af04464(SB)

        // Assign the value of 8(SP) to the AX register, i.e. assign the address of 16(SP) to AX
        0x0000 00000 (.\scratch.go:9)   MOVQ    "".&ret+8(SP), AX

        // assign 1 to the location where the AX register is stored. This operation is like *a = 1
        0x0005 00005 (.\scratch.go:9)   MOVQ    $1, (AX)
        0x000c 00012 (.\scratch.go:10)  RET

Copy the code

Here with the explanation of the first paragraph of the compilation will not do a special specific description, only note the focus of attention.

// go tool compile -S demo2.go
func demo2(a) (ret int) {
	defer func(a) {
		ret = 1} ()return ret
}

// Compile demo2 part of the code
"".demo2 STEXT size=138 args=0x8 locals=0x28
        0x0000 00000 (.\scratch.go:6)   TEXT    "".demo2(SB), ABIInternal, $40- 8 -.0x002c 00044 (.\scratch.go:6)   MOVB    $0.""..autotmp_2+15(SP)
        0x0031 00049 (.\scratch.go:6)   MOVQ    $0."".ret+48(SP)
        0x003a 00058 (.\scratch.go:7)   LEAQ    "". Demo2. Func1 f. (SB), AX0x0041 00065 (.\scratch.go:7)   MOVQ    AX, ""..autotmp_3+24(SP)
        
        // Give the return address to the AX register
        0x0046 00070 (.\scratch.go:7)   LEAQ    "".ret+48(SP), AX
        
        // Return value address given to 16(SP)
        0x004b 00075 (.\scratch.go:7)   MOVQ    AX, ""..autotmp_4+16(SP)
        0x0050 00080 (.\scratch.go:10)  MOVB    $0.""..autotmp_2+15(SP)

        // return the address to the AX register
        0x0055 00085 (.\scratch.go:10)  MOVQ    ""..autotmp_4+16(SP), AX

        // return address to 0(SP)
        // 0(SP) can be used as an input to call demo2.func1
        0x005a 00090 (.\scratch.go:10)  MOVQ    AX, (SP)
        0x005e 00094 (.\scratch.go:10)  PCDATA  $1, $1
        0x005e 00094 (.\scratch.go:10)  NOP

        // Called demo2.func1
        0x0060 00096 (.\scratch.go:10)  CALL    "".demo2.func1(SB)
        0x0065 00101 (.\scratch.go:10)  MOVQ    32(SP), BP
        0x006a 00106 (.\scratch.go:10)  ADDQ    $40, SP
        0x006e 00110 (.\scratch.go:10)  RET
        0x006f 00111 (.\scratch.go:10)  CALL    runtime.deferreturn(SB)
        0x0074 00116 (.\scratch.go:10)  MOVQ    32(SP), BP
        0x0079 00121 (.\scratch.go:10)  ADDQ    $40, SP
        0x007d 00125 (.\scratch.go:10)  RET
        0x007e 00126 (.\scratch.go:10)  NOP
        0x007e 00126 (.\scratch.go:6)   PCDATA  $1, $- 1
        0x007e 00126 (.\scratch.go:6)   PCDATA  $0, $2 -
        0x007e 00126 (.\scratch.go:6)   NOP
        0x0080 00128 (.\scratch.go:6)   CALL    runtime.morestack_noctxt(SB)
        0x0085 00133 (.\scratch.go:6)   PCDATA  $0, $- 1
        0x0085 00133 (.\scratch.go:6)   JMP     0

"".demo2.func1 STEXT nosplit size=13 args=0x8 locals=0x0
        0x0000 00000 (.\scratch.go:7)   TEXT    "".demo2.func1(SB), NOSPLIT|ABIInternal, $0- 8 -
        0x0000 00000 (.\scratch.go:7)   FUNCDATA        $0, gclocals1a65e721a2ccc325b382662e7ffee780(SB)
        0x0000 00000 (.\scratch.go:7)   FUNCDATA        $1, gclocals69c1753bd5f81501d95132d08af04464(SB)
        0x0000 00000 (.\scratch.go:8)   MOVQ    "".&ret+8(SP), AX
        // Change the return value to 1
        0x0005 00005 (.\scratch.go:8)   MOVQ    $1, (AX)
        0x000c 00012 (.\scratch.go:9)   RET

Copy the code

The above is the two methods of assembly source parsing, from the two chestnuts can be obtained.

Ret in Demo1 is a temporary variable, and although defer did change the ret value, it has nothing to do with the return value, and the return value of Demo1 was already determined in assembly before demo1.func1 was called, so demo1 returned -1.

Ret in Demo2 points directly to the address of the return value, and defer has changed the value of the return value, so Demo2 returns 1.

reference

  1. Golang.org/doc/asm#dir…
  2. Xargin.com/plan9-assem…
  3. www.doxsey.net/blog/go-and…
  4. davidwong.fr/goasm/