The main points of this section are:

  1. How does a non-main goroutine return to Goexit
  2. How does McAll switch to G0 to continue execution
  3. Scheduling cycle

There is the following code

package main

import "time"

func hello(a) {
	println("msg")}func main(a) {
	go hello()
	time.Sleep(time.Hour * 1)}Copy the code

Compile go build-gcflags “-n-l” -ldflags=-compressdwarf=false main.go with GDB: GDB main

(gdb) b main.hello     
Breakpoint 1 at 0x1057240: file /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go, line 5.
(gdb) r
Starting program: /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main
[New Thread 0x1a03 of process 88861]
[New Thread 0x1b03 of process 88861]
warning: unhandled dyld version (17)
[New Thread 0x1d03 of process 88861]
[New Thread 0x1f07 of process 88861]
[New Thread 0x2307 of process 88861]
[New Thread 0x5403 of process 88861]
[Switching to Thread 0x2307 of process 88861]

Thread 5 hit Breakpoint 1, main.hello () at /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go:5
5	func hello(a) {
(gdb) disas
Dump of assembler code for function main.hello:
=> 0x0000000001057240 <+0>:	cmp    0x10(%r14),%rsp
   0x0000000001057244 <+4>:	jbe    0x1057279 <main.hello+57>
   0x0000000001057246 <+6>:	sub    $0x18,%rsp
   0x000000000105724a <+10>:	mov    %rbp,0x10(%rsp)
   0x000000000105724f <+15>:	lea    0x10(%rsp),%rbp
   0x0000000001057254 <+20>:	call   0x102d2e0 <runtime.printlock>
   0x0000000001057259 <+25>:	lea    0xcc55(%rip),%rax        # 0x1063eb5 <go.string. * +309>
   0x0000000001057260 <+32>:	mov    $0x4,%ebx
   0x0000000001057265 <+37>:	call   0x102dc00 <runtime.printstring>
   0x000000000105726a <+42>:	call   0x102d360 <runtime.printunlock>
   0x000000000105726f <+47>:	mov    0x10(%rsp),%rbp
   0x0000000001057274 <+52>:	add    $0x18,%rsp
   0x0000000001057278 <+56>:	ret
   0x0000000001057279 <+57>:	call   0x1052da0 <runtime.morestack_noctxt>
   0x000000000105727e <+62>:	xchg   %ax,%ax
   0x0000000001057280 <+64>:	jmp    0x1057240 <main.hello>
End of assembler dump.
(gdb) b *0x0000000001057278
Breakpoint 2 at 0x1057278: file /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go, line 7.
(gdb) c
Continuing.
msg

Thread 5 hit Breakpoint 2.0x0000000001057278 in main.hello () at /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go:7
7	}
(gdb) disas
Dump of assembler code for function main.hello:
   0x0000000001057240 <+0>:	cmp    0x10(%r14),%rsp
   0x0000000001057244 <+4>:	jbe    0x1057279 <main.hello+57>
   0x0000000001057246 <+6>:	sub    $0x18,%rsp
   0x000000000105724a <+10>:	mov    %rbp,0x10(%rsp)
   0x000000000105724f <+15>:	lea    0x10(%rsp),%rbp
   0x0000000001057254 <+20>:	call   0x102d2e0 <runtime.printlock>
   0x0000000001057259 <+25>:	lea    0xcc55(%rip),%rax        # 0x1063eb5 <go.string. * +309>
   0x0000000001057260 <+32>:	mov    $0x4,%ebx
   0x0000000001057265 <+37>:	call   0x102dc00 <runtime.printstring>
   0x000000000105726a <+42>:	call   0x102d360 <runtime.printunlock>
   0x000000000105726f <+47>:	mov    0x10(%rsp),%rbp
   0x0000000001057274 <+52>:	add    $0x18,%rsp
=> 0x0000000001057278 <+56>:	ret
   0x0000000001057279 <+57>:	call   0x1052da0 <runtime.morestack_noctxt>
   0x000000000105727e <+62>:	xchg   %ax,%ax
   0x0000000001057280 <+64>:	jmp    0x1057240 <main.hello>
End of assembler dump.
(gdb) si
runtime.goexit () at /Users/zixuan.xu/.gvm/gos/go117./src/runtime/asm_amd64.s:1582
1582CALL the runtime, goexit1 (SB)// does not return
(gdb) disas
Dump of assembler code for function runtime.goexit:
   0x0000000001053060 <+0>:	nop
=> 0x0000000001053061 <+1>:	call   0x10551e0 <runtime.goexit1>
   0x0000000001053066 <+6>:	nop
End of assembler dump.
Copy the code
  1. Set a breakpoint at main.hello and execute there
  2. Place a breakpoint at RET and execute to ret
  3. Using si to enter the ret call, you can see that the function goexit is entered

asm_amd64.s:1580

// The top-most function running on a goroutine
// returns to goexit+PCQuantum.The TEXT, the runtime goexit (SB), NOSPLIT | TOPFRAME, $00
   BYTE   $0x90  // NOPCALL the runtime, goexit1 (SB)// does not return
   // traceback from goexit1 must hit code range of goexit
   BYTE   $0x90  // NOP
Copy the code

In fact, after si is called, it points to the instruction in the second line of goexit, which is the newg.sched.sp field in the previous section. It is now also possible to prove that non-Girouitne execution does not exit

proc.go:3622

Next, call goexit1

// Finishes execution of the current goroutine.
func goexit1(a) {
   if raceenabled {
      racegoend()
   }
   if trace.enabled {
      traceGoEnd()
   }
   mcall(goexit0)
}
Copy the code

Here we switch from goroutine’s stack to G0’s stack via McAll

asm_amd64.s:282

// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.#ifdef GOEXPERIMENT_regabiargs TEXT Runtime · McAll <ABIInternal>(SB), NOSPLIT, $0- 8 -
   MOVQ   AX, DX // DX = fn

   // save state in g->sched
   MOVQ   0(SP), BX  // caller's PC
   MOVQ   BX, (g_sched+gobuf_pc)(R14) // newg.sched.pc=return addr
   LEAQ   fn+0(FP), BX   // caller's SP // BX=&fn
   MOVQ   BX, (g_sched+gobuf_sp)(R14)   //newg.sched.sp=&fn
   MOVQ   BP, (g_sched+gobuf_bp)(R14)   //newg.sched.bp = BP

   // switch to m->g0 & its stack, call fn
   MOVQ   g_m(R14), BX     // BX=newg.m
   MOVQ   m_g0(BX), SI   // SI = g.m.g0
   CMPQ   SI, R14    // if g == m->g0 call badmcallJNE Goodm JMP Runtime · BADmCall (SB) GoodM: MOVQ R14, AX// AX (and arg 0) = g
   MOVQ   SI, R14       // g = g.m.g0
   get_tls(CX)       // Set G in TLS
   MOVQ   R14, g(CX)
   MOVQ   (g_sched+gobuf_sp)(R14), SP    // sp = g0.sched.sp
   PUSHQ  AX // open up space for fn's arg spill slot
   MOVQ   0(DX), R12
   CALL   R12       // fn(g)
   POPQ   AX
   JMP    runtime·badmcall2(SB)
   RET
#else
Copy the code

By MOVQ (g_sched+ goBUF_sp)(R14), SP switches to g0.sched. SP, so each stack switches to the same position each time

proc.go:3633

// goexit continuation on g0.
func goexit0(gp *g) {
   _g_ := getg()

   casgstatus(gp, _Grunning, _Gdead)
   if isSystemGoroutine(gp, false) {
      atomic.Xadd(&sched.ngsys, - 1)
   }
   gp.m = nillocked := gp.lockedm ! =0
   gp.lockedm = 0
   _g_.m.lockedg = 0
   gp.preemptStop = false
   gp.paniconfault = false
   gp._defer = nil // should be true already but just in case.
   gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
   gp.writebuf = nil
   gp.waitreason = 0
   gp.param = nil
   gp.labels = nil
   gp.timer = nil

   ifgcBlackenEnabled ! =0 && gp.gcAssistBytes > 0 {
      // Flush assist credit to the global pool. This gives
      // better information to pacing if the application is
      // rapidly creating an exiting goroutines.
      assistWorkPerByte := float64frombits(atomic.Load64(&gcController.assistWorkPerByte))
      scanCredit := int64(assistWorkPerByte * float64(gp.gcAssistBytes))
      atomic.Xaddint64(&gcController.bgScanCredit, scanCredit)
      gp.gcAssistBytes = 0
   }

   dropg()

   if GOARCH == "wasm" { // no threads yet on wasm
      gfput(_g_.m.p.ptr(), gp)
      schedule() // never returns
   }

   if_g_.m.lockedInt ! =0 {
      print("invalid m->lockedInt = ", _g_.m.lockedInt, "\n")
      throw("internal lockOSThread error")
   }
   gfput(_g_.m.p.ptr(), gp)
   if locked {
      // The goroutine may have locked this thread because
      // it put it in an unusual kernel state. Kill it
      // rather than returning it to the thread pool.

      // Return to mstart, which will release the P and exit
      // the thread.
      ifGOOS ! ="plan9" { // See golang.org/issue/22227.
         gogo(&_g_.m.g0.sched)
      } else {
         // Clear lockedExt on plan9 since we may end up re-using
         // this thread.
         _g_.m.lockedExt = 0
      }
   }
   schedule()
}
Copy the code
  1. Clear g’s information
  2. Put g in gfree
  3. Continue calling schedule

To summarize the process: