What is the execution of defer in golang?

Posted by Frozenlight777 on Thu, 18 Jul 2019 21:19:43 +0200

In the same goroutine:

What is the call stack principle for multiple defer s?
How is the defer function called?

To explore the mystery, I've prepared the following code:

package main
import "fmt"

func main() {
    xx()
}
func xx() {
    defer aaa(100, "hello aaa")
    defer bbb("hello bbb")
    return
}

func aaa(x int, arg string) {
    fmt.Println(x, arg)
}

func bbb(arg string) {
    fmt.Println(arg)
}

Output:
bbb
100 hello aaa
The output looks much like the data structure of a stack: LIFO.

Start with assembly to see how the xx() function is executed. The commands are as follows:
go tool compile -S main.go >> main.s

"".xx STEXT size=198 args=0x0 locals=0x30
    0x0000 00000 (main.go:9)    TEXT    "".xx(SB), ABIInternal, $48-0
    0x0000 00000 (main.go:9)    MOVQ    (TLS), CX
    0x0009 00009 (main.go:9)    CMPQ    SP, 16(CX)
    0x000d 00013 (main.go:9)    JLS    188
    0x0013 00019 (main.go:9)    SUBQ    $48, SP
    0x0017 00023 (main.go:9)    MOVQ    BP, 40(SP)
    0x001c 00028 (main.go:9)    LEAQ    40(SP), BP
    0x0021 00033 (main.go:9)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0021 00033 (main.go:9)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0021 00033 (main.go:9)    FUNCDATA    $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
    0x0021 00033 (main.go:10)    PCDATA    $2, $0
    0x0021 00033 (main.go:10)    PCDATA    $0, $0
    0x0021 00033 (main.go:10)    MOVL    $24, (SP)
    0x0028 00040 (main.go:10)    PCDATA    $2, $1
    0x0028 00040 (main.go:10)    LEAQ    "".aaa·f(SB), AX
    0x002f 00047 (main.go:10)    PCDATA    $2, $0
    0x002f 00047 (main.go:10)    MOVQ    AX, 8(SP)
    0x0034 00052 (main.go:10)    MOVQ    $100, 16(SP)
    0x003d 00061 (main.go:10)    PCDATA    $2, $1
    0x003d 00061 (main.go:10)    LEAQ    go.string."hello aaa"(SB), AX
    0x0044 00068 (main.go:10)    PCDATA    $2, $0
    0x0044 00068 (main.go:10)    MOVQ    AX, 24(SP)
    0x0049 00073 (main.go:10)    MOVQ    $9, 32(SP)
    0x0052 00082 (main.go:10)    CALL    runtime.deferproc(SB)
    0x0057 00087 (main.go:10)    TESTL    AX, AX
    0x0059 00089 (main.go:10)    JNE    172
    0x005b 00091 (main.go:11)    MOVL    $16, (SP)
    0x0062 00098 (main.go:11)    PCDATA    $2, $1
    0x0062 00098 (main.go:11)    LEAQ    "".bbb·f(SB), AX
    0x0069 00105 (main.go:11)    PCDATA    $2, $0
    0x0069 00105 (main.go:11)    MOVQ    AX, 8(SP)
    0x006e 00110 (main.go:11)    PCDATA    $2, $1
    0x006e 00110 (main.go:11)    LEAQ    go.string."hello bbb"(SB), AX
    0x0075 00117 (main.go:11)    PCDATA    $2, $0
    0x0075 00117 (main.go:11)    MOVQ    AX, 16(SP)
    0x007a 00122 (main.go:11)    MOVQ    $9, 24(SP)
    0x0083 00131 (main.go:11)    CALL    runtime.deferproc(SB)
    0x0088 00136 (main.go:11)    TESTL    AX, AX
    0x008a 00138 (main.go:11)    JNE    156
    0x008c 00140 (main.go:12)    XCHGL    AX, AX
    0x008d 00141 (main.go:12)    CALL    runtime.deferreturn(SB)

Find the parameters of the aaa() function and call function deferproc(SB):


 0x0021 00033 (main.go:10)   MOVL    $24, (SP)
 0x0028 00040 (main.go:10)   PCDATA  $2, $1 
 0x0028 00040 (main.go:10)   LEAQ    "".aaa·f(SB), AX
 0x002f 00047 (main.go:10)   PCDATA  $2, $0 
 0x002f 00047 (main.go:10)   MOVQ    AX, 8(SP)
 0x0034 00052 (main.go:10)   MOVQ    $100, 16(SP)
 0x003d 00061 (main.go:10)   PCDATA  $2, $1 
 0x003d 00061 (main.go:10)   LEAQ    go.string."hello aaa"(SB), AX
 0x0044 00068 (main.go:10)   PCDATA  $2, $0 
 0x0044 00068 (main.go:10)   MOVQ    AX, 24(SP)
 0x0049 00073 (main.go:10)   MOVQ    $9, 32(SP)
 0x0052 00082 (main.go:10)   CALL    runtime.deferproc(SB)

