Moment For Technology

Understand Go language function calls from the stack

Posted on Aug. 8, 2022, 6:35 p.m. by William Turner
Category: The back-end Tag: The back-end Go

Reprint, please declare the source ~ this article published on luozhiyun blog: www.luozhiyun.com/archives/51...

This article uses the go source code 1.15.7

preface

Function call type

The Function Calls in this article refer to any executable block of code in Go. As mentioned in Go 1.1 Function Calls, there are four types of functions in Go:

  • top-level func
  • method with value receiver
  • method with pointer receiver
  • func literal

Top-level func is just a normal function we write normally:

func TopLevel(x int) {}
Copy the code

Method with Value Receiver Method with Pointer Receiver refers to the value receiver method and pointer receiver method of a structure method.

Structure methods can add new behavior to user-defined types. The difference between a method and a function is that a method has a receiver. Add a receiver to a function and it becomes a method. The receiver can be a value receiver, value Receiver, or pointer receiver.

Let's take two simple structures, Man and Woman:

type Man struct{}type Woman struct{}func (*Man) Say(a){}func (Woman) Say(a){}Copy the code

Example :(*Man).say () uses pointer receiver; (Woman) Say() is the value receiver;

Function literal is defined as follows:

A function literal represents an anonymous function.

That is, include anonymous functions and closures.

The following analysis is also carried out in accordance with these types.

Basic knowledge of

In the article teach you understand Go stack operation www.luozhiyun.com/archives/51..." Stack manipulation is covered in "Stack manipulation", but there are a lot of things that have been ignored when it comes to function calls on the stack, so here's a look at function calls.

If you have not read the article mentioned above, I also write the basics here, those who have seen you can skip.

On modern mainstream machine architectures, such as x86, stacks grow downward. The stack grows from the high address down to the status address.

Let's first look at the definition of the assembly function of Plan9:

Assembly function

Let's first look at the definition of the assembly function of Plan9:

Stack frame size: the parameter space containing local variables and additional calls to functions;

Arguments size: Sizeof (int64) * 4; arguments size: Sizeof (int64) * 4;

Stack adjustment

Stack adjustments are made by performing operations on hardware SP registers, for example:

SUBQ $24, SP // allocate function stack frame to function... ADDQ $24, SP // add SP, clear function stack frameCopy the code

Since the stack grows down, SUBQ subtracts SP to allocate stack frames to the function, while ADDQ clears stack frames.

Common instruction

Addition and subtraction operations:

ADDQ  AX, BX   // BX += AX
SUBQ  AX, BX   // BX -= AX
Copy the code

Data handling:

Constants are expressed as $num in plan9 assembly and can be negative, and decimal by default. The length of removal 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
Copy the code

Another difference is that you will see the difference between using MOVQ with parentheses and without parentheses.

MOVQ (AX), BX = *AX // = BX = *AX BX // = BX = *(AX + 16) // Unparenthesized is a reference to the value MOVQ AX, BX // = BX = AX Assigns the contents stored in AX to BX, note the differenceCopy the code

Address operation:

LEAQ (AX)(AX*2), CX // = CX = AX + (AX * 2) = AX * 3
Copy the code

The 2 in the code above represents scale, which can only be 0, 2, 4, 8.

Function call analysis

Direct function call

Let's define a simple function here:

package main

func main(a) {
	add(1.2)}func add(a, b int) int {
	return a + b
}
Copy the code

Then print out the assembly using the command:

GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go
Copy the code

Let's take a piecewise look at assembly instructions and stacks. Start with a call to the main method:

"".main STEXT size=71 args=0x0 locals=0x20 0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $32-0 0x0000 00000 (main.go:3) MOVQ (TLS), CX 0x0009 00009 (main.go:3) CMPQ SP, 16(CX) ; Stack overflow detection 0x000d 00013 (main.go:3) PCDATA $0, $-2; GC related 0x000d 00013 (main.go:3) JLS 64 0x000f 00015 (main.go:3) PCDATA $0, $-1; GC related 0x000f 00015 (main.go:3) SUBQ $32, SP; 32bytes stack address 0x0013 00019 (main.go:3) MOVQ BP, 24(SP); Store the value of BP on stack 0x0018 00024 (main.go:3) LEAQ 24(SP), BP; Will just 8 bytes allocated stack space address assigned to the BP 0 x001d 00029 (main) go: 3) FUNCDATA $0, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); GC related 0 x001d 00029 (main. Go: 3) FUNCDATA $1, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); GC related 0x001D 00029 (main.go:4) MOVQ $1, (SP); MOVQ $2, 8(SP); MOVQ $2, 8(SP); MOVQ $2, 8(SP); SP 0x002E 00046 (main.go:4) PCDATA $1, $0 0x002E 00046 (main.go:4) CALL "".add(SB); Call add function 0x0033 00051 (main.go:5) MOVQ 24(SP), BP; Restore BP 0x0038 00056 (main.go:5) ADDQ $32, SP; Increase the value of SP and shrink the stack, retracting 32 bytes of stack space 0x003c 00060 (main.go:5) RETCopy the code

Let's see what the above assembly does in detail:

0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $32-0
Copy the code

0x0000: Offset of the current instruction relative to the current function;

TEXT: Since the program code is stored in memory at runtime, TEXT is an instruction that defines a function.

"".main(SB): indicates the package name. Function name, where package name is omitted. SB is a virtual register that holds the static-base pointer, the beginning address of our program's address space;

$32-0: the size of stack frames to be allocated by the $32 table; 0 specifies the size of the argument passed in by the caller.

0x000d 00013 (main.go:3) PCDATA $0, $-2 ; GC related 0x000f 00015 (main.go:3) PCDATA $0, $-1; GC related 0 x001d 00029 (main. Go: 3) FUNCDATA $0, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); GC related 0 x001d 00029 (main. Go: 3) FUNCDATA $1, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); The GC relatedCopy the code

The FUNCDATA and PCDATA directives contain information for use by the garbage collector; they are introduced by the compiler.

FUNCDATA and PCDATA directives contain information to be used by garbage collection; These instructions are inserted by the compiler.

0x000f 00015 (main.go:3)        SUBQ    $32, SP
Copy the code

SUBQ $32 is called based on the current size of the stack frame. SP means 32bytes of stack memory is allocated.

0x0013 00019 (main.go:3) MOVQ BP, 24(SP) ; Store the value of BP on stack 0x0018 00024 (main.go:3) LEAQ 24(SP), BP; Assign the address of the 8bytes of stack space just allocated to BPCopy the code

It takes 8 bytes (24(SP)-32(SP)) to store the current frame pointer BP.

0x001d 00029 (main.go:4) MOVQ $1, (SP) ; MOVQ $2, 8(SP); MOVQ $2, 8(SP); MOVQ $2, 8(SP); The second argument, 2, to add is written to SPCopy the code

The value 1 is pushed to the (0(SP)-8(SP) position on the stack;

The value of 2 is pushed to the (8(SP)-16(SP) position on the stack;

Note that our argument type here is int, which in 64 bits is 8 bytes. Although the stack grows from high address bits to low address bits, the data blocks in the stack are stored from low address bits to high address bits, and the pointer points to the starting position of the low address bits of the data block.

To sum up, we can know two things about passing parameters in a function call:

  1. Parameters are passed entirely through the stack
  2. Push the argument list from right to left

Here are the call details of the stack before the add function is called:

When we are ready with the function's input, we CALL the assembly instruction CALL "".add(SB), which first places the return address of main (8 bytes) on the stack, then changes the current stack pointer SP and executes the assembly instruction for Add.

Let's go to the add function:

"".add STEXT nosplit size=25 args=0x18 locals=0x0 0x0000 00000 (main.go:7) TEXT "".add(SB), NOSPLIT|ABIInternal, $24-00000 (main) go: 7) FUNCDATA x0000 $0, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); GC related 0 x0000 00000 (main. Go: 7) FUNCDATA $1, gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB); GC related 0x0000 00000 (main.go:7) MOVQ $0, "".~r2+24(SP); Initialization Return value 0x0009 00009 (main.go:8) MOVQ "". AX = 1 0x000e 00014 (main.go:8) ADDQ "".b+16(SP), AX ; AX = AX + 2 0x0013 00019 (main.go:8) MOVQ AX, "".~r2+24(SP) ; (24)SP = AX = 3 0x0018 00024 (main.go:8) RETCopy the code

Since it changes the current stack pointer SP, let's take a look at the stack data before we look at the assembly code of this function. Here we can actually DLV operation:

Before entering the add function, we can use regS to print the current Rsp and Rbp registers:

(dlv) regs 
   Rsp = 0x000000c000044760
   Rbp = 0x000000c000044778
 	 ...

(dlv)  print uintptr(0x000000c000044778)
824634001272
(dlv)  print uintptr(0x000000c000044760)
824634001248
Copy the code

The difference of 24 bytes between the Rsp and Rbp addresses is consistent with the above illustration.

After entering the add function, we can use regs to print the current Rsp and Rbp registers:

(dlv) regs
   Rsp = 0x000000c000044758
   Rbp = 0x000000c000044778
   ...

(dlv)  print uintptr(0x000000c000044778)
824634001272
(dlv)  print uintptr(0x000000c000044758)
824634001240
Copy the code

The difference between the Rsp and Rbp addresses is 32 bytes. This is because the return address (8-byte value) of the function is pushed to the top of the stack when the CALL instruction is called.

In this case, the position of parameter 1 and parameter 2 will also change:

The value of 1 in (0(SP)-8(SP)) will be moved to (8(SP)-16(SP));

The value of 2 at (8(SP)-16(SP)) will be moved to (16(SP)-24(SP));

We can also print the parameter values via DLV:

(dlv) print *(*int)(uintptr(0x000000c000044758)+8)
1
(dlv) print *(*int)(uintptr(0x000000c000044758)+16)
2
Copy the code

Here are the call details of the stack after the add function is called:

We can also draw the following conclusions from the above analysis of the add function call:

  • The return value is passed through the stack, and the stack space of the return value precedes the parameter

After the call, let's look at the return from add:

0x002e 00046 (main.go:4) CALL "".add(SB) ; Call add function 0x0033 00051 (main.go:5) MOVQ 24(SP), BP; Restore BP 0x0038 00056 (main.go:5) ADDQ $32, SP; Increase the value of SP and shrink the stack, retracting 32 bytes of stack space 0x003c 00060 (main.go:5) RETCopy the code

The BP pointer is restored after the add function is called, and the ADDQ instruction increases the SP value, performing stack shrinkage. As can be seen from this, the last caller (caller) is responsible for stack cleaning.

The following stack call rules are summarized:

  1. Parameters are passed entirely through the stack
  2. Push the argument list from right to left
  3. The return value is passed through the stack, and the stack space of the return value precedes the parameter
  4. After the function is called, the caller (caller) is responsible for cleaning the stack

Structure methods: value receiver and pointer receiver

As mentioned above, there are two kinds of Go method receivers, one is value receiver and the other is pointer receiver. Let's use an example to illustrate:

package main

func main(a) { 
	p := Point{2.5} 
	p.VIncr(10)
	p.PIncr(10)}type Point struct {
	X int
	Y int
}

func (p Point) VIncr(factor int) {
	p.X += factor
	p.Y += factor
}

func (p *Point) PIncr(factor int) {
	p.X += factor
	p.Y += factor
} 
Copy the code

You can view the manual assembly output together with the article.

Call the value Receiver method

In assembly, our structure is actually a contiguous memory at the assembly level, so p := Point{2, 5} is initialized as follows:

0x001d 00029 (main.go:5) XORPS X0, X0 ;; Initialize register X0 0x0020 00032 (main.go:5) MOVUPS X0, "".p+24(SP); Initial size 16bytes Continuous memory block 0x0025 00037 (main.go:5) MOVQ $2, "".p+24(SP); MOVQ $5, "".p+32(SP); Initializes the structure p parameter yCopy the code

Int is 8bytes on a 64-bit machine, so XORPS is used to initialize the 128-bit X0 register. Then use MOVUPS to assign 128-bit X0 to 24(SP) for a 16bytes memory block. It then initializes two parameters of Point, 2 and 5.

The next step is to initialize the variable and call the p.vincr method:

0x0037 00055 (main.go:7) MOVQ $2, (SP) ;; Initialize variable 2 0x003f 00063 (main.go:7) MOVQ $5, 8(SP); Initialize variable 5 0x0048 00072 (main.go:7) MOVQ $10, 16(SP);; Initialize variable 10 0x0051 00081 (main.go:7) PCDATA $1, $0 0x0051 00081 (main.go:7) CALL "".point.vincr (SB); Call the value Receiver methodCopy the code

At this point, the stack frame structure before the call looks something like this:

P.vincr assembler code:

"".Point.VIncr STEXT nosplit size=31 args=0x18 locals=0x0 0x0000 00000 (main.go:16) TEXT "".Point.VIncr(SB), NOSPLIT|ABIInternal, $0-24 0x0000 00000 (main.go:16) FUNCDATA $0, Gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB) 0 x0000 00000 (main) go: 16) FUNCDATA $1. Gclocals · 33 cdeccccebe80329f1fdbee7f5874cb (SB) 0 x0000 00000 (main) go: 17) MOVQ "" p + 8 (SP), AX;; AX = 8(SP) = 2 0x0005 00005 (main.go:17) ADDQ "".factor+24(SP), AX ;; AX = AX + 24(SP) = 2+10 0x000a 00010 (main.go:17) MOVQ AX, "".p+8(SP) ;; 8(SP) = AX = 12 0x000f 00015 (main.go:18) MOVQ "".p+16(SP), AX ;; AX = 16(SP) = 5 0x0014 00020 (main.go:18) ADDQ "".factor+24(SP), AX ;; AX = AX + 24(SP) = 5+10 0x0019 00025 (main.go:18) MOVQ AX, "".p+16(SP) ;; 16(SP) = AX = 15 0x001e 00030 (main.go:19) RETCopy the code

The stack frame structure would look something like this:

As can be seen from the above analysis, caller actually assigns values on the stack to VIncr as parameters in the call. As for the modification in VIncr, it actually changes the values of the last two parameters on the stack.

Call the pointer Receiver method

Inside main, the directive is called:

0x0056 00086 (main.go:8) LEAQ "".p+24(SP), AX ;; MOVQ AX, (SP);; MOVQ $10, 8(SP); MOVQ $10, 8(SP); MOVQ $10, 8(SP); Use 10 as the second argument 0x0068 00104 (main.go:8) CALL "".(*Point).pincr (SB); Call the Pointer Receiver methodCopy the code

We know from the above assembly that AX is actually the address of 24(SP), and that the pointer to AX is also assigned to the first argument to SP. The first argument to AX and SP is the address of 24(SP).

The entire stack frame structure should look like the following figure:

P.pinr assembler code

"".(*Point).PIncr STEXT nosplit size=53 args=0x10 locals=0x0 0x0000 00000 (main.go:21) TEXT "".(*Point).PIncr(SB), NOSPLIT|ABIInternal, $0-16 0x0000 00000 (main.go:21) FUNCDATA $0, Gclocals · 1 a65e721a2ccc325b382662e7ffee780 (SB) 0 x0000 00000 (main) go: 21) FUNCDATA $1, Gclocals · 69 c1753bd5f81501d95132d08af04464 (SB) 0 x0000 00000 (main) go: 22) MOVQ "" p + 8 (SP), AX;; MOVQ "". P +8(SP), CX;; Go :22) TESTB AL, (CX) 0x000e 00014 (main.go:22) MOVQ (AX), AX; ADDQ "". Factor +16(SP), AX;; MOVQ AX, (CX); MOVQ (CX); MOVQ (CX); MOVQ (CX); MOVQ (CX); Write the calculation result to CX memory address 0x0019 00025 (main.go:23) MOVQ "". MOVQ "". P +8(SP), CX;; Go :23) TESTB AL, (CX) 0x0027 00039 (main.go:23) MOVQ 8(AX), AX; ADDQ "". Factor +16(SP), AX;; AX = 5+10 0x0030 00048 (main.go:23) MOVQ AX, 8(CX) ;; Write the result 15 to the memory address 0x0034 00052 (main.go:24) RET of CX+8Copy the code

It's actually kind of interesting and convoluted in this method because a lot of it is actually manipulation of Pointers so that changes made by either side affect the other side.

Here we go step by step:

0x0000 00000 (main.go:22)       MOVQ    "".p+8(SP), AX
0x0007 00007 (main.go:22)       MOVQ    "".p+8(SP), CX 
0x000e 00014 (main.go:22)       MOVQ    (AX), AX
Copy the code

AX (SP); CX (SP); CX (SP);

0x0011 00017 (main.go:22)       ADDQ    "".factor+16(SP), AX 
0x0016 00022 (main.go:22)       MOVQ    AX, (CX)
Copy the code

This will add the 16(SP) argument passed to AX, so AX should have stored 12. CX (SP); CX (SP); AX (SP); CX (SP);

We can verify this by using DLV output regS:

(dlv) regs
	Rsp = 0x000000c000056748
	Rax = 0x000000000000000c
	Rcx = 0x000000c000056768
Copy the code

Then we can look at the values stored in 8(SP) and CX:

(dlv) print *(*int)(uintptr(0x000000c000056748) +8  ) 
824634074984
(dlv) print uintptr(0x000000c000056768)
824634074984

Copy the code

You can see that they both point to the same 32(SP) pointer:

(dlv) print uintptr(0x000000c000056748) +32
824634074984
Copy the code

We can then print the value to which the pointer points:

(dlv) print *(*int)(824634074984) 
12
Copy the code

The stack frame looks like this:

Let's move on:

0x0019 00025 (main.go:23)       MOVQ    "".p+8(SP), AX
0x0020 00032 (main.go:23)       MOVQ    "".p+8(SP), CX
Copy the code

We're going to assign the address at 8(SP) to AX and CX;

Here we run the code to MOVQ "". P +8(SP), CX after executing the line, and then check AX pointer position:

(dlv) disassemble
		...
        main.go:21      0x467980        488b4c2408      mov rcx, qword ptr [rsp+0x8]
=      main.go:21      0x467985        8401            test byte ptr [rcx], al
        main.go:21      0x467987        488b4008        mov rax, qword ptr [rax+0x8]
        ...
        
(dlv) regs
    Rsp = 0x000000c000056748
    Rax = 0x000000c000056768 
    Rcx = 0x000000c000056768

(dlv) print uintptr(0x000000c000056768)
824634074984
Copy the code

You can see that AX and CX point to the same memory location. Then we enter the following:

0x0027 00039 (main.go:23)       MOVQ    8(AX), AX
Copy the code

As mentioned earlier, the allocation of contiguous code blocks for the structure, 32(SP) to 48(SP) on the stack refer to the structure instantiated by the variable P, so in the print above, 824634074984 represents the value of the variable p.X. Then the address value of p.Y is 824634074984+8, we can also print the value of the address through DLV:

(dlv) print *(*int)(824634074984+8) 
5
Copy the code

So MOVQ 8(AX), AX is essentially adding 8 to the address, and then taking the result 5 and assigning it to AX.

0x002b 00043 (main.go:23)       ADDQ    "".factor+16(SP), AX ;; AX = AX +10
0x0030 00048 (main.go:23)       MOVQ    AX, 8(CX)
Copy the code

So what I'm doing here is I'm calculating AX is equal to 15, and then I'm writing that 15 into the memory address of CX plus 8, and I'm doing that and I'm changing the pointer at 40(SP).

At the end of this method, the stack frame looks like this:

An interesting thing to see from the above analysis is that when a pointer receiver method call is made, it actually copies the pointer to the structure onto the stack, and then all pointer-based operations are performed in the method call.

summary

Through analysis, we know that when calling the value Receiver method, the caller will write the parameter value on the stack, and the calling function callee actually operates the parameter value on the caller stack frame.

The difference between the pointer Receiver method and the value Receiver method is that the caller writes the address value of the parameter to the stack, so it can be directly reflected in the structure of the receiver after the call.

The literal method func literal

Func literal methods are called literal methods. In Go, these methods include anonymous functions and closures.

Anonymous functions

I will analyze it with a simple example:

package main

func main(a) {
	f := func(x int) int {
		x += x
		return x
	}
	f(100)}Copy the code

Let's look at the compilation below:

        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $32-0
        ...
        0x001d 00029 (main.go:4)        LEAQ    "".main.func1·f(SB), DX
        0x0024 00036 (main.go:4)        MOVQ    DX, "".f+16(SP)
        0x0029 00041 (main.go:8)        MOVQ    $100, (SP)
        0x0031 00049 (main.go:8)        MOVQ    "".main.func1·f(SB), AX
        0x0038 00056 (main.go:8)        PCDATA  $1, $0
        0x0038 00056 (main.go:8)        CALL    AX
        0x003a 00058 (main.go:9)        MOVQ    24(SP), BP
        0x003f 00063 (main.go:9)        ADDQ    $32, SP
        0x0043 00067 (main.go:9)        RET
Copy the code

The anonymous function actually passes the address of the entry of the anonymous function.

closure

What is a closure? There's a quote on Wikipedia about closures:

a closure is a record storing a function together with an environment.

Closures are entities composed of functions and their associated reference environments. Bear in mind that the following closure analysis will be much more complex.

I will analyze it with a simple example:

package main

func test(a) func(a) {
	x := 100
	return func(a) {
		x += 100}}func main(a) {
	f := test()
	f() //x= 200
	f() //x= 300
	f() //x= 400
} 
Copy the code

Since closures are contextual, let's use the test example as an example where the variable x changes every time the f() function is called. But as we know from other method calls, if a variable is on the stack then the variable is invalidated when the stack frame exits, so the closure's variables escape to the heap.

We can prove it by escape analysis:

[root@localhost gotest]$ go run -gcflags "-m -l" main.go 
# command-line-arguments
./main.go:4:2: moved to heap: x
./main.go:5:9: func literal escapes to heap
Copy the code

You can see that the variable X escaped into the heap.

Let's get straight to assembly:

Let's start with the main function:

"".main STEXT size=88 args=0x0 locals=0x18 0x0000 00000 (main.go:10) TEXT "".main(SB), ABIInternal, $24-0 0x0000 00000 (main.go:10) MOVQ (TLS), CX 0x0009 00009 (main.go:10) CMPQ SP, 16(CX) 0x000d 00013 (main.go:10) PCDATA $0, $-2 0x000d 00013 (main.go:10) JLS 81 0x000f 00015 (main.go:10) PCDATA $0, $-1 0x000f 00015 (main.go:10) SUBQ $24, SP 0x0013 00019 (main.go:10) MOVQ BP, 16(SP) 0x0018 00024 (main.go:10) LEAQ 16(SP), BP 0x001d 00029 (main.go:10) FUNCDATA $0, Gclocals · 69 c1753bd5f81501d95132d08af04464 (SB) 0 x001d 00029 (main) go: 10) FUNCDATA $1. Gclocals · 9 fb7f0986f647f17cb53dda1484e0f7a (SB) 0 x001d 00029 (main) go: 11) PCDATA $1. $0 0x001d 00029 (main.go:11) NOP 0x0020 00032 (main.go:11) CALL "".test(SB) ...Copy the code

In fact, this assembly is the same as any other assembly of function calls, nothing to talk about, just do some stack initialization before calling test.

Let's look directly at the test function:

0x0000 00000 (main.go:3) TEXT "".test(SB), ABIInternal, $40-8 0x0000 00000 (main.go:3) MOVQ (TLS), CX 0x0009 00009 (main.go:3) CMPQ SP, 16(CX) 0x000d 00013 (main.go:3) PCDATA $0, $-2 0x000d 00013 (main.go:3) JLS 171 0x0013 00019 (main.go:3) PCDATA $0, $-1 0x0013 00019 (main.go:3) SUBQ $40, SP 0x0017 00023 (main.go:3) MOVQ BP, 32(SP) 0x001c 00028 (main.go:3) LEAQ 32(SP), BP 0x0021 00033 (main.go:3) FUNCDATA $0, Gclocals · 263043 c8f03e3241528dfae4e2812ef4 (SB) 0 x0021 00033 (main) go: 3) FUNCDATA $1. Gclocals · 568470801006 e5c0dc3947ea998fe279 (SB) 0 x0021 00033 (main) go: 3) MOVQ $0, "".~r0+48(SP) 0x002a 00042 (main.go:4) LEAQ type.int(SB), AX 0x0031 00049 (main.go:4) MOVQ AX, (SP) 0x0035 00053 (main.go:4) PCDATA $1, $0 0x0035 00053 (main.go:4) CALL runtime.newobject(SB) ;; Apply memory 0x003a 00058 (main.go:4) MOVQ 8(SP), AX; MOVQ AX, "".x+24(SP); MOVQ $100, (AX);; Struct {F uintptr; "".x *int }(SB), AX ;; Create closure structure and write function address to AX 0x0052 00082 (main.go:5) MOVQ AX, (SP); Write (SP) 0x0056 00086 (main.go:5) PCDATA $1, $1 0x0056 00086 (main.go:5) CALL Runtime.newObject (SB);; MOVQ 8(SP), AX; MOVQ 8(SP), AX; Write memory address to AX 0x0060 00096 (main.go:5) MOVQ AX, ""... autotmp_4+16(SP) ;; Write the memory address to 16(SP) 0x0065 00101 (main.go:5) LEAQ "".test.func1(SB), CX; Write the address of the test.func1 function to CX 0x006c 00108 (main.go:5) MOVQ CX, (AX); 0x006f 00111 (main.go:5) MOVQ ""... autotmp_4+16(SP), AX ;; MOVQ "".x+24(SP), CX; Write the address value saved by 24(SP) to CX 0x007b 00123 (main.go:5) LEAQ 8(AX), DI; Write AX + 8 to DI 0x007f 00127 (main.go:5) PCDATA $0, $-2 0x007f 00127 (main.go:5) CMPL Runtime.writeBarrier (SB), $0 0x0086 00134 (main.go:5) JEQ 138 0x0088 00136 (main.go:5) JMP 164 0x008a 00138 (main.go:5) MOVQ CX, 8(AX) ;; AX+8 0x008E 00142 (main.go:5) JMP 144 0x0090 00144 (main.go:5) PCDATA $0, $-1 0x0090 00144 (main.go:5) MOVQ "".. autotmp_4+16(SP), AX 0x0095 00149 (main.go:5) MOVQ AX, "".~r0+48(SP) 0x009a 00154 (main.go:5) MOVQ 32(SP), BP 0x009f 00159 (main.go:5) ADDQ $40, SP 0x00a3 00163 (main.go:5) RETCopy the code

