An in depth look at golang multi value returns and the implementation of closures

  • 2020-06-03 06:49:13
  • OfStack

1. Introduction

golang has a lot of new features. When you use golang, have you ever thought about how these features are implemented? Of course, you might say, don't understand these features like also does not affect your use golang, what you said is reasonable, however, know more about the underlying implementation principle, for when using golang horizon is no 1 sample, is similar to read http after implementation, using http framework, and not seen http framework horizon is not 1 sample, of course, if you are 1 it amateur, curiosity will lead you to learn.

2. This article mainly analyzes two points:

1. Realization of golang multi-value return;

2. Implementation of golang closure;

3. Implementation of golang multi-value return

Learning C/C + +, we have a lot of people should understand C/C + + function call process, parameter is through registers di and si (assuming that two parameters) is passed to the called function, the called function returns the result can only be through eax register is returned to the calling function, thus C/C + + function can return a value, so we can imagine, more golang value can return by multiple registers, Like using multiple registers to pass argument 1?

This is also one option, but golang did not use it; My understanding is that the introduction of multiple registers to store return values will cause a rearrangement of the USES of multiple registers, which will undoubtedly increase the complexity. It can be said that the ABI of golang is very different from C/C++.

Before analyzing golang multi-value return from the perspective of assembly, it is necessary to be familiar with 1 convention of golang assembly code. It is explained in the golang official website, and 4 symbols are highlighted here. It should be noted that the register here is a pseudo register:

1.FP bottom stack register, pointing to the top of a function stack;

2.PC program counter, pointing to the next instruction;

3.SB refers to the base pointer of static data and global symbol;

SP top stack register; 4.

The most important of these are FP and SP. The FP registers are mainly used to fetch parameters and store return values. The implementation of golang function call largely depends on these two registers.

|  The return value 2 | \
+-----------+  \
|  The return value 1 |  \
|  parameter 2 |   These are in the calling function 
|  parameter 1 |   /
+-----------+  /
|  The return address  | /
+-----------+--\/-----fp value 
|  A local variable  | \
| ... |  Called stack frames 
|   | /
+-----------+--/+---sp value 

This is golang's 1 function stack, which means the function passes through fp+offset , and multiple return values are also passed fp+offset Stored in the stack frame of the calling function.

The following is an example to analyze

package main

import "fmt"