Unified description of the above key codes:

//1, (SP) puts 24 on the top of the stack (24 is actually the sum of the length of the deferd function parameter type referred to below).
 0x0021 00033 (main.go:10)   MOVL    $24, (SP)

//2, 8(SP) places the aaa function pointer in AX; the aaa function pointer in 8(SP).
 0x0028 00040 (main.go:10)   LEAQ    "".aaa·f(SB), AX
 0x002f 00047 (main.go:10)   MOVQ    AX, 8(SP)

//3, 16(SP) places the first parameter 100 of function aaa into 16(SP).
 0x0034 00052 (main.go:10)   MOVQ    $100, 16(SP)

//4, 24(SP) takes the memory address of the second parameter and assigns it to AX; the AX median is assigned to 24(SP).
 0x003d 00061 (main.go:10)   LEAQ    go.string."hello aaa"(SB), AX
 0x0044 00068 (main.go:10)   MOVQ    AX, 24(SP)

//5,32(SP), assign the second parameter string length 9 to 32(SP).
 0x0049 00073 (main.go:10)   MOVQ    $9, 32(SP)

//Call runtime.deferproc(SB)
 0x0052 00082 (main.go:10)   CALL    runtime.deferproc(SB)

0(SP) = 24 //aaa(int, string) parameter type length and
8 (SP) = &aaa (int, string)//deferd function pointer
16 (SP) = 100// First parameter value 100
24(SP) = "hello aaa" //second parameter
32(SP) = 9//second parameter string length
From the above two parts of the assembly code, you can see that the function-related data is placed in SP and is continuous.2, Discover
The defer aaa(int, string) compiler inserts the deferproc(SB) function.
Take a look at the source code:

//runtime/panic.go

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
    if getg().m.curg != getg() {
        throw("defer on system stack")
    }
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()

    d := newdefer(siz)
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }
    d.fn = fn
    d.pc = callerpc
    d.sp = sp
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }
    return0()
}
deferproc(siz int32, fn *funcval)

The parameter found for this function is int32, *funcval.What do they both represent?We have a gdb to track what it means:

siz=0x18 means siz=24.The aaa(int, string) parameter int takes up 8 bytes and the string 16 bytes.Why do string types account for 16 bytes?
Because the prototype of string type is:

type stringStruct struct {
    str unsafe.Pointer
    len int
}

unsafe.Pointer8 bytes and int 8 bytes.
Explanation of specific strings to see my previous articles string, encoding in golang
Next *funcval: Its prototype is as follows:

//runtime/runtime2.go

type funcval struct {
   fn uintptr
   // variable-size, fn-specific data here
}

funcval is a struct, and its members are fn uintptr, which is a pointer to a function by guessing what fn means literally.

As mentioned earlier, data about the bbb(int, string) function is put into SP. The parameter in func deferproc (siz int32, fn funcval) is that the runtime system takes siz and fn from SP and then calls deferproc(siz int32, fn * funcval).

Let's use gdb to see what exactly the function fn points to here is:

The original d.fn.fn was the specific instruction of the aaa(int, string) function.
What does d mean, Track Discovery:

d := newdefer(siz)

Take a look at its prototype:

func newdefer(siz int32) *_defer

Its return value is *_defer, take a look at its definition:

//runtime/runtime2.go

type _defer struct {
    siz     int32 
    started bool
    sp      uintptr // sp at time of defer
    pc      uintptr
    fn      *funcval 
    _panic  *_panic // panic that is running defer
    link    *_defer
}

It is a structure.Let's first look at the three parameters siz, fn, link. The other parameters are explained below due to limited space.
The sum of the byte lengths of the siz:deferd function parameter prototype.
fn:deferd function pointer.
link: What does it mean?????
Take a look at the implementation of newdefer(siz) with questions:

func newdefer(siz int32) *_defer {
    var d *_defer
    sc := deferclass(uintptr(siz))
    // g-struct object of current goroutine  
    gp := getg()
    if sc < uintptr(len(p{}.deferpool)) {
                //p of the current goroutine binding
        pp := gp.m.p.ptr()
                
        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
            // Take the slow path on the system stack so
            // we don't grow newdefer's stack.
            systemstack(func() {//Switch to System Stack
                lock(&sched.deferlock)
                //Get some defers from the global deferpool and place them in the local deferpool of p
                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }
                unlock(&sched.deferlock)
            })
        }
        if n := len(pp.deferpool[sc]); n > 0 {
            d = pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
        }
    }
    if d == nil {//defer not created in cache
        // Allocate new defer+args.
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
        if debugCachedWork {
            // Duplicate the tail below so if there's a
            // crash in checkPut we can tell if d was just
            // allocated or came from the pool.
            d.siz = siz
            d.link = gp._defer
            gp._defer = d
            return d
        }
    }
    d.siz = siz //Assigning siz
        //Assign g's_defer to d.link
    d.link = gp._defer
        //d is assigned to g._defer
    gp._defer = d
    
    return d
}

This is the defer generation process, which basically means finding a defer from the cache and creating one if it does not, then assigning size, link.
Focus on the following code:

    d.link = gp._defer
    gp._defer = d

The above two lines of code implementation have already been explained, which is explained in more detail here:
By binding the defer just generated to g._defer, you are putting the latest defer on
g._defer as the chain header.Then bind g._defer to d.link, as illustrated below:

[current g]{defer} => [new d1]{link} => [g]{old_defer}

If there is another newly generated defer(d2), the list of chains is as follows:

[current g]{defer} => [new d2]{link} => [new d1]{link} => [g]{old_defer}

Returning to the deferproc(siz int32, fn *funcval) function, what does the second line above newdefer(siz) mean?:

    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)

Continuing to track with gdb, we find that argp is involved in this line, as shown in Figure 2 below:

The memmove function was found, and it acts as a copy.Copy siz(24 bytes here) bytes from the argp position to the back of the d structure.
Run this line to see what data is copied to the back of the d structure?See Figure 3:

The first line in the red box in Figure 3 is 0x64 and its 10-digit representation is 100.This is proved to be the first parameter of the aaa function. Similarly, the second line 0x4b9621 is a pointer to the second parameter string to see if it is as expected, as shown in Figure 4:

The figure above is a 10-digit representation that makes it easy to find the corresponding character in ascii, and from the ASCII table you can see that it is really the second parameter of the AAA function, hello aaa.So I draw the conclusion that the parameter of the deferd function is behind the deferd structure.The third line represents the length of the string.That is, the second and third lines represent the values of the string prototype (struct).

Continue tracking function execution:

 defer bbb("hello bbb")

The execution of bbb(string) is the same as that of the aaa(int, string) function above, and the demonstration is not repeated here.
Run the return after the deferproc stack is executed, as shown in Figure 5:

Then press s to enter the return implementation (to the deferreturn stack), as shown in Figure 6 below:

Take a look at its implementation:

//rutime/painc.go

//go:nosplit
func deferreturn(arg0 uintptr) {
    gp := getg() //Get the current g 
    d := gp._defer //Get the _defer chain header for the current g

  //Why can d be nil because defer functions can be nested, for example:
  //  defer a -> defer b -> defer c
  //The deferreturn function is called at least once, returning all defers in the list directly after execution.
    if d == nil {
        return
    }
    sp := getcallersp()
    if d.sp != sp {
        return
    }

    //Copy the deferd function parameter to arg0 in preparation for calling the deferd function.
    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize://If the size of the siz is the pointer size, it is copied directly as follows to reduce the cpu operation.
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }

    fn := d.fn //Copy d.fn
    d.fn = nil //Set d.fn to null
    gp._defer = d.link//Bind the next defer of the current defer to the chain header.
    freedefer(d) //Release d
       //fn is the deferd function, the second parameter is the deferd function's parameter
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
fn := d.fn
d.fn = nil 
gp._defer = d.link
freedefer(d) 

Explain the four lines above: Bind the next defer in the list to gp._defer.Release the current defer.See the following diagram:

[current g]{defer} => [new d2]{link} => [new d1]{link} => [g]{old_defer}

Run d2:

[current g]{defer} => [new d1]{link} => [g]{old_defer}

Then take a look at the following jmpdefer function:

    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))

This function is where the defer function is executed. Before we see it implemented, remember the deferreturn entry address in Figure 7 below. This address will be described below.

The jmpdefer function implementation is shown in the following code:

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
    MOVQ    fv+0(FP), DX    // fn
    MOVQ    argp+8(FP), BX    // caller sp
    LEAQ    -8(BX), SP    // caller sp after CALL
    MOVQ    -8(SP), BP    // restore BP as if deferreturn returned (harmless if framepointers not in use)
    SUBQ    $5, (SP)    // return to CALL again
    MOVQ    0(DX), BX
    JMP    BX    // but first run the deferred function

Explain line by line:

    MOVQ    fv+0(FP), DX    // fn

Copy the fn pointer, the first parameter of the function, to DX so that subsequent code can execute the deferd function by taking the fn pointer from DX.

    MOVQ    argp+8(FP), BX  // caller sp

Copy the second parameter of the function, argp pointer, to BX, which is the address of the first parameter of the deferd function.

    LEAQ    -8(BX), SP  // caller sp after CALL

From the second instruction above, you can see that BX stores the address of the first parameter of the deferd function.Because gbd is debugging the bbb(string) function at this time, the parameter is a string structure, accounting for 16 bytes in total, the first 8 bytes are data pointers, and the last 8 are lengths.What's in -8(BX), that is, what's the East (bottom) before the bbb(string) parameter value.Follow this command with gdb to see what the memory value in SP (because it is assigned to SP) is, as shown in Figure 8.

The first line is the -8(BX) we want to determine
The second line is a parameter in bbb(string), which is a string pointer in the string structure and points to a specific string.
The third line is the length of the string, which is 9 here.
Let's look at the stack as shown in Figure 9:

What is 0x4872c6, pointer?Try to see if it can point to specific memory as shown in Figure 10 below

Originally the main.xx directive.Remember Figure 7 just now, I'm going to cut off Figure 7, see Figure 11:

The next line at the red line is 0x4872c6, which is the same value as Figure 10.According to Figure 11, this address is the next instruction of rutime.deferreturn(SB), that is, the return address of rutime.deferreturn(SB).
Look closely at these two addresses:

0x4872c1 == rutime.deferreturn(SB)
0x4872c6 == rutime.deferreturn(SB) next instruction address (also called return address)

They were found to be five bytes apart.According to assembly knowledge, how does the cpu find the next instruction is determined by the number of bytes occupied by the current instruction.
len(0x4872c6) - len(0x4872c1) == 5 knowable

call runtime.deferreturn(sb)

It takes up five bytes, so 0x4872c1+5 gets the next instruction header address.

Line 4:

    MOVQ    -8(SP), BP  // restore BP as if deferreturn returned (harmless if framepointers not in use)

Print BP value = 0xc000032778
Take a look at the stack, see Figure 12

The current stack is already main.xx.

Line 5:

SUBQ  $5, (SP)  # return to CALL again

From the explanation in line 3, if the data pointed at by SP (the return address of runtime.deferreturn) is minus 5, it is exactly the instruction entry for runtime.deferreturn(SB).See Figure 13:

Lines 6, 7:

MOVQ    0(DX), BX
JMP BX  // but first run the deferred function

Assign function instructions pointed to by DX to BX
Execute fn.fn, also known as bbb(string).
Execute to bbb(string), see Figure 14

rsp now moves 0x70 bytes to the low address.
Break the end of the bbb(string) and execute there as shown in Figure 15:

In Figure 14, the SP moves to the low address by 0x70.
In Figure 15, the SP moves to the high address by 0x70.
That is, the SP will return to its previous pointing state.Where did the previous SP point to?This is the runtime.deferreturn(SB) entry shown in Figure 13.
In Figure 15, add rsp, the next line of instruction 0x70 is a ret instruction.This function is not available in the bbb(string) function, which was added by the compiler to pop the eight bytes at the top of the current stack into the rip register so that the cpu executes the instructions in the rip into the runtime.deferreturn(SB), which implements a recursive call to deferreturn(SB).This in turn completes execution on the deferd chain.

Continue to runtime.deferreturn(SB)
The following code:

    if d == nil {
        return
    }

This if statement is to determine if there is a deferd function on the defer chain, if not, it returns directly.This avoids an infinite recursive loop.
There are a few more lines of code in it:

sp := getcallersp()
    if d.sp != sp {
        return
    }

Interested buddies can try to see why this is so, and because of time constraints, research on this code is not going on here.

This article is mainly about the execution of defer. For space reasons, I'll explore panic, recover, and error-prone defer sentences in the next article. Please look forward to ~

Reference resources
Defer--go Language Core Programming Technology
defer in golang

Topics: Go ascii encoding Programming