1. Preface
Golang has many novel features. I wonder if you have thought about how these features are implemented when you use them? Of course, you may say that not understanding these features does not seem to affect your use of golang, and what you said makes sense, but understanding the underlying implementation principles will have a completely different vision when using golang. It is similar to using http framework after seeing the implementation of http, which is different from the vision when you have not seen the http framework. Of course, if you are an IT enthusiast, your desire for knowledge will naturally guide you to learn.
2. This article mainly analyzes two points:
1. Implementation of golang multi-value return;
2. Implementation of golang closure;
Implementation of golang multi-value return
When we are learning C/C++, many people should have understood the C/C++ function calling process. Parameters are passed to the called function through registers di and si (assuming that there are two parameters). The return result of the called function can only be returned to the called function through the eax register. Therefore, the C/C++ function can only return one value. So can we imagine that golang's multi-value return can be achieved through multiple registers, just like using multiple registers to pass parameters?
This is also a way, but golang did not adopt it; my understanding is that introducing multiple registers to store the return value will cause reconciliation of the uses of multiple registers, which undoubtedly increases the complexity; it can be said that golang's ABI is very different from C/C++;
Before analyzing golang multi-value return from an assembly perspective, you need to be familiar with some conventions of golang assembly code. Golang official website has an explanation. Here we focus on explaining four symbols. It should be noted that the registers here are pseudo-registers:
The bottom register of the stack pointing to the top of a function stack;
Program counter, pointing to the next execution instruction;
Base pointer to static data, global symbol;
Top-stack register;
The most important ones here are FP and SP. FP registers are mainly used to retrieve parameters and store return values. The implementation of golang function calls depends largely on these two registers. Here is the result first.
+-----------+---\ | Return value2 | \ +-----------+ \ | Return value1 | \ +---------+-+ | parameter2 | These are in the calling function +-----------+ | parameter1 | / +-----------+ / | Return to address | / +-----------+--\/-----fpvalue | Local variables | \ | ... | Called number stack | | / +-----------+--/+---spvalue
This is a function stack of golang, which means that the function parameter is passed throughfp+offset
to achieve, and multiple return values are also achieved throughfp+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) (a, b) }
This example is very simple, mainly to illustrate the process of golang multi-value return; we compile the program through the following command
go tool compile -S >
Then, you can open it and take a look at the assembly code of this applet. First, let’s look at the assembly code of the test function
"".test t=1size=32value=0args=0x20locals=0x0 0x000000000(:5) TEXT"".test(SB),$0-32//The stack size is 32 bytes0x000000000(:5)NOP 0x000000000(:5)NOP 0x000000000(:5)MOVQ"".i+8(FP),CX//Please the first parameter i0x000500005(:5)MOVQ"".j+16(FP),AX//Take the second parameter j0x000a00010(:5) FUNCDATA$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB) 0x000a00010(:5) FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x000a00010(:6)MOVQCX,BX//Put i into bx0x000d00013(:6) ADDQAX,CX//I+j put cx0x001000016(:7) SUBQAX,BX//I-j put bx //Save the return result into the call function stack frame0x001300019(:8)MOVQCX,"".~r2+24(FP) //Save the return result into the call function stack frame0x001800024(:8)MOVQBX,"".~r3+32(FP) 0x001d00029(:8)RET
It can be seen from this assembly code thattest
Inside the function, the first parameter is taken through fp+8.fp+16
Take the second parameter; then save the returned first value infp+24
, the second value returned is stored infp+32
, exactly the same as what I said above; the golang function call process is throughfp+offset
To implement parameters and return values, unlike C/C++, which implement parameters and return values through registers;
However, there is a question here. My variables are all int type, so why are all allocated 8 bytes? This remains to be verified.
I originally wanted to verify the previous conclusion by checking the stack frame of the main function, but golang automatically converts the small function into an inline function, so you can compile it yourself. The main function does not call the test function, but directly copy the assembly code of the test function into the main function for execution.
IV. Implementation of golang closure
I've seen C++11 beforelambda
The implementation principle of function is actually functors; the compiler is compilinglambda
When a function is used, an anonymous functor class will be generated and then this is executed.lambda
When a function is used, the compiled and generated anonymous functor class overload function call method will be called, which is the methodlambda
Methods defined in functions; in fact, the implementation of golang closure is similar to this, we will illustrate through examples
packagemain import"fmt" functest(aint)func(iint)int{ returnfunc(iint)int{ a = a + i returna } } funcmain(){ f := test(1) a := f(2) (a) b := f(3) (b) }
This example program is very simple.test
Pass an integer parameter to the functiona
, return a function type; this function type passes an integer parameter and returns an integer value;main
Function Callstest
function, return a closure function.
Let's take a looktest
Assembly code of function:
"".test t=1size=160value=0args=0x10locals=0x20 0x000000000(:5) TEXT"".test(SB),$32-16 0x000000000(:5)MOVQ(TLS),CX 0x000900009(:5) CMPQSP,16(CX) 0x000d00013(:5) JLS142 0x000f00015(:5) SUBQ$32,SP 0x001300019(:5) FUNCDATA$0, gclocals·8edb5632446ada37b0a930d010725cc5(SB) 0x001300019(:5) FUNCDATA$1, gclocals·008e235a1392cc90d1ed9ad2f7e76d87(SB) 0x001300019(:5) LEAQ (SB),BX 0x001a00026(:5)MOVQBX, (SP) 0x001e00030(:5) PCDATA$0,$0 // Generate an int-type object, i.e. a0x001e00030(:5)(SB) //8(sp) is the address of the generated a, put into AX0x002300035(:5)MOVQ8(SP),AX //Save the address of a into the location of sp+240x002800040(:5)MOVQAX,"".&a+24(SP) //Take out the first parameter passed in the main function, that is, a0x002d00045(:5)MOVQ"".a+40(FP),BP //Put a into the memory pointed to by (AX), that is, the newly generated int object mentioned above0x003200050(:5)MOVQBP, (AX) 0x003500053(:6) LEAQ { F uintptr; a *int }(SB), BX 0x003c00060(:6)MOVQBX, (SP) 0x004000064(:6) PCDATA$0,$1 0x004000064(:6)(SB) //8(sp) This is the struct object address generated above0x004500069(:6)MOVQ8(SP),AX 0x004a00074(:6)NOP //Storing the anonymous function address inside the test into BP0x004a00074(:6) LEAQ"".test.func1(SB),BP //Put the anonymous function address into the address pointed to by (AX), that is, give the above //F uintptr assignment0x005100081(:6)MOVQBP, (AX) 0x005400084(:6)MOVQAX,"".autotmp_0001+16(SP) 0x005900089(:6)NOP //Storage the address of the integer object a generated above into BP0x005900089(:6)MOVQ"".&a+24(SP),BP 0x005e00094(:6) CMPB (SB),$0 0x006500101(:6)JNE$0,117 //Save address a into AX and point to memory +8, // That is, assign value to the above structure a *int0x006700103(:6)MOVQBP,8(AX) //Save the address of the above structure into the main function stack frame;0x006b00107(:9)MOVQAX,"".~r1+48(FP) 0x007000112(:9) ADDQ$32,SP 0x007400116(:9)RET
I saw a sentence before, which vividly described the closure
Classes are behavioral data, and closures are behavioral data-containing behavior;
In other words, the closure has a context. Let's take the test example as an example and passtest
The closure functions generated by the function have their own a, thisa
It is the context data of the closure, and thisa
It is always accompanied by its closure function, every time it is called,a
It will change;
We analyzed the above assembly code to see the closure implementation principle; in this test example, sincea
is the context data of the closure, soa
Must be allocated on the heap, if allocated on the stack, the function ends,a
It is also recycled; an anonymous structure will then be defined:
{ F uintptr//This is the function pointer for the closure call a *int//This is the context data of the closure}
Then generate a single object and the integer object that was previously allocated on the heapa
Assign the address of the a pointer in the structure, and then the closure is calledfunc
Assign function address to structureF
Pointer; in this way, each closure function is generated, which is actually a structure object mentioned above, and each closure object has its own dataa
and call functionsF
; Finally return the address of this structure tomain
function;
Let's take a lookmain
The process of function obtaining closures;
"".main t=1size=528value=0args=0x0locals=0x88 0x000000000(:12) TEXT"".main(SB),$136-0 0x000000000(:12)MOVQ(TLS),CX 0x000900009(:12) LEAQ -8(SP),AX 0x000e00014(:12) CMPQAX,16(CX) 0x001200018(:12) JLS506 0x001800024(:12) SUBQ$136,SP 0x001f00031(:12) FUNCDATA$0, gclocals·f5be5308b59e045b7c5b33ee8908cfb7(SB) 0x001f00031(:12) FUNCDATA$1, gclocals·9d868b227cedd8dd4b1bec8682560fff(SB) //Put parameter 1 (f:=test(1)) into the top of the main function stack0x001f00031(:13)MOVQ$1, (SP) 0x002700039(:13) PCDATA$0,$0 //Calling the main function to generate the closure object0x002700039(:13)CALL"".test(SB) //Put the address of the closure object into DX0x002c00044(:13)MOVQ8(SP),DX //Put parameter 2 (a:=f(2)) on top of the stack0x003100049(:14)MOVQ$2, (SP) 0x003900057(:14)MOVQDX,"".f+56(SP) // Assign the function pointer of the closure object to BX0x003e00062(:14)MOVQ(DX),BX 0x004100065(:14) PCDATA$0,$1 //Closed function is called here and the address of the closure object is also passed into //Closure function, in order to modify a0x004100065(:14)CALLDX,BX 0x004300067(:14)MOVQ8(SP),BX
Obviously,main
Function Callstest
The function obtains the address of the closure object. It finds the closure function through the address of this closure object, then executes the closure function, and passes the address of the closure object into the function. This is the same as the principle of passing this pointer in C++. In order to modify member variablesa
;
Finallytest
Internal anonymous functions (closure function implementation):
"".test.func1t=1size=32value=0args=0x10 locals=0x0 0x000000000(:6) TEXT"".test.func1(SB), $0-16 0x000000000(:6) NOP 0x000000000(:6) NOP 0x000000000(:6) FUNCDATA $0, gclocals·23e8278e2b69a3a75fa59b23c49ed6ad(SB) 0x000000000(:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) //DX is the address of the closure object, +8 is the address of a0x000000000(:6) MOVQ8(DX), AX //AX is the address of a, (AX) is the value of a0x000400004(:7) MOVQ (AX), BP //Save parameter i in R80x000700007(:7) MOVQ"".i+8(FP), R8 //The value of a+i is stored in BP0x000c00012(:7) ADDQ R8, BP //Save a+i into the address of a0x000f00015(:7) MOVQ BP, (AX) //Save the latest data of address a to BP0x001200018(:8) MOVQ (AX), BP //Put the latest value of a as the return value into the main function stack0x001500021(:8) MOVQ BP,"".~r1+16(FP) 0x001a00026(:8) RET
The calling process of closure function:
1. Obtain the address of closure context data a through the closure object address;
2. Then obtain the value of a through the address of a and add it to parameter i;
3. Store a+i as the latest value into the address of a;
4. Return the latest value of a to the main function;
5. Summary
This article simply analyzes the implementation of golang multi-valued return and closure from the assembly perspective;
Multi-value return is mainly achieved by obtaining parameters through the fp register + offset and storing the return value;
Closures are mainly implemented by generating structures containing closure functions and closure context data at compile time;
The above is the entire content of this article. I hope it will be of some help to everyone’s study or just use golang. If you have any questions, you can leave a message to communicate.