func test(i, j int) (int, int) {
a:=i+ j
b:=i- j
 return a,b

func main() {
a,b:= test(2,1)
 fmt.Println(a, b)

This example is very simple, mainly to illustrate the golang multi-value return process; We compile the program with the following command

go tool compile -S test.go > test.s

You can then open test.s to take a look at the assembly code for this small program. Let's start with the assembly code for the test function

"".test t=1size=32value=0args=0x20locals=0x0
0x000000000(test.go:5) TEXT"".test(SB),$0-32// The stack size for 32 byte 
0x000000000(test.go:5)MOVQ"".i+8(FP),CX// Take the first 1 A parameter i
0x000500005(test.go:5)MOVQ"".j+16(FP),AX// Take the first 2 A parameter j
0x000a00010(test.go:5) FUNCDATA$0, gclocals ・ a8eabfc4a4514ed6b3b0c61e9680e440(SB)
0x000a00010(test.go:5) FUNCDATA$1, gclocals ・ 33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000a00010(test.go:6)MOVQCX,BX// will i In the bx
0x000d00013(test.go:6) ADDQAX,CX//i+j In the cx
0x001000016(test.go:7) SUBQAX,BX//i-j In the bx
 // Stores the return result in the calling function stack frame 
 // Stores the return result in the calling function stack frame 

This assembly code can be seen in the test Inside the function, the first parameter is taken by fp+8, fp+16 Take the second parameter; Then store the first value returned fp+24 , the second value returned is saved fp+32 , which is exactly the same as what I said above; golang function call procedure is passed fp+offset To realize the transmission of parameters and return value, unlike C/C++ through registers to achieve the transmission of parameters and return value;

However, there is a problem here, my variables are all of type int, why the allocation is all 8 bytes, this remains to be proven.

I wanted to verify the previous conclusion by looking at the stack frame of main function, but golang automatically turns the small function into an inline function, so you can compile it yourself. Instead of calling the test function, main directly copies the assembly code of test function into main function to execute.

4. Implementation of golang closure

I went to see C++11 before lambda The realization of the function, in fact, the realization principle is the affine function; The compiler is compiling lambda Function generates an anonymous affine class and executes this lambda When a function is called, the compiled generated anonymous affine class overloads the function call method, which is lambda A method defined in a function; In fact, the implementation of golang closure is similar to this, as illustrated by an example



 a = a + i

 f := test(1)
 a := f(2)
 b := f(3)

This is a very simple example, test The function passes in an integer argument a , returns 1 function type; This function type passes in an integer argument and returns an integer value; main A function call test Function, which returns 1 closure function.

Look at test Assembly code of the function:

"".test t=1size=160value=0args=0x10locals=0x20
0x000000000(test.go:5) TEXT"".test(SB),$32-16
0x000900009(test.go:5) CMPQSP,16(CX)
0x000d00013(test.go:5) JLS142
0x000f00015(test.go:5) SUBQ$32,SP
0x001300019(test.go:5) FUNCDATA$0, gclocals ・ 8edb5632446ada37b0a930d010725cc5(SB)
0x001300019(test.go:5) FUNCDATA$1, gclocals ・ 008e235a1392cc90d1ed9ad2f7e76d87(SB)
0x001300019(test.go:5) LEAQ,BX
0x001a00026(test.go:5)MOVQBX, (SP)
0x001e00030(test.go:5) PCDATA$0,$0
 // generate 1 a int Type object, i.e a
 //8(sp) That is generated a The address, put in AX
 // will a To the address of sp+24 The location of the 
 // Take out the main The pass in of the function 1 Is the number of parameters, i.e a
 // will a In the (AX) Points to the newly generated memory described above int Type of object 
0x003200050(test.go:5)MOVQBP, (AX)
0x003500053(test.go:6) LEAQ type.struct { F uintptr; a *int }(SB), BX
0x003c00060(test.go:6)MOVQBX, (SP)
0x004000064(test.go:6) PCDATA$0,$1
 //8(sp)这就是上述 generate 的struct Address of the object 
 //test Internal anonymous function addresses are stored BP
0x004a00074(test.go:6) LEAQ"".test.func1(SB),BP
 // Put the anonymous function address in (AX) Points to the address given above 
 //F uintptr The assignment 
0x005100081(test.go:6)MOVQBP, (AX)
 // Will generate the above integer object a To the address of BP
0x005e00094(test.go:6) CMPB runtime.writeBarrier(SB),$0
 // will a Address in AX Pointing to the memory +8 . 
 // Is the above structure a *int The assignment 
 // Store the address of the above structure main Function stack frame; 
0x007000112(test.go:9) ADDQ$32,SP

I saw a sentence earlier that describes closures visually

Classes are behavior data, closures are behavior data;

That is, closures are context-aware, so let's take the test example and pass test The closure function generated by the function has its own a, this one a It's the context data of the closure, and this a 1 is accompanied by its closure function, every time it is called, a Will change;

We've analyzed the assembly code above to see how closures work; In this test example, because a Is the context data of the closure, therefore a Must be allocated on the heap, if allocated on the stack, the function ends, a It's also recycled; An anonymous structure is then defined:

 F uintptr// This is the function pointer called by the closure 
 a *int// This is the context data for the closure 

Then 1 of this object is generated and the integer object that was previously allocated on the heap is disposed a Is assigned to the a pointer in the structure, which is then called by the closure func Function addresses are assigned to the structure F Pointer; Thus, for each closure function generated, you are essentially generating one of the above struct objects, and each closure object has its own data a And the calling function F ; Finally, the address of the structure is returned to main Functions;

Look at main The process of obtaining a closure by a function;

"".main t=1size=528value=0args=0x0locals=0x88
0x000000000(test.go:12) TEXT"".main(SB),$136-0
0x000900009(test.go:12) LEAQ -8(SP),AX
0x000e00014(test.go:12) CMPQAX,16(CX)
0x001200018(test.go:12) JLS506
0x001800024(test.go:12) SUBQ$136,SP
0x001f00031(test.go:12) FUNCDATA$0, gclocals ・ f5be5308b59e045b7c5b33ee8908cfb7(SB)
0x001f00031(test.go:12) FUNCDATA$1, gclocals ・ 9d868b227cedd8dd4b1bec8682560fff(SB)
 // The parameter 1(f:=test(1)) In the main Function stack 
0x001f00031(test.go:13)MOVQ$1, (SP)
0x002700039(test.go:13) PCDATA$0,$0
 // call main The function generates the closure object 
 // Put the address of the closure object in DX
 // The parameter 2(a:=f(2)) In the stack 
0x003100049(test.go:14)MOVQ$2, (SP)
 // Assigns a function pointer to a closure object BX
0x004100065(test.go:14) PCDATA$0,$1
 // This calls the closure function and passes in the address of the closure object 
 // Closure functions, in order to modify a ! 

Obviously, main A function call test The function takes the address of the closure object, finds the closure function from the address of the closure object, executes the closure function, and passes the address of the closure object into the function, in the same way as C++ passes this pointer principle 1, in order to modify the member variable a ;

The last to see test Internal anonymous functions (closure function implementation):

"".test.func1t=1size=32value=0args=0x10 locals=0x0
0x000000000(test.go:6) TEXT"".test.func1(SB), $0-16
0x000000000(test.go:6) NOP
0x000000000(test.go:6) NOP
0x000000000(test.go:6) FUNCDATA $0, gclocals ・ 23e8278e2b69a3a75fa59b23c49ed6ad(SB)
0x000000000(test.go:6) FUNCDATA $1, gclocals ・ 33cdeccccebe80329f1fdbee7f5874cb(SB)
//DX Is the address of the closure object, +8 namely a The address of the 
0x000000000(test.go:6) MOVQ8(DX), AX
//AX for a The address, (AX) Is the a The value of the 
0x000400004(test.go:7) MOVQ (AX), BP
// The parameter i deposit R8
0x000700007(test.go:7) MOVQ"".i+8(FP), R8
//a+i The value of the deposit BP
0x000c00012(test.go:7) ADDQ R8, BP
// will a+i deposit a The address of the 
0x000f00015(test.go:7) MOVQ BP, (AX)
// will a Address latest data stored BP
0x001200018(test.go:8) MOVQ (AX), BP
// will a The latest value is put in as the return value main Function in the stack 
0x001500021(test.go:8) MOVQ BP,"".~r1+16(FP)
0x001a00026(test.go:8) RET

Closure function call procedure:

1. Get the address of closure context data a through the closure object address;

2. The value of a is then obtained through the address of a and added to the parameter i;

3. Store a+i as the latest value in the address of a;

4. Return the latest value of a to main function;

5. To summarize

This article briefly analyzes the implementation of golang multi-value returns and closures from an assembly perspective;

The multi-value return is mainly achieved by using THE fp register +offset to obtain parameters and store the return value.

Closures are implemented primarily by generating structures that contain closure functions and closure context data at compile time;

The above is the whole content of this article. I hope it can be helpful for you to study or only use golang. If you have any questions, you can leave a message to communicate.

Related articles: