The main point of calling a function is to pass parameters and return values using registers and memory frame stacks.Although Go is a compiled language, there are some differences in the use of registers and stacks compared to C. Go has its own protocol and introduces statements such as defer, which makes the call process more complex.Understanding the procedure of Go function calls in the CPU instruction layer can help you write efficient code and quickly identify key points when optimizing performance and Bug checking.This paper demonstrates the call process of Go with a short sample code and corresponding assembly code, shows the actual transfer process of parameters of different data types, and analyses the actual data structure in memory when anonymous functions, closures are passed as parameters or return values.This article does not expand on the use and implementation details of the stack by the protocol.
To read this article, you need to have a basic knowledge of computer architecture (at least program memory layout, stacks, registers), and the Go basic syntax.Reference documents provide a more detailed knowledge of these topics.
The following:
term
- Stack: Each process/thread/goroutine has its own call stack, parameter and return value transfers, and local variable storage of functions is usually done through the stack.Like stacks in data structures, memory stacks are LIFO, with addresses growing from high addresses to low addresses.
- stack frame: Also known as frame.A stack consists of many frames that describe the call relationships between functions.Each frame corresponds to a function call that has not yet been returned, and the frame itself stores data as a stack.
- Caller caller
- Callee callee, such as calling function B in function A, A is caller, B is callee
Register (X86)
-
ESP: An extended stack pointer that holds a pointer to the top of the stack at the top of the stack frame (that is, the stack of currently executing functions).Be careful:
- ESP points to an address in memory that already stores content, not an idle address.For example, from 0xC0000000 to 0xC00000FF is the stack space already used, ESP points to 0xC00000FF
- EBP: An extended base pointer, also known as a frame pointer, holds a pointer that points to the bottom of the top stack frame on the stack.
- EIP: The register stores the memory address of the next CPU instruction. When the CPU has executed the current instruction, it reads the memory address of the next instruction from the EIP register, and then proceeds.
Note: 16-bit registers have no prefix (SP, BP, IP), 32-bit prefix is E(ESP, EBP, EIP), and 64-bit prefix is R(RSP, RBP, RIP)
Assembly Instructions
- PUSH: A stack-up instruction that first decreases ESP by 4 when executed, then writes the contents to the stack memory pointed by the ESP.
-
POP: Out-of-stack instructions, POP instructions are executed by first reading out a word-long content of stack memory that ESP points to, then adding ESP to 4.Be careful:
- When using PUSH and POP instructions, the stack can only be accessed by word, not byte.
- CALL: Calls a function instruction, returns the address (the next instruction in the call instruction) stack, then jumps to the function entry.
- RET: Return the instruction, eject the top return address to the EIP, and then proceed according to the EIP.
- LEAVE: Equivalent to mov esp,ebp; pop ebp;
- MOVL: Transfer values between memory and registers, registers and registers
- LEAL: The operand used to assign a memory address directly to the destination
Note: 8-bit instruction suffix is B, 16-bit is S, 32-bit is L, 64-bit is Q
Call Convention
A call convention is a scheme for how functions are called in a program to pass parameters, allocate and clean stacks, etc.The contents of a calling convention include:
- Whether parameters are passed through registers, stacks, or a mixture of both
- Is a parameter passed through a stack left-to-right or right-to-left stack
- Is the result of the function passed by register or stack
- Whether caller or callee cleans up stack space
- Which registers should the callee save for the caller
For example, the calling convention for C (cdecl, C declaration) is:
- Function arguments are stacked on the thread stack in right-to-left order.
- Function results are saved in register EAX/AX/AL
- Floating-point results are stored in register ST0
- The compiled function name is prefixed with an underscore character
- The caller is responsible for popping arguments (that is, stacks) from the thread stack
- The reshaping argument of 8 or 16 bit length is promoted to 32 bit length.
- volatile registers affected by function calls: EAX, ECX, EDX, ST0 - ST7, ES, GS
- Registers unaffected by function calls: EBX, EBP, ESP, EDI, ESI, CS, DS
- RET directives are returned from the callee of the function to the caller (essentially reading the return address of the function saved at the thread stack indicated by register EBP and loading it into the IP register)
Cdecl saves function return values in registers, so C does not support multiple return values.In addition, cdecl is the caller's responsibility for stacking, thus enabling functions with variable parameters.Functions with variable parameters cannot be implemented if the callee is responsible for cleanup, but compiling the code is more efficient because the code that cleans up the stack does not need to be generated once per call (compiler calculation).(The ret directive for x86 allows an optional 16-bit parameter to specify the number of stack bytes used to unstack before returning to the caller.Code like ret 12, if you encounter such assembly code, it means the callee stack.)
Note that although C uses registers to pass return values, the size of return values can be handled differently.If it is less than 4 bytes, the return value is stored in the eax register and the eax is read by the function caller.If the return value is 5 to 8 bytes, it is returned by eax in conjunction with edx.If it is larger than 8 bytes, an extra space temp is first opened up on the stack, and the address of the temp object is stacked as a hidden parameter.When the function returns, the data is copied to the temp object and the address of the temp object is passed out using register eax.The caller copies the content of the temp object pointed to by eax.
You can see that when designing the features of a programming language, you need to choose the appropriate calling convention for them to implement them at the bottom level.(Calling conventions are chosen by the compiler of the programming language, and compilers with different languages may choose to implement different calling conventions)
A typical C function call
In caller:
- Put arguments on the right-to-left stack (under X86-64: write arguments to registers, if more than 6 arguments, more than the right-to-left stack)
- Execute the call command (returns the address stack and jumps to the callee entry)
Enter the callee:
- Push ebp; mov ebp, esp; at this point EBP and ESP already represent the bottom and top of the callee, respectively.The EBP value then remains fixed.Both local variables and temporary storage can then be found using the benchmark pointer EBP plus offset.
- sub xxx, esp; move down the top of the stack, allocate space for callee, store local variables, etc.Allocated memory units can be accessed through EBP-K or ESP+K.
- Stack the values of some registers (possible)
- callee execution
- Pop some register values out of the stack (possible)
- mov esp,ebp; pop ebp; (these two instructions can also be replaced with the leave directive) EBP and ESP return to the state before they entered the callee, that is, the bottom and top states of the caller, respectively.
- Execute ret directive
Back to the code in the caller
int add(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) { return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; } int main() { int i = add(1, 2, 3 , 4, 5, 6, 7, 8); }
x86 Version Compilation
.section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 14 sdk_version 10, 14 .globl _add ## -- Begin function add .p2align 4, 0x90 _add: ## @add .cfi_startproc ## %bb.0: pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset %ebp, -8 movl %esp, %ebp .cfi_def_cfa_register %ebp pushl %ebx pushl %edi pushl %esi subl $32, %esp .cfi_offset %esi, -20 .cfi_offset %edi, -16 .cfi_offset %ebx, -12 movl 36(%ebp), %eax movl 32(%ebp), %ecx movl 28(%ebp), %edx movl 24(%ebp), %esi movl 20(%ebp), %edi movl 16(%ebp), %ebx movl %eax, -16(%ebp) ## 4-byte Spill movl 12(%ebp), %eax movl %eax, -20(%ebp) ## 4-byte Spill movl 8(%ebp), %eax movl %eax, -24(%ebp) ## 4-byte Spill movl 8(%ebp), %eax addl 12(%ebp), %eax addl 16(%ebp), %eax addl 20(%ebp), %eax addl 24(%ebp), %eax addl 28(%ebp), %eax addl 32(%ebp), %eax addl 36(%ebp), %eax movl %ebx, -28(%ebp) ## 4-byte Spill movl %ecx, -32(%ebp) ## 4-byte Spill movl %edx, -36(%ebp) ## 4-byte Spill movl %esi, -40(%ebp) ## 4-byte Spill movl %edi, -44(%ebp) ## 4-byte Spill addl $32, %esp popl %esi popl %edi popl %ebx popl %ebp retl .cfi_endproc ## -- End function .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## %bb.0: pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset %ebp, -8 movl %esp, %ebp .cfi_def_cfa_register %ebp subl $40, %esp movl $1, (%esp) movl $2, 4(%esp) movl $3, 8(%esp) movl $4, 12(%esp) movl $5, 16(%esp) movl $6, 20(%esp) movl $7, 24(%esp) movl $8, 28(%esp) calll _add xorl %ecx, %ecx movl %eax, -4(%ebp) movl %ecx, %eax addl $40, %esp popl %ebp retl .cfi_endproc ## -- End function .subsections_via_symbols
x86-64 Compilation
.section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 14 sdk_version 10, 14 .globl _add ## -- Begin function add .p2align 4, 0x90 _add: ## @add .cfi_startproc ## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp movl 24(%rbp), %eax movl 16(%rbp), %r10d movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl %edx, -12(%rbp) movl %ecx, -16(%rbp) movl %r8d, -20(%rbp) movl %r9d, -24(%rbp) movl -4(%rbp), %ecx addl -8(%rbp), %ecx addl -12(%rbp), %ecx addl -16(%rbp), %ecx addl -20(%rbp), %ecx addl -24(%rbp), %ecx addl 16(%rbp), %ecx addl 24(%rbp), %ecx movl %eax, -28(%rbp) ## 4-byte Spill movl %ecx, %eax movl %r10d, -32(%rbp) ## 4-byte Spill popq %rbp retq .cfi_endproc ## -- End function .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp subq $32, %rsp movl $1, %edi movl $2, %esi movl $3, %edx movl $4, %ecx movl $5, %r8d movl $6, %r9d movl $7, (%rsp) movl $8, 8(%rsp) callq _add xorl %ecx, %ecx movl %eax, -4(%rbp) movl %ecx, %eax addq $32, %rsp popq %rbp retq .cfi_endproc ## -- End function .subsections_via_symbols
You can see that Clang's compiled X86 target code does not pass parameters using registers, whereas in X86-64 target code, the first six parameters are passed using registers.
A typical Go function call
The calling convention chosen by Go is:
- Parameters are passed entirely through the stack, from the right to the left stack of the parameter list
- The return value is passed through the stack, and the stack space of the return value precedes the parameter, that is, the return value is closer to the bottom of the caller stack
- caller is responsible for cleaning the stack
package main func main() { add(1,2) } //go:noinline func add(a , b int) int { c := 3 d := a + b + c return d }
TEXT main.main(SB) /Users/user/go/src/test/main.go main.go:4 0x104ea20 65488b0c2530000000 MOVQ GS:0x30, CX main.go:4 0x104ea29 483b6110 CMPQ 0x10(CX), SP main.go:4 0x104ea2d 762e JBE 0x104ea5d main.go:4 0x104ea2f 4883ec20 SUBQ $0x20, SP ; Increase 32 bytes The stack space of(Four qword,8 individual bytes For one qword) main.go:4 0x104ea33 48896c2418 MOVQ BP, 0x18(SP) ; take BP Write the value of the to the first of the stack spaces just allocated qword main.go:4 0x104ea38 488d6c2418 LEAQ 0x18(SP), BP ; Assign the address of the first word of the stack space just allocated to BP(That is BP Pointing at the point where you just saved the old BP Address of value) main.go:5 0x104ea3d 48c7042401000000 MOVQ $0x1, 0(SP); Will give add The first argument value of the function, 1, is written to the last one that has just allocated stack space qword main.go:5 0x104ea45 48c744240802000000 MOVQ $0x2, 0x8(SP); Will give add The second argument value of the function, 2, is written to the third just allocated stack space qword. The second qword Not used, actually given to callee Used to store returned values. main.go:5 0x104ea4e e81d000000 CALL main.add(SB); call add function main.go:6 0x104ea53 488b6c2418 MOVQ 0x18(SP), BP; Will be the fourth from the stack qword Will be old BP Value is assigned back to BP main.go:6 0x104ea58 4883c420 ADDQ $0x20, SP; increase SP Value, stack shrink, retract 32 bytes The stack space of main.go:6 0x104ea5c c3 RET TEXT main.add(SB) /Users/user/go/src/test/main.go main.go:11 0x104ea70 4883ec18 SUBQ $0x18, SP; Allocation 24 bytes Of stack space (3 qword). main.go:11 0x104ea74 48896c2410 MOVQ BP, 0x10(SP); take BP Value Writes First qword main.go:11 0x104ea79 488d6c2410 LEAQ 0x10(SP), BP; 24 that will have just been allocated bytes The address of the first word in stack space is assigned to BP(That is BP Pointing at the point where you just saved the old BP Address of value) main.go:11 0x104ea7e 48c744243000000000 MOVQ $0x0, 0x30(SP);Zero address to store return value, 0 x30(SP) The corresponding memory location is the previous segment main.main Second stack space allocated in qword. main.go:12 0x104ea87 48c744240803000000 MOVQ $0x3, 0x8(SP); Corresponding c := 3 This line of code.local variable c This corresponds to memory on the stack.3 is written to the 24 just assigned bytes Second in space qword. main.go:13 0x104ea90 488b442420 MOVQ 0x20(SP), AX; take add Argument 1 is written to AX Register. main.go:13 0x104ea95 4803442428 ADDQ 0x28(SP), AX; take add Increase argument 2 to AX Register. main.go:13 0x104ea9a 4883c003 ADDQ $0x3, AX; Increase local variable value 3 to AX register main.go:13 0x104ea9e 48890424 MOVQ AX, 0(SP); take AX Value of(Calculation results) Write to just assigned 24 bytes The third in space qword. (Correspondence Code d := a + b + c) main.go:14 0x104eaa2 4889442430 MOVQ AX, 0x30(SP); take AX Values written to main The stack space reserved for return values in(main 32 allocated in bytes Second in qword) main.go:14 0x104eaa7 488b6c2410 MOVQ 0x10(SP), BP; recovery BP The value of is the old saved at the function entry BP Value of. main.go:14 0x104eaac 4883c418 ADDQ $0x18, SP; take SP Add three words, retract add The stack space allocated at the entrance. main.go:14 0x104eab0 c3 RET
During a function call, the stack changes as shown in the following figure:
Initial state:
Calladd Execution Front Stack Status:
Stack state after entering add:
add Ri ret Execution Front Stack Status:
main ret Execution Front Stack Status:
You can see that the call process to Go is similar to C, except that Gos parameters are passed entirely through the stack, and Gos return values are also passed through the stack.For each data type's performance when passed as a parameter, you can test it:
How different data types are passed as parameters
Parameter Passing of Go Base Data Type
package main import ( "fmt" "runtime/debug" ) func main() { str := "hello" int8 := int8(8) int64 := int64(64) boolValue := true ExampleStr(str) ExampleBool(boolValue) ExampleInt8(int8) ExampleInt64(int64) ExampleMultiParams(false, 9, 8, 7) } func ExampleStr(str string){ fmt.Println(string(debug.Stack())) } func ExampleBool(boolValue bool){ boolValue = false fmt.Println(string(debug.Stack())) } func ExampleInt64(v int64){ fmt.Println(string(debug.Stack())) } func ExampleInt8(v int8){ fmt.Println(string(debug.Stack())) } func ExampleMultiParams(b bool, x, y, z int8){ bl := b xl := x yl := y zl := z fmt.Println(bl, xl, yl, zl) fmt.Println(string(debug.Stack())) }
goroutine 1 [running]: runtime/debug.Stack(0xc000084f38, 0x1057aad, 0x10aeb20) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleStr(0x10c6c34, 0x5) /Users/user/go/src/test/main.go:25 +0x26 main.main() /Users/user/go/src/test/main.go:16 +0x36 goroutine 1 [running]: runtime/debug.Stack(0x10e0580, 0xc000092000, 0xc000084f58) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleBool(0x10c6c01) /Users/user/go/src/test/main.go:32 +0x26 main.main() /Users/user/go/src/test/main.go:17 +0x3f goroutine 1 [running]: runtime/debug.Stack(0x10e0580, 0xc000092000, 0xc000084f58) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleInt8(0x10c6c08) /Users/user/go/src/test/main.go:42 +0x26 main.main() /Users/user/go/src/test/main.go:18 +0x48 goroutine 1 [running]: runtime/debug.Stack(0x10e0580, 0xc000092000, 0xc000084f58) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleInt64(0x40) /Users/user/go/src/test/main.go:38 +0x26 main.main() /Users/user/go/src/test/main.go:19 +0x55 false 9 8 7 goroutine 1 [running]: runtime/debug.Stack(0x10e0580, 0xc000092000, 0xc000084f28) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleMultiParams(0x7080900) /Users/user/go/src/test/main.go:52 +0xf6 main.main() /Users/user/go/src/test/main.go:20 +0x61
You can see:
- When string is passed, it is divided into pointer and length parameters.
- When int64 is passed, the value is copied for transfer.
- When looking at the call stack printed by debug.Stack(), int8 and bool passes, a memory address is passed, which seems to be misleading. Is it the memory address of the variable in the caller that is passed?Does that not result in a callee change and a caller median change?Of course not!Both int and bool are, of course, value passing.When a caller passes to a callee, both int and bool are copied into the caller's stack for use by the callee.(The parameter value is modified directly in the callee by referencing the parameter name, and the memory location of the parameter is actually on the caller's stack).
- The ExampleMultiParams function has four parameters, but the call stack prints out and passes only a value of 0x7080900. Why?Originally these four parameters were all a byte, which together is a double word.Looking at the assembly code, you can see that the compiler has been optimized to combine directly into a single value and write the command MOVL MOVL $0x7080900, 0 (SP) x7080900, 0(SP) on the stack in the caller.Of course, when the value is taken in the caller, the value is taken byte by byte with MOVB.Of course, if these four parameters are the four local variables in main, ExampleMultiParams are called b y passing the variable name (ExampleMultiParams(b, x, y, z) instead of ExampleMultiParams(true, 9, 8, 7), which is another form of assembly code.
Parameter Passing of Go Combination Data Type
package main import ( "fmt" "runtime/debug" ) type MyStruct struct { a int b string } func main() { slice := make([]string, 2, 4) array := [...]int{9,8,7,6,7,8,9} myMap := make(map[string]int) myStruct := MyStruct{8, "test"} myStructPtr := &myStruct myChan := make(chan int, 4) ExampleSlice(slice) ExampleArray(array) ExampleMap(myMap) ExampleStruct(myStruct) ExamplePtr(myStructPtr) ExampleChan(myChan) } func ExampleSlice(slice []string){ fmt.Println(string(debug.Stack())) } func ExampleArray(array [7]int){ fmt.Println(string(debug.Stack())) } func ExampleMap(myMap map[string]int){ fmt.Println(string(debug.Stack())) } func ExampleStruct(myStruct MyStruct){ fmt.Println(string(debug.Stack())) } func ExamplePtr(ptr *MyStruct){ fmt.Println(string(debug.Stack())) } func ExampleChan(myChan chan int){ fmt.Println(string(debug.Stack())) }
Call Stack
goroutine 1 [running]: runtime/debug.Stack(0x130a568, 0xc00007eda8, 0x1004218) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleSlice(0xc00007ee78, 0x2, 0x4) /Users/user/go/src/test/main.go:62 +0x26 main.main() /Users/user/go/src/test/main.go:46 +0x159 goroutine 1 [running]: runtime/debug.Stack(0x10e0a80, 0xc00008c000, 0xc00007ed98) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleArray(0x9, 0x8, 0x7, 0x6, 0x7, 0x8, 0x9) /Users/user/go/src/test/main.go:66 +0x26 main.main() /Users/user/go/src/test/main.go:48 +0x185 goroutine 1 [running]: runtime/debug.Stack(0x10e0a80, 0xc00008c000, 0xc00007ed98) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleMap(0xc00007ee48) /Users/user/go/src/test/main.go:74 +0x26 main.main() /Users/user/go/src/test/main.go:52 +0x1af goroutine 1 [running]: runtime/debug.Stack(0x10e0a80, 0xc00008c000, 0xc00007ed98) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleStruct(0x8, 0x10c6f88, 0x4) /Users/user/go/src/test/main.go:78 +0x26 main.main() /Users/user/go/src/test/main.go:54 +0x1d7 goroutine 1 [running]: runtime/debug.Stack(0x10e0a80, 0xc00008c000, 0xc00007ed98) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExamplePtr(0xc00007ee30) /Users/user/go/src/test/main.go:82 +0x26 main.main() /Users/user/go/src/test/main.go:56 +0x1e5 goroutine 1 [running]: runtime/debug.Stack(0x10e0a80, 0xc00008c000, 0xc00007ed98) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.ExampleChan(0xc000092000) /Users/user/go/src/test/main.go:86 +0x26 main.main() /Users/user/go/src/test/main.go:58 +0x1f3
You can see:
- For arrays, structs, and pointers, a copy is passed to callee.
- When an array is a parameter, the number of compiled parameters is the number of array elements.
- When a structure is used as a parameter, the number of compiled parameters requires a reanalysis of the type of elements in the structure.As in the above code, the structure consists of an int and a string, and the parameters passed are the int value, the address of the string, and the length of the string.
- When slice is passed, the pointer, len, cap at the bottom of the slice are passed as three parameters separately.(That is, after compilation, the number of parameters changes from one parameter in the source code to three parameters).So slice is actually value transfer.
- When map and chan are passed, the address pointers of map and chan are passed as parameters.
Method: pointer receiver and value receiver
In Java, when we call an object's method, we can certainly modify its member variables.In Go, however, the result depends on whether the recipient of the method is a value receiver or pointer receiver when defining the method.
Go s have two method receivers: value receiver and pointer receiver.Value Recipient, the type of Recipient is a value, a copy, and there is no way to change its true recipient internally.Pointer Receiver, the type of recipient is a pointer and is a reference to the recipient. Modifications to this reference will affect the real recipient.
See the following code:
package main import "fmt" type XAxis int type Point struct{ X int Y int } func (x XAxis)VIncr(offset XAxis){ x += offset fmt.Printf("In VIncr, new x = %d\n", x) } func (x *XAxis)PIncr(offset XAxis){ *x += offset fmt.Printf("In PIncr, new x = %d\n", *x) } func (p Point)VScale(factor int){ p.X *= factor p.Y *= factor fmt.Printf("In VScale, new p = %v\n", p) } func (p *Point)PScale(factor int){ p.X *= factor p.Y *= factor fmt.Printf("In PScale, new p = %v\n", p) } func main(){ var x XAxis = 10 fmt.Printf("In main, before VIncr, x = %v\n", x) x.VIncr(5) fmt.Printf("In main, after VIncr, new x = %v\n", x) fmt.Println() fmt.Printf("In main, before PIncr, x = %v\n", x) x.PIncr(5) fmt.Printf("In main, after PIncr, new x = %v\n", x) fmt.Println() p := Point{2, 2} fmt.Printf("In main, before VScale, p = %v\n", p) p.VScale(5) fmt.Printf("In main, after VScale, new p = %v\n", p) fmt.Println() fmt.Printf("In main, before PScale, p = %v\n", p) p.PScale(5) fmt.Printf("In main, after PScale, new p = %v\n", p) }
Output:
In main, before VIncr, x = 10 In VIncr, new x = 15 In main, after VIncr, new x = 10 In main, before PIncr, x = 10 In PIncr, new x = 15 In main, after PIncr, new x = 15 In main, before VScale, p = {2 2} In VScale, new p = {10 10} In main, after VScale, new p = {2 2} In main, before PScale, p = {2 2} In PScale, new p = &{10 10} In main, after PScale, new p = {10 10}
When defining a method, the receiver precedes the method name, not in the parameter list. How do the instructions in the method find the receiver when the method executes?
Let's simplify the code (comment out the print-related statements) and decompile it:
TEXT %22%22.XAxis.VIncr(SB) gofile../Users/user/go/src/test/main.go main.go:14 0xbb0 488b442408 MOVQ 0x8(SP), AX main.go:14 0xbb5 4803442410 ADDQ 0x10(SP), AX main.go:14 0xbba 4889442408 MOVQ AX, 0x8(SP) main.go:16 0xbbf c3 RET TEXT %22%22.(*XAxis).PIncr(SB) gofile../Users/user/go/src/test/main.go main.go:19 0xbd4 488b442408 MOVQ 0x8(SP), AX main.go:19 0xbd9 8400 TESTB AL, 0(AX) main.go:19 0xbdb 488b4c2408 MOVQ 0x8(SP), CX main.go:19 0xbe0 8401 TESTB AL, 0(CX) main.go:19 0xbe2 488b00 MOVQ 0(AX), AX main.go:19 0xbe5 4803442410 ADDQ 0x10(SP), AX main.go:19 0xbea 488901 MOVQ AX, 0(CX) main.go:21 0xbed c3 RET TEXT %22%22.Point.VScale(SB) gofile../Users/user/go/src/test/main.go main.go:24 0xc0a 488b442408 MOVQ 0x8(SP), AX main.go:24 0xc0f 488b4c2418 MOVQ 0x18(SP), CX main.go:24 0xc14 480fafc1 IMULQ CX, AX main.go:24 0xc18 4889442408 MOVQ AX, 0x8(SP) main.go:25 0xc1d 488b442410 MOVQ 0x10(SP), AX main.go:25 0xc22 488b4c2418 MOVQ 0x18(SP), CX main.go:25 0xc27 480fafc1 IMULQ CX, AX main.go:25 0xc2b 4889442410 MOVQ AX, 0x10(SP) main.go:28 0xc30 c3 RET TEXT %22%22.(*Point).PScale(SB) gofile../Users/user/go/src/test/main.go main.go:32 0xc47 488b442408 MOVQ 0x8(SP), AX main.go:32 0xc4c 8400 TESTB AL, 0(AX) main.go:32 0xc4e 488b4c2408 MOVQ 0x8(SP), CX main.go:32 0xc53 8401 TESTB AL, 0(CX) main.go:32 0xc55 488b00 MOVQ 0(AX), AX main.go:32 0xc58 488b542410 MOVQ 0x10(SP), DX main.go:32 0xc5d 480fafc2 IMULQ DX, AX main.go:32 0xc61 488901 MOVQ AX, 0(CX) main.go:33 0xc64 488b442408 MOVQ 0x8(SP), AX main.go:33 0xc69 8400 TESTB AL, 0(AX) main.go:33 0xc6b 488b4c2408 MOVQ 0x8(SP), CX main.go:33 0xc70 8401 TESTB AL, 0(CX) main.go:33 0xc72 488b4008 MOVQ 0x8(AX), AX main.go:33 0xc76 488b542410 MOVQ 0x10(SP), DX main.go:33 0xc7b 480fafc2 IMULQ DX, AX main.go:33 0xc7f 48894108 MOVQ AX, 0x8(CX) main.go:36 0xc83 c3 RET TEXT %22%22.main(SB) gofile../Users/user/go/src/test/main.go main.go:38 0xcaa 65488b0c2500000000 MOVQ GS:0, CX [5:9]R_TLS_LE main.go:38 0xcb3 483b6110 CMPQ 0x10(CX), SP main.go:38 0xcb7 0f86b3000000 JBE 0xd70 main.go:38 0xcbd 4883ec50 SUBQ $0x50, SP main.go:38 0xcc1 48896c2448 MOVQ BP, 0x48(SP) main.go:38 0xcc6 488d6c2448 LEAQ 0x48(SP), BP main.go:39 0xccb 48c74424300a000000 MOVQ $0xa, 0x30(SP) main.go:42 0xcd4 48c704240a000000 MOVQ $0xa, 0(SP) main.go:42 0xcdc 48c744240805000000 MOVQ $0x5, 0x8(SP) main.go:42 0xce5 e800000000 CALL 0xcea [1:5]R_CALL:%22%22.XAxis.VIncr main.go:48 0xcea 488d442430 LEAQ 0x30(SP), AX main.go:48 0xcef 48890424 MOVQ AX, 0(SP) main.go:48 0xcf3 48c744240805000000 MOVQ $0x5, 0x8(SP) main.go:48 0xcfc e800000000 CALL 0xd01 [1:5]R_CALL:%22%22.(*XAxis).PIncr main.go:53 0xd01 0f57c0 XORPS X0, X0 main.go:53 0xd04 0f11442438 MOVUPS X0, 0x38(SP) main.go:53 0xd09 48c744243802000000 MOVQ $0x2, 0x38(SP) main.go:53 0xd12 48c744244002000000 MOVQ $0x2, 0x40(SP) main.go:56 0xd1b 48c7042402000000 MOVQ $0x2, 0(SP) main.go:56 0xd23 48c744240802000000 MOVQ $0x2, 0x8(SP) main.go:56 0xd2c 48c744241005000000 MOVQ $0x5, 0x10(SP) main.go:56 0xd35 e800000000 CALL 0xd3a [1:5]R_CALL:%22%22.Point.VScale main.go:62 0xd3a 488d442438 LEAQ 0x38(SP), AX main.go:62 0xd3f 48890424 MOVQ AX, 0(SP) main.go:62 0xd43 48c744240805000000 MOVQ $0x5, 0x8(SP) main.go:62 0xd4c e800000000 CALL 0xd51 [1:5]R_CALL:%22%22.(*Point).PScale main.go:64 0xd51 48c7042400000000 MOVQ $0x0, 0(SP) main.go:64 0xd59 0f57c0 XORPS X0, X0 main.go:64 0xd5c 0f11442408 MOVUPS X0, 0x8(SP) main.go:64 0xd61 e800000000 CALL 0xd66 [1:5]R_CALL:fmt.Println main.go:65 0xd66 488b6c2448 MOVQ 0x48(SP), BP main.go:65 0xd6b 4883c450 ADDQ $0x50, SP main.go:65 0xd6f c3 RET main.go:38 0xd70 e800000000 CALL 0xd75 [1:5]R_CALL:runtime.morestack_noctxt main.go:38 0xd75 e930ffffff JMP %22%22.main(SB)
For example, in main.go line 42 x.VIncr(5), the instructions for passing parameters are
main.go:42 0xcd4 48c704240a000000 MOVQ $0xa, 0(SP); Make value 10 the first parameter main.go:42 0xcdc 48c744240805000000 MOVQ $0x5, 0x8(SP); Value 5 as second parameter main.go:42 0xce5 e800000000 CALL 0xcea [1:5]R_CALL:%22%22.XAxis.VIncr
Look again at the assembly code for x.VIncr:
main.go:14 0xbb0 488b442408 MOVQ 0x8(SP), AX; Take the first parameter to the register AX main.go:14 0xbb5 4803442410 ADDQ 0x10(SP), AX; Add the second parameter to the register AX main.go:14 0xbba 4889442408 MOVQ AX, 0x8(SP); Writes and to the position of the first parameter on the stack main.go:16 0xbbf c3 RET
You can see that the receiver of method VIncr is a value, then caller copies this value to the parameter area of VIncr on the stack when calling VIncr. VIncr's modification of the receiver is actually the value in the parameter area modified, not the value of the receiver in the area where the local variable is stored in the caller stack (in language)The user's perspective is to modify the copy instead of the original value.
Next, let's look at line 48 x.PIncr(5) of main.go, where the instruction invoked is:
main.go:39 0xccb 48c74424300a000000 MOVQ $0xa, 0x30(SP) ... main.go:48 0xcea 488d442430 LEAQ 0x30(SP), AX; take SP+0x30 This memory address is saved to AX; SP+0x30 10 is stored in this memory address(implement var x XAxis = 10 Local variable defined by time) main.go:48 0xcef 48890424 MOVQ AX, 0(SP); take AX The value of the is used as the first parameter. main.go:48 0xcf3 48c744240805000000 MOVQ $0x5, 0x8(SP); Make 5 the second parameter main.go:48 0xcfc e800000000 CALL 0xd01 [1:5]R_CALL:%22%22.(*XAxis).PIncr
Assembly code for PIncr:
main.go:19 0xbd4 488b442408 MOVQ 0x8(SP), AX; The first parameter will be(That is main Local variable on frame stack of x Memory Address)Read To AX main.go:19 0xbd9 8400 TESTB AL, 0(AX) main.go:19 0xbdb 488b4c2408 MOVQ 0x8(SP), CX; The first parameter will be(That is main Local variable on frame stack of x Memory Address)Read To CX main.go:19 0xbe0 8401 TESTB AL, 0(CX) main.go:19 0xbe2 488b00 MOVQ 0(AX), AX; from AX Read the memory address, get the value from the memory address, and then read AX(Namely main Local variable x Value of) main.go:19 0xbe5 4803442410 ADDQ 0x10(SP), AX; Add the second parameter 5 to AX in main.go:19 0xbea 488901 MOVQ AX, 0(CX); Write calculation results to CX Memory address in(That is main Local variable on frame stack of x Memory Address) main.go:21 0xbed c3 RET
You can see that the receiver of the method PIncr is a pointer, and when the caller calls PIncr, it copies the pointer into the parameter area on the stack to the PIncr. When the PIncr modifies the receiver, it actually modifies the memory area that the pointer points to, that is, the local variable x of the main.(From the language user's perspective, the original value is modified).
Print the call stack to see the parameters actually passed more easily than assembly:
goroutine 1 [running]: runtime/debug.Stack(0x1036126, 0x10a4360, 0xc000098000) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.XAxis.VIncr(0xa, 0x5) /Users/user/go/src/test/main.go:18 +0x26 main.main() /Users/user/go/src/test/main.go:47 +0x40 goroutine 1 [running]: runtime/debug.Stack(0x10e0480, 0xc000094000, 0xc000080f10) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.(*XAxis).PIncr(0xc000080f70, 0x5) /Users/user/go/src/test/main.go:24 +0x33 main.main() /Users/user/go/src/test/main.go:53 +0x57 goroutine 1 [running]: runtime/debug.Stack(0x10e0480, 0xc000094000, 0xc000080f10) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.Point.VScale(0x2, 0x2, 0x5) /Users/user/go/src/test/main.go:31 +0x26 main.main() /Users/user/go/src/test/main.go:61 +0x90 goroutine 1 [running]: runtime/debug.Stack(0x10e0480, 0xc000094000, 0xc000080f10) /usr/local/go/src/runtime/debug/stack.go:24 +0x9d main.(*Point).PScale(0xc000080f78, 0x5) /Users/user/go/src/test/main.go:39 +0x46 main.main() /Users/user/go/src/test/main.go:67 +0xa7
When the receiver of a method is a value, a copy of the value is passed to the callee as the first parameter when the method is called, so that the modification to the receiver in the caller is actually a copy of the modification, without affecting the original value.When the receiver of a method is a pointer, a copy of the pointer is passed to the caller as the first parameter so that the caller can modify the original value through the pointer.
A little supplement
- If modifying the value of the receiver in a method is to take effect on the caller, use pointer receiver
- For performance optimization, if a receiver is a data type that takes up more memory, such as a structure or an array, use pointer receiver first
Be careful:
- Value sinks are concurrently secure, while pointer sinks are not concurrently secure.
Invocation rules:
- Callable method set of type T contains all method sets whose recipient is T or T
- The callable method set of type T contains all methods whose recipient is T
- The set of callable methods of type T does not contain methods whose recipient is *T
Method Receiver vs. Function/Method Parameters:
- The argument type and the parameter type of the function/method must be the same (syntactically) not one pointer and the other value.
- The recipient of the method is smart and can be called on a value if it is a pointer receiver (the compiler automatically inserts a pointer-to-value instruction).If it is a value receiver, the compiler automatically converts the pointer to the value corresponding to the pointer when the method is called on the pointer.
Anonymous functions and closures
Anonymous function
An anonymous function consists of a function declaration and a body without a function name, which can be assigned to a variable, passed as a struct field, or in a channel.In the underlying implementation, what is actually passed is the entry address of the anonymous function.
package main func test() func(int) int { return func(x int) int { x += x return x } } func main() { f := test() f(100) }
TEXT %22%22.test(SB) gofile../Users/user/go/src/test/main.go main.go:4 0x4e7 48c744240800000000 MOVQ $0x0, 0x8(SP) main.go:5 0x4f0 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:%22%22.test.func1·f main.go:5 0x4f7 4889442408 MOVQ AX, 0x8(SP) main.go:5 0x4fc c3 RET TEXT %22%22.main(SB) gofile../Users/user/go/src/test/main.go main.go:11 0x517 65488b0c2500000000 MOVQ GS:0, CX [5:9]R_TLS_LE main.go:11 0x520 483b6110 CMPQ 0x10(CX), SP main.go:11 0x524 7633 JBE 0x559 main.go:11 0x526 4883ec20 SUBQ $0x20, SP main.go:11 0x52a 48896c2418 MOVQ BP, 0x18(SP) main.go:11 0x52f 488d6c2418 LEAQ 0x18(SP), BP main.go:12 0x534 e800000000 CALL 0x539 [1:5]R_CALL:%22%22.test main.go:12 0x539 488b1424 MOVQ 0(SP), DX main.go:12 0x53d 4889542410 MOVQ DX, 0x10(SP) main.go:13 0x542 48c7042464000000 MOVQ $0x64, 0(SP) main.go:13 0x54a 488b02 MOVQ 0(DX), AX main.go:13 0x54d ffd0 CALL AX [0:0]R_CALLIND main.go:14 0x54f 488b6c2418 MOVQ 0x18(SP), BP main.go:14 0x554 4883c420 ADDQ $0x20, SP main.go:14 0x558 c3 RET main.go:11 0x559 e800000000 CALL 0x55e [1:5]R_CALL:runtime.morestack_noctxt main.go:11 0x55e ebb7 JMP %22%22.main(SB) TEXT %22%22.test.func1(SB) gofile../Users/user/go/src/test/main.go main.go:5 0x58a 48c744241000000000 MOVQ $0x0, 0x10(SP) main.go:6 0x593 488b442408 MOVQ 0x8(SP), AX main.go:6 0x598 4803442408 ADDQ 0x8(SP), AX main.go:6 0x59d 4889442408 MOVQ AX, 0x8(SP) main.go:7 0x5a2 4889442410 MOVQ AX, 0x10(SP)
closure
When a function references a variable in an external scope, we call it a closure.In the underlying implementation, a closure consists of the address of a function and the address of the variable referenced, and is stored in a structure where the address is actually passed when the closure is passed.Because the values on the stack frame fail after the frame's function exits, variables from the external scope referenced by the closure are assigned to the heap.In the following implementation, the test() function returns a closure assignment to f, which is actually the address of the closure structure (on the heap) received in the main and saved on the DX register. The memory value corresponding to the address is the address of the closure function (the function address is taken to the register and then called via call), offset by 8 bytes (+8bytes) Is the address of the variable x. When closing function f is called in main, the address of the variable x is still obtained by reading the value of DX.That is, the main call f shares the value of a register DX, although it does not pass parameters or return values.
package main func test() func() { x := 100 return func() { x += 100 } } func main() { f := test() f() f() f() }
TEXT %22%22.test(SB) gofile../Users/user/go/src/test/main.go main.go:3 0x6ad 65488b0c2500000000 MOVQ GS:0, CX [5:9]R_TLS_LE main.go:3 0x6b6 483b6110 CMPQ 0x10(CX), SP main.go:3 0x6ba 0f869b000000 JBE 0x75b main.go:3 0x6c0 4883ec28 SUBQ $0x28, SP main.go:3 0x6c4 48896c2420 MOVQ BP, 0x20(SP) main.go:3 0x6c9 488d6c2420 LEAQ 0x20(SP), BP main.go:3 0x6ce 48c744243000000000 MOVQ $0x0, 0x30(SP) main.go:4 0x6d7 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:type.int main.go:4 0x6de 48890424 MOVQ AX, 0(SP) main.go:4 0x6e2 e800000000 CALL 0x6e7 [1:5]R_CALL:runtime.newobject main.go:4 0x6e7 488b442408 MOVQ 0x8(SP), AX main.go:4 0x6ec 4889442418 MOVQ AX, 0x18(SP) main.go:4 0x6f1 48c70064000000 MOVQ $0x64, 0(AX) main.go:5 0x6f8 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:type.noalg.struct { F uintptr; %22%22.x *int } main.go:5 0x6ff 48890424 MOVQ AX, 0(SP) main.go:5 0x703 e800000000 CALL 0x708 [1:5]R_CALL:runtime.newobject main.go:5 0x708 488b442408 MOVQ 0x8(SP), AX main.go:5 0x70d 4889442410 MOVQ AX, 0x10(SP) main.go:5 0x712 488d0d00000000 LEAQ 0(IP), CX [3:7]R_PCREL:%22%22.test.func1 main.go:5 0x719 488908 MOVQ CX, 0(AX) main.go:5 0x71c 488b442410 MOVQ 0x10(SP), AX main.go:5 0x721 8400 TESTB AL, 0(AX) main.go:5 0x723 488b4c2418 MOVQ 0x18(SP), CX main.go:5 0x728 488d7808 LEAQ 0x8(AX), DI main.go:5 0x72c 833d0000000000 CMPL $0x0, 0(IP) [2:6]R_PCREL:runtime.writeBarrier+-1 main.go:5 0x733 7402 JE 0x737 main.go:5 0x735 eb1a JMP 0x751 main.go:5 0x737 48894808 MOVQ CX, 0x8(AX) main.go:5 0x73b eb00 JMP 0x73d main.go:5 0x73d 488b442410 MOVQ 0x10(SP), AX main.go:5 0x742 4889442430 MOVQ AX, 0x30(SP) main.go:5 0x747 488b6c2420 MOVQ 0x20(SP), BP main.go:5 0x74c 4883c428 ADDQ $0x28, SP main.go:5 0x750 c3 RET main.go:5 0x751 4889c8 MOVQ CX, AX main.go:5 0x754 e800000000 CALL 0x759 [1:5]R_CALL:runtime.gcWriteBarrier main.go:5 0x759 ebe2 JMP 0x73d main.go:3 0x75b e800000000 CALL 0x760 [1:5]R_CALL:runtime.morestack_noctxt main.go:3 0x760 e948ffffff JMP %22%22.test(SB) TEXT %22%22.main(SB) gofile../Users/user/go/src/test/main.go main.go:10 0x7bc 65488b0c2500000000 MOVQ GS:0, CX [5:9]R_TLS_LE main.go:10 0x7c5 483b6110 CMPQ 0x10(CX), SP main.go:10 0x7c9 763f JBE 0x80a main.go:10 0x7cb 4883ec18 SUBQ $0x18, SP main.go:10 0x7cf 48896c2410 MOVQ BP, 0x10(SP) main.go:10 0x7d4 488d6c2410 LEAQ 0x10(SP), BP main.go:11 0x7d9 e800000000 CALL 0x7de [1:5]R_CALL:%22%22.test main.go:11 0x7de 488b1424 MOVQ 0(SP), DX main.go:11 0x7e2 4889542408 MOVQ DX, 0x8(SP) main.go:12 0x7e7 488b02 MOVQ 0(DX), AX main.go:12 0x7ea ffd0 CALL AX [0:0]R_CALLIND main.go:13 0x7ec 488b542408 MOVQ 0x8(SP), DX main.go:13 0x7f1 488b02 MOVQ 0(DX), AX main.go:13 0x7f4 ffd0 CALL AX [0:0]R_CALLIND main.go:14 0x7f6 488b542408 MOVQ 0x8(SP), DX main.go:14 0x7fb 488b02 MOVQ 0(DX), AX main.go:14 0x7fe ffd0 CALL AX [0:0]R_CALLIND main.go:15 0x800 488b6c2410 MOVQ 0x10(SP), BP main.go:15 0x805 4883c418 ADDQ $0x18, SP main.go:15 0x809 c3 RET main.go:10 0x80a e800000000 CALL 0x80f [1:5]R_CALL:runtime.morestack_noctxt main.go:10 0x80f ebab JMP %22%22.main(SB) TEXT %22%22.test.func1(SB) gofile../Users/user/go/src/test/main.go main.go:5 0x84b 4883ec10 SUBQ $0x10, SP main.go:5 0x84f 48896c2408 MOVQ BP, 0x8(SP) main.go:5 0x854 488d6c2408 LEAQ 0x8(SP), BP main.go:5 0x859 488b4208 MOVQ 0x8(DX), AX main.go:5 0x85d 48890424 MOVQ AX, 0(SP) main.go:6 0x861 48830064 ADDQ $0x64, 0(AX) main.go:7 0x865 488b6c2408 MOVQ 0x8(SP), BP main.go:7 0x86a 4883c410 ADDQ $0x10, SP main.go:7 0x86e c3 RET
Recursive function
Recursive functions are essentially no different from ordinary functions, just calling themselves and increasing stacks.Stack overflow is a concern because the stack size is fixed in C.However, the Go internal stack automatically expands as needed, so there is no need to worry about this.
About defer statements
defer and return
You can try to infer the return value of a function by looking at the following three functions first:
func f() (result int) { defer func() { result++ }() return 0 } func f() (r int) { t := 5 defer func() { t = t + 5 }() return t } func f() (r int) { defer func(r int) { r = r + 5 }(r) return 1 }
The correct answers are: 1, 5, 1.If your answer is correct, you can skip the following explanation:
The phrase "the function call after defer executes before the return statement" is not easy to understand correctly.In fact, the return xxx statement is not an atom. Instead, it writes the xxx to the stack space allocated by the caller for the return value, followed by the RET instruction.The defer function is inserted and executed before the RET instruction.
The control structure of the goroutine contains a table that records defer expressions, and the compiler inserts the instruction call runtime.deferproc where defer appears, which records defer expressions in the table.The expression is then executed from the defer table on the stack before the function returns, and the instruction inserted is call runtime.deferreturn.
defer and closure
The parameters of the function invoked by the defer statement are evaluated or copied at the time of defer registration.Therefore, after a function statement passed to defer as a parameter, subsequent modifications to the local variable will no longer affect the use of the value of the variable within the defer function.However, a variable in the defer function that uses an external function passed in as a non-parameter will use the final value of the variable in the life cycle of the external function.
package main import "fmt" func test() { x, y := 10, 20 defer func(i int) { fmt.Println("defer:", i, y) }(x) x += 10 y += 100 fmt.Println(x, y) } func main(){ test() }
Output: 20 120 defer: 10 120
Remarks:
- The stack in memory grows from high address space to low address space, the top of the stack is smaller than the bottom of the stack, and allocating stack space corresponds to a decrease in sp value.
- Write values are written from low addresses to high addresses, such as SP pointing to 0xff00 and writing a word (8 bytes) into the stack, taking up 8 bytes from 0xff00 to 0xff07.
- The order of intel storage bytes is small-end first: that is, low-valid bytes are stored in low-memory addresses.
- In IA-32 and X86-64, word length is defined as 16 bits, dword as 32 bits, and qword as 64 bits.
Methods for generating assembly files
- Generate using go tool compile-N-l-S go_file.go
- Use go tool compile -N-l go_file.go to compile into a binary file, then execute go tool objdump bin_name.o to disassemble the code, specifying the function name by -s to disassemble only specific functions: go tool objdump-s YOUR_FUNC_NAME bin_name.o
- Generate using go build-gcflags-S
Note: go tool compile and go build-gcflags-S produce assembly in process, go tool objdump produce assembly of final machine code.
summary
The Go language uses the stack to pass parameters and return values, and the caller is responsible for the stack. Passing return values through the stack enables the Go function to support multiple return values, while the caller stack enables functions with variable parameters.Gos pass parameters using value-passing mode, so when passing arrays and structures, you should try to use pointers as parameters to avoid large copies of data and improve performance.
The Go method is called by passing the receiver as an argument to the callee, the receiver score receiver, and the pointer receiver.
When passing an anonymous function, the pass is actually the function's entry pointer.When closures are used, Go s allocates variables to heap memory through an escape analysis mechanism, and variable addresses and function entry addresses form a structure on the heap that is passed when closures are passed.
Gos data types are divided into value and reference types, but Gos parameter transfer is value transfer.When a value type is passed, it is a complete copy, and changes to parameters in the callee do not affect the original value; when a reference type is passed, changes in the callee affect the original value.
A return statement with a return value corresponds to multiple machine instructions, starting with writing the return value to the space allocated by the caller on the stack for the return value, and then executing the ret command.When there is a defer statement, the function in the defer statement is executed before the ret instruction is inserted.
Reference and in-depth reading:
About invocation conventions
x86 calling conventions
Calling convention
About memory layout and function frame stack
Function Call and Stack Frame Principle under x86-64
Process Memory Layout
Anatomy of a Program in Memory
Linux Process Address Space &&Process Memory Layout
Runtime memory layout and stack frame structure
Memory Layout for C programs
C Function Call Procedure Principle and Function Stack Frame Analysis
About Plan9 Assemblies
Getting started with plan9 assembly
golang assembly Basics
golang assembly
About Go
Function declarations, Method declarations
Effective Go Functions
Delve into the goroutine stack
Go defer keyword
Go Language Advanced Programming Re-discussion Functions
This article references code samples and statements from these articles
Stack Traces In Go
Talking about the Implementation Principle of Go Language-Function Call