Let's look at the compilation step by step:

0x002a 00042 (main.go:4) LEAQ type.int(SB), AX ;; MOVQ AX, (SP); MOVQ (SP); MOVQ (SP); Write (SP) 0x0035 00053 (main.go:4) PCDATA $1, $0 0x0035 00053 (main.go:4) CALL Runtime.newObject (SB);; Apply memory 0x003a 00058 (main.go:4) MOVQ 8(SP), AX; MOVQ AX, "".x+24(SP); MOVQ $100, (AX) MOVQ $100, (AX)Copy the code

Int (SP); / / write (SP); / / write (SP); / / write (SP); / / write (SP); Finally, set the value of x to 100.

The stack frame structure should look like this:

0x004b 00075 (main.go:5)        LEAQ    type.noalg.struct { F uintptr; "".x *int }(SB), AX
Copy the code

This structure represents a closure and then puts the memory address of the created structure into the AX register.

0x0052 00082 (main.go:5)        MOVQ    AX, (SP)
Copy the code

This assembly instruction then writes the memory address saved in AX to (SP).

0x0056 00086 (main.go:5) CALL runtime.newobject(SB) ;; MOVQ 8(SP), AX; MOVQ 8(SP), AX; Write memory address to AX 0x0060 00096 (main.go:5) MOVQ AX, ""... autotmp_4+16(SP) ;; Write the memory address to 16(SP)Copy the code

I'm going to apply for a new block of memory, and I'm going to write the address from AX to 16(SP).

0x0065 00101 (main.go:5) LEAQ "".test.func1(SB), CX ;; Write the address of the test.func1 function to CX 0x006c 00108 (main.go:5) MOVQ CX, (AX); 0x006f 00111 (main.go:5) MOVQ ""... autotmp_4+16(SP), AX ;; Write the memory address saved by 16(SP) to AXCopy the code

Func1 writes the address of the function to CX, and then writes the address stored in CX to the memory pointed to by AX's memory address. We then write the address of 16(SP) to AX, which is the same as the address of 16(SP).

The stack frame structure is as follows: the stack frame structure is as follows: the stack frame structure is as follows: the stack frame structure is as follows: the stack frame structure is as follows: the stack frame structure is as follows:

0x0076 00118 (main.go:5) MOVQ "".x+24(SP), CX ;; Write the address value saved by 24(SP) to CX 0x007b 00123 (main.go:5) LEAQ 8(AX), DI; Write AX + 8 to DI 0x007f 00127 (main.go:5) CMPL Runtime.writeBarrier (SB), $0; Write barrier 0x0086 00134 (main.go:5) JEQ 138 0x0088 00136 (main.go:5) JMP 164 0x008A 00138 (main.go:5) MOVQ CX, 8(AX); Write the address saved in CX to AX+8Copy the code

24(SP) actually holds the pointer address of the x variable, which will be written to CX. The saved value of 8(AX) is then transferred to DI, and finally the saved value of CX is written to 8(AX).

Here's a quick reference to AX:

Func1 (AX - test.func1);

8(AX) - 24(SP) address value - 100, that is, 8(AX) store address value points to the 24(SP) address value, 24(SP) address value points to the memory store 100;

0x0090 00144 (main.go:5) MOVQ "".. autotmp_4+16(SP), AX ;; MOVQ AX, "".~r0+48(SP); Write the address saved in AX to 48(SP) 0x009A 00154 (main.go:5) MOVQ 32(SP), BP 0x009f 00159 (main.go:5) ADDQ $40, SPCopy the code

In this case, the value of 16(SP) will be written to the stack frame 48(SP) of the caller by AX. Finally, the stack contraction will be done. The callee stack call is completed.

After the call is complete, it returns to main, and the stack frame is as follows:

Let's go back to the position after main's test call:

0x0020 00032 (main.go:11) CALL "".test(SB) 0x0025 00037 (main.go:11) MOVQ (SP), DX ;; MOVQ DX, "".f+8(SP);; MOVQ (DX), AX; MOVQ (DX), AX; Write the address of the function stored in DX to AXCopy the code

Func1 (SP) on the top of main. Func1 (SP) on AX. Func1 (SP) on AX.

0x0031 00049 (main.go:12)       CALL    AX
Copy the code

Before going into the test.func1 function, we should now know that (SP) holds the address to AX.

Func1 is the function returned from the test wrapper:

"".test.func1 STEXT nosplit size=36 args=0x0 locals=0x10 0x0000 00000 (main.go:5) TEXT "".test.func1(SB), NOSPLIT|NEEDCTXT|ABIInternal, $16-0 0x0000 00000 (main.go:5) SUBQ $16, SP 0x0004 00004 (main.go:5) MOVQ BP, 8(SP) 0x0009 00009 (main.go:5) LEAQ 8(SP), BP 0x000e 00014 (main.go:5) MOVQ 8(DX), AX ;; MOVQ AX, "".x(SP) 0x0016 00022 (main.go:6) ADDQ $100, (AX); Add 100 0x001A 00026 (main.go:7) MOVQ 8(SP), BP 0x001f 00031 (main.go:7) ADDQ $16, SP 0x0023 00035 (main.go:7) RETCopy the code

Since DX holds the address value of AX, we can get the address value of variable x by 8(DX) and write it to AX. The ADDQ directive is then called to add 100 to the x address.

summary

From the above analysis, it can be found that anonymous functions are actually a kind of closure, but without passing variable information. In closure calls, context information escapes to the heap to avoid being reclaimed when the stack frame call ends.

In the above example closure call to test, which is very complicated and does a lot of variable passing, it actually does these things:

  1. Initialize a memory block for context information;
  2. Save the address value of the context information to the AX register;
  3. Func1 to the top of caller's stack;

The context information here refers to the X variable and the test.func1 function. Write the addresses of these two messages to the AX register and then go back to main, get the address of the function at the top of the stack and write it to AX and CALL AX.

Because the x address is written to AX + 8, the function test.func1 is called to change the closure context information by obtaining the x address from AX + 8.

conclusion

In this article, I first shared with you how the process of function call is, including the passing of parameters, the order of parameter pressing, the passing of function return value. It then looks at the differences between constructor method passes and what closure function calls look like.

The REGS command and step-instruction command of THE DLV tool help a lot when analyzing closures. Otherwise, it is easy to get confused when transferring the pointer between registers. It is suggested that you can move your hands and draw on the paper when reading.

Reference

━ Go function call stacks and register perspective segmentfault.com/a/119000001...

Function chai2010. Cn/advanced - go...

Berryjam. Making. IO / 2018/12 / gol...

Go Assembler primer github.com/go-internal...

Plan9 Assembly Full resolution github.com/cch123/gola...

Go Assembly by Example davidwong.fr/goasm/

golang.org/doc/asm

Under the x86-64 principle zhuanlan.zhihu.com/p/27339191 function calls and stack frame

www.cnblogs.com/binHome/p/1...

Chai2010. Cn/advanced - go...

Interfaces github.com/teh-cmc/go-...

Go 1.1 Function Calls docs.google.com/document/d/...

What is the difference between MOV and LEA? Stackoverflow.com/questions/1...

Function literals golang.org/ref/spec#Fu...

Closure hjlarry. Making. IO/docs/go/clo...

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.